From e8bd272189a3fbbe1bbffe4a624a3c364e908877 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 29 Nov 2023 14:35:46 +1100 Subject: [PATCH] Fix #4057. Data objects for the API are now in util.data instead of mixed into the shape builder utility and only depends on numpy. --- .../geometry/add_profile_representation.py | 10 +- .../api/geometry/add_slab_representation.py | 6 +- .../api/geometry/add_wall_representation.py | 10 +- .../ifcopenshell/util/data.py | 93 +++++++++++++++++++ .../ifcopenshell/util/shape_builder.py | 51 +--------- 5 files changed, 107 insertions(+), 63 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/util/data.py diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py index fca7f9e38c..c38aaf87df 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py @@ -18,7 +18,7 @@ import ifcopenshell.geom import ifcopenshell.util.unit -from ifcopenshell.util.shape_builder import ClippingInfo +from ifcopenshell.util.data import Clipping class Usecase: @@ -29,8 +29,8 @@ class Usecase: "profile": None, "depth": 1.0, "cardinal_point": 5, - # Planes are defined either by ClippingInfo objects - # or by dictionaries of arguments for `ClippingInfo.parse` + # Planes are defined either by Clipping objects + # or by dictionaries of arguments for `Clipping.parse` "clippings": [], # A list of planes that define clipping half space solids "placement_zx_axes": (None, None), } @@ -39,7 +39,7 @@ class Usecase: def execute(self): self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) - self.settings["clippings"] = [ClippingInfo.parse(c) for c in self.settings["clippings"]] + self.settings["clippings"] = [Clipping.parse(c) for c in self.settings["clippings"]] return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, @@ -71,7 +71,7 @@ class Usecase: new = ifcopenshell.util.element.copy(self.file, clipping) new.FirstOperand = first_operand first_operand = new - else: # ClippingInfo + else: # Clipping first_operand = clipping.apply(self.file, first_operand, self.settings["unit_scale"]) return first_operand diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py index ba09c7c1e1..2edd68d5c1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py @@ -27,8 +27,8 @@ class Usecase: "context": None, # IfcGeometricRepresentationContext "depth": 0.2, "x_angle": 0, # Radians - # Planes are defined either by ClippingInfo objects - # or by dictionaries of arguments for `ClippingInfo.parse` + # Planes are defined either by Clipping objects + # or by dictionaries of arguments for `Clipping.parse` "clippings": [], # A list of planes that define clipping half space solids } for key, value in settings.items(): @@ -83,7 +83,7 @@ class Usecase: new = ifcopenshell.util.element.copy(self.file, clipping) new.FirstOperand = first_operand first_operand = new - else: # ClippingInfo + else: # Clipping first_operand = clipping.apply(self.file, first_operand, self.settings["unit_scale"]) return first_operand diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py index 4c0488ea7c..6aa5466f58 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py @@ -18,7 +18,7 @@ import ifcopenshell.util.unit from math import sin, cos -from ifcopenshell.util.shape_builder import ClippingInfo +from ifcopenshell.util.data import Clipping class Usecase: @@ -32,8 +32,8 @@ class Usecase: "thickness": 0.2, # Sloped walls along the wall's X axis, provided in radians "x_angle": 0, - # Planes are defined either by ClippingInfo objects - # or by dictionaries of arguments for `ClippingInfo.parse` + # Planes are defined either by Clipping objects + # or by dictionaries of arguments for `Clipping.parse` "clippings": [], # A list of planes that define clipping half space solids "booleans": [], # Any existing IfcBooleanResults } @@ -42,7 +42,7 @@ class Usecase: def execute(self): self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) - self.settings["clippings"] = [ClippingInfo.parse(c) for c in self.settings["clippings"]] + self.settings["clippings"] = [Clipping.parse(c) for c in self.settings["clippings"]] return self.file.createIfcShapeRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, @@ -101,7 +101,7 @@ class Usecase: new = ifcopenshell.util.element.copy(self.file, clipping) new.FirstOperand = first_operand first_operand = new - else: # ClippingInfo + else: # Clipping first_operand = clipping.apply(self.file, first_operand, self.settings["unit_scale"]) return first_operand diff --git a/src/ifcopenshell-python/ifcopenshell/util/data.py b/src/ifcopenshell-python/ifcopenshell/util/data.py new file mode 100644 index 0000000000..3c1e2c1669 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/util/data.py @@ -0,0 +1,93 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2023 Dion Moult , @Andrej730 +# +# 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 numpy as np +import ifcopenshell +from typing import Any, Union +from dataclasses import dataclass + + +@dataclass +class Clipping: + location: tuple[float, float, float] + normal: tuple[float, float, float] + type: str = "IfcBooleanClippingResult" + operand_type: str = "IfcHalfSpaceSolid" + + @classmethod + def parse(cls, raw_data: Any) -> Union[ifcopenshell.entity_instance, Clipping, None]: + """Parse various formats into a clipping object + + `raw_data` can be either: + + - IfcBooleanResult IFC entity + - `Clipping` instance + - dictionary to define `Clipping` - either `location` and `normal` + or a `matrix` where XY plane is the clipping boundary and +Z is removed. + `matrix` method will be soon to be deprecated completely. + """ + if isinstance(raw_data, ifcopenshell.entity_instance): + if not raw_data.is_a("IfcBooleanResult"): + raise Exception(f"Provided clipping of unexpected IFC class: {raw_data}") + return raw_data + elif isinstance(raw_data, Clipping): + return raw_data + elif isinstance(raw_data, dict): + if "matrix" in raw_data: + raw_data = raw_data.copy() + matrix = np.array(raw_data["matrix"])[:3] + raw_data["normal"] = matrix[:, 2].tolist() + raw_data["location"] = matrix[:, 3].tolist() + del raw_data["matrix"] + clipping_data = cls(**raw_data) + if clipping_data.type != "IfcBooleanClippingResult": + raise Exception(f'Provided clipping with unexpected result type "{clipping_data.type}"') + if clipping_data.operand_type != "IfcHalfSpaceSolid": + raise Exception(f'Provided clipping with unexpected operand type "{clipping_data.operand_type}"') + return clipping_data + raise Exception(f"Unexpected clipping type provided: {raw_data}") + + def apply( + self, ifc_file: ifcopenshell.file, first_operand: ifcopenshell.entity_instance, unit_scale: float + ) -> ifcopenshell.entity_instance: + """Applies the clipping data as an IfcBooleanClippingResult to an operand + + :param ifc_file: The model to create the entities in + :param first_operand: The representation item to apply the boolean clipping to. + :param unit_scale: The unit scale value to convert from the Clipping's SI units to project units + :return: An IfcBooleanClippingResult which uses an IfcHalfSpaceSolid to clip the first operand + """ + + location = ifc_file.createIfcCartesianPoint([i / unit_scale for i in self.location]) + direction = ifc_file.createIfcDirection(self.normal) + + normal = np.array(self.normal) + if np.allclose(normal, np.array([0.0, 0.0, 1.0]), atol=1e-2): + arbitrary_vector = np.array([0.0, 1.0, 0.0]) + else: + arbitrary_vector = np.array([0.0, 0.0, 1.0]) + + x_axis = np.cross(normal, arbitrary_vector) + x_axis /= np.linalg.norm(x_axis) + x_axis = ifc_file.createIfcDirection(x_axis.tolist()) + + plane = ifc_file.createIfcPlane(ifc_file.createIfcAxis2Placement3D(location, direction, x_axis)) + + second_operand = ifc_file.createIfcHalfSpaceSolid(plane, False) + return ifc_file.createIfcBooleanClippingResult("DIFFERENCE", first_operand, second_operand) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index bbcd8e6f60..bde227e310 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -16,16 +16,14 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . -from __future__ import annotations import numpy as np import collections import ifcopenshell import ifcopenshell.api from math import cos, sin, pi, tan, radians, degrees, atan, sqrt, ceil -from typing import List, Any, Union +from typing import List from itertools import chain from mathutils import Vector, Matrix -from dataclasses import dataclass V = lambda *x: Vector([float(i) for i in x]) sign = lambda x: x and (1, -1)[x < 0] @@ -1476,50 +1474,3 @@ class ShapeBuilder: "main_profile_dimension": profile_dim[lateral_axis], } return rep, bend_data - - -@dataclass -class ClippingInfo: - location: tuple[float, float, float] - normal: tuple[float, float, float] - type: str = "IfcBooleanClippingResult" - operand_type: str = "IfcHalfSpaceSolid" - - @classmethod - def parse(cls, raw_data: Any) -> Union[ifcopenshell.entity_instance, ClippingInfo, None]: - """`raw_data` can be either: - - IfcBooleanResult IFC entity - - `ClippingInfo` instance - - dictionary to define `ClippingInfo` - either `location` and `normal` - or a `matrix` where XY plane is the clipping boundary and +Z is removed. - `matrix` method will be soon to be deprecated completely. - """ - if isinstance(raw_data, ifcopenshell.entity_instance): - if not raw_data.is_a("IfcBooleanResult"): - raise Exception(f"Provided clipping of unexpected IFC class: {raw_data}") - return raw_data - elif isinstance(raw_data, ClippingInfo): - return raw_data - elif isinstance(raw_data, dict): - if "matrix" in raw_data: - raw_data = raw_data.copy() - matrix = np.array(raw_data["matrix"])[:3] - raw_data["normal"] = matrix[:, 2].tolist() - raw_data["location"] = matrix[:, 3].tolist() - del raw_data["matrix"] - clipping_data = cls(**raw_data) - if clipping_data.type != "IfcBooleanClippingResult": - raise Exception(f'Provided clipping with unexpected result type "{clipping_data.type}"') - if clipping_data.operand_type != "IfcHalfSpaceSolid": - raise Exception(f'Provided clipping with unexpected operand type "{clipping_data.operand_type}"') - return clipping_data - raise Exception(f"Unexpected clipping type provided: {raw_data}") - - def apply( - self, file: ifcopenshell.file, first_operand: ifcopenshell.entity_instance, unit_scale: float - ) -> ifcopenshell.entity_instance: - builder = ShapeBuilder(file) - plane = builder.plane(location=Vector([i / unit_scale for i in self.location]), normal=Vector(self.normal)) - second_operand = file.createIfcHalfSpaceSolid(plane, False) - first_operand = file.createIfcBooleanClippingResult("DIFFERENCE", first_operand, second_operand) - return first_operand