Compare commits

...

3 Commits

Author SHA1 Message Date
Bruno Postle 2f05d36272 shape_builder: fix half_space_solid agreement_flag docstring 2026-03-23 22:31:28 +00:00
Bruno Postle 7a4c825d9f shape_builder: warn about mixed item types in get_representation
Generated with the assistance of an AI coding tool.
2026-03-23 22:30:33 +00:00
Bruno Postle 412477d0c7 shape_builder: complete docstrings and return type annotations
Generated with the assistance of an AI coding tool.
2026-03-23 22:30:18 +00:00
@@ -517,11 +517,18 @@ class ShapeBuilder:
trim_points_mask: Sequence[int],
position_offset: Optional[VectorType] = None,
) -> np.ndarray:
"""Handy way to get edge points of the ellipse like shape of a given radiuses.
"""Get cardinal-point coordinates of an ellipse by index mask.
Mask points are numerated from 0 to 3 ccw starting from (x_axis_radius/2; 0).
The four cardinal points are numbered 03 counter-clockwise starting from the
positive X axis: 0 → ``(x, 0)``, 1 → ``(0, y)``, 2 → ``(-x, 0)``, 3 → ``(0, -y)``.
Example: mask (0, 1, 2, 3) will return points (x, 0), (0, y), (-x, 0), (0, -y)
Example: mask ``(0, 1, 2, 3)`` returns all four points in order.
:param x_axis_radius: Radius (semi-axis length) along the X axis.
:param y_axis_radius: Radius (semi-axis length) along the Y axis.
:param trim_points_mask: Sequence of cardinal-point indices (03) to select.
:param position_offset: Optional 2D offset added to all returned points.
:return: Numpy array of the selected 2D points.
"""
points = np.array(
(
@@ -546,15 +553,23 @@ class ShapeBuilder:
ref_x_direction: VectorType = (1.0, 0.0),
trim_points_mask: Sequence[int] = (),
) -> ifcopenshell.entity_instance:
"""
Ellipse trimming points should be specified in counter clockwise order.
"""Create an IfcEllipse, optionally trimmed to an arc.
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)
If neither ``trim_points`` nor ``trim_points_mask`` is provided, a full IfcEllipse is returned.
Trimming points must be given in counter-clockwise order. For example, to get the arc
above the Y-axis use mask ``(0, 2)``; below the Y-axis use ``(2, 0)``.
For more information about trim_points_mask check builder.get_trim_points_from_mask
A trimmed result (IfcTrimmedCurve) includes a closing segment between the trim points,
making it suitable for use as a profile in :meth:`extrude`.
Notion: trimmed ellipse also contains polyline between trim points, meaning IfcTrimmedCurve could be used
for further extrusion.
:param x_axis_radius: Semi-axis length along the local X axis.
:param y_axis_radius: Semi-axis length along the local Y axis.
:param position: 2D centre of the ellipse.
:param trim_points: Explicit pair of 2D trim points. Takes precedence over ``trim_points_mask``.
:param ref_x_direction: Direction of the local X axis.
:param trim_points_mask: Pair of cardinal-point indices (03) used when ``trim_points`` is empty.
See :meth:`get_trim_points_from_mask` for index definitions.
:return: IfcEllipse (untrimmed) or IfcTrimmedCurve (trimmed).
"""
ifc_position = self.create_axis2_placement_2d(position, ref_x_direction)
ifc_ellipse = self.file.createIfcEllipse(
@@ -685,6 +700,14 @@ class ShapeBuilder:
pivot_point: VectorType = (0.0, 0.0),
counter_clockwise: bool = False,
) -> np.ndarray:
"""Rotate a single 2D point around a pivot.
:param point_2d: The 2D point to rotate.
:param angle: Rotation angle, in degrees. Defaults to 90.
:param pivot_point: The point to rotate around.
:param counter_clockwise: If True, rotate counter-clockwise. Defaults to clockwise.
:return: Rotated 2D point as a numpy array.
"""
angle_rad = radians(angle) * (1 if counter_clockwise else -1)
relative_point = np.array(point_2d) - pivot_point
relative_point = np_rotation_matrix(angle_rad, 2) @ relative_point
@@ -752,7 +775,16 @@ class ShapeBuilder:
mirror_axes: VectorType = (1.0, 1.0),
mirror_point: VectorType = (0.0, 0.0),
) -> np.ndarray:
"""mirror_axes - along which axes mirror will be applied"""
"""Mirror a single 2D point across the specified axes.
:param point_2d: The 2D point to mirror.
:param mirror_axes: Indicates which axes to mirror across. A positive value in a
component means that axis is mirrored (negated relative to ``mirror_point``).
Example: ``(1, 0)`` mirrors across the Y-axis (negates X only),
``(1, 1)`` mirrors across both axes.
:param mirror_point: Origin of the mirror operation.
:return: Mirrored 2D point as a numpy array.
"""
mirror_axes: np.ndarray = np.where(np.array(mirror_axes) > 0, -1, 1)
mirror_point: np.ndarray = np.array(mirror_point)
relative_point = point_2d - mirror_point
@@ -798,7 +830,13 @@ class ShapeBuilder:
def create_axis2_placement_2d(
self, position: VectorType = (0.0, 0.0), x_direction: Optional[VectorType] = None
) -> ifcopenshell.entity_instance:
"""Create IfcAxis2Placement2D."""
"""Create IfcAxis2Placement2D.
:param position: 2D origin of the placement.
:param x_direction: Direction of the local X axis. If not provided, defaults to
the global X axis ``(1, 0)``.
:return: IfcAxis2Placement2D
"""
ref_direction = (
self.file.create_entity("IfcDirection", ifc_safe_vector_type(x_direction)) if x_direction else None
)
@@ -1000,7 +1038,7 @@ class ShapeBuilder:
) -> ifcopenshell.entity_instance:
"""
:param plane: The IfcPlane representing the half space.
:param agreement_flag: False if +Z represents the void
:param agreement_flag: If False (default), the plane normal points toward the **removed** material (the void). The kept region is on the opposite side from the normal.
:return: IfcHalfSpaceSolid
"""
return self.file.createIfcHalfSpaceSolid(plane, AgreementFlag=agreement_flag)
@@ -1053,7 +1091,14 @@ class ShapeBuilder:
def create_swept_disk_solid(
self, path_curve: ifcopenshell.entity_instance, radius: float
) -> ifcopenshell.entity_instance:
"""Create IfcSweptDiskSolid from `path_curve` (must be 3D) and `radius`"""
"""Create an IfcSweptDiskSolid — a circular cross-section swept along a 3D path.
Useful for modelling round pipes, conduits, and cables.
:param path_curve: A 3D curve entity defining the centreline path. Must have ``Dim == 3``.
:param radius: Radius of the circular disk cross-section.
:return: IfcSweptDiskSolid
"""
if path_curve.Dim != 3:
raise Exception(
f"Path curve for IfcSweptDiskSolid should be 3D to be valid, currently it has {path_curve.Dim} dimensions.\n"
@@ -1071,10 +1116,22 @@ class ShapeBuilder:
) -> ifcopenshell.entity_instance:
"""Create IFC representation for the specified context and items.
**All items must belong to the same geometry category.** IFC prohibits
mixing incompatible item types in one representation (e.g.
``IfcExtrudedAreaSolid`` with ``IfcBlock``, or solids with curves).
When ``representation_type`` is omitted the type is inferred via
:func:`ifcopenshell.util.representation.guess_type`; if the items are
heterogeneous ``guess_type`` returns ``None`` and the representation is
written with no ``RepresentationType``, which fails IFC validation.
Avoid mixing swept-solid primitives (``IfcExtrudedAreaSolid``,
``IfcRevolvedAreaSolid``) with CSG primitives (``IfcBlock``,
``IfcSphere``, etc.) or any other category in a single call.
:param context: IfcGeometricRepresentationSubContext
:param items: could be a list or single curve/IfcExtrudedAreaSolid
:param items: A single item or list of items, all of the same geometry
category (e.g. all ``IfcExtrudedAreaSolid``, all ``IfcIndexedPolyCurve``)
:param representation_type: Explicitly specified RepresentationType.
If not provided it will be guessed from the items types
If not provided it will be guessed from the items types.
:return: IfcShapeRepresentation
"""
if not isinstance(items, collections.abc.Iterable):
@@ -1096,18 +1153,26 @@ class ShapeBuilder:
)
def deep_copy(self, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
"""Create a deep copy of an IFC element and all its referenced entities.
:param element: The IFC entity to copy.
:return: A new independent copy of the element.
"""
return ifcopenshell.util.element.copy_deep(self.file, element)
# UTILITIES
def extrude_kwargs(self, axis: Literal["Y", "X", "Z"]) -> dict[str, tuple[float, float, float]]:
"""Shortcut to get kwargs for `ShapeBuilder.extrude` to extrude by some axis.
"""Shortcut to get kwargs for :meth:`extrude` to extrude along a principal axis.
It assumes you have 2D profile in:
XZ plane for Y axis extrusion, \n
YZ plane for X axis extrusion, \n
XY plane for Z axis extrusion, \n
Assumes the 2D profile lies in the plane perpendicular to the extrusion axis:
XZ plane for Y-axis extrusion, YZ plane for X-axis extrusion, XY plane for Z-axis extrusion.
Extruding by X/Y using other kwargs might break ValidExtrusionDirection."""
Extruding along X or Y with other kwargs may violate the IFC ValidExtrusionDirection constraint.
:param axis: The extrusion axis: ``'X'``, ``'Y'``, or ``'Z'``.
:return: A dict with keys ``position_x_axis``, ``position_z_axis``, and ``extrusion_vector``
suitable for passing as ``**kwargs`` to :meth:`extrude`.
"""
if axis == "Y":
return {
@@ -1131,13 +1196,16 @@ class ShapeBuilder:
def rotate_extrusion_kwargs_by_z(
self, kwargs: dict[str, Any], angle: float, counter_clockwise: bool = False
) -> dict[str, VectorType]:
"""shortcut to rotate extrusion kwargs by z axis
"""Rotate extrusion kwargs around the Z axis.
`kwargs` expected to have `position_x_axis` and `position_z_axis` keys
A shortcut to rotate the ``position_x_axis`` and ``position_z_axis`` values returned by
:meth:`extrude_kwargs` around the Z axis before passing them to :meth:`extrude`.
`angle` is a rotation value in radians
by default rotation is clockwise, to make it counter clockwise use `counter_clockwise` flag
:param kwargs: A dict with ``position_x_axis`` and ``position_z_axis`` keys,
as returned by :meth:`extrude_kwargs`. The original dict is not mutated.
:param angle: Rotation angle, in radians.
:param counter_clockwise: If True, rotate counter-clockwise. Defaults to clockwise.
:return: A new dict with ``position_x_axis`` and ``position_z_axis`` rotated around Z.
"""
rot = np_rotation_matrix(-angle, 3, "Z")
kwargs = kwargs.copy() # prevent mutation of original kwargs
@@ -1146,7 +1214,11 @@ class ShapeBuilder:
return kwargs
def get_polyline_coords(self, polyline: ifcopenshell.entity_instance) -> np.ndarray:
"""polyline should be either `IfcIndexedPolyCurve` or `IfcPolyline`"""
"""Extract the coordinate array from a polyline entity.
:param polyline: An ``IfcIndexedPolyCurve`` or ``IfcPolyline`` entity.
:return: Numpy array of the polyline's point coordinates.
"""
coords = None
if polyline.is_a("IfcIndexedPolyCurve"):
coords = np.array(polyline.Points.CoordList)
@@ -1157,7 +1229,12 @@ class ShapeBuilder:
return coords
def set_polyline_coords(self, polyline: ifcopenshell.entity_instance, coords: SequenceOfVectors) -> None:
"""polyline should be either `IfcIndexedPolyCurve` or `IfcPolyline`"""
"""Update the coordinates of a polyline entity in-place.
:param polyline: An ``IfcIndexedPolyCurve`` or ``IfcPolyline`` entity.
:param coords: New sequence of point coordinates. Must contain the same number of
points as the original polyline.
"""
if polyline.is_a("IfcIndexedPolyCurve"):
polyline.Points.CoordList = ifc_safe_vector_type(coords)
elif polyline.is_a("IfcPolyline"):
@@ -1296,6 +1373,18 @@ class ShapeBuilder:
WallThickness: float,
FilletRadius: float,
) -> ifcopenshell.entity_instance:
"""Create a Z-profile (cold-formed steel section) outline curve with lips and fillets.
All dimensions are in the IFC project's length units.
:param FirstFlangeWidth: Width of the first (top) flange, measured from the web centreline.
:param SecondFlangeWidth: Width of the second (bottom) flange, measured from the web centreline.
:param Depth: Total depth of the section (web height).
:param Girth: Length of the return lips on each flange.
:param WallThickness: Uniform material thickness.
:param FilletRadius: Inner bend radius at each corner.
:return: IfcIndexedPolyCurve representing the closed Z-profile outline.
"""
x1 = FirstFlangeWidth
x2 = SecondFlangeWidth
y = Depth / 2
@@ -1337,10 +1426,17 @@ class ShapeBuilder:
def create_transition_arc_ifc(
self, width: float, height: float, create_ifc_curve: bool = False
) -> tuple[SequenceOfVectors, list[list[int]], Union[ifcopenshell.entity_instance, None]]:
"""Create an arc in the rectangle with specified width and height.
"""Create an arc fitting inside a rectangle of the given width and height.
If it's not possible to make a complete arc, create an arc with longest radius possible
and straight segment in the middle.
If a single arc cannot span the full width, the longest possible radius is used and
a straight segment is inserted in the middle.
:param width: Width of the bounding rectangle.
:param height: Height of the bounding rectangle (also the maximum arc radius).
:param create_ifc_curve: If True, also create and return an ``IfcIndexedPolyCurve``.
If False, only return the raw point and segment data.
:return: A tuple ``(points, segments, ifc_curve)`` where ``ifc_curve`` is an
``IfcIndexedPolyCurve`` when ``create_ifc_curve=True``, otherwise ``None``.
"""
fillet_size = (width / 2) / height
if fillet_size <= 1:
@@ -1370,6 +1466,14 @@ class ShapeBuilder:
return points, segments, transition_arc
def mesh(self, points: SequenceOfVectors, faces: Sequence[Sequence[int]]) -> ifcopenshell.entity_instance:
"""Create a tessellated mesh from points and face indices.
Delegates to :meth:`faceted_brep` for IFC2X3, or :meth:`polygonal_face_set` for IFC4 and later.
:param points: List of 3D coordinates.
:param faces: List of faces, each face a sequence of zero-based point indices.
:return: IfcFacetedBrep (IFC2X3) or IfcPolygonalFaceSet (IFC4+).
"""
if self.file.schema == "IFC2X3":
return self.faceted_brep(points, faces)
return self.polygonal_face_set(points, faces)
@@ -1723,11 +1827,20 @@ class ShapeBuilder:
angle: float,
profile_offset: VectorType = (0.0, 0.0),
verbose: bool = True,
):
"""get the final transition length for two profiles dimensions, angle and XY offset between them,
) -> Optional[float]:
"""Get the transition length for two profile half-dimensions, an angle, and an XY offset.
the difference from `calculate_transition` - `get_transition_length` is making sure
that length will fit both sides of the transition
Unlike :meth:`mep_transition_calculate`, this method checks that the resulting length
satisfies the angle constraint from both the start and end profile perspectives.
:param start_half_dim: Half-dimensions of the start profile as a 3-element array
``[half_x, half_y, depth]``. For circular profiles ``half_x == half_y == radius``.
:param end_half_dim: Half-dimensions of the end profile in the same format.
:param angle: Maximum allowed transition angle, in degrees.
:param profile_offset: 2D XY offset between the centrelines of the start and end profiles.
:param verbose: If True, print diagnostic values during calculation.
:return: Transition length in project length units, or ``None`` if no valid length exists
for the given angle and offset.
"""
print = lambda *args, **kwargs: __builtins__["print"](*args, **kwargs) if verbose else None
np_X, np_Y = 0, 1
@@ -1788,9 +1901,23 @@ class ShapeBuilder:
angle: Optional[float] = None,
verbose: bool = True,
) -> Union[float, None]:
"""will return transition length based on the profile dimension differences and offset.
"""Calculate MEP transition length from angle, or transition angle from length.
If `length` is provided will return transition angle"""
Low-level calculation kernel used by :meth:`mep_transition_length`. Provide either
``angle`` or ``length`` (not both); the other value is computed and returned.
:param start_half_dim: Half-dimensions of the start profile ``[half_x, half_y, depth]``.
:param end_half_dim: Half-dimensions of the end profile ``[half_x, half_y, depth]``.
:param offset: 2D XY offset between profile centrelines.
:param diff: Pre-computed absolute difference of start and end half-dimensions (XY only).
Computed from ``start_half_dim`` and ``end_half_dim`` if not provided.
:param end_profile: If True, swap X and Y axes to compute from the end-profile perspective.
:param length: Known transition length. If provided, the corresponding angle is returned.
:param angle: Known transition angle, in degrees. If provided, the corresponding length is returned.
:param verbose: If True, print diagnostic values during calculation.
:return: Transition length (if ``angle`` was given) or transition angle in degrees
(if ``length`` was given), or ``None`` if the geometry is not feasible.
"""
print = lambda *args, **kwargs: __builtins__["print"](*args, **kwargs) if verbose else None