mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-16 13:46:54 +00:00
mathutils -> numpy - polyline, rectangle #5192
This commit is contained in:
@@ -37,6 +37,10 @@ PRECISION = 1.0e-5
|
|||||||
VectorTuple = type[tuple[float, float, float]]
|
VectorTuple = type[tuple[float, float, float]]
|
||||||
"tuple of 3 `float` values"
|
"tuple of 3 `float` values"
|
||||||
|
|
||||||
|
# Support both numpy arrays and python sequences as inputs.
|
||||||
|
VectorType = Union[Sequence[float], Vector, np.ndarray]
|
||||||
|
SequenceOfVectors = Union[Sequence[VectorType], np.ndarray]
|
||||||
|
|
||||||
|
|
||||||
def is_x(value, x, si_conversion=None):
|
def is_x(value, x, si_conversion=None):
|
||||||
if si_conversion:
|
if si_conversion:
|
||||||
@@ -60,27 +64,22 @@ class ShapeBuilder:
|
|||||||
|
|
||||||
def polyline(
|
def polyline(
|
||||||
self,
|
self,
|
||||||
points: list[Vector],
|
points: SequenceOfVectors,
|
||||||
closed: bool = False,
|
closed: bool = False,
|
||||||
position_offset: Optional[Vector] = None,
|
position_offset: Optional[VectorType] = None,
|
||||||
arc_points: list[int] = [],
|
arc_points: Sequence[int] = (),
|
||||||
) -> ifcopenshell.entity_instance:
|
) -> ifcopenshell.entity_instance:
|
||||||
"""
|
"""
|
||||||
Generate an IfcIndexedPolyCurve based on the provided points.
|
Generate an IfcIndexedPolyCurve based on the provided points.
|
||||||
|
|
||||||
:param points: List of 2d or 3d points
|
:param points: List of 2d or 3d points
|
||||||
:type points: list[Vector]
|
|
||||||
:param closed: Whether polyline should be closed. Default is `False`
|
:param closed: Whether polyline should be closed. Default is `False`
|
||||||
:type closed: bool, optional
|
|
||||||
:param position_offset: offset to be applied to all points
|
:param position_offset: offset to be applied to all points
|
||||||
:type position_offset: Vector, optional
|
|
||||||
:param arc_points: Indices of the middle points for arcs. For creating an arc segment,
|
:param arc_points: Indices of the middle points for arcs. For creating an arc segment,
|
||||||
provide 3 points: `arc_start`, `arc_middle` and `arc_end` to `points` and add the `arc_middle`
|
provide 3 points: `arc_start`, `arc_middle` and `arc_end` to `points` and add the `arc_middle`
|
||||||
point's index to `arc_points`
|
point's index to `arc_points`
|
||||||
:type arc_points: list[int], optional
|
|
||||||
|
|
||||||
:return: IfcIndexedPolyCurve
|
:return: IfcIndexedPolyCurve
|
||||||
:rtype: ifcopenshell.entity_instance
|
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -102,21 +101,25 @@ class ShapeBuilder:
|
|||||||
if arc_points and self.file.schema == "IFC2X3":
|
if arc_points and self.file.schema == "IFC2X3":
|
||||||
raise Exception("Arcs are not supported for IFC2X3.")
|
raise Exception("Arcs are not supported for IFC2X3.")
|
||||||
|
|
||||||
if position_offset:
|
points: np.ndarray
|
||||||
points = [Vector(p) + position_offset for p in points]
|
points = np.array(points)
|
||||||
|
if position_offset is not None:
|
||||||
|
points = points + position_offset
|
||||||
|
|
||||||
if self.file.schema == "IFC2X3":
|
if self.file.schema == "IFC2X3":
|
||||||
points = [self.file.createIfcCartesianPoint(p) for p in points]
|
ifc_points = [self.file.create_entity("IfcCartesianPoint", p) for p in points.tolist()]
|
||||||
if closed:
|
if closed:
|
||||||
points.append(points[0])
|
ifc_points.append(ifc_points[0])
|
||||||
ifc_curve = self.file.createIfcPolyline(Points=points)
|
ifc_curve = self.file.createIfcPolyline(Points=points)
|
||||||
return ifc_curve
|
return ifc_curve
|
||||||
|
|
||||||
dimensions = len(points[0])
|
dimensions = len(points[0])
|
||||||
if dimensions == 2:
|
if dimensions == 2:
|
||||||
ifc_points = self.file.createIfcCartesianPointList2D(points)
|
ifc_points = self.file.create_entity("IfcCartesianPointList2D", points.tolist())
|
||||||
elif dimensions == 3:
|
elif dimensions == 3:
|
||||||
ifc_points = self.file.createIfcCartesianPointList3D(points)
|
ifc_points = self.file.create_entity("IfcCartesianPointList3D", points.tolist())
|
||||||
|
else:
|
||||||
|
raise Exception(f"Point has unexpected number of dimensions - {dimensions}.")
|
||||||
|
|
||||||
if not closed and not arc_points:
|
if not closed and not arc_points:
|
||||||
ifc_curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points)
|
ifc_curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points)
|
||||||
@@ -164,9 +167,7 @@ class ShapeBuilder:
|
|||||||
ifc_curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
|
ifc_curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
|
||||||
return ifc_curve
|
return ifc_curve
|
||||||
|
|
||||||
def get_rectangle_coords(
|
def get_rectangle_coords(self, size: VectorType = (1.0, 1.0), position: Optional[VectorType] = None) -> np.ndarray:
|
||||||
self, size: Vector = Vector((1.0, 1.0)).freeze(), position: Optional[Vector] = None
|
|
||||||
) -> list[Vector]:
|
|
||||||
"""
|
"""
|
||||||
Get rectangle coords arranged as below:
|
Get rectangle coords arranged as below:
|
||||||
|
|
||||||
@@ -176,45 +177,37 @@ class ShapeBuilder:
|
|||||||
0 1
|
0 1
|
||||||
|
|
||||||
:param size: rectangle size, could be either 2d or 3d, defaults to `(1,1)`
|
:param size: rectangle size, could be either 2d or 3d, defaults to `(1,1)`
|
||||||
:type size: Vector, optional
|
|
||||||
:param position: rectangle position, default to `None`.
|
:param position: rectangle position, default to `None`.
|
||||||
if `position` not specified zero-vector will be used
|
if `position` not specified zero-vector will be used
|
||||||
:type position: Vector, optional
|
|
||||||
|
|
||||||
:return: list of rectangle coords
|
:return: list of rectangle coords
|
||||||
:rtype: List[Vector]
|
|
||||||
"""
|
"""
|
||||||
dimensions = len(size)
|
size_np = np.array(size)
|
||||||
|
|
||||||
if not position:
|
if position is None:
|
||||||
position = Vector([0] * dimensions)
|
dimensions = len(size_np)
|
||||||
|
points = np.full((4, dimensions), 0.0)
|
||||||
|
else:
|
||||||
|
points = np.tile(position, (4, 1))
|
||||||
|
|
||||||
# adds support both 2d and 3d sizes
|
# Support both 2d and 3d sizes defined in different dimensions.
|
||||||
non_empty_coords = [i for i, v in enumerate(size) if v]
|
non_empty_coords = np.nonzero(size_np)[0]
|
||||||
id_matrix = Matrix.Identity(dimensions)
|
points[1, non_empty_coords[0]] += size_np[non_empty_coords[0]]
|
||||||
|
points[2] += size_np
|
||||||
points = [
|
points[3, non_empty_coords[1]] += size_np[non_empty_coords[1]]
|
||||||
position,
|
|
||||||
position + size * id_matrix[non_empty_coords[0]],
|
|
||||||
position + size,
|
|
||||||
position + size * id_matrix[non_empty_coords[1]],
|
|
||||||
]
|
|
||||||
return points
|
return points
|
||||||
|
|
||||||
def rectangle(
|
def rectangle(
|
||||||
self, size: Vector = Vector((1.0, 1.0)).freeze(), position: Vector = None
|
self, size: VectorType = (1.0, 1.0), position: Optional[VectorType] = None
|
||||||
) -> ifcopenshell.entity_instance:
|
) -> ifcopenshell.entity_instance:
|
||||||
"""
|
"""
|
||||||
Generate a rectangle polyline.
|
Generate a rectangle polyline.
|
||||||
|
|
||||||
:param size: rectangle size, could be either 2d or 3d, defaults to `(1,1)`
|
:param size: rectangle size, could be either 2d or 3d, defaults to `(1,1)`
|
||||||
:type size: Vector, optional
|
|
||||||
:param position: rectangle position, default to `None`.
|
:param position: rectangle position, default to `None`.
|
||||||
if `position` not specified zero-vector will be used
|
if `position` not specified zero-vector will be used
|
||||||
:type position: Vector, optional
|
|
||||||
|
|
||||||
:return: IfcIndexedPolyCurve
|
:return: IfcIndexedPolyCurve
|
||||||
:rtype: ifcopenshell.entity_instance
|
|
||||||
"""
|
"""
|
||||||
return self.polyline(self.get_rectangle_coords(size, position), closed=True)
|
return self.polyline(self.get_rectangle_coords(size, position), closed=True)
|
||||||
|
|
||||||
@@ -888,43 +881,44 @@ class ShapeBuilder:
|
|||||||
|
|
||||||
def get_simple_2dcurve_data(
|
def get_simple_2dcurve_data(
|
||||||
self,
|
self,
|
||||||
coords: list[Vector],
|
coords: SequenceOfVectors,
|
||||||
fillets: Sequence[int] = (),
|
fillets: Sequence[int] = (),
|
||||||
fillet_radius: Sequence[float] = (),
|
fillet_radius: Union[float, Sequence[float]] = (),
|
||||||
closed: bool = True,
|
closed: bool = True,
|
||||||
create_ifc_curve: bool = False,
|
create_ifc_curve: bool = False,
|
||||||
) -> tuple[list[Vector], list[tuple[int, int], Union[ifcopenshell.entity_instance, None]]]:
|
) -> tuple[list[VectorType], list[list[int]], Union[ifcopenshell.entity_instance, None]]:
|
||||||
"""
|
"""
|
||||||
Creates simple 2D curve from set of 2d coords and list of points with fillets.
|
Creates simple 2D curve from set of 2d coords and list of points with fillets.
|
||||||
Simple curve means that all fillets are based on 90 degree angle.
|
Simple curve means that all fillets are based on 90 degree angle.
|
||||||
|
|
||||||
> coords: list of 2d coords. Example: ((x0,y0), (x1,y1), (x2, y2))
|
:param coords: list of 2d coords. Example: ((x0,y0), (x1,y1), (x2, y2))
|
||||||
> fillets: list of points from `coords` to base fillet on. Example: (1,)
|
:param fillets: list of points from `coords` to base fillet on. Example: (1,)
|
||||||
> fillet_radius: list of fillet radius for each of corresponding point form `fillets`. Example: (5.,)
|
:param fillet_radius: list of fillet radius for each of corresponding point form `fillets`.
|
||||||
Note: filler_radius could be just 1 float value if it's the same for all fillets.
|
Example: (5.,) Note: `fillet_radius` could be just 1 float value if it's the same for all fillets.
|
||||||
|
:param closed: boolean whether curve should be closed (whether last point connected to first one). Default: True
|
||||||
|
:param create_ifc_curve: create IfcIndexedPolyCurve or just return the data. Default: False
|
||||||
|
|
||||||
Optional arguments:
|
:return: (points, segments, ifc_curve) for the created simple curve
|
||||||
> closed: boolean whether curve should be closed (whether last point connected to first one). Default: True
|
if both points in e are equally far from pt, then v1 is returned.
|
||||||
> create_ifc_curve: create IfcIndexedPolyCurve or just return the data. Default: False
|
"""
|
||||||
|
|
||||||
< returns (points, segments, ifc_curve) for the created simple curve
|
def remove_redundant_points(
|
||||||
if both points in e are equally far from pt, then v1 is returned."""
|
points: list[VectorType], segments: list[list[int]]
|
||||||
|
) -> tuple[list[VectorType], list[list[int]]]:
|
||||||
def remove_redundant_points(points, segments):
|
|
||||||
# prevent mutating
|
# prevent mutating
|
||||||
points = [tuple(p) for p in points]
|
points = [tuple(p) for p in points]
|
||||||
segments = segments.copy()
|
segments = segments.copy()
|
||||||
|
|
||||||
# find duplicate points, reindex them in segments
|
# find duplicate points, reindex them in segments
|
||||||
# and mark them to delete later
|
# and mark them to delete later
|
||||||
points_to_remove = []
|
points_to_remove: list[int] = []
|
||||||
prev_point = 0
|
prev_point = 0
|
||||||
for i, p in enumerate(points[1:], 1):
|
for i, p in enumerate(points[1:], 1):
|
||||||
if p != points[prev_point]:
|
if p != points[prev_point]:
|
||||||
prev_point = i
|
prev_point = i
|
||||||
continue
|
continue
|
||||||
|
|
||||||
valid_segments = []
|
valid_segments: list[list[int]] = []
|
||||||
for s in segments:
|
for s in segments:
|
||||||
s = [ps if ps != i else prev_point for ps in s]
|
s = [ps if ps != i else prev_point for ps in s]
|
||||||
valid_segments.append(s)
|
valid_segments.append(s)
|
||||||
@@ -942,12 +936,13 @@ class ShapeBuilder:
|
|||||||
return points, valid_segments
|
return points, valid_segments
|
||||||
|
|
||||||
# option to use same fillet radius for all fillets
|
# option to use same fillet radius for all fillets
|
||||||
if isinstance(fillet_radius, float):
|
if isinstance(fillet_radius, (float, int)):
|
||||||
fillet_radius = [fillet_radius] * len(fillets)
|
fillet_radius = [fillet_radius] * len(fillets)
|
||||||
|
|
||||||
fillets = dict(zip(fillets, fillet_radius))
|
fillets: dict[int, float] = dict(zip(fillets, fillet_radius))
|
||||||
segments = []
|
segments: list[list[int]] = []
|
||||||
points = []
|
points: list[VectorType] = []
|
||||||
|
|
||||||
for co_i, co in enumerate(coords, 0):
|
for co_i, co in enumerate(coords, 0):
|
||||||
current_point = len(points)
|
current_point = len(points)
|
||||||
if co_i in fillets:
|
if co_i in fillets:
|
||||||
@@ -991,7 +986,7 @@ class ShapeBuilder:
|
|||||||
points, segments = remove_redundant_points(points, segments)
|
points, segments = remove_redundant_points(points, segments)
|
||||||
ifc_curve = None
|
ifc_curve = None
|
||||||
if create_ifc_curve:
|
if create_ifc_curve:
|
||||||
ifc_points = self.file.createIfcCartesianPointList2D(points)
|
ifc_points = self.file.createIfcCartesianPointList2D(ifc_safe_vector_type(points))
|
||||||
ifc_segments = []
|
ifc_segments = []
|
||||||
for segment in segments:
|
for segment in segments:
|
||||||
segment = [i + 1 for i in segment]
|
segment = [i + 1 for i in segment]
|
||||||
@@ -1046,6 +1041,7 @@ class ShapeBuilder:
|
|||||||
fillet_radius=(r+t, r+t, r, r, r+t, r+t, r, r),
|
fillet_radius=(r+t, r+t, r, r, r+t, r+t, r, r),
|
||||||
closed=True, create_ifc_curve=True)
|
closed=True, create_ifc_curve=True)
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
assert ifc_curve
|
||||||
|
|
||||||
return ifc_curve
|
return ifc_curve
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,23 @@ from math import degrees, radians, tan
|
|||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
|
|
||||||
|
|
||||||
|
class TestRectangle(test.bootstrap.IFC4):
|
||||||
|
def test_get_rectangle_coords(self):
|
||||||
|
builder = ShapeBuilder(self.file)
|
||||||
|
|
||||||
|
# 2D.
|
||||||
|
coords = builder.get_rectangle_coords((1, 2), (3, 4))
|
||||||
|
assert np.allclose(coords, [[3.0, 4.0], [4.0, 4.0], [4.0, 6.0], [3.0, 6.0]])
|
||||||
|
|
||||||
|
# 3D, XY plane.
|
||||||
|
coords = builder.get_rectangle_coords((1, 2, 0), (3, 4, 0))
|
||||||
|
assert np.allclose(coords, [[3.0, 4.0, 0.0], [4.0, 4.0, 0.0], [4.0, 6.0, 0.0], [3.0, 6.0, 0.0]])
|
||||||
|
|
||||||
|
# 3D, XZ plane.
|
||||||
|
coords = builder.get_rectangle_coords((1, 0, 2), (3, 0, 4))
|
||||||
|
assert np.allclose(coords, [[3.0, 0.0, 4.0], [4.0, 0.0, 4.0], [4.0, 0.0, 6.0], [3.0, 0.0, 6.0]])
|
||||||
|
|
||||||
|
|
||||||
class TestCreatePolyline(test.bootstrap.IFC4):
|
class TestCreatePolyline(test.bootstrap.IFC4):
|
||||||
def test_simple_polyline(self):
|
def test_simple_polyline(self):
|
||||||
builder = ShapeBuilder(self.file)
|
builder = ShapeBuilder(self.file)
|
||||||
|
|||||||
Reference in New Issue
Block a user