This commit is contained in:
Andrej730
2025-03-04 11:58:59 +05:00
parent 33d3984990
commit 53a3afaded
25 changed files with 282 additions and 167 deletions
@@ -21,159 +21,203 @@ import ifcopenshell.util.element
import ifcopenshell.util.shape
import ifcopenshell.util.unit
from ifcopenshell.util.data import Clipping
from typing import Any, Union, Optional, Literal
from typing import Any, Union, Optional, Literal, get_args
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,
# IfcGeometricRepresentationContext
context: ifcopenshell.entity_instance,
profile: ifcopenshell.entity_instance,
# in meters
depth: float = 1.0,
cardinal_point: Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] = 5,
# 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`
# 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
usecase.settings = {
"context": context,
"profile": profile,
"depth": depth,
"cardinal_point": cardinal_point,
"clippings": clippings if clippings is not None else [],
"placement_zx_axes": placement_zx_axes,
}
return usecase.execute()
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
settings: dict[str, Any]
clippings: list[Clipping]
def execute(self):
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
self.settings["clippings"] = [Clipping.parse(c) for c in self.settings["clippings"]]
return self.file.createIfcShapeRepresentation(
self.settings["context"],
self.settings["context"].ContextIdentifier,
"Clipping" if self.settings["clippings"] else "SweptSolid",
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):
def create_item(self) -> ifcopenshell.entity_instance:
point = self.get_point()
placement = self.file.createIfcAxis2Placement3D(
point,
self.file.createIfcDirection(self.settings["placement_zx_axes"][0] or (0.0, 0.0, 1.0)),
self.file.createIfcDirection(self.settings["placement_zx_axes"][1] or (1.0, 0.0, 0.0)),
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.createIfcExtrudedAreaSolid(
self.settings["profile"],
extrusion = self.file.create_entity(
"IfcExtrudedAreaSolid",
self.profile,
placement,
self.file.createIfcDirection((0.0, 0.0, 1.0)),
self.convert_si_to_unit(self.settings["depth"]),
self.convert_si_to_unit(self.depth),
)
if self.settings["clippings"]:
if self.clippings:
return self.apply_clippings(extrusion)
return extrusion
def apply_clippings(self, first_operand):
while self.settings["clippings"]:
clipping = self.settings["clippings"].pop()
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.settings["unit_scale"])
first_operand = clipping.apply(self.file, first_operand, self.unit_scale)
return first_operand
def convert_si_to_unit(self, co):
return co / self.settings["unit_scale"]
def convert_si_to_unit(self, co: float) -> float:
return co / self.unit_scale
def get_point(self):
if not self.settings["cardinal_point"]:
def get_point(self) -> ifcopenshell.entity_instance:
if not self.cardinal_point:
return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
elif self.settings["cardinal_point"] == 1:
elif self.cardinal_point == "bottom left":
return self.file.createIfcCartesianPoint((-self.get_x() / 2, self.get_y() / 2, 0.0))
elif self.settings["cardinal_point"] == 2:
elif self.cardinal_point == "bottom centre":
return self.file.createIfcCartesianPoint((0.0, self.get_y() / 2, 0.0))
elif self.settings["cardinal_point"] == 3:
elif self.cardinal_point == "bottom right":
return self.file.createIfcCartesianPoint((self.get_x() / 2, self.get_y() / 2, 0.0))
elif self.settings["cardinal_point"] == 4:
elif self.cardinal_point == "mid-depth left":
return self.file.createIfcCartesianPoint((-self.get_x() / 2, 0.0, 0.0))
elif self.settings["cardinal_point"] == 5:
elif self.cardinal_point == "mid-depth centre":
return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))
elif self.settings["cardinal_point"] == 6:
elif self.cardinal_point == "mid-depth right":
return self.file.createIfcCartesianPoint((self.get_x() / 2, 0.0, 0.0))
elif self.settings["cardinal_point"] == 7:
elif self.cardinal_point == "top left":
return self.file.createIfcCartesianPoint((-self.get_x() / 2, -self.get_y() / 2, 0.0))
elif self.settings["cardinal_point"] == 8:
elif self.cardinal_point == "top centre":
return self.file.createIfcCartesianPoint((0.0, -self.get_y() / 2, 0.0))
elif self.settings["cardinal_point"] == 9:
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):
if self.settings["profile"].is_a("IfcAsymmetricIShapeProfileDef"):
return self.settings["profile"].OverallWidth
elif self.settings["profile"].is_a("IfcCShapeProfileDef"):
return self.settings["profile"].Width
elif self.settings["profile"].is_a("IfcCircleProfileDef"):
return self.settings["profile"].Radius * 2
elif self.settings["profile"].is_a("IfcEllipseProfileDef"):
return self.settings["profile"].SemiAxis1 * 2
elif self.settings["profile"].is_a("IfcIShapeProfileDef"):
return self.settings["profile"].OverallWidth
elif self.settings["profile"].is_a("IfcLShapeProfileDef"):
return self.settings["profile"].Width
elif self.settings["profile"].is_a("IfcRectangleProfileDef"):
return self.settings["profile"].XDim
elif self.settings["profile"].is_a("IfcTShapeProfileDef"):
return self.settings["profile"].FlangeWidth
elif self.settings["profile"].is_a("IfcUShapeProfileDef"):
return self.settings["profile"].FlangeWidth
elif self.settings["profile"].is_a("IfcZShapeProfileDef"):
return (self.settings["profile"].FlangeWidth * 2) - self.settings["profile"].WebThickness
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.settings["profile"])
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):
if self.settings["profile"].is_a("IfcAsymmetricIShapeProfileDef"):
return self.settings["profile"].OverallDepth
elif self.settings["profile"].is_a("IfcCShapeProfileDef"):
return self.settings["profile"].Depth
elif self.settings["profile"].is_a("IfcCircleProfileDef"):
return self.settings["profile"].Radius * 2
elif self.settings["profile"].is_a("IfcEllipseProfileDef"):
return self.settings["profile"].SemiAxis2 * 2
elif self.settings["profile"].is_a("IfcIShapeProfileDef"):
return self.settings["profile"].OverallDepth
elif self.settings["profile"].is_a("IfcLShapeProfileDef"):
return self.settings["profile"].Depth
elif self.settings["profile"].is_a("IfcRectangleProfileDef"):
return self.settings["profile"].YDim
elif self.settings["profile"].is_a("IfcTShapeProfileDef"):
return self.settings["profile"].Depth
elif self.settings["profile"].is_a("IfcUShapeProfileDef"):
return self.settings["profile"].Depth
elif self.settings["profile"].is_a("IfcZShapeProfileDef"):
return self.settings["profile"].Depth
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.settings["profile"])
shape = ifcopenshell.geom.create_shape(settings, self.profile)
return self.convert_si_to_unit(ifcopenshell.util.shape.get_y(shape))
return 0.0
@@ -149,6 +149,8 @@ class Usecase:
self.settings_2d.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(self.settings_2d, dummy_solid)
# NOTE: points do not need unit conversion
# as dummy file is inherently using project units.
if self.cardinal_point == 1:
return self.get_bottom_left(shape)
elif self.cardinal_point == 2: