From a3d970e0149ffbbc262421a3adde318d10765827 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 6 Mar 2024 12:47:57 +0500 Subject: [PATCH] shape_builder.create_axis2_placement_3d utility method and docs --- .../ifcopenshell/util/placement.py | 5 +- .../ifcopenshell/util/shape_builder.py | 95 +++++++++++++++---- 2 files changed, 78 insertions(+), 22 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/placement.py b/src/ifcopenshell-python/ifcopenshell/util/placement.py index 4f3d80cd35..1f40fcc497 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/placement.py +++ b/src/ifcopenshell-python/ifcopenshell/util/placement.py @@ -17,6 +17,7 @@ # along with IfcOpenShell. If not, see . import numpy as np +import ifcopenshell def a2p(o, z, x): @@ -44,7 +45,7 @@ def a2p(o, z, x): return r.T -def get_axis2placement(placement): +def get_axis2placement(placement: ifcopenshell.entity_instance) -> np.array: """Parses an IfcAxis2Placement (2D or 3D) to a 4x4 transformation matrix Note that this function only parses a single placement axis. If you want to @@ -202,6 +203,7 @@ def rotation(angle, axis, is_degrees=True): theta = np.radians(angle) if is_degrees else angle cos, sin = np.cos(theta), np.sin(theta) + # fmt: off if axis == "X": return np.array([ [1, 0, 0, 0], @@ -223,3 +225,4 @@ def rotation(angle, axis, is_degrees=True): [0, 0, 1, 0], [0, 0, 0, 1] ]) + # fmt: on diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index 230936c134..1e2d395ba2 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -21,7 +21,7 @@ import collections import ifcopenshell import ifcopenshell.api from math import cos, sin, pi, tan, radians, degrees, atan, sqrt, ceil -from typing import List +from typing import List, Tuple, Type, Union from itertools import chain from mathutils import Vector, Matrix @@ -29,6 +29,9 @@ V = lambda *x: Vector([float(i) for i in x]) sign = lambda x: x and (1, -1)[x < 0] PRECISION = 1.0e-5 +VectorTuple = Type[Tuple[float, float, float]] +"tuple of 3 `float` values" + def is_x(value, x, si_conversion=None): if si_conversion: @@ -217,15 +220,24 @@ class ShapeBuilder: def plane( self, location: Vector = Vector((0.0, 0.0, 0.0)).freeze(), normal: Vector = Vector((0.0, 0.0, 1.0)).freeze() - ): - location = self.file.createIfcCartesianPoint(location) - direction = self.file.createIfcDirection(normal) + ) -> ifcopenshell.entity_instance: + """ + Create IfcPlane. + + :param location: plane position, defaults to `(0.0, 0.0, 0.0)` + :type location: Vector, optional + :param normal: plane normal direction, defaults to `(0.0, 0.0, 1.0)` + :type normal: Vector, optional + :return: IfcPlane + :rtype: ifcopenshell.entity_instance + """ + if normal.to_tuple(2) == Vector((0.0, 0.0, 1.0)): arbitrary_vector = Vector((0.0, 1.0, 0.0)) else: arbitrary_vector = Vector((0.0, 0.0, 1.0)) - x_axis = self.file.createIfcDirection(normal.cross(arbitrary_vector).normalized()) - axis_placement = self.file.createIfcAxis2Placement3D(location, direction, x_axis) + x_axis = normal.cross(arbitrary_vector).normalized() + axis_placement = self.create_axis2_placement_3d(location, normal, x_axis) return self.file.createIfcPlane(axis_placement) # TODO: explain points order for the curve_between_two_points @@ -465,8 +477,15 @@ class ShapeBuilder: point_2d = relative_point + mirror_point return point_2d - def get_axis2_placement_3d_matrix(self, axis2_placement_3d): - # > IfcAxis2Placement3D + def get_axis2_placement_3d_matrix(self, axis2_placement_3d: ifcopenshell.entity_instance) -> Matrix: + """ + Generate a Matrix from IfcAxis2Placement3D. + + :param axis2_placement_3d: IfcAxis2Placement3D + :type axis2_placement_3d: ifcopenshell.entity_instance + :return: generated matrix + :rtype: Matrix + """ p = axis2_placement_3d M = Matrix.Identity(3) @@ -484,6 +503,48 @@ class ShapeBuilder: return rotation_matrix + def create_axis2_placement_3d( + self, + position: VectorTuple = (0.0, 0.0, 0.0), + z_axis: VectorTuple = (0.0, 0.0, 1.0), + x_axis: VectorTuple = (1.0, 0.0, 0.0), + ) -> ifcopenshell.entity_instance: + """ + Create IfcAxis2Placement3D. + + :param position: placement position (Axis), defaults to `(0.0, 0.0, 0.0)` + :type position: VectorTuple, optional + :param z_axis: local Z axis direction, defaults to `(0.0, 0.0, 1.0)` + :type z_axis: VectorTuple, optional + :param x_axis: local X axis direction (RefDirection), defaults to `(1.0, 0.0, 0.0)` + :type x_axis: VectorTuple, optional + :return: IfcAxis2Placement3D + :rtype: ifcopenshell.entity_instance + """ + return self.file.createIfcAxis2Placement3D( + self.file.createIfcCartesianPoint(position), + Axis=self.file.createIfcDirection(z_axis), + RefDirection=self.file.createIfcDirection(x_axis), + ) + + def create_axis2_placement_3d_from_matrix( + self, + matrix: Union[np.ndarray, None] = None, + ) -> ifcopenshell.entity_instance: + """ + Create IfcAxis2Placement3D from numpy matrix. + + :param matrix: 4x4 transformation matrix, defaults to `np.eye(4)` + :type matrix: np.array[np.array[float]], optional + :return: IfcAxis2Placement3D + :rtype: ifcopenshell.entity_instance + """ + if matrix is None: + matrix = np.eye(4, dtype=float) + return self.create_axis2_placement_3d( + position=matrix[:, 3][:3].tolist(), z_axis=matrix[:, 2][:3].tolist(), x_axis=matrix[:, 0][:3].tolist() + ) + def mirror( self, curve_or_item, @@ -599,22 +660,18 @@ class ShapeBuilder: return processed_objects if (multiple_objects or multiple_transformations) else processed_objects[0] - def sphere(self, radius: float = 1.0, center: Vector = Vector((0, 0, 0)).freeze()) -> ifcopenshell.entity_instance: + def sphere(self, radius: float = 1.0, center: VectorTuple = (0.0, 0.0, 0.0)) -> ifcopenshell.entity_instance: """ :param radius: radius of the sphere, defaults to 1.0 :type radius: float, optional - :param center: sphere position, defaults to zero-vector - :type center: Vector, optional + :param center: sphere position, defaults to `(0.0, 0.0, 0.0)` + :type center: VectorTuple, optional :return: IfcSphere :rtype: ifcopenshell.entity_instance """ - ifc_position = self.file.createIfcAxis2Placement3D( - self.file.createIfcCartesianPoint(center), # position - self.file.createIfcDirection((0.0, 0.0, 1.0)), # Z-axis / Axis - self.file.createIfcDirection((1.0, 0.0, 0.0)), # X-axis / RefDirection - ) + ifc_position = self.create_axis2_placement_3d(position=center) return self.file.createIfcSphere(Radius=radius, Position=ifc_position) def extrude( @@ -655,11 +712,7 @@ class ShapeBuilder: if position_y_axis: position_z_axis = position_x_axis.cross(position_y_axis) - ifc_position = self.file.createIfcAxis2Placement3D( - self.file.createIfcCartesianPoint(position), # position - self.file.createIfcDirection(position_z_axis), # Z-axis / Axis - self.file.createIfcDirection(position_x_axis), # X-axis / RefDirection - ) + ifc_position = self.create_axis2_placement_3d(position, position_z_axis, position_x_axis) ifc_direction = self.file.createIfcDirection(extrusion_vector) extruded_area = self.file.createIfcExtrudedAreaSolid( SweptArea=profile_or_curve, Position=ifc_position, ExtrudedDirection=ifc_direction, Depth=magnitude