diff --git a/CHANGELOG.md b/CHANGELOG.md index e8ce2d58..e9bb0bdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +### Changed + +### Removed + + +## [1.7.0] 2025-10-24 + +### Added + +* Added `volume` property to `TessellatedBrep` class for calculating volume of closed meshes using COMPAS geometry functions +* Added `surface_area` property to `TessellatedBrep` class for calculating surface area by summing face areas + +### Changed + +### Removed + + +## [1.6.1] 2025-07-28 + +### Added + +* Added `linear_deflection` parameter to `Model.show()`, defaulting to 100 for faster BRep tesselation. + +### Changed + +### Removed + +* Removed `Model.update_linear_deflection()` + +## [1.6.0] 2025-06-04 + +### Added + +### Changed + +### Removed + + +## [1.5.0] 2025-01-21 + +### Added + * Added `extensions` keyword argument to `Model` to for inserting custom extensions to IFC classes. ### Changed diff --git a/docs/api.rst b/docs/api.rst index 4333cc91..553dc95a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -14,7 +14,6 @@ This is the API reference for the Compas IFC package. api/compas_ifc.model api/compas_ifc.entities - api/compas_ifc.resources api/compas_ifc.entities.generated diff --git a/docs/api/compas_ifc.resources.rst b/docs/api/compas_ifc.resources.rst deleted file mode 100644 index 61bf88d8..00000000 --- a/docs/api/compas_ifc.resources.rst +++ /dev/null @@ -1,43 +0,0 @@ -******************************************************************************** -resources -******************************************************************************** - -.. currentmodule:: compas_ifc.resources - -This module contains functions for converting between IFC geometry resources and equivalent COMPAS geometry objects. - -Functions -========= - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - IfcCartesianPoint_to_point - IfcDirection_to_vector - IfcVector_to_vector - IfcLine_to_line - IfcPlane_to_plane - IfcAxis2Placement3D_to_frame - IfcCartesianTransformationOperator3D_to_frame - IfcCompoundPlaneAngleMeasure_to_degrees - IfcGridPlacement_to_transformation - IfcLocalPlacement_to_transformation - IfcLocalPlacement_to_frame - IfcAdvancedBrep_to_brep - IfcAdvancedBrepWithVoids_to_brep - IfcBlock_to_box - IfcBooleanClippingResult_to_brep - IfcBoundingBox_to_box - IfcIndexedPolyCurve_to_lines - IfcExtrudedAreaSolid_to_brep - IfcFacetedBrep_to_brep - IfcFacetedBrepWithVoids_to_brep - IfcIndexedPolygonalFaceSet_to_brep - IfcPolygonalBoundedHalfSpace_to_brep - IfcPolygonalFaceSet_to_brep - IfcTessellatedFaceSet_to_brep - IfcTriangulatedFaceSet_to_brep - IfcShape_to_brep - IfcShape_to_tessellatedbrep - frame_to_ifc_axis2_placement_3d diff --git a/docs/examples/Basics.1_overview.rst b/docs/examples/Basics.1_overview.rst index 5f0f1671..e311d083 100644 --- a/docs/examples/Basics.1_overview.rst +++ b/docs/examples/Basics.1_overview.rst @@ -53,7 +53,7 @@ This example shows how to load an IFC file and print a summary of the model. print("\nProperties") print("=" * 53 + "\n") - pprint(project.properties) + pprint(project.property_sets) print("\nRepresentation Contexts") print("=" * 53 + "\n") diff --git a/docs/examples/Basics.4_element_info.rst b/docs/examples/Basics.4_element_info.rst index f2f246a9..86486256 100644 --- a/docs/examples/Basics.4_element_info.rst +++ b/docs/examples/Basics.4_element_info.rst @@ -45,7 +45,7 @@ This example shows how to get information of a building element, such as a windo print("\nProperties") print("=" * 53 + "\n") - pprint(window.properties) + pprint(window.property_sets) Example Output: diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 2072cb2d..73571fbe 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -13,6 +13,6 @@ In this section you will find tutorials on how to use the COMPAS IFC package. tutorials/basics.hello_world tutorials/basics.entity_apis - tutorials/basics.create_model - tutorials/intermediate.multi_story_building - tutorials/advanced.custom_extensions + .. tutorials/basics.create_model + .. tutorials/intermediate.multi_story_building + .. tutorials/advanced.custom_extensions diff --git a/pyproject.toml b/pyproject.toml index 852ac9d6..fd57eb53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ doctest_optionflags = [ # ============================================================================ [tool.bumpversion] -current_version = "1.4.1" +current_version = "1.7.0" message = "Bump version to {new_version}" commit = true tag = true diff --git a/scripts/2.2_site_info.py b/scripts/2.2_site_info.py index 842a1722..f9c9b20b 100644 --- a/scripts/2.2_site_info.py +++ b/scripts/2.2_site_info.py @@ -26,7 +26,7 @@ print("\nProperties") print("=" * 53 + "\n") -pprint(site.properties) +pprint(site.property_sets) print("\nBuildings") print("=" * 53 + "\n") diff --git a/scripts/2.3_window_info.py b/scripts/2.3_window_info.py index af26c2a4..7bb301e2 100644 --- a/scripts/2.3_window_info.py +++ b/scripts/2.3_window_info.py @@ -21,4 +21,4 @@ print("\nProperties") print("=" * 53 + "\n") -pprint(window.properties) +pprint(window.property_sets) diff --git a/scripts/7.1_geometry_ops.py b/scripts/7.1_geometry_ops.py new file mode 100644 index 00000000..9f017621 --- /dev/null +++ b/scripts/7.1_geometry_ops.py @@ -0,0 +1,10 @@ +from compas_ifc.model import Model + +model = Model("data/wall-with-opening-and-window.ifc") +model.print_summary() + +unit = model.unit + +element = model.get_entities_by_type("IfcWindow")[0] +print("Volume:", element.geometry.volume, unit + "³") +print("Surface Area:", element.geometry.surface_area, unit + "²") diff --git a/src/compas_ifc/__init__.py b/src/compas_ifc/__init__.py index d19a690e..3dc48896 100644 --- a/src/compas_ifc/__init__.py +++ b/src/compas_ifc/__init__.py @@ -28,7 +28,7 @@ __copyright__ = "ETH Zurich" __license__ = "MIT License" __email__ = "van.mele@arch.ethz.ch" -__version__ = "1.4.1" +__version__ = "1.7.0" HERE = os.path.dirname(__file__) diff --git a/src/compas_ifc/attributes.py b/src/compas_ifc/__old/attributes.py similarity index 100% rename from src/compas_ifc/attributes.py rename to src/compas_ifc/__old/attributes.py diff --git a/src/compas_ifc/resources/geometricmodel.py b/src/compas_ifc/__old/geometricmodel.py similarity index 100% rename from src/compas_ifc/resources/geometricmodel.py rename to src/compas_ifc/__old/geometricmodel.py diff --git a/src/compas_ifc/brep/ifcbrepobject.py b/src/compas_ifc/brep/ifcbrepobject.py index 7ab42b8f..508539ba 100644 --- a/src/compas_ifc/brep/ifcbrepobject.py +++ b/src/compas_ifc/brep/ifcbrepobject.py @@ -1,18 +1,17 @@ try: import numpy as np from compas.colors import Color - from compas.tolerance import TOL from compas_occ.brep import OCCBrep from compas_viewer.scene.brepobject import BRepObject class IFCBrepObject(BRepObject): - def __init__(self, shellcolors=None, **kwargs): + def __init__(self, shellcolors=None, linear_deflection=100, **kwargs): brep = kwargs["item"] brep.simplify() brep.heal() super().__init__(**kwargs) - self.shells = [shell.to_tesselation(TOL.lineardeflection)[0] for shell in self.brep.shells] + self.shells = [shell.to_tesselation(linear_deflection)[0] for shell in self.brep.shells] self.shellcolors = shellcolors or [self.facecolor.rgba for _ in self.shells] self._bounding_box_center = None @@ -33,6 +32,8 @@ def _read_frontfaces_data(self): for shell, color in zip(self.shells, self.shellcolors): shell_positions, shell_elements = shell.to_vertices_and_faces() + if len(shell_elements) == 0: + continue shell_elements = np.array(shell_elements) + len(positions) positions += shell_positions elements = np.vstack((elements, shell_elements)) @@ -49,6 +50,8 @@ def _read_backfaces_data(self): for shell, color in zip(self.shells, self.shellcolors): shell_positions, shell_elements = shell.to_vertices_and_faces() + if len(shell_elements) == 0: + continue for element in shell_elements: element.reverse() shell_elements = np.array(shell_elements) + len(positions) diff --git a/src/compas_ifc/brep/tessellatedbrep.py b/src/compas_ifc/brep/tessellatedbrep.py index bd6db58e..ce26d364 100644 --- a/src/compas_ifc/brep/tessellatedbrep.py +++ b/src/compas_ifc/brep/tessellatedbrep.py @@ -1,8 +1,10 @@ import numpy as np from compas.datastructures import Mesh from compas.geometry import Geometry +from compas.geometry import area_polygon from compas.geometry import bounding_box from compas.geometry import transform_points_numpy +from compas.geometry import volume_polyhedron class TessellatedBrep(Geometry): @@ -41,3 +43,65 @@ def obb(self): from compas.geometry import oriented_bounding_box_numpy return Box.from_bounding_box(oriented_bounding_box_numpy(self.vertices)) + + @property + def volume(self): + """Calculate the volume of the tessellated BREP by converting to mesh. + + Returns + ------- + float or None + The volume of the BREP if the mesh is closed, None otherwise. + """ + # Handle edge case: empty vertices or faces + if len(self.vertices) == 0 or len(self.faces) == 0: + return None + + try: + mesh = self.to_mesh() + + # Check if mesh is closed - volume can only be calculated for closed meshes + if not mesh.is_closed(): + return None + + # Get vertices and faces for volume calculation + vertices, faces = mesh.to_vertices_and_faces() + + # Calculate volume using COMPAS geometry function + return volume_polyhedron((vertices, faces)) + + except Exception: + # Return None if any error occurs during calculation + return None + + @property + def surface_area(self): + """Calculate the surface area of the tessellated BREP by converting to mesh. + + Returns + ------- + float or None + The surface area of the BREP, or None if calculation fails. + """ + # Handle edge case: empty vertices or faces + if len(self.vertices) == 0 or len(self.faces) == 0: + return None + + try: + mesh = self.to_mesh() + + # Calculate total surface area by summing face areas + total_area = 0.0 + for face_key in mesh.faces(): + # Get face vertices coordinates + face_vertices = [mesh.vertex_coordinates(vertex) for vertex in mesh.face_vertices(face_key)] + + # Calculate area of this face using COMPAS geometry function + face_area = area_polygon(face_vertices) + total_area += face_area + + return total_area + + except Exception: + # Return None if any error occurs during calculation + return None diff --git a/src/compas_ifc/brep/tessellatedbrepobject.py b/src/compas_ifc/brep/tessellatedbrepobject.py index 3f57d761..12f9b0f9 100644 --- a/src/compas_ifc/brep/tessellatedbrepobject.py +++ b/src/compas_ifc/brep/tessellatedbrepobject.py @@ -36,7 +36,7 @@ def _read_points_data(self): def _read_lines_data(self): positions = self.tessellatedbrep.vertices.tolist() elements = self.tessellatedbrep.edges.tolist() - colors = [Color(0.1, 0.1, 0.1)] * len(elements) + colors = [Color(0.1, 0.1, 0.1)] * len(positions) return positions, colors, elements def _read_frontfaces_data(self): diff --git a/src/compas_ifc/resources/brep.py b/src/compas_ifc/conversions/brep.py similarity index 60% rename from src/compas_ifc/resources/brep.py rename to src/compas_ifc/conversions/brep.py index ddd4298c..d2cc9da4 100644 --- a/src/compas_ifc/resources/brep.py +++ b/src/compas_ifc/conversions/brep.py @@ -1,12 +1,64 @@ -from typing import List +""" +This module contains functions for converting BREP geometry to IFC. + +These need to be re-implemented with new COMPAS infrastructure. +""" + +# def IfcProfileDef_to_curve(profile_def): +# from compas_occ.brep import OCCBrepFace + +# pd = profile_def + +# if pd.is_a("IfcParameterizedProfileDef"): +# if pd.is_a("IfcRectangleProfileDef"): +# frame = IfcAxis2Placement2D_to_frame(pd.Position) +# points = [ +# frame.point + frame.xaxis * +0.5 * pd.XDim + frame.yaxis * -0.5 * pd.YDim, +# frame.point + frame.xaxis * +0.5 * pd.XDim + frame.yaxis * +0.5 * pd.YDim, +# frame.point + frame.xaxis * -0.5 * pd.XDim + frame.yaxis * +0.5 * pd.YDim, +# frame.point + frame.xaxis * -0.5 * pd.XDim + frame.yaxis * -0.5 * pd.YDim, +# ] +# return OCCBrepFace.from_polygon(points) + +# else: +# raise NotImplementedError(pd) +# elif pd.is_a("IfcArbitraryClosedProfileDef"): +# return IfcCurve_to_face(pd.OuterCurve) +# elif pd.is_a("IfcCompositeProfileDef"): +# faces = [] +# for profile in pd.Profiles: +# if profile.is_a("IfcArbitraryClosedProfileDef"): +# faces.append(IfcCurve_to_face(profile.OuterCurve)) +# else: +# raise NotImplementedError(profile) +# return faces +# else: +# raise NotImplementedError(pd) + + +# def IfcCurve_to_face(curve): +# from compas_occ.brep import OCCBrepFace + +# if curve.is_a("IfcIndexedPolyCurve"): +# points = [(x, y, 0) for x, y in curve.Points[0]] +# return OCCBrepFace.from_polygon(points) +# elif curve.is_a("IfcPolyline"): +# points = [IfcCartesianPoint_to_point(pt) for pt in curve.Points] +# return OCCBrepFace.from_polygon(points) +# else: +# raise NotImplementedError(curve) -import ifcopenshell import numpy as np +from compas.geometry import Brep +from compas.geometry import Frame from compas.tolerance import TOL -from .primities import frame_to_ifc_plane -from .primities import occ_plane_to_frame -from .primities import point_to_ifc_cartesian_point +from compas_ifc.entities.base import Base +from compas_ifc.model import Model + +from .primitives import frame_to_IfcAxis2Placement3D +from .primitives import frame_to_IfcPlane +from .primitives import point_to_IfcCartesianPoint from .shapes import occ_cylinder_to_ifc_cylindrical_surface @@ -24,7 +76,7 @@ def calculate_knots_and_multiplicities(knot_sequence): return knots, multiplicities -def brep_to_ifc_advanced_brep(file: ifcopenshell.file, brep) -> List[ifcopenshell.entity_instance]: +def brep_to_IfcAdvancedBrep(model: Model, brep: Brep) -> Base: brep.fix() brep.sew() brep.make_solid() @@ -37,7 +89,7 @@ def get_ifc_point(point): key = TOL.geometric_key(point) if key in points: return points[key] - points[key] = point_to_ifc_cartesian_point(file, point) + points[key] = point_to_IfcCartesianPoint(model, point) return points[key] def get_ifc_line(edge): @@ -45,7 +97,7 @@ def get_ifc_line(edge): return lines.get(line_key) def get_ifc_curve(edge): - curve = edge.nurbscurve + curve = edge.curve for occ_curve in curves: if occ_curve.IsEqual(curve.occ_curve, 1e-6): return curves[occ_curve] @@ -57,13 +109,13 @@ def get_ifc_curve(edge): if get_ifc_curve(edge): continue - start_vertex = file.create_entity("IfcVertexPoint", get_ifc_point(edge.first_vertex.point)) - if edge.nurbscurve.is_closed: + start_vertex = model.create("IfcVertexPoint", VertexGeometry=get_ifc_point(edge.first_vertex.point)) + if edge.curve.is_closed: end_vertex = start_vertex else: - end_vertex = file.create_entity("IfcVertexPoint", get_ifc_point(edge.last_vertex.point)) + end_vertex = model.create("IfcVertexPoint", VertexGeometry=get_ifc_point(edge.last_vertex.point)) - curve = edge.nurbscurve + curve = edge.curve control_points = [get_ifc_point(point) for point in curve.points] weights = curve.weights @@ -75,7 +127,7 @@ def get_ifc_curve(edge): control_points.append(control_points[0]) weights.append(weights[0]) - IfcBSpline = file.create_entity( + IfcBSpline = model.create( "IfcRationalBSplineCurveWithKnots", Degree=curve.degree, ControlPointsList=control_points, @@ -87,7 +139,7 @@ def get_ifc_curve(edge): WeightsData=weights, ) - IfcEdgeCurve = file.create_entity( + IfcEdgeCurve = model.create( "IfcEdgeCurve", EdgeStart=start_vertex, EdgeEnd=end_vertex, @@ -103,15 +155,15 @@ def get_ifc_curve(edge): start_point = get_ifc_point(edge.first_vertex.point) end_point = get_ifc_point(edge.last_vertex.point) - start_vertex = file.create_entity("IfcVertexPoint", get_ifc_point(edge.first_vertex.point)) - end_vertex = file.create_entity("IfcVertexPoint", get_ifc_point(edge.last_vertex.point)) + start_vertex = model.create("IfcVertexPoint", VertexGeometry=start_point) + end_vertex = model.create("IfcVertexPoint", VertexGeometry=end_point) - IfcPolyLine = file.create_entity( + IfcPolyLine = model.create( "IfcPolyLine", Points=[start_point, end_point], ) - IfcEdgeCurve = file.create_entity( + IfcEdgeCurve = model.create( "IfcEdgeCurve", EdgeStart=start_vertex, EdgeEnd=end_vertex, @@ -122,6 +174,8 @@ def get_ifc_curve(edge): line_key = TOL.geometric_key(edge.first_vertex.point) + "-" + TOL.geometric_key(edge.last_vertex.point) lines[line_key] = IfcEdgeCurve + elif edge.is_circle: + pass else: raise NotImplementedError("Only BSpline and Line edges are supported") @@ -142,33 +196,41 @@ def get_ifc_curve(edge): elif edge.is_line: oriented = edge.occ_edge.Orientation() == 0 IfcEdgeCurve = get_ifc_line(edge) + elif edge.is_circle: + oriented = edge.occ_edge.Orientation() == 0 + circle = edge.curve + IfcCircle = model.create("IfcCircle", Position=frame_to_IfcAxis2Placement3D(model, circle.frame), Radius=circle.radius) + IfcEdgeCurve = IfcCircle else: raise NotImplementedError("Only BSpline and Line edges are supported") if not IfcEdgeCurve: raise ValueError("Edge not found") - ifc_oriented_edge = file.create_entity("IFCORIENTEDEDGE", EdgeElement=IfcEdgeCurve, Orientation=oriented) + ifc_oriented_edge = model.create("IfcOrientedEdge", EdgeElement=IfcEdgeCurve, Orientation=oriented) ifc_oriented_edges.append(ifc_oriented_edge) - edge_loop = file.create_entity("IfcEdgeLoop", ifc_oriented_edges) + edge_loop = model.create("IfcEdgeLoop", EdgeList=ifc_oriented_edges) if is_outer: - ifc_face_bound = file.create_entity("IfcFaceOuterBound", edge_loop, True) + ifc_face_bound = model.create("IfcFaceOuterBound", Bound=edge_loop, Orientation=True) is_outer = False else: - ifc_face_bound = file.create_entity("IfcFaceBound", edge_loop, True) + ifc_face_bound = model.create("IfcFaceBound", Bound=edge_loop, Orientation=False) face_bounds.append(ifc_face_bound) same_sense = face.orientation == 0 if face.is_plane: occ_plane = face.occ_adaptor.Plane() - frame = occ_plane_to_frame(occ_plane) - ifc_plane = frame_to_ifc_plane(file, frame) - IfcAdvancedFace = file.create_entity("IfcAdvancedFace", face_bounds, ifc_plane, SameSense=same_sense) + location = occ_plane.Location().Coord() + x_axis = occ_plane.XAxis().Direction().Coord() + y_axis = occ_plane.YAxis().Direction().Coord() + frame = Frame(location, x_axis, y_axis) + ifc_plane = frame_to_IfcPlane(model, frame) + IfcAdvancedFace = model.create("IfcAdvancedFace", Bounds=face_bounds, FaceSurface=ifc_plane, SameSense=same_sense) elif face.is_cylinder: cylinder = face.occ_adaptor.Cylinder() - IfcCylindricalSurface = occ_cylinder_to_ifc_cylindrical_surface(file, cylinder) - IfcAdvancedFace = file.create_entity("IfcAdvancedFace", face_bounds, IfcCylindricalSurface, SameSense=same_sense) + IfcCylindricalSurface = occ_cylinder_to_ifc_cylindrical_surface(model, cylinder) + IfcAdvancedFace = model.create("IfcAdvancedFace", Bounds=face_bounds, FaceSurface=IfcCylindricalSurface, SameSense=same_sense) else: control_points = np.array(face.nurbssurface.points.points, dtype=float) control_points = control_points.swapaxes(0, 1) @@ -210,7 +272,7 @@ def get_ifc_curve(edge): for i, row in enumerate(ifc_weights): row.append(row[0]) - IfcBSplineSurfaceWithKnots = file.create_entity( + IfcBSplineSurfaceWithKnots = model.create( "IfcRationalBSplineSurfaceWithKnots", UDegree=face.nurbssurface.degree_u, VDegree=face.nurbssurface.degree_v, @@ -226,13 +288,16 @@ def get_ifc_curve(edge): WeightsData=ifc_weights, ) - IfcAdvancedFace = file.create_entity("IfcAdvancedFace", face_bounds, IfcBSplineSurfaceWithKnots, SameSense=same_sense) + IfcAdvancedFace = model.create("IfcAdvancedFace", Bounds=face_bounds, FaceSurface=IfcBSplineSurfaceWithKnots, SameSense=same_sense) ifc_faces.append(IfcAdvancedFace) - ifc_shell = file.create_entity("IFCCLOSEDSHELL", ifc_faces) + ifc_shell = model.create("IfcClosedShell", CfsFaces=ifc_faces) - ifc_brep = file.create_entity("IFCADVANCEDBREP", ifc_shell) + ifc_brep = model.create("IfcAdvancedBrep", Outer=ifc_shell) ifc_breps.append(ifc_brep) + if len(ifc_breps) == 0: + print("WARNING: No BREPs found") + return ifc_breps diff --git a/src/compas_ifc/conversions/frame.py b/src/compas_ifc/conversions/frame.py new file mode 100644 index 00000000..c0f3341c --- /dev/null +++ b/src/compas_ifc/conversions/frame.py @@ -0,0 +1,135 @@ +from functools import reduce +from operator import mul + +from compas.geometry import Frame +from compas.geometry import Point +from compas.geometry import Transformation +from compas.geometry import Vector + +from compas_ifc.conversions.primitives import IfcCartesianPoint_to_point +from compas_ifc.conversions.primitives import IfcDirection_to_vector +from compas_ifc.entities.base import Base +from compas_ifc.model import Model + + +def create_IfcAxis2Placement3D(model: Model, point: Point = None, dir1: Vector = None, dir2: Vector = None) -> Base: + """ + Create an IFC Axis2Placement3D from a point, a direction and a second direction. + """ + point = model.create("IfcCartesianPoint", Coordinates=point or [0.0, 0.0, 0.0]) + dir1 = model.create("IfcDirection", DirectionRatios=dir1 or [0.0, 0.0, 1.0]) + dir2 = model.create("IfcDirection", DirectionRatios=dir2 or [1.0, 0.0, 0.0]) + axis2placement = model.create("IfcAxis2Placement3D", Location=point, Axis=dir1, RefDirection=dir2) + return axis2placement + + +def frame_to_ifc_axis2_placement_3d(model: Model, frame: Frame) -> Base: + return create_IfcAxis2Placement3D(model, point=frame.point, dir1=frame.zaxis, dir2=frame.xaxis) + + +def assign_entity_frame(entity: Base, frame: Frame): + local_placement = frame_to_ifc_axis2_placement_3d(entity.model, frame) + placement = entity.model.create("IfcLocalPlacement", RelativePlacement=local_placement) + entity.ObjectPlacement = placement + + +def IfcLocalPlacement_to_transformation(placement: Base, scale: float = 1) -> Transformation: + """ + Convert an IFC LocalPlacement [localplacement]_ to a COMPAS transformation. + This will resolve all relative placements into one transformation wrt the global coordinate system. + + """ + stack = [] + while True: + Location = placement.RelativePlacement.Location + Axis = placement.RelativePlacement.Axis + RefDirection = placement.RelativePlacement.RefDirection + + if Axis and RefDirection: + zaxis = Vector(*Axis.DirectionRatios) + xaxis = Vector(*RefDirection.DirectionRatios) + yaxis = zaxis.cross(xaxis) + xaxis = yaxis.cross(zaxis) + else: + xaxis = Vector.Xaxis() + yaxis = Vector.Yaxis() + + point = Point(*Location.Coordinates) * scale + frame = Frame(point, xaxis, yaxis) + stack.append(frame) + + if not placement.PlacementRelTo: + break + + placement = placement.PlacementRelTo + + matrices = [Transformation.from_frame(f) for f in stack] + return reduce(mul, matrices[::-1]) + + +def IfcLocalPlacement_to_frame(placement: Base) -> Frame: + """ + Convert an IFC LocalPlacement to a COMPAS frame. + """ + + Location = placement.RelativePlacement.Location + Axis = placement.RelativePlacement.Axis + RefDirection = placement.RelativePlacement.RefDirection + + if Axis and RefDirection: + zaxis = Vector(*Axis.DirectionRatios) + xaxis = Vector(*RefDirection.DirectionRatios) + yaxis = zaxis.cross(xaxis) + xaxis = yaxis.cross(zaxis) + else: + xaxis = Vector.Xaxis() + yaxis = Vector.Yaxis() + + point = Point(*Location.Coordinates) + return Frame(point, xaxis, yaxis) + + +def IfcGridPlacement_to_transformation(placement: Base) -> Transformation: + pass + + +def IfcAxis2Placement2D_to_frame(placement: Base) -> Frame: + """ + Convert an IFC Axis2Placement2D [axis2placement2d]_ to a COMPAS frame. + + An Axis2Placement2D is a 2D placement based on a frame defined by a point and 2 vectors. + + """ + # use the coordinate system of the representation context to replace missing axes + # use defaults if also those not available + point = IfcCartesianPoint_to_point(placement.Location) + zaxis = Vector.Zaxis() + if placement.RefDirection: + xaxis = IfcDirection_to_vector(placement.RefDirection) + else: + xaxis = Vector.Xaxis() + yaxis = zaxis.cross(xaxis) + return Frame(point, xaxis, yaxis) + + +def IfcAxis2Placement3D_to_frame(placement: Base) -> Frame: + """ + Convert an IFC Axis2Placement3D [axis2placement3d]_ to a COMPAS frame. + + An Axis2Placement3D is a 3D placement based on a frame defined by a point and 2 vectors. + + """ + # use the coordinate system of the representation context to replace missing axes + # use defaults if also those not available + point = IfcCartesianPoint_to_point(placement.Location) + if placement.Axis: + zaxis = IfcDirection_to_vector(placement.Axis) + else: + zaxis = Vector.Zaxis() + if placement.RefDirection: + xaxis = IfcDirection_to_vector(placement.RefDirection) + else: + xaxis = Vector.Xaxis() + yaxis = zaxis.cross(xaxis) + xaxis = yaxis.cross(zaxis) + return Frame(point, xaxis, yaxis) diff --git a/src/compas_ifc/conversions/geometries.py b/src/compas_ifc/conversions/geometries.py deleted file mode 100644 index 9512aa24..00000000 --- a/src/compas_ifc/conversions/geometries.py +++ /dev/null @@ -1,84 +0,0 @@ -from typing import TYPE_CHECKING - -from compas.geometry import Scale - -from compas_ifc.resources import IfcLocalPlacement_to_transformation # TODO: move this -from compas_ifc.resources import IfcShape_to_brep # TODO: move this -from compas_ifc.resources import IfcShape_to_tessellatedbrep # TODO: move this - -if TYPE_CHECKING: - from compas_ifc.entities.generated.IFC4 import IfcElement - - -def entity_transformation(entity: "IfcElement"): - """ - Construct the transformation of an entity. - """ - scale = entity.model.project.length_scale - - if entity.ObjectPlacement: - scaled_placement = IfcLocalPlacement_to_transformation(entity.ObjectPlacement, scale=scale) - else: - scaled_placement = Scale.from_factors([scale, scale, scale]) - - return scaled_placement - - -def entity_body_geometry(entity: "IfcElement", context="Model", use_occ=False, apply_transformation=True): - """ - Construct the body geometry representations of an entity. - """ - bodies = [] - - representation = None - if entity.Representation: - for temp in entity.Representation.Representations: - if temp.RepresentationIdentifier == "Body" and temp.ContextOfItems.ContextType == context: - representation = temp - break - - if not representation: - return bodies - - for item in representation.Items: - if use_occ: - brep = IfcShape_to_brep(item.entity) # TODO: improve this, should just give item - bodies.append(brep) - else: - tessellatedbrep = IfcShape_to_tessellatedbrep(item.entity) # TODO: improve this, should just give item - bodies.append(tessellatedbrep) - - if apply_transformation: - transformation = entity_transformation(entity) - for body in bodies: - body.transform(transformation) - - return bodies - - -def entity_opening_geometry(entity: "IfcElement", use_occ=False, apply_transformation=True): - """ - Construct the opening geometry representations of an entity. - """ - - voids = [] - for opening in entity.HasOpenings(): - element = opening.RelatedOpeningElement - - if apply_transformation: - transformation = entity_transformation(element) - - for representation in element.Representation.Representations: - for item in representation.Items: - if use_occ: - brep = IfcShape_to_brep(item.entity) # TODO: improve this, should just give item - if apply_transformation: - brep.transform(transformation) - voids.append(brep) - else: - tessellatedbrep = IfcShape_to_tessellatedbrep(item.entity) # TODO: improve this, should just give item - if apply_transformation: - tessellatedbrep.transform(transformation) - voids.append(tessellatedbrep) - - return voids diff --git a/src/compas_ifc/conversions/mesh.py b/src/compas_ifc/conversions/mesh.py new file mode 100644 index 00000000..b7aa67a5 --- /dev/null +++ b/src/compas_ifc/conversions/mesh.py @@ -0,0 +1,51 @@ +from compas.datastructures import Mesh + +from compas_ifc.entities.base import Base +from compas_ifc.model import Model + + +def mesh_to_IfcPolygonalFaceSet(model: Model, mesh: Mesh) -> Base: + """ + Convert a COMPAS mesh to an IFC PolygonalFaceSet. + """ + keys = sorted(mesh.vertices()) + vertices = [] + for key in keys: + coords = mesh.vertex_coordinates(key) + vertices.append((float(coords[0]), float(coords[1]), float(coords[2]))) + + faces = [] + for fkey in mesh.faces(): + indexes = [keys.index(i) + 1 for i in mesh.face_vertices(fkey)] + faces.append(model.create("IfcIndexedPolygonalFace", CoordIndex=indexes)) + + return model.create( + "IfcPolygonalFaceSet", + Closed=mesh.is_closed(), + Coordinates=model.create("IfcCartesianPointList3D", CoordList=vertices), + Faces=faces, + ) + + +def mesh_to_IfcFaceBasedSurfaceModel(model: Model, mesh: Mesh) -> Base: + """ + Convert a COMPAS mesh to an IFC FaceBasedSurfaceModel. + """ + vertices = {} + for key in mesh.vertices(): + coords = mesh.vertex_coordinates(key) + vertex = model.create("IfcCartesianPoint", Coordinates=(float(coords[0]), float(coords[1]), float(coords[2]))) + vertices[key] = vertex + + faces = [] + for fkey in mesh.faces(): + indexes = [vertices[key] for key in mesh.face_vertices(fkey)] + polyloop = model.create("IfcPolyLoop", Polygon=indexes) + bound = model.create("IfcFaceOuterBound", Bound=polyloop, Orientation=True) + face = model.create("IfcFace", Bounds=[bound]) + faces.append(face) + + face_set = model.create("IfcConnectedFaceSet", CfsFaces=faces) + ifc_face_based_surface_model = model.create("IfcFaceBasedSurfaceModel", FbsmFaces=[face_set]) + + return ifc_face_based_surface_model diff --git a/src/compas_ifc/conversions/primitives.py b/src/compas_ifc/conversions/primitives.py new file mode 100644 index 00000000..e175eaa0 --- /dev/null +++ b/src/compas_ifc/conversions/primitives.py @@ -0,0 +1,87 @@ +from compas.geometry import Frame +from compas.geometry import Line +from compas.geometry import Plane +from compas.geometry import Point +from compas.geometry import Vector + +from compas_ifc.entities.base import Base +from compas_ifc.model import Model + + +def IfcCartesianPoint_to_point(cartesian_point: Base) -> Point: + """ + Convert an IFC CartesianPoint [cartesianpoint]_ to a COMPAS point. + + """ + return Point(*cartesian_point.Coordinates) + + +def IfcDirection_to_vector(direction: Base) -> Vector: + """ + Convert an IFC Direction [direction]_ to a COMPAS vector. + + """ + return Vector(*direction.DirectionRatios) + + +def IfcVector_to_vector(vector: Base) -> Vector: + """ + Convert an IFC Vector [vector]_ to a COMPAS vector. + + """ + direction = IfcDirection_to_vector(vector.Orientation) + direction.scale(vector.Magnitude) + return direction + + +def IfcLine_to_line(line: Base) -> Line: + """ + Convert an IFC Line [line]_ to a COMPAS line. + + """ + point = IfcCartesianPoint_to_point(line.Pnt) + vector = IfcDirection_to_vector(line.Dir) + return Line(point, point + vector) + + +def IfcPlane_to_plane(plane: Base) -> Plane: + """ + Convert an IFC Plane [plane]_ to a COMPAS plane. + + """ + point = IfcCartesianPoint_to_point(plane.Position.Location) + normal = IfcDirection_to_vector(plane.Position.P[3]) + return Plane(point, normal) + + +def point_to_IfcCartesianPoint(model: Model, point: Point) -> Base: + """ + Convert a COMPAS point to an IFC CartesianPoint. + """ + return model.create("IfcCartesianPoint", Coordinates=(float(point.x), float(point.y), float(point.z))) + + +def vector_to_IfcDirection(model: Model, vector: Vector) -> Base: + """ + Convert a COMPAS vector to an IFC Direction. + """ + return model.create("IfcDirection", DirectionRatios=(float(vector.x), float(vector.y), float(vector.z))) + + +def frame_to_IfcAxis2Placement3D(model: Model, frame: Frame) -> Base: + """ + Convert a COMPAS frame to an IFC Axis2Placement3D. + """ + return model.create( + "IfcAxis2Placement3D", + Location=point_to_IfcCartesianPoint(model, frame.point), + Axis=vector_to_IfcDirection(model, frame.zaxis), + RefDirection=vector_to_IfcDirection(model, frame.xaxis), + ) + + +def frame_to_IfcPlane(model: Model, frame: Frame) -> Base: + """ + Convert a COMPAS frame to an IFC Plane. + """ + return model.create("IfcPlane", Position=frame_to_IfcAxis2Placement3D(model, frame)) diff --git a/src/compas_ifc/conversions/pset.py b/src/compas_ifc/conversions/pset.py new file mode 100644 index 00000000..cd441adc --- /dev/null +++ b/src/compas_ifc/conversions/pset.py @@ -0,0 +1,62 @@ +from ifcopenshell.util.element import get_psets + +from compas_ifc.entities.base import Base +from compas_ifc.model import Model + +PRIMARY_MEASURE_TYPES = { + str: "IfcLabel", + float: "IfcReal", + bool: "IfcBoolean", + int: "IfcInteger", +} + + +def from_dict_to_ifc_properties(model: Model, properties: dict) -> list[Base]: + """Convert a dictionary to a list of IfcProperties""" + + ifc_properties = [] + + for key, value in properties.items(): + if isinstance(value, dict): + subproperties = from_dict_to_ifc_properties(model, value) + ifc_property = model.create("IfcComplexProperty", Name=key, UsageName="{}", HasProperties=subproperties) + ifc_properties.append(ifc_property) + + elif isinstance(value, list): + subproperties = from_dict_to_ifc_properties(model, {str(k): v for k, v in enumerate(value)}) + ifc_property = model.create("IfcComplexProperty", Name=key, UsageName="[]", HasProperties=subproperties) + ifc_properties.append(ifc_property) + + elif isinstance(value, (str, float, bool, int)): + nominal_value = model.create_value(value) + ifc_property = model.create("IfcPropertySingleValue", Name=key, NominalValue=nominal_value) + ifc_properties.append(ifc_property) + else: + raise ValueError(f"Unsupported value type: {type(value)}") + + return ifc_properties + + +def from_dict_to_pset(model: Model, properties: dict, name: str = None) -> Base: + ifc_properties = from_dict_to_ifc_properties(model, properties) + pset = model.create("IfcPropertySet", Name=name, HasProperties=ifc_properties) + return pset + + +def from_psets_to_dict(element: Base) -> dict: + psets = get_psets(element.entity, psets_only=True) + + def _convert_property(property): + if isinstance(property, dict): + if property.get("UsageName", None) == "[]": + return [_convert_property(value) for value in property["properties"].values()] + elif property.get("UsageName", None) == "{}": + return {key: _convert_property(value) for key, value in property["properties"].items()} + else: + if "id" in property: + del property["id"] + return {key: _convert_property(value) for key, value in property.items()} + else: + return property + + return _convert_property(psets) diff --git a/src/compas_ifc/conversions/representation.py b/src/compas_ifc/conversions/representation.py new file mode 100644 index 00000000..c25d34dc --- /dev/null +++ b/src/compas_ifc/conversions/representation.py @@ -0,0 +1,127 @@ +""" +This module contains functions for converting geometry representations between COMPAS and IFC. +""" + +from typing import Union + +from compas.datastructures import Mesh +from compas.geometry import Box +from compas.geometry import Brep +from compas.geometry import Cone +from compas.geometry import Cylinder +from compas.geometry import Shape +from compas.geometry import Sphere + +from compas_ifc.conversions.brep import brep_to_IfcAdvancedBrep +from compas_ifc.conversions.mesh import mesh_to_IfcFaceBasedSurfaceModel +from compas_ifc.conversions.shapes import box_to_IfcBlock +from compas_ifc.conversions.shapes import cone_to_IfcRightCircularCone +from compas_ifc.conversions.shapes import cylinder_to_IfcRightCircularCylinder +from compas_ifc.conversions.shapes import sphere_to_IfcSphere +from compas_ifc.entities.extensions import IfcProduct +from compas_ifc.model import Model + +REPRESENTATION_CACHE = {} + + +def assign_body_representation(entity: IfcProduct, representation: Union[Shape, Mesh, Brep]): + """ + Assign a representation to an entity. + """ + + model: Model = entity.model + + if id(representation) in REPRESENTATION_CACHE: + entity.Representation = REPRESENTATION_CACHE[id(representation)] + return + + # Convert COMPAS geometries to IFC corresponding representation + if isinstance(representation, Shape): + if isinstance(representation, Box): + ifc_csg_primitive3d = box_to_IfcBlock(model, representation) + elif isinstance(representation, Sphere): + ifc_csg_primitive3d = sphere_to_IfcSphere(model, representation) + elif isinstance(representation, Cone): + ifc_csg_primitive3d = cone_to_IfcRightCircularCone(model, representation) + elif isinstance(representation, Cylinder): + ifc_csg_primitive3d = cylinder_to_IfcRightCircularCylinder(model, representation) + else: + raise NotImplementedError(f"Conversion of {type(representation)} to IFC not implemented.") + + ifc_csg_solid = model.create("IfcCsgSolid", TreeRootExpression=ifc_csg_primitive3d) + + items = [ifc_csg_solid] + representation_type = "CSG" + + elif isinstance(representation, Mesh): + ifc_representation = mesh_to_IfcFaceBasedSurfaceModel(model, representation) + representation_type = "SurfaceModel" + items = [ifc_representation] + + elif isinstance(representation, Brep): + if model.file.use_occ: + try: + items = brep_to_IfcAdvancedBrep(model, representation) + representation_type = "SolidModel" + except Exception as e: + print(f"WARNING BREP conversion failed: {e}") + items = [] + representation_type = "SurfaceModel" + + else: + mesh, _ = representation.to_tesselation() + ifc_representation = mesh_to_IfcFaceBasedSurfaceModel(model, mesh) + representation_type = "SurfaceModel" + items = [ifc_representation] + + else: + raise NotImplementedError(f"Conversion of {type(representation)} to IFC not implemented.") + + # QUESTION: When using OCCBrep from Extrusion, can we still keep the extrusion data? + + ifc_shape_representation = model.create( + "IfcShapeRepresentation", + ContextOfItems=model.file.default_body_context, + RepresentationIdentifier="Body", + RepresentationType=representation_type, + Items=items, + ) + + ifc_product_definition_shape = model.create( + "IfcProductDefinitionShape", + Representations=[ifc_shape_representation], + ) + + entity.Representation = ifc_product_definition_shape + REPRESENTATION_CACHE[id(representation)] = ifc_product_definition_shape + + # TODO: should not overwrite all property sets here + # TODO: alternative 1: restructure the metadata, remove duplicated info like vertices + # TODO: alternative 2: save data as compact json string + # entity.property_sets = { + # "Pset_COMPAS": { + # "representation_id": ifc_product_definition_shape.id(), + # "compas_data": json.loads(representation.to_jsonstring()), + # } + # } + + +def read_representation(model: Model, entity: IfcProduct): + pass + + +if __name__ == "__main__": + import compas + from compas.geometry import Frame + + model = Model.template(schema="IFC2X3", unit="m") + + # geometry = Box.from_width_height_depth(1, 1, 1) + geometry = Mesh.from_ply(compas.get("bunny.ply")) + # geometry = Mesh.from_meshgrid(5, 2, 5, 2) + + product = model.create(geometry=geometry, parent=model.building_storeys[0], name="test", frame=Frame.worldXY()) + + model.show() + + model.save("temp/representations/test.ifc") diff --git a/src/compas_ifc/conversions/shapes.py b/src/compas_ifc/conversions/shapes.py new file mode 100644 index 00000000..057b57d2 --- /dev/null +++ b/src/compas_ifc/conversions/shapes.py @@ -0,0 +1,91 @@ +import ifcopenshell +from compas.geometry import Box +from compas.geometry import Cone +from compas.geometry import Cylinder +from compas.geometry import Sphere + +from compas_ifc.conversions.frame import create_IfcAxis2Placement3D +from compas_ifc.entities.base import Base +from compas_ifc.model import Model + + +def create_IfcShapeRepresentation(file: ifcopenshell.file, item: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: + """ + Create an IFC Shape Representation from an IFC item and a context. + """ + return file.create_entity( + "IfcShapeRepresentation", + ContextOfItems=context, + RepresentationIdentifier="Body", + RepresentationType="SolidModel", + Items=[item], + ) + + +def box_to_IfcBlock(model: Model, box: Box) -> Base: + """ + Convert a COMPAS box to an IFC Block. + """ + pt = box.frame.point.copy() + pt -= [box.xsize / 2, box.ysize / 2, box.zsize / 2] + print(pt) + return model.create( + "IfcBlock", + Position=create_IfcAxis2Placement3D(model, pt, box.frame.zaxis, box.frame.xaxis), + XLength=box.xsize, + YLength=box.ysize, + ZLength=box.zsize, + ) + + +def sphere_to_IfcSphere(model: Model, sphere: Sphere) -> ifcopenshell.entity_instance: + """ + Convert a COMPAS sphere to an IFC Sphere. + """ + return model.create( + "IfcSphere", + Position=create_IfcAxis2Placement3D(model, sphere.base), + Radius=sphere.radius, + ) + + +def cone_to_IfcRightCircularCone(model: Model, cone: Cone) -> ifcopenshell.entity_instance: + """ + Convert a COMPAS cone to an IFC Cone. + """ + plane = cone.circle.plane + return model.create( + "IfcRightCircularCone", + Position=create_IfcAxis2Placement3D(model, plane.point, plane.normal), + Height=cone.height, + BottomRadius=cone.circle.radius, + ) + + +def cylinder_to_IfcRightCircularCylinder(model: Model, cylinder: Cylinder) -> ifcopenshell.entity_instance: + """ + Convert a COMPAS cylinder to an IFC Cylinder. + """ + plane = cylinder.circle.plane + return model.create( + "IfcRightCircularCylinder", + Position=create_IfcAxis2Placement3D(model, plane.point, plane.normal), + Height=cylinder.height, + Radius=cylinder.circle.radius, + ) + + +def occ_cylinder_to_ifc_cylindrical_surface(model: Model, occ_cylinder): + location = occ_cylinder.Location().Coord() + xdir = occ_cylinder.XAxis().Direction().Coord() + zdir = occ_cylinder.Axis().Direction().Coord() + IfcAxis2Placement3D = create_IfcAxis2Placement3D(model, location, zdir, xdir) + return model.create("IfcCylindricalSurface", Position=IfcAxis2Placement3D, Radius=occ_cylinder.Radius()) + + +if __name__ == "__main__": + model = Model() + print(create_IfcAxis2Placement3D(model)) + + box = Box(10, 10, 10) + box_to_IfcBlock(model, box).print_attributes(max_depth=5) diff --git a/src/compas_ifc/conversions/unit.py b/src/compas_ifc/conversions/unit.py new file mode 100644 index 00000000..b938463c --- /dev/null +++ b/src/compas_ifc/conversions/unit.py @@ -0,0 +1,25 @@ +def IfcCompoundPlaneAngleMeasure_to_degrees(angle_components): + if len(angle_components) != 4: + raise ValueError("Input must be a list or tuple of four elements: [degrees, minutes, seconds, millionths_of_second]") + + degrees, minutes, seconds, millionths = angle_components + + # Determine the sign based on the degrees component + sign = -1 if degrees < 0 else 1 + + # Use absolute values to avoid negative components in calculation + degrees = abs(degrees) + minutes = abs(minutes) + seconds = abs(seconds) + millionths = abs(millionths) + + # Convert millionths of a second to fractional seconds + fractional_seconds = millionths / 1_000_000 + + # Total seconds including the fractional part + total_seconds = seconds + fractional_seconds + + # Convert everything to decimal degrees + decimal_degrees = sign * (degrees + (minutes / 60) + (total_seconds / 3600)) + + return decimal_degrees diff --git a/src/compas_ifc/entities/base.py b/src/compas_ifc/entities/base.py index 3762c87b..a253a368 100644 --- a/src/compas_ifc/entities/base.py +++ b/src/compas_ifc/entities/base.py @@ -1,5 +1,6 @@ import importlib from typing import TYPE_CHECKING +from typing import Union from compas.data import Data from compas.datastructures import Tree @@ -305,7 +306,7 @@ def add_property(item, parent_node): tree = Tree() root = EntityNode(name=f"{self}") tree.add(root) - add_property(self.properties, root) + add_property(self.property_sets, root) print("=" * 80 + "\n" + f"Properties of {self}\n" + "=" * 80) print(tree.get_hierarchy_string(max_depth=max_depth)) @@ -314,6 +315,54 @@ def add_property(item, parent_node): def show(self): self.model.show(self) + def validate(self, schema): + raise NotImplementedError + + def validate_geometry(self, schema): + raise NotImplementedError + + def validate_relationships(self, schema): + raise NotImplementedError + + def validate_properties(self, schema: Union[str, dict], verbose: bool = True) -> bool: + """Validate the properties of the entity against a schema. + + Parameters + ---------- + schema : Union[str, dict] + The schema to validate against. Either a path to a JSON file or a dictionary for schemas for each property set. + verbose : bool, optional + Whether to print the validation results. + + Returns + ------- + bool + True if the validation passes, False otherwise. + """ + + from compas import json_load + from jsonschema import Draft202012Validator + + if isinstance(schema, str): + validator = Draft202012Validator(json_load(schema)) # type: ignore + validator.validate(self.property_sets) + + if verbose: + print(f"Schema validation passed on {self}.property_sets") + + elif isinstance(schema, dict): + for pset_name, path in schema.items(): + validator = Draft202012Validator(json_load(path)) + validator.validate(self.property_sets[pset_name]) + + if verbose: + print(f"Schema validation passed on {self}.property_sets[{pset_name}]") + + else: + raise TypeError("Invalid schema type, expected str or dict") + + return True + class EntityNode(TreeNode): def __repr__(self): diff --git a/src/compas_ifc/entities/extensions/IfcObject.py b/src/compas_ifc/entities/extensions/IfcObject.py index 4c13db33..11e725f4 100644 --- a/src/compas_ifc/entities/extensions/IfcObject.py +++ b/src/compas_ifc/entities/extensions/IfcObject.py @@ -1,9 +1,10 @@ from typing import TYPE_CHECKING -import ifcopenshell.guid -from ifcopenshell.api import run from ifcopenshell.util.element import get_psets +from compas_ifc.conversions.pset import from_dict_to_pset +from compas_ifc.conversions.pset import from_psets_to_dict + if TYPE_CHECKING: from compas_ifc.entities.generated.IFC4 import IfcObject else: @@ -24,42 +25,34 @@ class IfcObject(IfcObject): _psetsmap = {} @property - def properties(self): - psets = get_psets(self.entity, psets_only=True) - for pset in psets.values(): - del pset["id"] - return psets - - @properties.setter - def properties(self, psets): + def psetsmap(self): if id(self.file) not in self._psetsmap: self._psetsmap[id(self.file)] = {} + return self._psetsmap[id(self.file)] - psetsmap = self._psetsmap[id(self.file)] - - for name, properties in psets.items(): - if id(properties) in psetsmap: - pset = psetsmap[id(properties)] - # TODO: Check if relation already exists - self.file._create_entity( - "IfcRelDefinesByProperties", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=self.file.default_owner_history, - RelatingPropertyDefinition=pset, - RelatedObjects=[self.entity], - ) - + @property + def property_sets(self): + return from_psets_to_dict(self) + + @property_sets.setter + def property_sets(self, psets): + for name, pset in psets.items(): + if id(pset) in self.psetsmap: + ifc_property_set = self.psetsmap[id(pset)] else: - pset = run("pset.add_pset", self.file._file, product=self.entity, name=name) - psetsmap[id(properties)] = pset - for key, value in properties.items(): - if not isinstance(value, (str, int, float)): - properties[key] = str(value) - run("pset.edit_pset", self.file._file, pset=pset, properties=properties) + ifc_property_set = from_dict_to_pset(self.file, pset, name) + self.psetsmap[id(pset)] = ifc_property_set # TODO: remove unused psets + self.file.create( + "IfcRelDefinesByProperties", + OwnerHistory=self.file.default_owner_history, + RelatingPropertyDefinition=ifc_property_set, + RelatedObjects=[self], + ) + @property - def quantities(self): + def quantity_sets(self): qtos = get_psets(self.entity, qtos_only=True) for qto in qtos.values(): del qto["id"] diff --git a/src/compas_ifc/entities/extensions/IfcProduct.py b/src/compas_ifc/entities/extensions/IfcProduct.py index 4edae65a..101a754e 100644 --- a/src/compas_ifc/entities/extensions/IfcProduct.py +++ b/src/compas_ifc/entities/extensions/IfcProduct.py @@ -1,8 +1,8 @@ from typing import TYPE_CHECKING -from compas_ifc.resources import IfcLocalPlacement_to_frame -from compas_ifc.resources.representation import write_body_representation -from compas_ifc.resources.shapes import frame_to_ifc_axis2_placement_3d +from compas_ifc.conversions.frame import IfcLocalPlacement_to_frame +from compas_ifc.conversions.frame import assign_entity_frame +from compas_ifc.conversions.representation import assign_body_representation if TYPE_CHECKING: from compas_ifc.entities.generated.IFC4 import IfcProduct @@ -44,10 +44,8 @@ def geometry(self): @geometry.setter def geometry(self, geometry): self._geometry = geometry - # Update the representation in the IFC file + assign_body_representation(self, geometry) # TODO: delete existing representation - # TODO: make this function more transparent - write_body_representation(self.file._file, geometry, self.entity, self.file.default_body_context.entity) @property def frame(self): @@ -63,9 +61,5 @@ def frame(self): @frame.setter def frame(self, frame): self._frame = frame - # Update the placement in the IFC file # TODO: consider parent frame - # TODO: make this function more transparent - loacal_placement = frame_to_ifc_axis2_placement_3d(self.file._file, frame) - placement = self.file._create_entity("IfcLocalPlacement", RelativePlacement=loacal_placement) - self.ObjectPlacement = placement + assign_entity_frame(self, frame) diff --git a/src/compas_ifc/entities/extensions/IfcProject.py b/src/compas_ifc/entities/extensions/IfcProject.py index f0966795..270daa46 100644 --- a/src/compas_ifc/entities/extensions/IfcProject.py +++ b/src/compas_ifc/entities/extensions/IfcProject.py @@ -53,7 +53,7 @@ def geographic_elements(self): def contexts(self): from compas.geometry import Vector - from compas_ifc.resources import IfcAxis2Placement3D_to_frame + from compas_ifc.conversions.frame import IfcAxis2Placement3D_to_frame contexts = [] diff --git a/src/compas_ifc/entities/extensions/IfcSite.py b/src/compas_ifc/entities/extensions/IfcSite.py index 50b7cb55..bdfc8726 100644 --- a/src/compas_ifc/entities/extensions/IfcSite.py +++ b/src/compas_ifc/entities/extensions/IfcSite.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from compas_ifc.resources import IfcCompoundPlaneAngleMeasure_to_degrees +from compas_ifc.conversions.unit import IfcCompoundPlaneAngleMeasure_to_degrees if TYPE_CHECKING: from compas_ifc.entities.generated.IFC4 import IfcSite diff --git a/src/compas_ifc/file.py b/src/compas_ifc/file.py index 1c4e767a..761703de 100644 --- a/src/compas_ifc/file.py +++ b/src/compas_ifc/file.py @@ -99,6 +99,8 @@ def __init__( self.use_occ = use_occ if filepath is None: self._file = ifcopenshell.file(schema=schema) + self._file.wrapped_data.header.file_name.author = ["Unknown Author"] + self._file.wrapped_data.header.file_name.organization = ["Unknown Organization"] if self.verbose: print("IFC file created in schema: {}".format(schema)) else: @@ -412,8 +414,8 @@ def export_entity(entity: Union[Base, Any], file: IFCFile): setattr(new_entity, key, attr) - if export_properties and hasattr(entity, "properties"): - new_entity.properties = entity.properties + if export_properties and hasattr(entity, "property_sets"): + new_entity.property_sets = entity.property_sets if export_materials and hasattr(entity, "HasAssociations"): # TODO: create material settor on class extension. @@ -435,7 +437,7 @@ def export_entity(entity: Union[Base, Any], file: IFCFile): new_file.save(path) - def create(self, cls="IfcBuildingElementProxy", parent=None, geometry=None, frame=None, properties=None, **kwargs) -> Base: + def create(self, cls=None, parent=None, geometry=None, frame=None, properties=None, **kwargs) -> Base: """ Create an entity in this model. @@ -465,6 +467,9 @@ def create(self, cls="IfcBuildingElementProxy", parent=None, geometry=None, fram else: cls_name = cls + if cls_name is None: + cls_name = "IfcBuildingElementProxy" + if cls_name not in self.classes: matched_classes = self.search_ifc_classes(cls_name) if not matched_classes: @@ -475,10 +480,7 @@ def create(self, cls="IfcBuildingElementProxy", parent=None, geometry=None, fram entity = self._create_entity(cls_name, **kwargs) if parent: - if hasattr(entity, "ContainedInStructure"): - self._create_entity("IfcRelContainedInSpatialStructure", RelatingStructure=parent, RelatedElements=[entity]) - else: - self._create_entity("IfcRelAggregates", RelatingObject=parent, RelatedObjects=[entity]) + self.create_relationship(parent, entity) if geometry: # TODO: Deal with instancing @@ -488,13 +490,54 @@ def create(self, cls="IfcBuildingElementProxy", parent=None, geometry=None, fram entity.frame = frame if properties: - entity.properties = properties + entity.property_sets = properties if entity.is_a("IfcRoot"): entity.GlobalId = ifcopenshell.guid.new() return entity + def create_relationship(self, parent: Base, child: Base) -> Base: + """ + Create the correct relationship between two entities based on their types. + + Parameters + ---------- + parent : :class:`compas_ifc.entities.base.Base` + The parent entity. + child : :class:`compas_ifc.entities.base.Base` + The child entity. + + Returns + ------- + :class:`compas_ifc.entities.base.Base` + The created relationship. + + """ + + guid = ifcopenshell.guid.new() + if parent.is_a("IfcSpatialStructureElement"): + if child.is_a("IfcSpatialStructureElement"): + return self._create_entity("IfcRelAggregates", GlobalId=guid, RelatingObject=parent, RelatedObjects=[child]) + return self._create_entity("IfcRelContainedInSpatialStructure", GlobalId=guid, RelatingStructure=parent, RelatedElements=[child]) + + if parent.is_a("IfcElementAssembly") or child.is_a("IfcBuildingElementPart"): + return self._create_entity("IfcRelAggregates", GlobalId=guid, RelatingObject=parent, RelatedObjects=[child]) + + if parent.is_a("IfcPort"): + if not child.is_a("IfcPort"): + return self._create_entity("IfcRelConnectsPortToElement", GlobalId=guid, RelatingPort=parent, RelatedElement=child) + return self._create_entity("IfcRelConnectsPorts", GlobalId=guid, RelatingPort=parent, RelatedPort=child) + + if parent.is_a("IfcElement") and child.is_a("IfcElement"): + return self._create_entity("IfcRelConnectsElements", GlobalId=guid, RelatingElement=parent, RelatedElement=child) + + if parent.is_a("IfcGroup"): + return self._create_entity("IfcRelAssignsToGroup", GlobalId=guid, RelatingGroup=parent, RelatedObjects=[child]) + + # Default case + return self._create_entity("IfcRelAggregates", GlobalId=guid, RelatingObject=parent, RelatedObjects=[child]) + def search_ifc_classes(self, name: str, n: int = 5) -> list[Type["Base"]]: """ Search for IFC classes by name @@ -557,13 +600,29 @@ def _create_entity(self, cls_name, **kwargs) -> Base: entity = self._file.create_entity(cls_name, **camel_case_kwargs) return self.from_entity(entity) + def create_value(self, value): + """ + Create corresponding IfcValue from a Python value. + """ + + PRIMARY_MEASURE_TYPES = { + str: "IfcLabel", + float: "IfcReal", + bool: "IfcBoolean", + int: "IfcInteger", + } + + primary_measure_type = PRIMARY_MEASURE_TYPES[type(value)] + ifc_value = self._file.create_entity(primary_measure_type, value) + return ifc_value + @property def default_project(self) -> Base: projects = self._file.by_type("IfcProject") if projects: self._default_project = self.from_entity(projects[0]) else: - self._default_project = self._create_entity("IfcProject", Name="Default Project") + self._default_project = self._create_entity("IfcProject", GlobalId=ifcopenshell.guid.new(), Name="Default Project") self.default_units self.default_body_context self.default_owner_history @@ -577,7 +636,13 @@ def default_units(self) -> Base: if self.default_project.UnitsInContext: self._default_units = self.default_project.UnitsInContext else: - self._default_units = self.from_entity(run("unit.assign_unit", self._file)) + length_unit = self.create("IfcUnit", UnitType="LENGTHUNIT", Prefix="MILLI", Name="METRE") + area_unit = self.create("IfcUnit", UnitType="AREAUNIT", Prefix="MILLI", Name="SQUARE_METRE") + volume_unit = self.create("IfcUnit", UnitType="VOLUMEUNIT", Prefix="MILLI", Name="CUBIC_METRE") + plane_angle_unit = self.create("IfcUnit", UnitType="PLANEANGLEUNIT", Name="RADIAN") + unit_assignment = self.create("IfcUnitAssignment", Units=[length_unit, area_unit, volume_unit, plane_angle_unit]) + self.default_project.UnitsInContext = unit_assignment + self._default_units = unit_assignment return self._default_units @property @@ -618,7 +683,7 @@ def default_body_context(self) -> Base: def default_owner_history(self) -> Base: # We will create a new owner history since we are updating the file if not self._default_owner_history: - person = self._create_entity("IfcPerson") + person = self._create_entity("IfcPerson", FamilyName="Unknown Author") organization = self._create_entity("IfcOrganization", Name="compas.dev") person_and_org = self._create_entity("IfcPersonAndOrganization", ThePerson=person, TheOrganization=organization) application = self._create_entity( @@ -633,7 +698,6 @@ def default_owner_history(self) -> Base: "IfcOwnerHistory", OwningUser=person_and_org, OwningApplication=application, - ChangeAction="ADDED", CreationDate=int(time.time()), ) diff --git a/src/compas_ifc/model.py b/src/compas_ifc/model.py index f04da057..ff0b7f4e 100644 --- a/src/compas_ifc/model.py +++ b/src/compas_ifc/model.py @@ -9,7 +9,6 @@ from compas.geometry import Frame from compas.geometry import Geometry from compas.geometry import Transformation -from compas.tolerance import TOL from compas_ifc.file import IFCFile @@ -25,7 +24,8 @@ class Model(Data): - """The Model class is the COMPAS IFC's main entry point. It is a intuitive abstraction on top of an IFC file, providing a set of easy-to-use methods for interacting with the IFC data. + """The Model class is the COMPAS IFC's main entry point. + It is a intuitive abstraction on top of an IFC file, providing a set of easy-to-use methods for interacting with the IFC data. Attributes ---------- @@ -73,8 +73,6 @@ def __init__(self, filepath: str = None, schema: str = "IFC4", use_occ: bool = F """ self.file = IFCFile(self, filepath=filepath, schema=schema, use_occ=use_occ, load_geometries=load_geometries, verbose=verbose, extensions=extensions) - if filepath: - self.update_linear_deflection() @property def schema(self) -> "ifcopenshell.ifcopenshell_wrapper.schema_definition": @@ -256,13 +254,16 @@ def export(self, path: str, entities: list["Base"] = [], as_snippet: bool = Fals """ self.file.export(path, entities=entities, as_snippet=as_snippet, export_materials=export_materials, export_properties=export_properties, export_styles=export_styles) - def show(self, entity: "Base" = None): + def show(self, entity: "Base" = None, linear_deflection: float = 100): """Show the IFC file in a viewer, either the entire project or a single entity. Parameters ---------- entity : :class:`compas_ifc.entities.base.Base` The entity to show. If None, the entire project will be shown. + linear_deflection : float + The linear deflection to use for the tesselation of BREP geometries. + Should be adjusted based on the size of the model. """ try: from compas_viewer import Viewer @@ -273,6 +274,7 @@ def show(self, entity: "Base" = None): viewer = Viewer() print(f"Unit: {self.unit}") + print(f"Using Linear Deflection: {linear_deflection}") viewer.unit = self.unit viewer.ui.sidebar.show_objectsetting = False @@ -283,10 +285,10 @@ def parse_entity(entity, parent=None): name = f"[{entity.__class__.__name__}]{entity.Name}" transformation = Transformation.from_frame(entity.frame) if entity.frame else None if getattr(entity, "geometry", None) and not entity.is_a("IfcSpace"): - obj = viewer.scene.add(entity.geometry, name=name, parent=parent, **entity.style) + obj = viewer.scene.add(entity.geometry, name=name, parent=parent, hide_coplanaredges=True, **entity.style, linear_deflection=linear_deflection) obj.transformation = transformation else: - obj = viewer.scene.add([], name=name, parent=parent) + obj = viewer.scene.add_group(name=name, parent=parent) obj.attributes["entity"] = entity @@ -299,13 +301,13 @@ def parse_entity(entity, parent=None): parse_entity(entity or self.project) treeform = Treeform() - viewer.ui.sidebar.widget.addWidget(treeform) + viewer.ui.sidebar.add(treeform) def update_treeform(form, node): entity = node.attributes["entity"] - treeform.update_from_dict({"Attributes": entity.attributes, "Properties": getattr(entity, "properties", {})}) + treeform.update_from_dict({"Attributes": entity.attributes, "PSets": getattr(entity, "property_sets", {})}) - viewer.ui.sidebar.sceneform.callback = update_treeform + viewer.ui.sidebar.sceneform.action = update_treeform viewer.show() @@ -338,6 +340,9 @@ def create( """ return self.file.create(cls=cls, parent=parent, geometry=geometry, frame=frame, properties=properties, **kwargs) + def create_value(self, value): + return self.file.create_value(value) + def create_default_project(self) -> "IfcProject": """Create a default project in this model if one does not exist.""" return self.file.default_project @@ -354,18 +359,8 @@ def remove(self, entity: Union["Base", list["Base"]]): """ self.file.remove(entity) - def update_linear_deflection(self): - """Update the linear deflection tolerance settings in COMPAS based on the unit of the model.""" - # TODO: deal with conversion based units like "FOOT" - if self.unit == "mm": - TOL.lineardeflection = 1 - elif self.unit == "cm": - TOL.lineardeflection = 1e-1 - elif self.unit == "m": - TOL.lineardeflection = 1e-3 - @classmethod - def template(cls, schema: str = "IFC4", building_count: int = 1, storey_count: int = 1, unit: str = "mm") -> "Model": + def template(cls, schema: str = "IFC4", building_count: int = 1, storey_count: int = 1, unit: str = "mm", use_occ: bool = False) -> "Model": """Create a template model with a default project, site, building, and storey. Parameters @@ -384,7 +379,7 @@ def template(cls, schema: str = "IFC4", building_count: int = 1, storey_count: i :class:`compas_ifc.model.Model` The newly created model. """ - model = cls(schema=schema) + model = cls(schema=schema, use_occ=use_occ) project = model.file.default_project site = model.create("IfcSite", parent=project, Name="Default Site") for i in range(building_count): diff --git a/src/compas_ifc/resources/__init__.py b/src/compas_ifc/resources/__init__.py deleted file mode 100644 index 128e46f7..00000000 --- a/src/compas_ifc/resources/__init__.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -******************************************************************************** -resources -******************************************************************************** - -.. currentmodule:: compas_ifc.resources - -This module contains functions for converting between IFC geometry resources and equivalent COMPAS geometry objects. -For more information about IFC geometric entities see [geometricmodelresource]_, [geometryresource]_ and [geometricconstraintresource]_. - - -Functions -========= - -Geometry --------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - IfcCartesianPoint_to_point - IfcDirection_to_vector - IfcVector_to_vector - IfcLine_to_line - IfcPlane_to_plane - IfcAxis2Placement3D_to_frame - IfcCartesianTransformationOperator3D_to_frame - - -Geometric Constraint --------------------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - IfcGridPlacement_to_transformation - IfcLocalPlacement_to_transformation - - -Geometric Model ---------------- - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - IfcAdvancedBrep_to_brep - IfcAdvancedBrepWithVoids_to_brep - IfcBlock_to_box - IfcBooleanClippingResult_to_brep - IfcBoundingBox_to_box - IfcExtrudedAreaSolid_to_brep - IfcFacetedBrep_to_brep - IfcFacetedBrepWithVoids_to_brep - IfcIndexedPolygonalFaceSet_to_brep - IfcPolygonalBoundedHalfSpace_to_brep - IfcPolygonalFaceSet_to_brep - IfcTriangulatedFaceSet_to_brep - - -References -========== - -.. [geometricmodelresource] :ifc:`ifcgeometricmodelresource` -.. [geometryresource] :ifc:`ifcgeometryresource` -.. [geometricconstraintresource] :ifc:`ifcgeometricconstraintresource` - -.. [cartesianpoint] :ifc:`ifccartesianpoint` -.. [direction] :ifc:`ifcdirection` -.. [vector] :ifc:`ifcvector` -.. [line] :ifc:`ifcline` -.. [plane] :ifc:`ifcplane` -.. [axis2placement3d] :ifc:`ifcaxis2placement3d` -.. [cartesiantransformationoperator3d] :ifc:`ifccartesiantransformationoperator3d` - -.. [gridplacement] :ifc:`ifcgridplacement` -.. [localplacement] :ifc:`ifclocalplacement` - -.. [advancedbrep] :ifc:`ifcadvancedbrep` -.. [advancedbrepwithvoids] :ifc:`ifcadvancedbrepwithvoids` -.. [boundingbox] :ifc:`ifcboundingbox` - -""" - -from .geometry import IfcCartesianPoint_to_point # noqa: F401 -from .geometry import IfcDirection_to_vector # noqa: F401 -from .geometry import IfcVector_to_vector # noqa: F401 -from .geometry import IfcLine_to_line # noqa: F401 -from .geometry import IfcPlane_to_plane # noqa: F401 -from .geometry import IfcAxis2Placement3D_to_frame # noqa: F401 -from .geometry import IfcCartesianTransformationOperator3D_to_frame # noqa: F401 -from .geometry import IfcCompoundPlaneAngleMeasure_to_degrees # noqa: F401 - -from .geometricconstraint import IfcGridPlacement_to_transformation # noqa: F401 -from .geometricconstraint import IfcLocalPlacement_to_transformation # noqa: F401 -from .geometricconstraint import IfcLocalPlacement_to_frame # noqa: F401 - -from .geometricmodel import IfcAdvancedBrep_to_brep # noqa: F401 -from .geometricmodel import IfcAdvancedBrepWithVoids_to_brep # noqa: F401 -from .geometricmodel import IfcBlock_to_box # noqa: F401 -from .geometricmodel import IfcBooleanClippingResult_to_brep # noqa: F401 -from .geometricmodel import IfcBoundingBox_to_box # noqa: F401 -from .geometricmodel import IfcIndexedPolyCurve_to_lines # noqa: F401 -from .geometricmodel import IfcExtrudedAreaSolid_to_brep # noqa: F401 -from .geometricmodel import IfcFacetedBrep_to_brep # noqa: F401 -from .geometricmodel import IfcFacetedBrepWithVoids_to_brep # noqa: F401 -from .geometricmodel import IfcIndexedPolygonalFaceSet_to_brep # noqa: F401 -from .geometricmodel import IfcPolygonalBoundedHalfSpace_to_brep # noqa: F401 -from .geometricmodel import IfcPolygonalFaceSet_to_brep # noqa: F401 -from .geometricmodel import IfcTessellatedFaceSet_to_brep # noqa: F401 -from .geometricmodel import IfcTriangulatedFaceSet_to_brep # noqa: F401 -from .geometricmodel import IfcShape_to_brep # noqa: F401 -from .geometricmodel import IfcShape_to_tessellatedbrep # noqa: F401 - -from .shapes import frame_to_ifc_axis2_placement_3d # noqa: F401 - -# from .geometricmodel import IfcBoxedHalfSpace -# from .geometricmodel import IfcCartesianPointList diff --git a/src/compas_ifc/resources/geometricconstraint.py b/src/compas_ifc/resources/geometricconstraint.py deleted file mode 100644 index e3a927dc..00000000 --- a/src/compas_ifc/resources/geometricconstraint.py +++ /dev/null @@ -1,67 +0,0 @@ -from functools import reduce -from operator import mul - -from compas.geometry import Frame -from compas.geometry import Point -from compas.geometry import Transformation -from compas.geometry import Vector - - -def IfcLocalPlacement_to_transformation(placement, scale=1) -> Transformation: - """ - Convert an IFC LocalPlacement [localplacement]_ to a COMPAS transformation. - This will resolve all relative placements into one transformation wrt the global coordinate system. - - """ - stack = [] - while True: - Location = placement.RelativePlacement.Location - Axis = placement.RelativePlacement.Axis - RefDirection = placement.RelativePlacement.RefDirection - - if Axis and RefDirection: - zaxis = Vector(*Axis.DirectionRatios) - xaxis = Vector(*RefDirection.DirectionRatios) - yaxis = zaxis.cross(xaxis) - xaxis = yaxis.cross(zaxis) - else: - xaxis = Vector.Xaxis() - yaxis = Vector.Yaxis() - - point = Point(*Location.Coordinates) * scale - frame = Frame(point, xaxis, yaxis) - stack.append(frame) - - if not placement.PlacementRelTo: - break - - placement = placement.PlacementRelTo - - matrices = [Transformation.from_frame(f) for f in stack] - return reduce(mul, matrices[::-1]) - - -def IfcLocalPlacement_to_frame(placement) -> Frame: - """ - Convert an IFC LocalPlacement to a COMPAS frame. - """ - - Location = placement.RelativePlacement.Location - Axis = placement.RelativePlacement.Axis - RefDirection = placement.RelativePlacement.RefDirection - - if Axis and RefDirection: - zaxis = Vector(*Axis.DirectionRatios) - xaxis = Vector(*RefDirection.DirectionRatios) - yaxis = zaxis.cross(xaxis) - xaxis = yaxis.cross(zaxis) - else: - xaxis = Vector.Xaxis() - yaxis = Vector.Yaxis() - - point = Point(*Location.Coordinates) - return Frame(point, xaxis, yaxis) - - -def IfcGridPlacement_to_transformation(placement) -> Transformation: - pass diff --git a/src/compas_ifc/resources/geometry.py b/src/compas_ifc/resources/geometry.py deleted file mode 100644 index 3e2081f5..00000000 --- a/src/compas_ifc/resources/geometry.py +++ /dev/null @@ -1,198 +0,0 @@ -# from compas_occ.geometry import OCCNurbsCurve -from compas.geometry import Frame -from compas.geometry import Line -from compas.geometry import Plane -from compas.geometry import Point -from compas.geometry import Vector - -# from compas.geometry import Transformation - - -def IfcCartesianPoint_to_point(cartesian_point) -> Point: - """ - Convert an IFC CartesianPoint [cartesianpoint]_ to a COMPAS point. - - Attributes - ---------- - cartesian_point : :class:`compas_ifc.entities.generated.IFC4.IfcCartesianPoint` or :class:`ifcopenshell.entity_instance` - The IFC CartesianPoint. - - Returns - ------- - :class:`compas.geometry.Point` - The COMPAS point. - - """ - return Point(*cartesian_point.Coordinates) - - -def IfcDirection_to_vector(direction) -> Vector: - """ - Convert an IFC Direction [direction]_ to a COMPAS vector. - - """ - return Vector(*direction.DirectionRatios) - - -def IfcVector_to_vector(vector) -> Vector: - """ - Convert an IFC Vector [vector]_ to a COMPAS vector. - - """ - direction = IfcDirection_to_vector(vector.Orientation) - direction.scale(vector.Magnitude) - return direction - - -def IfcLine_to_line(line) -> Line: - """ - Convert an IFC Line [line]_ to a COMPAS line. - - """ - point = IfcCartesianPoint_to_point(line.Pnt) - vector = IfcDirection_to_vector(line.Dir) - return Line(point, point + vector) - - -def IfcPlane_to_plane(plane) -> Plane: - """ - Convert an IFC Plane [plane]_ to a COMPAS plane. - - """ - point = IfcCartesianPoint_to_point(plane.Position.Location) - normal = IfcDirection_to_vector(plane.Position.P[3]) - return Plane(point, normal) - - -def IfcAxis2Placement2D_to_frame(placement, context=None) -> Frame: - """ - Convert an IFC Axis2Placement2D [axis2placement2d]_ to a COMPAS frame. - - An Axis2Placement2D is a 2D placement based on a frame defined by a point and 2 vectors. - - """ - # use the coordinate system of the representation context to replace missing axes - # use defaults if also those not available - point = IfcCartesianPoint_to_point(placement.Location) - zaxis = Vector.Zaxis() - if placement.RefDirection: - xaxis = IfcDirection_to_vector(placement.RefDirection) - else: - xaxis = Vector.Xaxis() - yaxis = zaxis.cross(xaxis) - return Frame(point, xaxis, yaxis) - - -def IfcAxis2Placement3D_to_frame(placement, context=None) -> Frame: - """ - Convert an IFC Axis2Placement3D [axis2placement3d]_ to a COMPAS frame. - - An Axis2Placement3D is a 3D placement based on a frame defined by a point and 2 vectors. - - """ - # use the coordinate system of the representation context to replace missing axes - # use defaults if also those not available - point = IfcCartesianPoint_to_point(placement.Location) - if placement.Axis: - zaxis = IfcDirection_to_vector(placement.Axis) - else: - zaxis = Vector.Zaxis() - if placement.RefDirection: - xaxis = IfcDirection_to_vector(placement.RefDirection) - else: - xaxis = Vector.Xaxis() - yaxis = zaxis.cross(xaxis) - xaxis = yaxis.cross(zaxis) - return Frame(point, xaxis, yaxis) - - -# this needs to output a transformation -# not a frame -def IfcCartesianTransformationOperator3D_to_frame(operator) -> Frame: - """ - Convert an IFC CartesianTransformationOperator3D to a COMPAS Frame. - - """ - Axis1 = operator.Axis1 - Axis2 = operator.Axis2 - # Axis3 = operator.Axis3 - LocalOrigin = operator.LocalOrigin - # Scale = operator.Scale - - xaxis = Vector.Xaxis() if not Axis1 else IfcDirection_to_vector(Axis1) - yaxis = Vector.Yaxis() if not Axis2 else IfcDirection_to_vector(Axis2) - # zaxis = Vector.Zaxis() if not Axis3 else IfcDirection_to_vector(Axis3) - point = IfcCartesianPoint_to_point(LocalOrigin) - return Frame(point, xaxis, yaxis) - - -def IfcProfileDef_to_curve(profile_def): - from compas_occ.brep import OCCBrepFace - - pd = profile_def - - if pd.is_a("IfcParameterizedProfileDef"): - if pd.is_a("IfcRectangleProfileDef"): - frame = IfcAxis2Placement2D_to_frame(pd.Position) - points = [ - frame.point + frame.xaxis * +0.5 * pd.XDim + frame.yaxis * -0.5 * pd.YDim, - frame.point + frame.xaxis * +0.5 * pd.XDim + frame.yaxis * +0.5 * pd.YDim, - frame.point + frame.xaxis * -0.5 * pd.XDim + frame.yaxis * +0.5 * pd.YDim, - frame.point + frame.xaxis * -0.5 * pd.XDim + frame.yaxis * -0.5 * pd.YDim, - ] - return OCCBrepFace.from_polygon(points) - - else: - raise NotImplementedError(pd) - elif pd.is_a("IfcArbitraryClosedProfileDef"): - return IfcCurve_to_face(pd.OuterCurve) - elif pd.is_a("IfcCompositeProfileDef"): - faces = [] - for profile in pd.Profiles: - if profile.is_a("IfcArbitraryClosedProfileDef"): - faces.append(IfcCurve_to_face(profile.OuterCurve)) - else: - raise NotImplementedError(profile) - return faces - else: - raise NotImplementedError(pd) - - -def IfcCurve_to_face(curve): - from compas_occ.brep import OCCBrepFace - - if curve.is_a("IfcIndexedPolyCurve"): - points = [(x, y, 0) for x, y in curve.Points[0]] - return OCCBrepFace.from_polygon(points) - elif curve.is_a("IfcPolyline"): - points = [IfcCartesianPoint_to_point(pt) for pt in curve.Points] - return OCCBrepFace.from_polygon(points) - else: - raise NotImplementedError(curve) - - -def IfcCompoundPlaneAngleMeasure_to_degrees(angle_components): - if len(angle_components) != 4: - raise ValueError("Input must be a list or tuple of four elements: [degrees, minutes, seconds, millionths_of_second]") - - degrees, minutes, seconds, millionths = angle_components - - # Determine the sign based on the degrees component - sign = -1 if degrees < 0 else 1 - - # Use absolute values to avoid negative components in calculation - degrees = abs(degrees) - minutes = abs(minutes) - seconds = abs(seconds) - millionths = abs(millionths) - - # Convert millionths of a second to fractional seconds - fractional_seconds = millionths / 1_000_000 - - # Total seconds including the fractional part - total_seconds = seconds + fractional_seconds - - # Convert everything to decimal degrees - decimal_degrees = sign * (degrees + (minutes / 60) + (total_seconds / 3600)) - - return decimal_degrees diff --git a/src/compas_ifc/resources/mesh.py b/src/compas_ifc/resources/mesh.py deleted file mode 100644 index 5868e865..00000000 --- a/src/compas_ifc/resources/mesh.py +++ /dev/null @@ -1,47 +0,0 @@ -import ifcopenshell -from compas.datastructures import Mesh - - -def mesh_to_IfcPolygonalFaceSet(file: ifcopenshell.file, mesh: Mesh) -> ifcopenshell.entity_instance: - """ - Convert a COMPAS mesh to an IFC PolygonalFaceSet. - """ - _vertices = [] - for key, attr in mesh.vertices(True): - _vertices.append((key, (float(attr["x"]), float(attr["y"]), float(attr["z"])))) - _vertices = sorted(_vertices, key=lambda x: x[0]) - vertices = [v[1] for v in _vertices] - - keys = [v[0] for v in _vertices] - - faces = [] - for fkey in mesh.faces(): - indexes = [keys.index(i) + 1 for i in mesh.face_vertices(fkey)] - faces.append(file.createIfcIndexedPolygonalFace(indexes)) - - return file.create_entity( - "IfcPolygonalFaceSet", - Coordinates=file.createIfcCartesianPointList3D(vertices), - Faces=faces, - ) - - -def mesh_to_IfcShellBasedSurfaceModel(file: ifcopenshell.file, mesh: Mesh) -> ifcopenshell.entity_instance: - """ - Convert a COMPAS mesh to an IFC PolygonalFaceSet. - """ - vertices = {} - for key, attr in mesh.vertices(True): - vertex = file.createIfcCartesianPoint((float(attr["x"]), float(attr["y"]), float(attr["z"]))) - vertices[key] = vertex - - faces = [] - for fkey in mesh.faces(): - indexes = [vertices[key] for key in mesh.face_vertices(fkey)] - polyloop = file.create_entity("IfcPolyLoop", indexes) - bound = file.create_entity("IfcFaceOuterBound", polyloop, True) - face = file.create_entity("IfcFace", [bound]) - faces.append(face) - - shell = file.create_entity("IfcOpenShell", faces) - return file.create_entity("IfcShellBasedSurfaceModel", [shell]) diff --git a/src/compas_ifc/resources/primities.py b/src/compas_ifc/resources/primities.py deleted file mode 100644 index 9df02771..00000000 --- a/src/compas_ifc/resources/primities.py +++ /dev/null @@ -1,31 +0,0 @@ -import ifcopenshell -from compas.geometry import Frame -from compas.geometry import Point - -from .shapes import create_IfcAxis2Placement3D - - -def point_to_ifc_cartesian_point(file: ifcopenshell.file, point: Point) -> ifcopenshell.entity_instance: - """ - Convert a COMPAS point to an IFC Cartesian Point. - """ - point = [float(i) for i in point] - return file.create_entity("IFCCARTESIANPOINT", (*point,)) - - -def occ_plane_to_frame(occ_plane) -> Frame: - """ - Convert an OCC plane to a COMPAS frame. - """ - location = occ_plane.Location().Coord() - xdir = occ_plane.XAxis().Direction().Coord() - ydir = occ_plane.YAxis().Direction().Coord() - return Frame(location, xdir, ydir) - - -def frame_to_ifc_plane(file: ifcopenshell.file, frame: Frame) -> ifcopenshell.entity_instance: - """ - Convert a COMPAS frame to an IFC Plane. - """ - IfcAxis2Placement3D = create_IfcAxis2Placement3D(file, frame.point, frame.zaxis, frame.xaxis) - return file.create_entity("IfcPlane", IfcAxis2Placement3D) diff --git a/src/compas_ifc/resources/representation.py b/src/compas_ifc/resources/representation.py deleted file mode 100644 index 3aa46651..00000000 --- a/src/compas_ifc/resources/representation.py +++ /dev/null @@ -1,76 +0,0 @@ -from compas.datastructures import Mesh -from compas.geometry import Box -from compas.geometry import Cone -from compas.geometry import Cylinder -from compas.geometry import Sphere -from compas.tolerance import TOL -from ifcopenshell.api import run - -from .brep import brep_to_ifc_advanced_brep -from .mesh import mesh_to_IfcPolygonalFaceSet -from .mesh import mesh_to_IfcShellBasedSurfaceModel -from .shapes import box_to_IfcBlock -from .shapes import cone_to_IfcRightCircularCone -from .shapes import cylinder_to_IfcRightCircularCylinder -from .shapes import sphere_to_IfcSphere - - -def write_body_representation(file, body, ifc_entity, context): - try: - from compas_occ.brep import OCCBrep - except ImportError: - OCCBrep = None - - def _body_to_shape(body): - if isinstance(body, Box): - shape = box_to_IfcBlock(file, body) - elif isinstance(body, Sphere): - shape = sphere_to_IfcSphere(file, body) - elif isinstance(body, Cone): - shape = cone_to_IfcRightCircularCone(file, body) - elif isinstance(body, Cylinder): - shape = cylinder_to_IfcRightCircularCylinder(file, body) - elif isinstance(body, Mesh): - if file.schema == "IFC4" or file.schema == "IFC4x3": - shape = mesh_to_IfcPolygonalFaceSet(file, body) - else: - shape = mesh_to_IfcShellBasedSurfaceModel(file, body) - elif OCCBrep and isinstance(body, OCCBrep): - if file.schema == "IFC2X3": - print("IFC2X3 does not support advanced brep, converting representation of {} to mesh.".format(ifc_entity)) - shape = mesh_to_IfcShellBasedSurfaceModel(file, body.to_viewmesh(linear_deflection=TOL.lineardeflection)[0]) - else: - shape = brep_to_ifc_advanced_brep(file, body) - else: - raise Exception("Unsupported body type.") - return shape - - if isinstance(body, list): - shape = [] - for b in body: - s = _body_to_shape(b) - if not isinstance(s, list): - shape.append(s) - else: - shape.extend(s) - else: - shape = _body_to_shape(body) - if not isinstance(shape, list): - shape = [shape] - - RepresentationType = "SolidModel" - - if shape and shape[0].is_a("IfcShellBasedSurfaceModel"): - RepresentationType = "SurfaceModel" - - representation = file.create_entity( - "IfcShapeRepresentation", - ContextOfItems=context, - RepresentationIdentifier="Body", - RepresentationType=RepresentationType, - Items=shape, - ) - - run("geometry.assign_representation", file, product=ifc_entity, representation=representation) - - return representation diff --git a/src/compas_ifc/resources/shapes.py b/src/compas_ifc/resources/shapes.py deleted file mode 100644 index da9dc08d..00000000 --- a/src/compas_ifc/resources/shapes.py +++ /dev/null @@ -1,93 +0,0 @@ -import ifcopenshell -from compas.geometry import Box -from compas.geometry import Cone -from compas.geometry import Cylinder -from compas.geometry import Sphere - - -def frame_to_ifc_axis2_placement_3d(file, frame): - return create_IfcAxis2Placement3D(file, point=frame.point, dir1=frame.zaxis, dir2=frame.xaxis) - - -def create_IfcAxis2Placement3D(file, point=None, dir1=None, dir2=None): - """ - Create an IFC Axis2Placement3D from a point, a direction and a second direction. - """ - point = file.createIfcCartesianPoint(point or (0.0, 0.0, 0.0)) - dir1 = file.createIfcDirection(dir1 or (0.0, 0.0, 1.0)) - dir2 = file.createIfcDirection(dir2 or (1.0, 0.0, 0.0)) - axis2placement = file.createIfcAxis2Placement3D(point, dir1, dir2) - return axis2placement - - -def create_IfcShapeRepresentation(file: ifcopenshell.file, item: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: - """ - Create an IFC Shape Representation from an IFC item and a context. - """ - return file.create_entity( - "IfcShapeRepresentation", - ContextOfItems=context, - RepresentationIdentifier="Body", - RepresentationType="SolidModel", - Items=[item], - ) - - -def box_to_IfcBlock(file: ifcopenshell.file, box: Box) -> ifcopenshell.entity_instance: - """ - Convert a COMPAS box to an IFC Block. - """ - pt = box.frame.point.copy() - pt -= [box.xsize / 2, box.ysize / 2, box.zsize / 2] - return file.create_entity( - "IfcBlock", - Position=create_IfcAxis2Placement3D(file, pt, box.frame.zaxis, box.frame.xaxis), - XLength=box.xsize, - YLength=box.ysize, - ZLength=box.zsize, - ) - - -def sphere_to_IfcSphere(file: ifcopenshell.file, sphere: Sphere) -> ifcopenshell.entity_instance: - """ - Convert a COMPAS sphere to an IFC Sphere. - """ - return file.create_entity( - "IfcSphere", - Position=create_IfcAxis2Placement3D(file, sphere.base), - Radius=sphere.radius, - ) - - -def cone_to_IfcRightCircularCone(file: ifcopenshell.file, cone: Cone) -> ifcopenshell.entity_instance: - """ - Convert a COMPAS cone to an IFC Cone. - """ - plane = cone.circle.plane - return file.create_entity( - "IfcRightCircularCone", - Position=create_IfcAxis2Placement3D(file, plane.point, plane.normal), - Height=cone.height, - BottomRadius=cone.circle.radius, - ) - - -def cylinder_to_IfcRightCircularCylinder(file: ifcopenshell.file, cylinder: Cylinder) -> ifcopenshell.entity_instance: - """ - Convert a COMPAS cylinder to an IFC Cylinder. - """ - plane = cylinder.circle.plane - return file.create_entity( - "IfcRightCircularCylinder", - Position=create_IfcAxis2Placement3D(file, plane.point, plane.normal), - Height=cylinder.height, - Radius=cylinder.circle.radius, - ) - - -def occ_cylinder_to_ifc_cylindrical_surface(file, occ_cylinder): - location = occ_cylinder.Location().Coord() - xdir = occ_cylinder.XAxis().Direction().Coord() - zdir = occ_cylinder.Axis().Direction().Coord() - IfcAxis2Placement3D = create_IfcAxis2Placement3D(file, location, zdir, xdir) - return file.create_entity("IfcCylindricalSurface", IfcAxis2Placement3D, occ_cylinder.Radius())