Files
IfcOpenShell/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py
T
Andrej730 ec882a1a7d Fixed validation error for ELEVATION_VIEW curves #2925
Error occured because ShapeBuilder was assigning "Curve2D" representation type for elevation view curves when the correct type is "Curve3D".

It use to occur for both doors and windows created with ifc modifier.

```
Validation error text:
2023-04-03:18:21:28,879 ERROR   [rule_executor.py:154] On instance:
    #135=IfcShapeRepresentation(#21,'Profile','Curve2D',(#134))
Rule IfcShapeRepresentation_CorrectItemsForType:
    (IfcShapeRepresentationTypes(self.RepresentationType,self.Items))
Violated by:
    False
     +  where False = IfcShapeRepresentationTypes('Curve2D', (#134=IfcIndexedPolyCurve(#133,(IfcLineIndex((1,2)),IfcLineIndex((2,3)),IfcLineIndex((3,4)),IfcLineIndex((4,1))),$),))
     +    where 'Curve2D' = #135=IfcShapeRepresentation(#21,'Profile','Curve2D',(#134)).RepresentationType
     +    and   (#134=IfcIndexedPolyCurve(#133,(IfcLineIndex((1,2)),IfcLineIndex((2,3)),IfcLineIndex((3,4)),IfcLineIndex((4,1))),$),) = #135=IfcShapeRepresentation(#21,'Profile','Curve2D',(#134)).Items
```
2024-04-15 09:25:48 +02:00

533 lines
23 KiB
Python

# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 @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 <http://www.gnu.org/licenses/>.
import collections
import ifcopenshell
import ifcopenshell.api
from math import cos, sin, pi
from mathutils import Vector, Matrix
V = lambda *x: Vector([float(i) for i in x])
sign = lambda x: x and (1, -1)[x < 0]
# Note: using ShapeBuilder try not to reuse IFC elements in the process
# otherwise you might run into situation where builder.mirror or other operation
# is applied twice during one run to the same element
# which might produce undesirable results
class ShapeBuilder:
def __init__(self, ifc_file):
self.file = ifc_file
def polyline(self, points, closed=False, position_offset=None):
# > points - list of points formatted like ( (x0, y0), (x1, y1) )
# < IfcIndexedPolyCurve
segments = [(i, i + 1) for i in range(1, len(points))]
if closed:
segments.append((len(points), 1))
if position_offset:
points = [Vector(p) + position_offset for p in points]
dimensions = len(points[0])
if dimensions == 2:
ifc_points = self.file.createIfcCartesianPointList2D(points)
elif dimensions == 3:
ifc_points = self.file.createIfcCartesianPointList3D(points)
ifc_segments = [self.file.createIfcLineIndex(segment) for segment in segments]
ifc_curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=ifc_segments)
return ifc_curve
def get_rectangle_coords(self, size: Vector = Vector((1.0, 1.0)).freeze(), position: Vector = None):
dimensions = len(size)
if not position:
position = Vector([0] * dimensions)
# adds support both 2d and 3d sizes
non_empty_coords = [i for i, v in enumerate(size) if v]
id_matrix = Matrix.Identity(dimensions)
points = [
position,
position + size * id_matrix[non_empty_coords[0]],
position + size,
position + size * id_matrix[non_empty_coords[1]],
]
return points
def rectangle(self, size: Vector = Vector((1.0, 1.0)).freeze(), position: Vector = None):
"""
function supports both 2d and 3d rectangle sizes
if `position` not specified zero-vector will be used
returns IfcIndexedPolyCurve
"""
# < IfcIndexedPolyCurve
return self.polyline(self.get_rectangle_coords(size, position), closed=True)
def circle(self, center: Vector = Vector((0.0, 0.0)).freeze(), radius=1.0):
# < returns IfcCircle
ifc_center = self.file.createIfcAxis2Placement2D(self.file.createIfcCartesianPoint(center))
ifc_curve = self.file.createIfcCircle(ifc_center, radius)
# self.file_file.createIfcAxis2Placement2D(tool.Ifc.get().createIfcCartesianPoint(center[0:2]))
return ifc_curve
# TODO: explain points order for the curve_between_two_points
# because the order is important and defines the center of the curve
# currently it seems like the first point shifted by x-axis defines the center
def curve_between_two_points(self, points):
# > points - list of 2 Vectors
"""Simple circle based curve between two points
Good for creating curves and fillets, won't work for continuous ellipse shapes.
"""
diff = points[1] - points[0]
max_diff_i = list(diff).index(max(diff, key=lambda x: abs(x)))
diff_sign = V(*[(sign(e) if i == max_diff_i else 0) for i, e in enumerate(diff)])
# diff should be applied only to one axis
# if it's applied to two (like in a case of circle) it will create
# a straight line instead of a curve
diff = V(0.01, 0.01) * diff_sign
middle_point = points[0] + diff
points = [points[0], middle_point, points[1]]
seg = self.file.createIfcArcIndex((1, 2, 3))
ifc_points = self.file.createIfcCartesianPointList2D(points)
curve = self.file.createIfcIndexedPolyCurve(Points=ifc_points, Segments=[seg])
return curve
def get_trim_points_from_mask(self, x_axis_radius, y_axis_radius, trim_points_mask, position_offset=None):
"""Handy way to get edge points of the ellipse like shape of a given radiuses.
Mask points are numerated from 0 to 3 ccw starting from (x_axis_radius/2; 0).
Example: mask (0, 1, 2, 3) will return points (x, 0), (0, y), (-x, 0), (0, -y)
"""
points = (
V(x_axis_radius, 0),
V(0, y_axis_radius),
V(-x_axis_radius, 0),
V(0, -y_axis_radius),
)
if position_offset:
trim_points = [points[i] + position_offset for i in trim_points_mask]
else:
trim_points = [points[i] for i in trim_points_mask]
return trim_points
def create_ellipse_curve(
self,
x_axis_radius,
y_axis_radius,
position=Vector((0.0, 0.0)).freeze(),
trim_points=[],
ref_x_direction=Vector((1.0, 0.0)),
trim_points_mask=[],
):
"""
Ellipse trimming points should be specified in counter clockwise order.
For example, if you need to get the part of the ellipse ABOVE y-axis, you need to use mask (0,2). Below y-axis - (2,0)
For more information about trim_points_mask check builder.get_trim_points_from_mask
Notion: trimmed ellipse also contains polyline between trim points, meaning IfcTrimmedCurve could be used
for further extrusion.
"""
direction = self.file.createIfcDirection(ref_x_direction)
ifc_position = self.file.createIfcAxis2Placement2D(
self.file.createIfcCartesianPoint(position), RefDirection=direction
)
ifc_ellipse = self.file.createIfcEllipse(
Position=ifc_position, SemiAxis1=x_axis_radius, SemiAxis2=y_axis_radius
)
if not trim_points:
if not trim_points_mask:
return ifc_ellipse
trim_points = self.get_trim_points_from_mask(
x_axis_radius, y_axis_radius, trim_points_mask, position_offset=position
)
trim1 = [self.file.createIfcCartesianPoint(trim_points[0])]
trim2 = [self.file.createIfcCartesianPoint(trim_points[1])]
trim_ellipse = self.file.createIfcTrimmedCurve(
BasisCurve=ifc_ellipse, Trim1=trim1, Trim2=trim2, SenseAgreement=True, MasterRepresentation="CARTESIAN"
)
return trim_ellipse
def profile(self, outer_curve, name=None, inner_curves=[], profile_type="AREA"):
# > inner_curves - list of IfcCurve;
# inner_curves could be used as a tool for boolean operation
# but if any point of inner curve will go outside the outer curve
# it will just add shape on top instead of "boolean" it
# because of that you can't create bool edges of outer_curve this way
# < returns IfcArbitraryClosedProfileDef or IfcArbitraryProfileDefWithVoids
if outer_curve.Dim != 2:
# TODO: replace with exception
print(
f"WARNING. Outer curve for IfcArbitraryClosedProfileDef/IfcIfcArbitraryProfileDefWithVoid should be 2D to be valid, currently it has {outer_curve.Dim} dimensions.\n"
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcArbitraryClosedProfileDef.htm#8.15.3.1.4-Formal-propositions"
)
import traceback
traceback.print_stack()
if inner_curves:
if not isinstance(inner_curves, collections.abc.Iterable):
inner_curves = [inner_curves]
# TODO: replace with exception
if any(curve.Dim != 2 for curve in inner_curves):
print(
"WARNING. InnerCurve for IfcIfcArbitraryProfileDefWithVoid sould be 2D to be valid, "
"currently on one of the inner curves is using different amount of dimensions.\n"
"Ref: https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcArbitraryClosedProfileDef.htm#8.15.3.1.4-Formal-propositions"
)
import traceback
traceback.print_stack()
profile = self.file.createIfcArbitraryProfileDefWithVoids(
ProfileName=name, ProfileType=profile_type, OuterCurve=outer_curve, InnerCurves=inner_curves
)
else:
profile = self.file.createIfcArbitraryClosedProfileDef(
ProfileName=name, ProfileType=profile_type, OuterCurve=outer_curve
)
return profile
def translate(self, curve_or_item, translation: Vector, create_copy=False):
# > curve_or_item - could be a list of curves or items or representations
# < returns translated object
multiple_objects = isinstance(curve_or_item, collections.abc.Iterable)
if not multiple_objects:
curve_or_item = [curve_or_item]
processed_objects = []
for c in curve_or_item:
if create_copy:
c = ifcopenshell.util.element.copy_deep(self.file, c)
if c.is_a("IfcIndexedPolyCurve"):
coords = [Vector(co) + translation for co in c.Points.CoordList]
c.Points.CoordList = coords
elif c.is_a("IfcCircle") or c.is_a("IfcExtrudedAreaSolid") or c.is_a("IfcEllipse"):
base_position = Vector(c.Position.Location.Coordinates)
c.Position.Location.Coordinates = base_position + translation
elif c.is_a("IfcShapeRepresentation"):
for item in c.Items:
self.translate(item, translation)
elif c.is_a("IfcTrimmedCurve"):
base_position = Vector(c.Trim1[0].Coordinates)
c.Trim1[0].Coordinates = base_position + translation
base_position = Vector(c.Trim2[0].Coordinates)
c.Trim2[0].Coordinates = base_position + translation
self.translate(c.BasisCurve, translation)
else:
raise Exception(f"{c} is not supported for translate() method.")
processed_objects.append(c)
return processed_objects if multiple_objects else processed_objects[0]
def rotate_2d_point(
self, point_2d: Vector, angle=90, pivot_point: Vector = Vector((0.0, 0.0)).freeze(), counter_clockwise=False
):
# > angle - in degrees
# < rotated Vector
angle_rad = angle / 180 * pi * (1 if counter_clockwise else -1)
relative_point = point_2d - pivot_point
relative_point = Matrix.Rotation(angle_rad, 2, "Z") @ relative_point
point_2d = relative_point + pivot_point
return point_2d
def rotate(
self,
curve_or_item,
angle=90,
pivot_point: Vector = Vector((0.0, 0.0)).freeze(),
counter_clockwise=False,
create_copy=False,
):
# > curve_or_item - could be a list of curves or items
# > angle - in degrees
# < returns rotated object
multiple_objects = isinstance(curve_or_item, collections.abc.Iterable)
if not multiple_objects:
curve_or_item = [curve_or_item]
processed_objects = []
for c in curve_or_item:
if create_copy:
c = ifcopenshell.util.element.copy_deep(self.file, c)
if c.is_a("IfcIndexedPolyCurve"):
coords = [
self.rotate_2d_point(Vector(co), angle, pivot_point, counter_clockwise) for co in c.Points.CoordList
]
c.Points.CoordList = coords
elif c.is_a("IfcCircle"):
base_position = Vector(c.Position.Location.Coordinates)
new_position = self.rotate_2d_point(base_position, angle, pivot_point, counter_clockwise)
c.Position.Location.Coordinates = new_position
elif c.is_a("IfcExtrudedAreaSolid"):
# TODO: add support for Z-axis too
base_position = Vector(c.Position.Location.Coordinates)
new_position = self.rotate_2d_point(base_position.to_2d(), angle, pivot_point, counter_clockwise)
new_position = new_position.to_3d()
new_position.z = base_position.z
c.Position.Location.Coordinates = new_position
# TODO: add inner axis too and test it
self.rotate(c.SweptArea.OuterCurve, angle, pivot_point, counter_clockwise)
else:
raise Exception(f"{c} is not supported for rotate() method.")
processed_objects.append(c)
return processed_objects if multiple_objects else processed_objects[0]
def mirror_2d_point(
self,
point_2d: Vector,
mirror_axes: Vector = Vector((1.0, 1.0)).freeze(),
mirror_point: Vector = Vector((0.0, 0.0)).freeze(),
):
"""mirror_axes - along which axes mirror will be applied"""
base = point_2d # prevent mutating the argument
mirror_axes = Vector([-1 if i > 0 else 1 for i in mirror_axes])
relative_point = base - mirror_point
relative_point = relative_point * mirror_axes
point_2d = relative_point + mirror_point
return point_2d
def get_axis2_placement_3d_matrix(self, axis2_placement_3d):
# > IfcAxis2Placement3D
p = axis2_placement_3d
M = Matrix.Identity(3)
x_axis = Vector(p.RefDirection.DirectionRatios)
z_axis = Vector(p.Axis.DirectionRatios)
x_angle = -x_axis.angle(M[0])
rotation_vector = x_axis.cross(M[0])
M_X_rotation = Matrix.Rotation(x_angle, 3, rotation_vector)
z_angle = -z_axis.angle(M[2])
rotation_vector = z_axis.cross(M[2])
M_Z_rotation = Matrix.Rotation(z_angle, 3, rotation_vector)
rotation_matrix = M_X_rotation @ M_Z_rotation
return rotation_matrix
def mirror(
self,
curve_or_item,
mirror_axes: Vector = Vector((1.0, 1.0)).freeze(),
mirror_point: Vector = Vector((0.0, 0.0)).freeze(),
create_copy=False,
placement_matrix=None,
):
"""mirror_axes - along which axes mirror will be applied
For example, mirroring `A(1,0)` by axis `(1,0)` will result in `A'(-1,0)`
"""
# > curve_or_item - could be a list of curves or items
# > mirror_axes - could be a list of mirrors to apply to curve_or_item
# multiple mirror_axes will result in multiple resulting curves
# example: curve_or_item = [a, b], mirror_axes=[v1, v2], result = [av1, av2, bv1, bv2]
# < returns mirrored object
# TODO: need to add placement_matrix for other types besides polycurve?
multiple_objects = isinstance(curve_or_item, collections.abc.Iterable)
curve_or_item = [curve_or_item] if not multiple_objects else curve_or_item
multiple_transformations = isinstance(mirror_axes, collections.abc.Iterable)
mirror_axes_data = [mirror_axes] if not multiple_transformations else mirror_axes
processed_objects = []
for curve_or_item_el in curve_or_item:
for mirror_axes in mirror_axes_data:
c = (
ifcopenshell.util.element.copy_deep(self.file, curve_or_item_el)
if create_copy
else curve_or_item_el
)
if c.is_a("IfcIndexedPolyCurve"):
inverted_placement_matrix = placement_matrix.inverted() if placement_matrix else None
coords = []
for co in c.Points.CoordList:
co_base = Vector(co)
if placement_matrix:
# TODO: add support for Z-axis too
co_base = placement_matrix @ co_base.to_3d()
co = self.mirror_2d_point(co_base.to_2d(), mirror_axes, mirror_point).to_3d()
co.z = co_base.z
co = (inverted_placement_matrix @ co).to_2d()
else:
co = self.mirror_2d_point(co_base, mirror_axes, mirror_point)
coords.append(co)
c.Points.CoordList = coords
elif c.is_a("IfcCircle") or c.is_a("IfcEllipse"):
base_position = Vector(c.Position.Location.Coordinates)
new_position = self.mirror_2d_point(base_position, mirror_axes, mirror_point)
c.Position.Location.Coordinates = new_position
elif c.is_a("IfcExtrudedAreaSolid"):
placement_matrix = self.get_axis2_placement_3d_matrix(c.Position)
base_position = Vector(c.Position.Location.Coordinates)
# TODO: add support for Z-axis too
new_position = self.mirror_2d_point(base_position.to_2d(), mirror_axes, mirror_point)
new_position = new_position.to_3d()
new_position.z = base_position.z
c.Position.Location.Coordinates = new_position
# TODO: add support for Z-axis too
self.translate(c.SweptArea.OuterCurve, base_position.to_2d())
self.mirror(c.SweptArea.OuterCurve, mirror_axes, mirror_point, placement_matrix=placement_matrix)
self.translate(c.SweptArea.OuterCurve, -new_position.to_2d())
if hasattr(c.SweptArea, "InnerCurves"):
for inner_curve in c.SweptArea.InnerCurves:
self.translate(inner_curve, base_position.to_2d())
self.mirror(inner_curve, mirror_axes, mirror_point, placement_matrix=placement_matrix)
self.translate(inner_curve, -new_position.to_2d())
# extrusion converted to world space
base_extruded_direction = Vector(c.ExtrudedDirection.DirectionRatios)
extruded_direction = placement_matrix @ base_extruded_direction
# TODO: add support for Z-axis too
# mirror point is ignored for extrusion direction
new_direction = self.mirror_2d_point(extruded_direction.to_2d(), mirror_axes, mirror_point=V(0, 0))
new_direction = new_direction.to_3d()
new_direction.z = extruded_direction.z
# extrusion direction converted back to placement space
new_direction = placement_matrix.inverted() @ new_direction
c.ExtrudedDirection.DirectionRatios = new_direction
elif c.is_a("IfcTrimmedCurve"):
trim_coords = [c.Trim1[0].Coordinates, c.Trim2[0].Coordinates]
trim_coords = [Vector(coords) for coords in trim_coords]
trim_coords = [
self.mirror_2d_point(base_position, mirror_axes, mirror_point) for base_position in trim_coords
]
# if mirror only by 1 axis we need to preserve the counter-clockwise order
# for the trim points
if 0 in mirror_axes:
trim_coords = [trim_coords[1], trim_coords[0]]
base_position = Vector(c.Trim1[0].Coordinates)
c.Trim1[0].Coordinates, c.Trim2[0].Coordinates = trim_coords
self.mirror(c.BasisCurve, mirror_axes, mirror_point)
else:
raise Exception(f"{c} is not supported for mirror() method.")
processed_objects.append(c)
return processed_objects if (multiple_objects or multiple_transformations) else processed_objects[0]
def extrude(
self,
profile_or_curve,
magnitude=1.0,
position: Vector = Vector([0.0, 0.0, 0.0]).freeze(),
extrusion_vector: Vector = Vector((0.0, 0.0, 1.0)).freeze(),
position_z_axis: Vector = Vector((0.0, 0.0, 1.0)).freeze(),
position_x_axis: Vector = Vector((1.0, 0.0, 0.0)).freeze(),
position_y_axis: Vector = None,
):
"""Extrude profile or curve to get IfcExtrudedAreaSolid.
REMEMBER when handling custom axes - IFC is using RIGHT handed coordinate system.
Position and position axes are in world space, extrusion vector in placement space defined by
position_x_axis/position_y_axis/position_z_axis
"""
# > profile_or_curve
# > extrusion vector - as defined in coordinate system position_x_axis+position_z_axis
# > position - as defined in default IFC coordinate system, not in position_x_axis+position_z_axis
# > position_y_axis - optional, could be used to calculate Z-axis based on Y-axis
# < IfcExtrudedAreaSolid
if profile_or_curve.is_a() not in ("IfcArbitraryClosedProfileDef", "IfcArbitraryProfileDefWithVoids"):
profile_or_curve = self.profile(profile_or_curve)
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_direction = self.file.createIfcDirection(extrusion_vector)
extruded_area = self.file.createIfcExtrudedAreaSolid(
SweptArea=profile_or_curve, Position=ifc_position, ExtrudedDirection=ifc_direction, Depth=magnitude
)
return extruded_area
def get_representation(self, context, items):
# > items - could be a list or single curve/IfcExtrudedAreaSolid
# < IfcShapeRepresentation
if not isinstance(items, collections.abc.Iterable):
items = [items]
if items[0].is_a("IfcExtrudedAreaSolid"):
representation_type = "SweptSolid"
elif items[0].is_a("IfcCurve") and items[0].Dim == 3:
representation_type = "Curve3D"
else:
representation_type = "Curve2D"
representation = self.file.createIfcShapeRepresentation(
ContextOfItems=context,
RepresentationIdentifier=context.ContextIdentifier,
RepresentationType=representation_type,
Items=items,
)
return representation
def deep_copy(self, element):
return ifcopenshell.util.element.copy_deep(self.file, element)