# IfcOpenShell - IFC toolkit and geometry engine # Copyright (C) 2021 Dion Moult # # This file is part of IfcOpenShell. # # IfcOpenShell is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # IfcOpenShell is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . from __future__ import annotations import math from typing import TYPE_CHECKING, Any, Literal, Optional, Union import bmesh # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import] import bpy # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import] import numpy as np import numpy.typing as npt from mathutils import Matrix, Vector # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import] import ifcopenshell.util.shape_builder import ifcopenshell.util.unit from ifcopenshell.util.shape_builder import ifc_safe_vector_type if TYPE_CHECKING: import bonsai.tool as tool from bonsai.bim.module.geometry.helper import Helper Z_AXIS = Vector((0, 0, 1)) X_AXIS = Vector((1, 0, 0)) EPSILON = 1e-6 def add_representation( file: ifcopenshell.file, *, # keywords only as this API implementation is probably not final context: ifcopenshell.entity_instance, blender_object: bpy.types.Object, geometry: Union[bpy.types.Mesh, bpy.types.Curve], coordinate_offset: Optional[npt.NDArray[np.float64]] = None, total_items: int = 1, unit_scale: Optional[float] = None, should_force_faceted_brep: bool = False, should_force_triangulation: bool = False, should_generate_uvs: bool = False, ifc_representation_class: Optional[ Literal[ "IfcExtrudedAreaSolid/IfcRectangleProfileDef", "IfcExtrudedAreaSolid/IfcCircleProfileDef", "IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef", "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids", "IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage", "IfcGeometricCurveSet/IfcTextLiteral", "IfcTextLiteral", ] ] = None, profile_set_usage: Optional[ifcopenshell.entity_instance] = None, text_literal: Optional[ifcopenshell.entity_instance] = None, ) -> Union[ifcopenshell.entity_instance, None]: """Add an IfcShapeRepresentation. :param context: The IfcGeometricRepresentationContext. :param blender_object: This is (currently) a Blender object, hence this depends on Blender now. :param geometry: This is (currently) a Blender data object, hence this depends on Blender now. :param coordinate_offset: Optionally apply a vector offset to all coordinates (in SI units). :param total_items: How many representation items to create. :param unit_scale: A scale factor to apply for all vectors in case the unit is different. :param should_force_faceted_brep: If we should force faceted breps for meshes. :param should_force_triangulation: If we should force triangulation for meshes. :param should_generate_uvs: If UV coordinates should also be generated. :param ifc_representation_class: Whether to cast a mesh into a particular class :param profile_set_usage: The material profile set if the extrusion requires it :param text_literal: The text literal if the representation requires it :return: IfcShapeRepresentation or None if couldn't create representation for the provided context. """ # lazy import Helper to avoid circular import if "Helper" not in globals(): import bonsai.tool as tool from bonsai.bim.module.geometry.helper import Helper globals()["Helper"] = Helper globals()["tool"] = tool usecase = Usecase() # TODO: This usecase currently depends on Blender's data model usecase.file = file usecase.settings = { "context": context, "blender_object": blender_object, "geometry": geometry, "coordinate_offset": coordinate_offset if coordinate_offset is not None else None, "total_items": total_items, "unit_scale": unit_scale, "should_force_faceted_brep": should_force_faceted_brep, "should_force_triangulation": should_force_triangulation, "should_generate_uvs": should_generate_uvs, "ifc_representation_class": ifc_representation_class, "profile_set_usage": profile_set_usage, "text_literal": text_literal, } usecase.ifc_vertices = [] return usecase.execute() class Usecase: file: ifcopenshell.file settings: dict[str, Any] ifc_vertices: list[ifcopenshell.entity_instance] coordinate_offset: Union[npt.NDArray[np.float64], None] geometry: Union[bpy.types.Mesh, bpy.types.Curve, bpy.types.Camera] blender_object: bpy.types.Object def execute(self) -> Union[ifcopenshell.entity_instance, None]: self.is_manifold = None self.coordinate_offset = self.settings["coordinate_offset"] self.geometry = self.settings["geometry"] self.blender_object = self.settings["blender_object"] if isinstance(self.geometry, bpy.types.Mesh) and self.geometry == self.blender_object.data: self.evaluate_geometry() if self.settings["unit_scale"] is None: self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) if self.settings["context"].ContextType == "Model": return self.create_model_representation() elif self.settings["context"].ContextType == "Plan": return self.create_plan_representation() return self.create_variable_representation() def should_triangulate_face(self, face: bmesh.types.BMFace, threshold: float = EPSILON) -> bool: vz = face.normal co = face.verts[0].co if vz.length < 0.5: return True if abs(vz.z) < 0.5: vx = vz.cross(Z_AXIS) else: vx = vz.cross(X_AXIS) vy = vx.cross(vz) assert isinstance(vy, Vector) tM = Matrix( [[vx.x, vy.x, vz.x, co.x], [vx.y, vy.y, vz.y, co.y], [vx.z, vy.z, vz.z, co.z], [0, 0, 0, 1]] ).inverted() return any([abs((tM @ v.co).z) > threshold for v in face.verts]) def evaluate_geometry(self) -> None: for modifier in self.blender_object.modifiers: if modifier.type == "BOOLEAN": modifier.show_viewport = False mesh = self.blender_object.evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh() bm = bmesh.new() bm.from_mesh(mesh) self.is_manifold = True for edge in bm.edges: if not edge.is_manifold: self.is_manifold = False break if self.settings["should_force_triangulation"]: faces = bm.faces else: faces = [f for f in bm.faces if self.should_triangulate_face(f)] bmesh.ops.triangulate(bm, faces=faces) bm.to_mesh(mesh) mesh.update() bm.free() del bm self.settings["geometry"] = mesh for modifier in self.blender_object.modifiers: if modifier.type == "BOOLEAN": modifier.show_viewport = True def create_model_representation(self) -> Union[ifcopenshell.entity_instance, None]: if self.settings["context"].is_a() == "IfcGeometricRepresentationContext": return self.create_variable_representation() elif self.settings["ifc_representation_class"] == "IfcTextLiteral": return self.create_text_representation(is_2d=False) elif self.settings["ifc_representation_class"] == "IfcGeometricCurveSet/IfcTextLiteral": shape_representation = self.create_geometric_curve_set_representation(is_2d=True) shape_representation.RepresentationType = "Annotation3D" items = list(shape_representation.Items) items.append(self.create_text()) shape_representation.Items = items return shape_representation elif self.settings["context"].ContextIdentifier == "Annotation": return self.create_annotation3d_representation() elif self.settings["context"].ContextIdentifier == "Axis": return self.create_curve3d_representation() elif self.settings["context"].ContextIdentifier == "Body": return self.create_variable_representation() elif self.settings["context"].ContextIdentifier == "Box": return self.create_box_representation() elif self.settings["context"].ContextIdentifier == "Clearance": return self.create_variable_representation() elif self.settings["context"].ContextIdentifier == "CoG": return self.create_cog_representation() elif self.settings["context"].ContextIdentifier == "FootPrint": return self.create_variable_representation() elif self.settings["context"].ContextIdentifier == "Reference": if self.settings["context"].TargetView == "GRAPH_VIEW": return self.create_structural_reference_representation() return self.create_variable_representation() elif self.settings["context"].ContextIdentifier == "Profile": return self.create_curve3d_representation() elif self.settings["context"].ContextIdentifier == "SurveyPoints": return self.create_geometric_curve_set_representation() elif self.settings["context"].ContextIdentifier == "Lighting": return self.create_lighting_representation() def create_plan_representation(self) -> Union[ifcopenshell.entity_instance, None]: if self.settings["ifc_representation_class"] == "IfcTextLiteral": return self.create_text_representation(is_2d=True) elif self.settings["ifc_representation_class"] == "IfcGeometricCurveSet/IfcTextLiteral": shape_representation = self.create_geometric_curve_set_representation(is_2d=True) shape_representation.RepresentationType = "Annotation2D" items = list(shape_representation.Items) items.append(self.create_text()) shape_representation.Items = items return shape_representation elif self.settings["context"].ContextIdentifier == "Annotation": return self.create_annotation2d_representation() elif self.settings["context"].ContextIdentifier == "Axis": return self.create_curve2d_representation() elif self.settings["context"].ContextIdentifier == "Body": return self.create_annotation2d_representation() elif self.settings["context"].ContextIdentifier == "Box": pass elif self.settings["context"].ContextIdentifier == "Clearance": pass elif self.settings["context"].ContextIdentifier == "CoG": pass elif self.settings["context"].ContextIdentifier == "FootPrint": if self.settings["context"].TargetView in ["SKETCH_VIEW", "PLAN_VIEW", "REFLECTED_PLAN_VIEW"]: return self.create_geometric_curve_set_representation(is_2d=True) elif self.settings["context"].ContextIdentifier == "Reference": pass elif self.settings["context"].ContextIdentifier == "Profile": pass elif self.settings["context"].ContextIdentifier == "SurveyPoints": pass else: return self.create_annotation2d_representation() def create_lighting_representation(self) -> ifcopenshell.entity_instance: return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "LightSource", [self.create_light_source()], ) def create_light_source(self) -> Union[ifcopenshell.entity_instance, None]: if self.settings["geometry"].type == "POINT": return self.create_light_source_positional() def create_light_source_positional(self) -> ifcopenshell.entity_instance: return self.file.create_entity( "IfcLightSourcePositional", **{ "LightColour": self.file.createIfcColourRgb(None, *self.settings["geometry"].color), "Position": self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)), "Radius": self.convert_si_to_unit(self.settings["geometry"].shadow_soft_size), }, ) def create_text_representation(self, is_2d: bool = False) -> ifcopenshell.entity_instance: return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "Annotation2D" if is_2d else "Annotation3D", [self.create_text()], ) def create_text(self) -> ifcopenshell.entity_instance: if self.settings["text_literal"]: return self.settings["text_literal"] origin = self.file.createIfcAxis2Placement3D( self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)), self.file.createIfcDirection((0.0, 0.0, 1.0)), self.file.createIfcDirection((1.0, 0.0, 0.0)), ) # TODO: Planar extent right now is wrong ... return self.file.createIfcTextLiteralWithExtent( "TEXT", origin, "RIGHT", self.file.createIfcPlanarExtent(1000, 1000), "bottom-left" ) def create_variable_representation(self) -> Union[ifcopenshell.entity_instance, None]: if isinstance(self.settings["geometry"], bpy.types.Curve) and self.settings["geometry"].bevel_depth: return self.create_swept_disk_solid_representation() elif isinstance(self.settings["geometry"], bpy.types.Curve): return self.create_curve3d_representation() elif isinstance(self.geometry, bpy.types.Camera): if self.geometry.type == "ORTHO": return self.create_camera_block_representation() elif self.geometry.type == "PERSP": return self.create_camera_pyramid_representation() else: raise ValueError(f"Unsupported camera type: '{self.geometry.type}'.") elif not len(self.settings["geometry"].edges): return self.create_point_cloud_representation() elif not len(self.settings["geometry"].polygons): return self.create_curve3d_representation() elif self.settings["ifc_representation_class"] == "IfcExtrudedAreaSolid/IfcRectangleProfileDef": return self.create_rectangle_extrusion_representation() elif self.settings["ifc_representation_class"] == "IfcExtrudedAreaSolid/IfcCircleProfileDef": return self.create_circle_extrusion_representation() elif self.settings["ifc_representation_class"] == "IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef": return self.create_arbitrary_extrusion_representation() elif self.settings["ifc_representation_class"] == "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids": return self.create_arbitrary_void_extrusion_representation() elif self.settings["ifc_representation_class"] == "IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage": return self.create_material_profile_set_extrusion_representation() return self.create_mesh_representation() def create_camera_block_representation(self) -> ifcopenshell.entity_instance: assert isinstance(self.geometry, bpy.types.Camera) props = tool.Drawing.get_camera_props(self.geometry) raster_x = props.raster_x raster_y = props.raster_y if self.is_camera_landscape(): width = self.settings["geometry"].ortho_scale height = width / raster_x * raster_y else: height = self.settings["geometry"].ortho_scale width = height / raster_y * raster_x block = self.file.create_entity( "IfcBlock", Position=self.file.createIfcAxis2Placement3D( self.create_cartesian_point(-width / 2, -height / 2, -self.settings["geometry"].clip_end) ), XLength=self.convert_si_to_unit(width), YLength=self.convert_si_to_unit(height), ZLength=self.convert_si_to_unit(self.settings["geometry"].clip_end), ) return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "CSG", [self.file.createIfcCsgSolid(block)], ) def create_camera_pyramid_representation(self) -> ifcopenshell.entity_instance: assert isinstance(self.geometry, bpy.types.Camera) props = tool.Drawing.get_camera_props(self.geometry) raster_x = props.raster_x raster_y = props.raster_y fov = self.settings["geometry"].angle clip_end = self.settings["geometry"].clip_end clip_start = self.settings["geometry"].clip_start if self.is_camera_landscape(): half_width = math.tan(fov / 2) * clip_end half_height = half_width * raster_y / raster_x else: half_height = math.tan(fov / 2) * clip_end half_width = half_height * raster_x / raster_y x_length = 2 * half_width y_length = 2 * half_height pyramid = self.file.create_entity( "IfcRectangularPyramid", Position=self.file.createIfcAxis2Placement3D( self.create_cartesian_point(-x_length / 2, -y_length / 2, -clip_end) ), XLength=self.convert_si_to_unit(x_length), YLength=self.convert_si_to_unit(y_length), Height=self.convert_si_to_unit(clip_end), ) surface = self.file.createIfcPlane( self.file.createIfcAxis2Placement3D(self.create_cartesian_point(0, 0, -clip_start)) ) half_space = self.file.createIfcHalfSpaceSolid(surface, False) clipping_result = self.file.create_entity("IfcBooleanResult", "DIFFERENCE", pyramid, half_space) return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "CSG", [self.file.createIfcCsgSolid(clipping_result)], ) def is_camera_landscape(self) -> bool: assert isinstance(self.geometry, bpy.types.Camera) props = tool.Drawing.get_camera_props(self.geometry) return props.raster_x > props.raster_y def create_swept_disk_solid_representation(self) -> ifcopenshell.entity_instance: return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "AdvancedSweptSolid", self.create_swept_disk_solids(), ) def create_curve3d_representation(self) -> Union[ifcopenshell.entity_instance, None]: if curves := self.create_curves(): return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "Curve3D", curves ) def create_curve2d_representation(self) -> ifcopenshell.entity_instance: return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "Curve2D", self.create_curves(is_2d=True), ) def create_curve_bounded_planes(self, is_2d: bool = False) -> list[ifcopenshell.entity_instance]: items = [] if self.file.schema != "IFC2X3": points = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices, is_2d=False) for polygon in self.settings["geometry"].polygons: plane = self.create_plane(polygon) if self.file.schema == "IFC2X3": curve = self.create_curve_from_polygon_ifc2x3(polygon, is_2d=False) else: curve = self.create_curve_from_polygon(points, polygon, is_2d=False) items.append(self.file.createIfcCurveBoundedPlane(BasisSurface=plane, OuterBoundary=curve)) return items def create_plane(self, polygon: bpy.types.MeshPolygon) -> ifcopenshell.entity_instance: return self.file.createIfcPlane( Position=self.file.createIfcAxis2Placement3D( Location=self.file.createIfcCartesianPoint(polygon.center), Axis=self.file.createIfcDirection(polygon.normal), ) ) def create_annotation_fill_areas(self, is_2d: bool = False) -> list[ifcopenshell.entity_instance]: items = [] if self.file.schema != "IFC2X3": points = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices, is_2d=is_2d) for polygon in self.settings["geometry"].polygons: if self.file.schema == "IFC2X3": curve = self.create_curve_from_polygon_ifc2x3(polygon, is_2d=is_2d) else: curve = self.create_curve_from_polygon(points, polygon, is_2d=is_2d) items.append(self.file.createIfcAnnotationFillArea(OuterBoundary=curve)) return items def create_curve_from_polygon( self, points: ifcopenshell.entity_instance, polygon: bpy.types.MeshPolygon, is_2d: bool = False ) -> ifcopenshell.entity_instance: indices = list(polygon.vertices) indices.append(indices[0]) edge_loop = [self.file.createIfcLineIndex((v1 + 1, v2 + 1)) for v1, v2 in zip(indices, indices[1:])] return self.file.createIfcIndexedPolyCurve(points, edge_loop) def create_curve_from_polygon_ifc2x3( self, polygon: bpy.types.MeshPolygon, is_2d: bool = False ) -> ifcopenshell.entity_instance: indices = list(polygon.vertices) indices.append(indices[0]) points = [ self.create_cartesian_point(v.co.x, v.co.y, v.co.z if not is_2d else None) for v in self.settings["geometry"].vertices ] return self.file.createIfcPolyline([points[i] for i in indices]) def create_swept_disk_solids(self) -> list[ifcopenshell.entity_instance]: curves = self.create_curves() results = [] radius = self.convert_si_to_unit(round(self.settings["geometry"].bevel_depth, 3)) for curve in curves: results.append(self.file.createIfcSweptDiskSolid(curve, radius)) return results def is_mesh_curve_consecutive(self, geom_data: bpy.types.Mesh) -> bool: import bonsai.tool as tool bm = tool.Blender.get_bmesh_for_mesh(geom_data) bm.verts.ensure_lookup_table() start_vert = bm.verts[0] n_verts = len(bm.verts) cur_edges = bm.verts[0].link_edges if len(cur_edges) > 2: return False elif cur_edges == 2: edge0, edge1 = cur_edges else: edge0, edge1 = cur_edges[0], None processed_verts = set() processed_verts.add(start_vert) def validate_edge(edge, start_vert, processed_verts): cur_vert = edge.other_vert(start_vert) while True: if cur_vert == start_vert: break if cur_vert in processed_verts: return processed_verts.add(cur_vert) edges = cur_vert.link_edges if len(edges) > 2: return elif len(edges) == 1: return True edge = next(e for e in edges if e != edge) cur_vert = edge.other_vert(cur_vert) return True if not validate_edge(edge0, start_vert, processed_verts): return False if edge1 and not validate_edge(edge1, start_vert, processed_verts): return False if len(processed_verts) != n_verts: return False return True def create_curves( self, should_exclude_faces: bool = False, is_2d: bool = False, ignore_non_loose_edges: bool = False ) -> list[ifcopenshell.entity_instance]: geom_data = self.settings["geometry"] if isinstance(geom_data, bpy.types.Mesh): if self.is_mesh_curve_consecutive(geom_data): if self.file.schema == "IFC2X3": return self.create_curves_from_mesh_ifc2x3(should_exclude_faces=should_exclude_faces, is_2d=is_2d) return self.create_curves_from_mesh(should_exclude_faces=should_exclude_faces, is_2d=is_2d) import bonsai.tool as tool selected_objects = bpy.context.selected_objects active_object = bpy.context.active_object # create dummy object that will have more detailed curves # since now we do not really support splines curves natively obj = self.blender_object dummy = bpy.data.objects.new("Dummy", obj.data.copy()) bpy.context.scene.collection.objects.link(dummy) tool.Blender.select_and_activate_single_object(bpy.context, dummy) if not isinstance(geom_data, bpy.types.Mesh): bpy.ops.object.convert(target="MESH") self.remove_doubles_from_mesh(dummy.data) bpy.ops.object.convert(target="CURVE") if self.file.schema == "IFC2X3": curves = self.create_curves_from_curve_ifc2x3(is_2d=is_2d, curve_object_data=dummy.data) else: curves = self.create_curves_from_curve(is_2d=is_2d, curve_object_data=dummy.data) # restore objects selection bpy.data.objects.remove(dummy) tool.Blender.set_objects_selection(bpy.context, active_object, selected_objects) return curves def create_curves_from_mesh( self, should_exclude_faces: bool = False, is_2d: bool = False ) -> list[ifcopenshell.entity_instance]: geom_data = self.settings["geometry"].copy() self.remove_doubles_from_mesh(geom_data) curves = [] points = self.create_cartesian_point_list_from_vertices(geom_data.vertices, is_2d=is_2d) edge_loops = [] previous_edge = None edge_loop = [] face_edges = set() if should_exclude_faces: [face_edges.union([geom_data.edge_keys.index(ek) for ek in p.edge_keys]) for p in geom_data.polygons] for i, edge in enumerate(geom_data.edges): if should_exclude_faces and i in face_edges: continue elif previous_edge is None: edge_loop = [self.file.createIfcLineIndex((edge.vertices[0] + 1, edge.vertices[1] + 1))] elif edge.vertices[0] == previous_edge.vertices[1]: edge_loop.append(self.file.createIfcLineIndex((edge.vertices[0] + 1, edge.vertices[1] + 1))) else: edge_loops.append(edge_loop) edge_loop = [self.file.createIfcLineIndex((edge.vertices[0] + 1, edge.vertices[1] + 1))] previous_edge = edge edge_loops.append(edge_loop) for edge_loop in edge_loops: curves.append(self.file.createIfcIndexedPolyCurve(points, edge_loop)) return curves def remove_doubles_from_mesh(self, mesh: bpy.types.Mesh) -> None: import bonsai.tool as tool bm = tool.Blender.get_bmesh_for_mesh(mesh) bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) tool.Blender.apply_bmesh(mesh, bm) def create_curves_from_mesh_ifc2x3( self, should_exclude_faces=False, is_2d=False ) -> list[ifcopenshell.entity_instance]: geom_data = self.settings["geometry"].copy() self.remove_doubles_from_mesh(geom_data) curves = [] points = [ self.create_cartesian_point(v.co.x, v.co.y, v.co.z if not is_2d else None) for v in geom_data.vertices ] coord_list = [p.Coordinates for p in points] edge_loops = [] previous_edge = None edge_loop = [] face_edges = set() if should_exclude_faces: [face_edges.union([geom_data.edge_keys.index(ek) for ek in p.edge_keys]) for p in geom_data.polygons] for i, edge in enumerate(geom_data.edges): if should_exclude_faces and i in face_edges: continue elif previous_edge is None: edge_loop = [edge.vertices] elif edge.vertices[0] == previous_edge.vertices[1]: edge_loop.append(edge.vertices) else: edge_loops.append(edge_loop) edge_loop = [edge.vertices] previous_edge = edge edge_loops.append(edge_loop) for edge_loop in edge_loops: loop_points = [points[p[0]] for p in edge_loop] loop_points.append(points[edge_loop[-1][1]]) curves.append(self.file.createIfcPolyline(loop_points)) return curves def create_curves_from_curve_ifc2x3( self, is_2d: bool = False, curve_object_data: Optional[bpy.types.Curve] = None ) -> list[ifcopenshell.entity_instance]: # TODO: support interpolated curves, not just polylines if not curve_object_data: curve_object_data = self.settings["geometry"] dim = (lambda v: v.xy) if is_2d else (lambda v: v.xyz) results = [] for spline in curve_object_data.splines: points = self.get_spline_points(spline) ifc_points = [self.create_cartesian_point(*dim(point.co)) for point in points] results.append(self.file.createIfcPolyline(ifc_points)) return results def create_curves_from_curve( self, is_2d: bool = False, curve_object_data: Optional[bpy.types.Curve] = None ) -> list[ifcopenshell.entity_instance]: # TODO: support interpolated curves, not just polylines if not curve_object_data: curve_object_data = self.settings["geometry"] dim = (lambda v: v.xy) if is_2d else (lambda v: v.xyz) to_units = lambda v: Vector([self.convert_si_to_unit(i) for i in v]) builder = ifcopenshell.util.shape_builder.ShapeBuilder(self.file) results = [] for spline in curve_object_data.splines: points = spline.bezier_points[:] + spline.points[:] points = [to_units(dim(p.co)) for p in points] closed_polyline = spline.use_cyclic_u and len(points) > 1 results.append(builder.polyline(points, closed=closed_polyline)) return results def create_point_cloud_representation(self, is_2d: bool = False) -> ifcopenshell.entity_instance: if self.file.schema == "IFC2X3": geometric_set = [] for point in self.settings["geometry"].vertices: if is_2d: geometric_set.append(self.create_cartesian_point(point.co.x, point.co.y)) else: geometric_set.append(self.create_cartesian_point(point.co.x, point.co.y, point.co.z)) return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "GeometricSet", geometric_set, ) point_cloud = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices, is_2d) return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "Point" if self.file.schema == "IFC4X3" else "PointCloud", [point_cloud], ) def create_rectangle_extrusion_representation(self) -> ifcopenshell.entity_instance: helper = Helper(self.file) indices = helper.auto_detect_rectangle_profile_extruded_area_solid(self.settings["geometry"]) profile_def = helper.create_rectangle_profile_def(self.settings["geometry"], indices["profile"]) item = helper.create_extruded_area_solid(self.settings["geometry"], indices["extrusion"], profile_def) return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "SweptSolid", [item], ) def create_circle_extrusion_representation(self) -> ifcopenshell.entity_instance: helper = Helper(self.file) indices = helper.auto_detect_circle_profile_extruded_area_solid(self.settings["geometry"]) profile_def = helper.create_circle_profile_def(self.settings["geometry"], indices["profile"]) item = helper.create_extruded_area_solid(self.settings["geometry"], indices["extrusion"], profile_def) return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "SweptSolid", [item], ) def create_arbitrary_extrusion_representation(self) -> ifcopenshell.entity_instance: helper = Helper(self.file) indices = helper.auto_detect_arbitrary_closed_profile_extruded_area_solid(self.settings["geometry"]) profile_def = helper.create_arbitrary_closed_profile_def(self.settings["geometry"], indices["profile"]) item = helper.create_extruded_area_solid(self.settings["geometry"], indices["extrusion"], profile_def) return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "SweptSolid", [item], ) def create_arbitrary_void_extrusion_representation(self) -> ifcopenshell.entity_instance: helper = Helper(self.file) indices = helper.auto_detect_arbitrary_profile_with_voids_extruded_area_solid(self.settings["geometry"]) if not indices["inner_curves"]: return self.create_arbitrary_extrusion_representation() profile_def = helper.create_arbitrary_profile_def_with_voids( self.settings["geometry"], indices["profile"], indices["inner_curves"] ) item = helper.create_extruded_area_solid(self.settings["geometry"], indices["extrusion"], profile_def) return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "SweptSolid", [item], ) def create_material_profile_set_extrusion_representation(self) -> ifcopenshell.entity_instance: profile_set = self.settings["profile_set_usage"].ForProfileSet profile_def = profile_set.CompositeProfile or profile_set.MaterialProfiles[0].Profile position = None if self.file.schema == "IFC2X3": position = self.file.createIfcAxis2Placement3D( self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)), self.file.createIfcDirection((0.0, 0.0, 1.0)), self.file.createIfcDirection((1.0, 0.0, 0.0)), ) item = self.file.createIfcExtrudedAreaSolid( profile_def, position, self.file.createIfcDirection((0.0, 0.0, 1.0)), self.convert_si_to_unit(self.blender_object.dimensions[2]), ) return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "SweptSolid", [item], ) def create_mesh_representation(self) -> ifcopenshell.entity_instance: if self.file.schema == "IFC2X3" or self.settings["should_force_faceted_brep"]: return self.create_faceted_brep() if self.settings["should_force_triangulation"]: return self.create_triangulated_face_set() return self.create_polygonal_face_set() def create_faceted_brep(self) -> ifcopenshell.entity_instance: self.create_vertices() ifc_raw_items = [None] * self.settings["total_items"] for i, value in enumerate(ifc_raw_items): ifc_raw_items[i] = [] for polygon in self.settings["geometry"].polygons: ifc_raw_items[polygon.material_index % self.settings["total_items"]].append( self.file.createIfcFace( [ self.file.createIfcFaceOuterBound( self.file.createIfcPolyLoop([self.ifc_vertices[vertice] for vertice in polygon.vertices]), True, ) ] ) ) # TODO: May not actually be a closed shell, but who checks anyway? items = [self.file.createIfcFacetedBrep(self.file.createIfcClosedShell(i)) for i in ifc_raw_items if i] return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "Brep", items, ) def create_triangulated_face_set(self) -> ifcopenshell.entity_instance: ifc_raw_items = [None] * self.settings["total_items"] if self.settings["should_generate_uvs"]: ifc_raw_uv_items = [None] * self.settings["total_items"] for i, value in enumerate(ifc_raw_items): ifc_raw_items[i] = [] if self.settings["should_generate_uvs"]: ifc_raw_uv_items[i] = [] for polygon in self.settings["geometry"].polygons: ifc_raw_items[polygon.material_index % self.settings["total_items"]].append( [v + 1 for v in polygon.vertices] ) if self.settings["should_generate_uvs"]: ifc_raw_uv_items[polygon.material_index % self.settings["total_items"]].append( [uv + 1 for uv in polygon.loop_indices] ) coordinates = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices) if self.settings["should_generate_uvs"]: # Blender supports multiple UV layers. We don't. Too bad. tex_coords = self.file.createIfcTextureVertexList( [tuple(x.uv) for x in self.settings["geometry"].uv_layers[0].data] ) items = [] for i, coord_index in enumerate(ifc_raw_items): if not coord_index: continue tex_coords_index = ifc_raw_uv_items[i] face_set = self.file.createIfcTriangulatedFaceSet(coordinates, None, None, coord_index) texture_map = self.file.createIfcIndexedTriangleTextureMap( MappedTo=face_set, TexCoords=tex_coords, TexCoordIndex=tex_coords_index ) items.append(face_set) else: items = [self.file.createIfcTriangulatedFaceSet(coordinates, None, None, i) for i in ifc_raw_items if i] return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "Tessellation", items, ) def create_polygonal_face_set(self) -> ifcopenshell.entity_instance: ifc_raw_items = [None] * self.settings["total_items"] for i, value in enumerate(ifc_raw_items): ifc_raw_items[i] = [] for polygon in self.settings["geometry"].polygons: ifc_raw_items[polygon.material_index % self.settings["total_items"]].append( self.file.createIfcIndexedPolygonalFace([v + 1 for v in polygon.vertices]) ) coordinates = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices) items = [self.file.createIfcPolygonalFaceSet(coordinates, self.is_manifold, i) for i in ifc_raw_items if i] return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "Tessellation", items, ) def create_vertices(self, is_2d: bool = False) -> None: if is_2d: for v in self.settings["geometry"].vertices: co = self.convert_si_to_unit(v.co) self.ifc_vertices.append(self.file.createIfcCartesianPoint((co[0], co[1]))) return self.ifc_vertices.extend( [ self.file.createIfcCartesianPoint(self.convert_si_to_unit(v.co)) for v in self.settings["geometry"].vertices ] ) def create_cartesian_point( self, x: float, y: float, z: Optional[float] = None, is_model_coords: bool = True ) -> ifcopenshell.entity_instance: """Create IfcCartesianPoint. x, y, z coords are provided in SI units. """ if is_model_coords and self.coordinate_offset is not None: x += self.coordinate_offset[0] y += self.coordinate_offset[1] if z is not None: z += self.coordinate_offset[2] x = self.convert_si_to_unit(x) y = self.convert_si_to_unit(y) if z is None: return self.file.createIfcCartesianPoint((x, y)) z = self.convert_si_to_unit(z) return self.file.createIfcCartesianPoint((x, y, z)) def create_cartesian_point_list_from_vertices( self, vertices: bpy.types.MeshVertices, is_2d: bool = False, is_model_coords: bool = True ) -> ifcopenshell.entity_instance: # Catch values as floats to benefit from fast buffer copy. coords = np.empty(len(vertices) * 3, dtype="f") vertices.foreach_get("co", coords) coords = coords.reshape(-1, 3) if is_2d: coords = coords[:, :2] coords_class = "IfcCartesianPointList2D" else: coords_class = "IfcCartesianPointList3D" if is_model_coords and (offset := self.coordinate_offset) is not None: coords = coords.astype("d") if is_2d: offset = offset[:2] coords += offset return self.file.create_entity(coords_class, ifc_safe_vector_type(self.convert_si_to_unit(coords))) def convert_si_to_unit(self, co): return co / self.settings["unit_scale"] def create_annotation2d_representation(self) -> ifcopenshell.entity_instance: if isinstance(self.settings["geometry"], bpy.types.Mesh) and len(self.settings["geometry"].polygons): items = self.create_annotation_fill_areas(is_2d=True) elif isinstance(self.settings["geometry"], bpy.types.Mesh) and not len(self.settings["geometry"].edges): return self.create_point_cloud_representation(is_2d=True) else: items = [self.file.createIfcGeometricCurveSet(self.create_curves(is_2d=True))] return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "Annotation2D", items, ) def create_annotation3d_representation(self) -> ifcopenshell.entity_instance: items = [] if isinstance(self.settings["geometry"], bpy.types.Mesh) and len(self.settings["geometry"].polygons): items = self.create_annotation_fill_areas(is_2d=False) else: items = [self.file.createIfcGeometricCurveSet(self.create_curves(is_2d=False))] # TODO Unsure when it is appropriate to use curve bounded planes # surfaces = self.create_curve_bounded_planes() # if surfaces: # items.append(self.file.createIfcGeometricSet(surfaces)) return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "GeometricSet", items, ) def create_geometric_curve_set_representation(self, is_2d: bool = False) -> ifcopenshell.entity_instance: geometric_curve_set = self.file.createIfcGeometricCurveSet(self.create_curves(is_2d=is_2d)) return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "GeometricCurveSet", [geometric_curve_set], ) def create_box_representation(self) -> ifcopenshell.entity_instance: obj = self.blender_object bounding_box = self.file.createIfcBoundingBox( self.create_cartesian_point(obj.bound_box[0][0], obj.bound_box[0][1], obj.bound_box[0][2]), self.convert_si_to_unit(obj.dimensions[0]), self.convert_si_to_unit(obj.dimensions[1]), self.convert_si_to_unit(obj.dimensions[2]), ) return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "BoundingBox", [bounding_box], ) def create_cog_representation(self) -> Union[ifcopenshell.entity_instance, None]: mesh = self.settings["geometry"] if not isinstance(mesh, bpy.types.Mesh) or len(verts := mesh.vertices) == 0: return vert = verts[0].co cog = self.create_cartesian_point(vert.x, vert.y, vert.z) return self.file.create_entity( "IfcShapeRepresentation", self.settings["context"], self.settings["context"].ContextIdentifier, "Point", (cog,), ) def create_structural_reference_representation(self) -> ifcopenshell.entity_instance: if isinstance(self.geometry, bpy.types.Mesh) and len(self.geometry.vertices) == 1: return self.file.createIfcTopologyRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "Vertex", [self.create_vertex_point(self.geometry.vertices[0].co)], ) return self.file.createIfcTopologyRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "Edge", [self.create_edge()], ) def create_vertex_point(self, point: Vector) -> ifcopenshell.entity_instance: return self.file.createIfcVertexPoint(self.create_cartesian_point(point.x, point.y, point.z)) def get_spline_points( self, spline: bpy.types.Spline ) -> list[Union[bpy.types.SplinePoint, bpy.types.BezierSplinePoint]]: points = spline.bezier_points[:] + spline.points[:] if spline.use_cyclic_u: points.append(points[0]) return points def create_edge(self) -> Union[ifcopenshell.entity_instance, None]: geometry = self.geometry if isinstance(geometry, bpy.types.Curve): points = self.get_spline_points(geometry.splines[0]) elif isinstance(geometry, bpy.types.Mesh): points = geometry.vertices else: assert False, type(geometry) if not points: return return self.file.createIfcEdge(self.create_vertex_point(points[0].co), self.create_vertex_point(points[1].co))