Files
2026-05-31 10:17:09 +07:00

225 lines
9.9 KiB
Python

# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# 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/>.
from typing import Any, Literal, Optional, Union, get_args
import ifcopenshell.geom
import ifcopenshell.util.element
import ifcopenshell.util.shape
import ifcopenshell.util.unit
from ifcopenshell.util.data import Clipping
VECTOR_3D = tuple[float, float, float]
CardinalPointNumeric = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
CardinalPointString = Literal[
"bottom left",
"bottom centre",
"bottom right",
"mid-depth left",
"mid-depth centre",
"mid-depth right",
"top left",
"top centre",
"top right",
"geometric centroid",
"bottom in line with the geometric centroid",
"left in line with the geometric centroid",
"right in line with the geometric centroid",
"top in line with the geometric centroid",
"shear centre",
"bottom in line with the shear centre",
"left in line with the shear centre",
"right in line with the shear centre",
"top in line with the shear centre",
]
CARDINAL_POINT_VALUES: tuple[CardinalPointString, ...] = get_args(CardinalPointString)
CardinalPoint = Union[CardinalPointNumeric, CardinalPointString]
def add_profile_representation(
file: ifcopenshell.file,
context: ifcopenshell.entity_instance,
profile: ifcopenshell.entity_instance,
depth: float = 1.0,
# TODO: None makes more sense as default value?
cardinal_point: Union[CardinalPoint, None] = 5,
clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None,
placement_zx_axes: tuple[Union[VECTOR_3D, None], Union[VECTOR_3D, None]] = (None, None),
) -> ifcopenshell.entity_instance:
"""Add profile representation.
:param context: The IfcGeometricRepresentationContext for the representation,
only Model/Body/MODEL_VIEW type of representations are currently supported.
:param profile: The IfcProfileDef to extrude.
:param depth: The depth of the extrusion in meters.
:param cardinal_point: The cardinal point of the profile.
:param clippings: A list of planes that define clipping half space solids.
Planes are defined either by Clipping objects
or by dictionaries of arguments for `Clipping.parse`.
:param placement_zx_axes: A tuple of two vectors that define the placement of the profile.
The first vector is the Z axis, the second vector is the X axis.
:return: IfcShapeRepresentation.
"""
usecase = Usecase()
usecase.file = file
clippings = clippings if clippings is not None else []
return usecase.execute(context, profile, depth, cardinal_point, clippings, placement_zx_axes)
class Usecase:
file: ifcopenshell.file
clippings: list[Clipping]
def execute(
self,
context: ifcopenshell.entity_instance,
profile: ifcopenshell.entity_instance,
depth: float,
cardinal_point: Union[CardinalPoint, None],
clippings: list[Union[Clipping, dict[str, Any]]],
placement_zx_axes: tuple[Union[VECTOR_3D, None], Union[VECTOR_3D, None]],
) -> ifcopenshell.entity_instance:
if isinstance(cardinal_point, int):
cardinal_point = CARDINAL_POINT_VALUES[cardinal_point - 1]
self.cardinal_point = cardinal_point
self.profile = profile
self.clippings = [Clipping.parse(c) for c in clippings]
self.depth = depth
self.placement_zx_axes = placement_zx_axes
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
return self.file.create_entity(
"IfcShapeRepresentation",
context,
context.ContextIdentifier,
"Clipping" if self.clippings else "SweptSolid",
[self.create_item()],
)
def create_item(self) -> ifcopenshell.entity_instance:
point = self.get_point()
placement = self.file.createIfcAxis2Placement3D(
point,
self.file.create_entity("IfcDirection", self.placement_zx_axes[0] or (0.0, 0.0, 1.0)),
self.file.create_entity("IfcDirection", self.placement_zx_axes[1] or (1.0, 0.0, 0.0)),
)
extrusion = self.file.create_entity(
"IfcExtrudedAreaSolid",
self.profile,
placement,
self.file.createIfcDirection((0.0, 0.0, 1.0)),
self.convert_si_to_unit(self.depth),
)
if self.clippings:
return self.apply_clippings(extrusion)
return extrusion
def apply_clippings(self, first_operand: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
while self.clippings:
clipping = self.clippings.pop()
if isinstance(clipping, ifcopenshell.entity_instance):
new = ifcopenshell.util.element.copy(self.file, clipping)
new.FirstOperand = first_operand
first_operand = new
else: # Clipping
first_operand = clipping.apply(self.file, first_operand, self.unit_scale)
return first_operand
def convert_si_to_unit(self, co: float) -> float:
return co / self.unit_scale
def get_point(self) -> ifcopenshell.entity_instance:
if not self.cardinal_point:
return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
elif self.cardinal_point == "bottom left":
return self.file.createIfcCartesianPoint((-self.get_x() / 2, self.get_y() / 2, 0.0))
elif self.cardinal_point == "bottom centre":
return self.file.createIfcCartesianPoint((0.0, self.get_y() / 2, 0.0))
elif self.cardinal_point == "bottom right":
return self.file.createIfcCartesianPoint((self.get_x() / 2, self.get_y() / 2, 0.0))
elif self.cardinal_point == "mid-depth left":
return self.file.createIfcCartesianPoint((-self.get_x() / 2, 0.0, 0.0))
elif self.cardinal_point == "mid-depth centre":
return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
elif self.cardinal_point == "mid-depth right":
return self.file.createIfcCartesianPoint((self.get_x() / 2, 0.0, 0.0))
elif self.cardinal_point == "top left":
return self.file.createIfcCartesianPoint((-self.get_x() / 2, -self.get_y() / 2, 0.0))
elif self.cardinal_point == "top centre":
return self.file.createIfcCartesianPoint((0.0, -self.get_y() / 2, 0.0))
elif self.cardinal_point == "top right":
return self.file.createIfcCartesianPoint((self.get_x() / 2, -self.get_y() / 2, 0.0))
# TODO other cardinal points
return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
def get_x(self) -> float:
if self.profile.is_a("IfcAsymmetricIShapeProfileDef"):
return self.profile.OverallWidth
elif self.profile.is_a("IfcCShapeProfileDef"):
return self.profile.Width
elif self.profile.is_a("IfcCircleProfileDef"):
return self.profile.Radius * 2
elif self.profile.is_a("IfcEllipseProfileDef"):
return self.profile.SemiAxis1 * 2
elif self.profile.is_a("IfcIShapeProfileDef"):
return self.profile.OverallWidth
elif self.profile.is_a("IfcLShapeProfileDef"):
return self.profile.Width
elif self.profile.is_a("IfcRectangleProfileDef"):
return self.profile.XDim
elif self.profile.is_a("IfcTShapeProfileDef"):
return self.profile.FlangeWidth
elif self.profile.is_a("IfcUShapeProfileDef"):
return self.profile.FlangeWidth
elif self.profile.is_a("IfcZShapeProfileDef"):
return (self.profile.FlangeWidth * 2) - self.profile.WebThickness
else:
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, self.profile)
return self.convert_si_to_unit(ifcopenshell.util.shape.get_x(shape))
return 0.0
def get_y(self) -> float:
if self.profile.is_a("IfcAsymmetricIShapeProfileDef"):
return self.profile.OverallDepth
elif self.profile.is_a("IfcCShapeProfileDef"):
return self.profile.Depth
elif self.profile.is_a("IfcCircleProfileDef"):
return self.profile.Radius * 2
elif self.profile.is_a("IfcEllipseProfileDef"):
return self.profile.SemiAxis2 * 2
elif self.profile.is_a("IfcIShapeProfileDef"):
return self.profile.OverallDepth
elif self.profile.is_a("IfcLShapeProfileDef"):
return self.profile.Depth
elif self.profile.is_a("IfcRectangleProfileDef"):
return self.profile.YDim
elif self.profile.is_a("IfcTShapeProfileDef"):
return self.profile.Depth
elif self.profile.is_a("IfcUShapeProfileDef"):
return self.profile.Depth
elif self.profile.is_a("IfcZShapeProfileDef"):
return self.profile.Depth
else:
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, self.profile)
return self.convert_si_to_unit(ifcopenshell.util.shape.get_y(shape))
return 0.0