From bb8e84e5ec89baf49fafdbcbcdf6acb1e2df4acc Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 15 May 2024 10:55:29 +0500 Subject: [PATCH] typing --- .../blenderbim/bim/module/search/operator.py | 1 + .../boundary/assign_connection_geometry.py | 1 - .../ifcopenshell/api/context/add_context.py | 13 +- .../ifcopenshell/api/context/edit_context.py | 9 +- .../api/cost/add_cost_schedule.py | 4 +- .../api/document/edit_information.py | 8 +- .../api/document/edit_reference.py | 8 +- .../api/geometry/add_axis_representation.py | 7 +- .../ifcopenshell/api/geometry/add_boolean.py | 50 +++- .../api/geometry/add_door_representation.py | 267 +++++++++++++----- .../geometry/add_footprint_representation.py | 14 +- .../api/geometry/add_mesh_representation.py | 45 ++- .../geometry/add_profile_representation.py | 34 ++- .../geometry/add_railing_representation.py | 100 +++++-- .../api/geometry/add_representation.py | 76 +++-- .../api/geometry/add_slab_representation.py | 28 +- .../api/geometry/add_wall_representation.py | 40 ++- .../api/geometry/add_window_representation.py | 258 +++++++++++++---- .../api/geometry/assign_representation.py | 8 +- .../api/geometry/connect_element.py | 16 +- .../ifcopenshell/api/geometry/connect_path.py | 22 +- .../api/geometry/create_2pt_wall.py | 12 +- .../api/geometry/disconnect_element.py | 33 +-- .../api/geometry/disconnect_path.py | 39 +-- .../api/geometry/edit_object_placement.py | 4 +- .../api/geometry/map_representation.py | 11 +- .../api/geometry/remove_boolean.py | 6 +- .../api/geometry/unassign_representation.py | 8 +- .../api/georeference/add_georeferencing.py | 4 +- .../api/georeference/edit_georeferencing.py | 16 +- .../api/georeference/remove_georeferencing.py | 4 +- .../api/grid/create_axis_curve.py | 7 +- .../ifcopenshell/api/grid/create_grid_axis.py | 24 +- .../ifcopenshell/api/grid/remove_grid_axis.py | 13 +- .../ifcopenshell/api/group/add_group.py | 5 +- .../ifcopenshell/api/group/edit_group.py | 8 +- .../ifcopenshell/api/group/remove_group.py | 2 +- .../api/group/update_group_products.py | 4 +- .../ifcopenshell/api/library/add_library.py | 2 +- .../api/material/edit_profile_usage.py | 4 +- .../api/sequence/assign_product.py | 2 +- .../api/sequence/edit_work_schedule.py | 2 +- .../api/sequence/remove_work_plan.py | 2 +- .../api/style/add_surface_textures.py | 2 +- src/ifcopenshell-python/ifcopenshell/file.py | 4 +- .../ifcopenshell/util/element.py | 2 +- .../ifcopenshell/util/shape_builder.py | 158 +++++++---- .../test/api/pset/test_remove_pset.py | 1 + 48 files changed, 978 insertions(+), 410 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/search/operator.py b/src/blenderbim/blenderbim/bim/module/search/operator.py index 1c22ea0f01..9865e7cd28 100644 --- a/src/blenderbim/blenderbim/bim/module/search/operator.py +++ b/src/blenderbim/blenderbim/bim/module/search/operator.py @@ -20,6 +20,7 @@ import re import bpy import json import ifcopenshell +import ifcopenshell.api import ifcopenshell.guid import ifcopenshell.util.element import ifcopenshell.util.selector diff --git a/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py b/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py index 90994d8499..a5693324e4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py +++ b/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py @@ -89,7 +89,6 @@ def assign_connection_geometry( usecase.axis = axis usecase.ref_direction = ref_direction usecase.unit_scale = unit_scale - usecase.ifc_vertices = [] return usecase.execute() diff --git a/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py b/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py index 95a0929ab6..38c79d20f2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py +++ b/src/ifcopenshell-python/ifcopenshell/api/context/add_context.py @@ -16,8 +16,17 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import ifcopenshell +from typing import Optional -def add_context(file, context_type=None, context_identifier=None, target_view=None, parent=None) -> None: + +def add_context( + file: ifcopenshell.file, + context_type: str, + context_identifier: Optional[str] = None, + target_view: Optional[str] = None, + parent: Optional[ifcopenshell.entity_instance] = None, +) -> ifcopenshell.entity_instance: """Adds a new geometric representation context In IFC, physical objects may have zero, one, or multiple geometric @@ -104,7 +113,7 @@ def add_context(file, context_type=None, context_identifier=None, target_view=No :type parent: ifcopenshell.entity_instance, optional :return: the newly created IfcGeometricRepresentationContext or IfcGeometricRepresentationSubContext entity - :rtype: ifcopenshell.entity_instance, optional + :rtype: ifcopenshell.entity_instance Example: diff --git a/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py b/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py index 30f6d642b1..5bef4c9956 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py +++ b/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py @@ -16,8 +16,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import ifcopenshell +from typing import Any -def edit_context(file, context, attributes) -> None: + +def edit_context(file: ifcopenshell.file, context: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: """Edits the attributes of an IfcGeometricRepresentationContext For more information about the attributes and data types of an @@ -26,7 +29,7 @@ def edit_context(file, context, attributes) -> None: :param context: The IfcGeometricRepresentationContext entity you want to edit :type context: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict, optional + :type attributes: dict :return: None :rtype: None @@ -44,7 +47,7 @@ def edit_context(file, context, attributes) -> None: ifcopenshell.api.run("context.edit_context", model, context=body, attributes={"ContextIdentifier": "Body"}) """ - settings = {"context": context, "attributes": attributes or {}} + settings = {"context": context, "attributes": attributes} for name, value in settings["attributes"].items(): setattr(settings["context"], name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_schedule.py index 65c95ca0d1..cb8e678420 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_schedule.py @@ -22,7 +22,9 @@ from datetime import datetime from typing import Optional -def add_cost_schedule(file: ifcopenshell.file, name: Optional[str] = None, predefined_type="NOTDEFINED") -> None: +def add_cost_schedule( + file: ifcopenshell.file, name: Optional[str] = None, predefined_type: str = "NOTDEFINED" +) -> ifcopenshell.entity_instance: """Add a new cost schedule A cost schedule is a group of cost items which typically represent a diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py b/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py index 478c1c11da..0c9ef1cce0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py @@ -16,13 +16,13 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell -from typing import Any, Optional +from typing import Any def edit_information( file: ifcopenshell.file, information: ifcopenshell.entity_instance, - attributes: Optional[dict[str, Any]] = None, + attributes: dict[str, Any], ) -> None: """Edits the attributes of an IfcDocumentInformation @@ -32,7 +32,7 @@ def edit_information( :param reference: The IfcDocumentInformation entity you want to edit :type reference: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict, optional + :type attributes: dict :return: None :rtype: None @@ -46,7 +46,7 @@ def edit_information( attributes={"Identification": "A-GA-6100", "Name": "Overall Plan", "Location": "A-GA-6100 - Overall Plan.pdf"}) """ - settings = {"information": information, "attributes": attributes or {}} + settings = {"information": information, "attributes": attributes} for name, value in settings["attributes"].items(): setattr(settings["information"], name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py b/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py index fb705fbbc2..3ba210cef1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py @@ -16,13 +16,13 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell -from typing import Any, Optional +from typing import Any def edit_reference( file: ifcopenshell.file, reference: ifcopenshell.entity_instance, - attributes: Optional[dict[str, Any]] = None, + attributes: dict[str, Any], ) -> None: """Edits the attributes of an IfcDocumentReference @@ -32,7 +32,7 @@ def edit_reference( :param reference: The IfcDocumentReference entity you want to edit :type reference: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict, optional + :type attributes: dict :return: None :rtype: None @@ -49,7 +49,7 @@ def edit_reference( ifcopenshell.api.run("document.edit_reference", model, reference=reference, attributes={"Identification": "2.1.15"}) """ - settings = {"reference": reference, "attributes": attributes or {}} + settings = {"reference": reference, "attributes": attributes} for name, value in settings["attributes"].items(): setattr(settings["reference"], name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py index d8180d287f..0c78866ccc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py @@ -17,9 +17,14 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.util.unit +from typing import Union + +COORD = Union[tuple[float, float], tuple[float, float, float]] -def add_axis_representation(file, context=None, axis=None) -> None: +def add_axis_representation( + file: ifcopenshell.file, context: ifcopenshell.entity_instance, axis: tuple[COORD, COORD] +) -> ifcopenshell.entity_instance: """Adds a new axis representation Certain objects are typically "axis-based", such as walls, beams, diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py index 3ec590a6f3..026e118805 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_boolean.py @@ -16,27 +16,51 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +from __future__ import annotations import ifcopenshell.util.unit import numpy as np +import numpy.typing as npt +from typing import Optional, TYPE_CHECKING, Literal + +if TYPE_CHECKING: + import bpy.types -def add_boolean(file, **usecase_settings) -> None: +NPArrayOfFloats = npt.NDArray[np.float64] + + +def add_boolean( + file: ifcopenshell.file, + representation: ifcopenshell.entity_instance, + # A matrix to define a clipping Ifchalfspacesolid. + # The XY plane is the clipping boundary and +Z is removed. + operator: str = "DIFFERENCE", + # IfcHalfSpaceSolid, Mesh + type: Literal["IfcHalfSpaceSolid", "Mesh"] = "IfcHalfSpaceSolid", + matrix: Optional[NPArrayOfFloats] = None, + # A Blender OBJ to define the voided OBJ for a "Mesh" type + blender_obj: Optional[bpy.types.Object] = None, + # A Blender OBJ to define the void OBJ for a "Mesh" type + blender_void: Optional[bpy.types.Object] = None, + should_force_faceted_brep: bool = False, + should_force_triangulation: bool = False, +) -> list[ifcopenshell.entity_instance]: + """For `type` values: + - "IfcHalfSpaceSolid" - `matrix` is not optional. + - "Mesh" - `blender_obj` and `blender_void` are not optional + """ usecase = Usecase() usecase.file = file usecase.settings = { - "representation": None, - "operator": "DIFFERENCE", - # IfcHalfSpaceSolid, Mesh - "type": "IfcHalfSpaceSolid", - # The XY plane is the clipping boundary and +Z is removed. - "matrix": None, # A matrix to define a clipping Ifchalfspacesolid. - "blender_obj": None, # A Blender OBJ to define the voided OBJ for a "Mesh" type - "blender_void": None, # A Blender OBJ to define the void OBJ for a "Mesh" type - "should_force_faceted_brep": False, - "should_force_triangulation": False, + "representation": representation, + "operator": operator, + "type": type, + "matrix": matrix, + "blender_obj": blender_obj, + "blender_void": blender_void, + "should_force_faceted_brep": should_force_faceted_brep, + "should_force_triangulation": should_force_triangulation, } - for key, value in usecase_settings.items(): - usecase.settings[key] = value return usecase.execute() diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py index da7678aac4..73feca9326 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py @@ -16,13 +16,15 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +from __future__ import annotations +import collections.abc import ifcopenshell.util.unit from ifcopenshell.util.shape_builder import ShapeBuilder, V from ifcopenshell.api.geometry.add_window_representation import create_ifc_window from mathutils import Vector from math import cos, radians - -import collections +from typing import Any, Optional, Literal, Union +import dataclasses SUPPORTED_DOOR_TYPES = ( @@ -38,9 +40,14 @@ SUPPORTED_DOOR_TYPES = ( ) +def mm(x: float) -> float: + """mm to meters shortcut for readability""" + return x / 1000 + + def create_ifc_door_lining( builder: ShapeBuilder, size: Vector, thickness: list, position: Vector = V(0, 0, 0).freeze() -): +) -> ifcopenshell.entity_instance: """`thickness` of the profile is defined as list in the following order: `(SIDE, TOP)` `thickness` can be also defined just as 1 float value. @@ -69,80 +76,212 @@ def create_ifc_door_lining( return door_lining -def create_ifc_box(builder: ShapeBuilder, size: Vector, position: Vector = V(0, 0, 0).freeze()): +def create_ifc_box( + builder: ShapeBuilder, size: Vector, position: Vector = V(0, 0, 0).freeze() +) -> ifcopenshell.entity_instance: rect = builder.rectangle(size.xy) box = builder.extrude(rect, size.z, position=position, extrusion_vector=V(0, 0, 1)) return box -def add_door_representation(file, **usecase_settings) -> None: - """units in usecase_settings expected to be in ifc project units""" +# we use dataclass as we need default values for arguments +# it's okay to use slots since we don't need dynamic attributes +@dataclasses.dataclass(slots=True) +class DoorLiningProperties: + LiningDepth: Optional[float] = None + """Optional, defaults to 50mm.""" + + LiningThickness: Optional[float] = None + """Optional, defaults to 50mm.""" + + LiningOffset: Optional[float] = None + """Offset from the outer side of the wall (by Y-axis). Optional, defaults to 0.0.""" + + LiningToPanelOffsetX: Optional[float] = None + """Offset from the wall. Optional, defaults to 25mm.""" + + LiningToPanelOffsetY: Optional[float] = None + """Offset from the X-axis (unlike windows). Optional, defaults to 25mm.""" + + TransomThickness: Optional[float] = None + """Vertical distance between door and window panels. Optional, defaults to 0.0.""" + + TransomOffset: Optional[float] = None + """Distance from the bottom door opening + to the beginning of the transom + unlike windows TransomOffset which goes to the center of the transom. + Optional, defaults 1.525m.""" + + ShapeAspectStyle: None = None + """Optional. Deprecated argument.""" + + CasingDepth: Optional[float] = None + """Casing cover wall faces around the opening + on the left, right and upper sides + Casing should be either on both sides of the wall or no casing + If `LiningOffset` is present then therefore casing is not possible on outer wall + therefore there will be no casing on inner wall either. Optional, defaults to 5mm.""" + + CasingThickness: Optional[float] = None + """Casing thickness by Z-axis. Optional, defaults to 75mm.""" + + ThresholdDepth: Optional[float] = None + """Threshold covers the bottom side of the opening. Optional, defaults to 100mm.""" + + ThresholdThickness: Optional[float] = None + """Theshold thickness by Z-axis. Optional, defaults to 25mm.""" + + ThresholdOffset: Optional[float] = None + """Threshold offset by Y-axis. Optional, defaults to 0.0.""" + + def initialize_properties(self, unit_scale: float) -> None: + # in meters + # fmt: off + default_values: dict[str, float] = dict( + LiningDepth = mm(50), + LiningThickness = mm(50), + LiningOffset = 0.0, + LiningToPanelOffsetX = mm(25), + LiningToPanelOffsetY = mm(25), + TransomThickness = 0.0, + TransomOffset = mm(1525), + CasingDepth = mm(5), + CasingThickness = mm(75), + ThresholdDepth = mm(100), + ThresholdThickness = mm(25), + ThresholdOffset = 0.0, + ) + # fmt: on + + si_conversion = 1 / unit_scale + for attr, default_value in default_values.items(): + if getattr(self, attr) is not None: + continue + setattr(self, attr, default_value * si_conversion) + + +@dataclasses.dataclass(slots=True) +class DoorPanelProperties: + PanelDepth: Optional[float] = None + """Frame thickness by Y axis. Optional, defaults to 35 mm.""" + + PanelWidth: float = 1.0 + """Ratio to the clear door opening. Optional, defaults to 1.0.""" + + FrameDepth: Optional[float] = None + """Frame thickness by Y axis. Optional, defaults to 35 mm.""" + + FrameThickness: Optional[float] = None + """Frame thickness by X axis. Optional, defaults to 35 mm.""" + + PanelPosition: None = None + """Optional, value is never used""" + + PanelOperation: None = None + """Optional, value is never used. + Defines the basic ways to describe how door panels operate.""" + + ShapeAspectStyle: None = None + """Optional. Deprecated argument.""" + + def initialize_properties(self, unit_scale: float) -> None: + # in meters + # fmt: off + default_values: dict[str, float] = dict( + PanelDepth = mm(35), + FrameDepth = mm(35), + FrameThickness = mm(35), + ) + # fmt: on + + si_conversion = 1 / unit_scale + for attr, default_value in default_values.items(): + if getattr(self, attr) is not None: + continue + setattr(self, attr, default_value * si_conversion) + + +def add_door_representation( + file: ifcopenshell.file, + *, # keywords only as this API implementation is probably not final + context: ifcopenshell.entity_instance, + overall_height: Optional[float] = None, + overall_width: Optional[float] = None, + # door type + # http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorTypeOperationEnum.htm + operation_type: Literal[ + "SINGLE_SWING_LEFT", + "SINGLE_SWING_RIGHT", + "DOUBLE_SWING_RIGHT", + "DOUBLE_SWING_LEFT", + "DOUBLE_DOOR_SINGLE_SWING", + "DOUBLE_DOOR_DOUBLE_SWING", + "SLIDING_TO_LEFT", + "SLIDING_TO_RIGHT", + "DOUBLE_DOOR_SLIDING", + ] = "SINGLE_SWING_LEFT", + lining_properties: Optional[Union[DoorLiningProperties, dict[str, Any]]] = None, + panel_properties: Optional[Union[DoorPanelProperties, dict[str, Any]]] = None, + unit_scale: Optional[float] = None, +) -> ifcopenshell.entity_instance: + """units in usecase_settings expected to be in ifc project units + + :param context: IfcGeometricRepresentationContext for the representation. + :type context: ifcopenshell.entity_instance + :param overall_height: Overall door height. Defaults to 2m. + :type overall_height: float, optional + :param overall_width: Overall door width. Defaults to 0.9m. + :type overall_width: float, optional + :param operation_type: Type of the door. Defaults to SINGLE_SWING_LEFT. + :type operation_type: str, optional + :param lining_properties: DoorLiningProperties or a dictionary to create one. + See DoorLiningProperties description for details. + :type lining_properties: Union[DoorLiningProperties, dict[str, Any]]] + :param panel_properties: DoorPanelProperties or a dictionary to create one. + See DoorPanelProperties description for details. + :type panel_properties: Union[DoorPanelProperties, dict[str, Any]]] + :param unit_scale: The unit scale as calculated by + ifcopenshell.util.unit.calculate_unit_scale. If not provided, it + will be automatically calculated for you. + :type unit_scale: float, optional + :return: IfcShapeRepresentation for a door. + :rtype: ifcopenshell.entity_instance + + """ usecase = Usecase() usecase.file = file # http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoor.htm - # http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorTypeOperationEnum.htm # http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorLiningProperties.htm # http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcDoorPanelProperties.htm - usecase.settings = {"unit_scale": ifcopenshell.util.unit.calculate_unit_scale(usecase.file)} - usecase.settings.update( + # define unit_scale first as it's going to be used setting default arguments + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) if unit_scale is None else unit_scale + settings: dict[str, Any] = {"unit_scale": unit_scale} + + if lining_properties is None: + lining_properties = DoorLiningProperties() + elif not isinstance(lining_properties, DoorLiningProperties): + lining_properties = DoorLiningProperties(**lining_properties) + lining_properties.initialize_properties(unit_scale) + lining_properties = dataclasses.asdict(lining_properties) + + if panel_properties is None: + panel_properties = DoorPanelProperties() + elif not isinstance(panel_properties, DoorPanelProperties): + panel_properties = DoorPanelProperties(**panel_properties) + panel_properties.initialize_properties(unit_scale) + panel_properties = dataclasses.asdict(panel_properties) + + settings.update( { - "context": None, # IfcGeometricRepresentationContext - "overall_height": usecase.convert_si_to_unit(2.0), - "overall_width": usecase.convert_si_to_unit(0.9), - # DOUBLE_DOOR_DOUBLE_SWING, DOUBLE_DOOR_FOLDING, DOUBLE_DOOR_LIFTING_VERTICAL, - # DOUBLE_DOOR_SINGLE_SWING, DOUBLE_DOOR_SINGLE_SWING_OPPOSITE_LEFT, - # DOUBLE_DOOR_SINGLE_SWING_OPPOSITE_RIGHT, DOUBLE_DOOR_SLIDING, - # DOUBLE_SWING_LEFT, DOUBLE_SWING_RIGHT, FOLDING_TO_LEFT, - # FOLDING_TO_RIGHT, LIFTING_HORIZONTAL, LIFTING_VERTICAL_LEFT, - # LIFTING_VERTICAL_RIGHT, REVOLVING, REVOLVING_VERTICAL, - # ROLLINGUP, SINGLE_SWING_LEFT, SINGLE_SWING_RIGHT, SLIDING_TO_LEFT, - # SLIDING_TO_RIGHT, SWING_FIXED_LEFT, SWING_FIXED_RIGHT - "operation_type": "SINGLE_SWING_LEFT", # door type - "lining_properties": { - "LiningDepth": usecase.convert_si_to_unit(0.050), - "LiningThickness": usecase.convert_si_to_unit(0.050), - # offset from the outer side of the wall (by Y-axis) - "LiningOffset": usecase.convert_si_to_unit(0.0), - # offset from the wall - "LiningToPanelOffsetX": usecase.convert_si_to_unit(0.025), - # offset from the X-axis (unlike windows) - "LiningToPanelOffsetY": usecase.convert_si_to_unit(0.025), - # transom - vertical distance between door and window panels - "TransomThickness": usecase.convert_si_to_unit(0.000), - # TransomOffset - distance from the bottom door opening - # to the beginning of the transom - # unlike windows TransomOffset which goes to the center of the transom - "TransomOffset": usecase.convert_si_to_unit(1.525), - "ShapeAspectStyle": None, # DEPRECATED - # Casing cover wall faces around the opening - # on the left, right and upper sides - # Casing should be either on both sides of the wall or no casing - # If `LiningOffset` is present then therefore casing is not possible on outer wall - # therefore there will be no casing on inner wall either - "CasingDepth": usecase.convert_si_to_unit(0.005), - "CasingThickness": usecase.convert_si_to_unit(0.075), # by Z-axis - # Threshold covers the bottom side of the opening - "ThresholdDepth": usecase.convert_si_to_unit(0.1), - "ThresholdThickness": usecase.convert_si_to_unit(0.025), # by Z-axis - # offset by Y-axis - "ThresholdOffset": usecase.convert_si_to_unit(0.000), - }, - "panel_properties": { - "PanelDepth": usecase.convert_si_to_unit(0.035), # by Y - "PanelWidth": 1.0, # as ratio to the clear door opening - "FrameDepth": usecase.convert_si_to_unit(0.035), # by Y - "FrameThickness": usecase.convert_si_to_unit(0.035), # by X - # LEFT, MIDDLE, RIGHT, NOTDEFINED - "PanelPosition": ..., # NEVER USED - # defines the basic ways to describe how door panels operate - # basically how it opens - "PanelOperation": None, # NEVER USED - "ShapeAspectStyle": None, # DEPRECATED - }, + "context": context, + "overall_height": overall_height if overall_height is not None else usecase.convert_si_to_unit(2.0), + "overall_width": overall_width if overall_width is not None else usecase.convert_si_to_unit(0.9), + "operation_type": operation_type, + "lining_properties": lining_properties, + "panel_properties": panel_properties, } ) - for key, value in usecase_settings.items(): - usecase.settings[key] = value + usecase.settings = settings return usecase.execute() diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py index 976b48e5ce..6b48845e4e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py @@ -19,13 +19,17 @@ import ifcopenshell.util.unit -def add_footprint_representation(file, **usecase_settings) -> None: +def add_footprint_representation( + file, + # IfcGeometricRepresentationContext + context: ifcopenshell.entity_instance, + # A list of IFC curves to include in the curve set + curves: list[ifcopenshell.entity_instance], +) -> ifcopenshell.entity_instance: settings = { - "context": None, # IfcGeometricRepresentationContext - "curves": [], # A list of IFC curves to include in the curve set + "context": context, + "curves": curves, } - for key, value in usecase_settings.items(): - settings[key] = value return file.createIfcShapeRepresentation( settings["context"], diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py index fbe42063d9..e26c5c39b8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py @@ -17,26 +17,43 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.util.unit +from typing import Optional + +COORD_3D = tuple[float, float, float] -def add_mesh_representation(file: ifcopenshell.file, **usecase_settings) -> None: +def add_mesh_representation( + file: ifcopenshell.file, + # IfcGeometricRepresentationContext + context: ifcopenshell.entity_instance, + # Vertices, edges, and faces are given in the form of: [item1, item2, item3, ...] + # A list of coordinates + # ... where itemN = [(0., 0., 0.), (1., 1., 1.), (x, y, z), ...] + vertices: list[COORD_3D], + # A list of edges, represented by vertex index pairs + # ... where itemN = [(0, 1), (1, 2), (v1, v2), ...] + edges: list[tuple[int, int]], + # A list of polygons, represented by vertex indices + # ... where itemN = [(0, 1, 2), (5, 4, 2, 3), (v1, v2, v3, ... vN), ...] + faces: list[list[int]], + # Optionally apply a vector offset to all coordinates + cooridnate_offset: Optional[COORD_3D] = None, + # A scale factor to apply for all vectors in case the unit is different + unit_scale: Optional[float] = None, + # Force using IfcFacetedBreps instead of IfcPolygonalFaceSets + force_faceted_brep: bool = False, +) -> ifcopenshell.entity_instance: usecase = Usecase() usecase.file = file usecase.settings = { - "context": None, # IfcGeometricRepresentationContext - # Vertices, edges, and faces are given in the form of: [item1, item2, item3, ...] - # ... where itemN = [(0., 0., 0.), (1., 1., 1.), (x, y, z), ...] - "vertices": None, # A list of coordinates - # ... where itemN = [(0, 1), (1, 2), (v1, v2), ...] - "edges": None, # A list of edges, represented by vertex index pairs - # ... where itemN = [(0, 1, 2), (5, 4, 2, 3), (v1, v2, v3, ... vN), ...] - "faces": None, # A list of polygons, represented by vertex indices - "coordinate_offset": None, # Optionally apply a vector offset to all coordinates - "unit_scale": None, # A scale factor to apply for all vectors in case the unit is different - "force_faceted_brep": False, # Force using IfcFacetedBreps instead of IfcPolygonalFaceSets + "context": context, + "vertices": vertices, + "edges": edges, + "faces": faces, + "coordinate_offset": cooridnate_offset, + "unit_scale": unit_scale, + "force_faceted_brep": force_faceted_brep, } - for key, value in usecase_settings.items(): - usecase.settings[key] = value return usecase.execute() diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py index 025f09f0fc..18dffbc6b3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py @@ -19,23 +19,35 @@ import ifcopenshell.geom import ifcopenshell.util.unit from ifcopenshell.util.data import Clipping +from typing import Any, Union, Optional, Literal + +VECTOR_3D = tuple[float, float, float] -def add_profile_representation(file, **usecase_settings) -> None: +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` + clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None, + placement_zx_axes: tuple[Union[VECTOR_3D, None], Union[VECTOR_3D, None]] = (None, None), +) -> None: usecase = Usecase() usecase.file = file usecase.settings = { - "context": None, # IfcGeometricRepresentationContext - "profile": None, - "depth": 1.0, - "cardinal_point": 5, - # Planes are defined either by Clipping objects - # or by dictionaries of arguments for `Clipping.parse` - "clippings": [], # A list of planes that define clipping half space solids - "placement_zx_axes": (None, None), + "context": context, + "profile": profile, + "depth": depth, + "cardinal_point": cardinal_point, + "clippings": clippings if clippings is not None else [], + "placement_zx_axes": placement_zx_axes, } - for key, value in usecase_settings.items(): - usecase.settings[key] = value return usecase.execute() diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_railing_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_railing_representation.py index 9de884c8c2..b6dc8653d8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_railing_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_railing_representation.py @@ -22,46 +22,100 @@ from itertools import chain from mathutils import Vector, Matrix import collections import mathutils -from pprint import pprint from math import pi, cos, sin, tan, radians +from typing import Literal, Optional, Any -def mm(x): +def mm(x: float) -> float: """mm to meters shortcut for readability""" return x / 1000 -def add_railing_representation(file, **usecase_settings) -> None: +def add_railing_representation( + file: ifcopenshell.file, + *, # keywords only as this API implementation is probably not final + # IfcGeometricRepresentationContext + context: ifcopenshell.entity_instance, + railing_type: Literal["WALL_MOUNTED_HANDRAIL"] = "WALL_MOUNTED_HANDRAIL", + railing_path: list[Vector], + use_manual_supports: bool = False, + support_spacing: Optional[float] = None, + railing_diameter: Optional[float] = None, + clear_width: Optional[float] = None, + terminal_type: Literal[ + "180", + "TO_END_POST", + "TO_WALL", + "TO_FLOOR", + "TO_END_POST_AND_FLOOR", + ] = "180", + height: Optional[float] = None, + looped_path: bool = False, + unit_scale: Optional[float] = None, +) -> ifcopenshell.entity_instance: """ - units in usecase_settings expected to be in ifc project units + Units are expected to be in IFC project units. - `railing_path` is a list of point coordinates for the railing path, - coordinates are expected to be at the top of the railing, not at the center + :param context: IfcGeometricRepresentationContext for the representation. + :type context: ifcopenshell.entity_instance + :param railing_type: Type of the railing. Defaults to "WALL_MOUNTED_HANDRAIL". + :type railing_type: Literal["WALL_MOUNTED_HANDRAIL"], optional + :param railing_path: A list of points coordinates for the railing path, + coordinates are expected to be at the top of the railing, not at the center. + If not provided, default path [(0, 0, 1), (1, 0, 1), (2, 0, 1)] (in meters) will be used + :type railing_path: list[Vector], optional. + :param use_manual_supports: If enabled, supports are added on every vertex on the edges of the railing path. + If disabled, supports are added automatically based on the support spacing. Default to False. + :type use_manual_supports: bool, optional + :param support_spacing: Distance between supports if automatic supports are used. Defaults to 1m. + :type support_spacing: float, optional + :param railing_diameter: Railing diameter. Defaults to 50mm. + :type railing_diameter: float, optional + :param clear_width: Clear width between the railing and the wall. Defaults to 40mm. + :type clear_width: float, optional + :param terminal_type: type of the cap. Defaults to "180". + :type terminal_type: Literal["180","TO_END_POST","TO_WALL","TO_FLOOR","TO_END_POST_AND_FLOOR"], optional + :param height: defaults to 1m + :type height: float, optional + :param looped_path: Whether to end the railing on the first point of `railing_path`. Defaults to False. + :type looped_path: bool, optional + :param unit_scale: The unit scale as calculated by + ifcopenshell.util.unit.calculate_unit_scale. If not provided, it + will be automatically calculated for you. + :type unit_scale: float, optional + :return: IfcShapeRepresentation for a railing. + :rtype: ifcopenshell.entity_instance - `railing_path` is expected to be a list of Vector objects """ usecase = Usecase() usecase.file = file - usecase.settings = {"unit_scale": ifcopenshell.util.unit.calculate_unit_scale(usecase.file)} - usecase.settings.update( + # define unit_scale first as it's going to be used setting default arguments + settings: dict[str, Any] = { + "unit_scale": ifcopenshell.util.unit.calculate_unit_scale(file) if unit_scale is None else unit_scale, + } + settings.update( { - "context": None, # IfcGeometricRepresentationContext - "railing_type": "WALL_MOUNTED_HANDRAIL", - "railing_path": usecase.path_si_to_units([V(0, 0, 1), V(1, 0, 1), V(2, 0, 1)]), - "use_manual_supports": False, - "support_spacing": usecase.convert_si_to_unit(mm(1000)), - "railing_diameter": usecase.convert_si_to_unit(mm(50)), - "clear_width": usecase.convert_si_to_unit(mm(40)), - "terminal_type": "180", - "height": usecase.convert_si_to_unit(mm(1000)), - "looped_path": False, + "context": context, + "railing_type": railing_path, + "railing_path": ( + railing_path + if railing_path is not None + else usecase.path_si_to_units([V(0, 0, 1), V(1, 0, 1), V(2, 0, 1)]) + ), + "use_manual_supports": use_manual_supports, + "support_spacing": support_spacing if support_spacing is not None else usecase.convert_si_to_unit(mm(1000)), + "railing_diameter": ( + railing_diameter if railing_diameter is not None else usecase.convert_si_to_unit(mm(50)) + ), + "clear_width": clear_width if clear_width is not None else usecase.convert_si_to_unit(mm(40)), + "terminal_type": terminal_type, + "height": height if height is not None else usecase.convert_si_to_unit(mm(1000)), + "looped_path": looped_path, } ) + usecase.settings = settings - for key, value in usecase_settings.items(): - usecase.settings[key] = value - - if usecase.settings["railing_type"] != "WALL_MOUNTED_HANDRAIL": + if railing_type != "WALL_MOUNTED_HANDRAIL": raise Exception('Only "WALL_MOUNTED_HANDRAIL" railing_type is supported at the moment.') return usecase.execute() diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py index aa98a15f3c..9693e46340 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py @@ -16,11 +16,12 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . from __future__ import annotations -import bpy +import bpy.types import math import bmesh import ifcopenshell.util.unit from mathutils import Vector, Matrix +from typing import Union, Optional, Literal Z_AXIS = Vector((0, 0, 1)) @@ -28,7 +29,44 @@ X_AXIS = Vector((1, 0, 0)) EPSILON = 1e-6 -def add_representation(file: ifcopenshell.file, **usecase_settings) -> ifcopenshell.entity_instance: +def add_representation( + file: ifcopenshell.file, + *, # keywords only as this API implementation is probably not final + # IfcGeometricRepresentationContext + context: ifcopenshell.entity_instance, + # This is (currently) a Blender object, hence this depends on Blender now + blender_object: bpy.types.Object, + # This is (currently) a Blender data object, hence this depends on Blender now + geometry: Union[bpy.types.Mesh, bpy.types.Curve], + # Optionally apply a vector offset to all coordinates + coordinate_offset: Optional[Vector] = None, + # How many representation items to create + total_items: int = 1, + # A scale factor to apply for all vectors in case the unit is different + unit_scale: Optional[float] = None, + # If we should force faceted breps for meshes + should_force_faceted_brep: bool = False, + # If we should force triangulation for meshes + should_force_triangulation: bool = False, + # If UV coordinates should also be generated + should_generate_uvs: bool = False, + # Whether to cast a mesh into a particular class + ifc_representation_class: Optional[ + Literal[ + "IfcExtrudedAreaSolid/IfcRectangleProfileDef", + "IfcExtrudedAreaSolid/IfcCircleProfileDef", + "IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef", + "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids", + "IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage", + "IfcGeometricCurveSet/IfcTextLiteral", + "IfcTextLiteral", + ] + ] = None, + # The material profile set if the extrusion requires it + profile_set_usage: Optional[ifcopenshell.entity_instance] = None, + # The text literal if the representation requires it + text_literal: Optional[ifcopenshell.entity_instance] = None, +) -> ifcopenshell.entity_instance: # lazy import Helper to avoid circular import if "Helper" not in globals(): from blenderbim.bim.module.geometry.helper import Helper @@ -37,30 +75,20 @@ def add_representation(file: ifcopenshell.file, **usecase_settings) -> ifcopensh # TODO: This usecase currently depends on Blender's data model usecase.file = file usecase.settings = { - "context": None, # IfcGeometricRepresentationContext - "blender_object": None, # This is (currently) a Blender object, hence this depends on Blender now - "geometry": None, # This is (currently) a Blender data object, hence this depends on Blender now - "coordinate_offset": None, # Optionally apply a vector offset to all coordinates - "total_items": 1, # How many representation items to create - "unit_scale": None, # A scale factor to apply for all vectors in case the unit is different - "should_force_faceted_brep": False, # If we should force faceted breps for meshes - "should_force_triangulation": False, # If we should force triangulation for meshes - "should_generate_uvs": False, # If UV coordinates should also be generated - # Possible IFC representation classes: - # IfcExtrudedAreaSolid/IfcRectangleProfileDef - # IfcExtrudedAreaSolid/IfcCircleProfileDef - # IfcExtrudedAreaSolid/IfcArbitraryClosedProfileDef - # IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids - # IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage - # IfcGeometricCurveSet/IfcTextLiteral - # IfcTextLiteral - "ifc_representation_class": None, # Whether to cast a mesh into a particular class - "profile_set_usage": None, # The material profile set if the extrusion requires it - "text_literal": None, # The text literal if the representation requires it + "context": context, + "blender_object": blender_object, + "geometry": geometry, + "coordinate_offset": coordinate_offset, + "total_items": total_items, + "unit_scale": unit_scale, + "should_force_faceted_brep": should_force_faceted_brep, + "should_force_triangulation": should_force_triangulation, + "should_generate_uvs": should_generate_uvs, + "ifc_representation_class": ifc_representation_class, + "profile_set_usage": profile_set_usage, + "text_literal": text_literal, } usecase.ifc_vertices = [] - for key, value in usecase_settings.items(): - usecase.settings[key] = value return usecase.execute() diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py index 7514ade38f..5a46ba0f49 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py @@ -17,22 +17,32 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.util.unit +from ifcopenshell.util.data import Clipping from math import sin, cos +from typing import Any, Optional, Union -def add_slab_representation(file, **usecase_settings) -> None: +def add_slab_representation( + file, + # IfcGeometricRepresentationContext + context: ifcopenshell.entity_instance, + # in meters + depth: float = 0.2, + # in radians + x_angle: float = 0.0, + # 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` + clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None, +) -> ifcopenshell.entity_instance: usecase = Usecase() usecase.file = file usecase.settings = { - "context": None, # IfcGeometricRepresentationContext - "depth": 0.2, - "x_angle": 0, # Radians - # Planes are defined either by Clipping objects - # or by dictionaries of arguments for `Clipping.parse` - "clippings": [], # A list of planes that define clipping half space solids + "context": context, + "depth": depth, + "x_angle": x_angle, + "clippings": clippings if clippings is not None else [], } - for key, value in usecase_settings.items(): - usecase.settings[key] = value return usecase.execute() diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py index 504a89078b..945400fcfa 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py @@ -18,27 +18,39 @@ import ifcopenshell.util.unit from math import sin, cos +from typing import Optional, Union, Any from ifcopenshell.util.data import Clipping -def add_wall_representation(file, **usecase_settings) -> None: +def add_wall_representation( + file: ifcopenshell.file, + context: ifcopenshell.entity_instance, # IfcGeometricRepresentationContext + # all lengths are in meters + length: float = 1.0, + height: float = 3.0, + offset: float = 0.0, + thickness: float = 0.2, + # Sloped walls along the wall's X axis, provided in radians + x_angle: float = 0.0, + # 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` + clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None, + # Any existing IfcBooleanResults + booleans: Optional[list[ifcopenshell.entity_instance]] = None, +) -> ifcopenshell.entity_instance: usecase = Usecase() usecase.file = file usecase.settings = { - "context": None, # IfcGeometricRepresentationContext - "length": 1.0, - "height": 3.0, - "offset": 0.0, - "thickness": 0.2, - # Sloped walls along the wall's X axis, provided in radians - "x_angle": 0, - # Planes are defined either by Clipping objects - # or by dictionaries of arguments for `Clipping.parse` - "clippings": [], # A list of planes that define clipping half space solids - "booleans": [], # Any existing IfcBooleanResults + "context": context, + "length": length, + "height": height, + "offset": offset, + "thickness": thickness, + "x_angle": x_angle, + "clippings": clippings if clippings is not None else [], + "booleans": booleans if booleans is not None else [], } - for key, value in usecase_settings.items(): - usecase.settings[key] = value return usecase.execute() diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py index b0a46e69ab..73a9c255a2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py @@ -16,11 +16,14 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +from __future__ import annotations +import collections.abc import ifcopenshell.util.unit from ifcopenshell.util.shape_builder import ShapeBuilder, V from itertools import chain from mathutils import Vector -import collections +import dataclasses +from typing import Any, Optional, Literal, Union # SCHEMAS describe panels setup @@ -42,6 +45,11 @@ DEFAULT_PANEL_SCHEMAS = { } +def mm(x: float) -> float: + """mm to meters shortcut for readability""" + return x / 1000 + + def create_ifc_window_frame_simple( builder: ShapeBuilder, size: Vector, thickness: list, position: Vector = V(0, 0, 0).freeze() ): @@ -210,71 +218,209 @@ def create_ifc_window( return output_items -def add_window_representation(file, **usecase_settings) -> None: - """units in usecase_settings expected to be in ifc project units""" +# we use dataclass as we need default values for arguments +# it's okay to use slots since we don't need dynamic attributes +@dataclasses.dataclass(slots=True) +class WindowLiningProperties: + LiningDepth: Optional[float] = None + """Optional, defaults to 50mm.""" + + LiningThickness: Optional[float] = None + """Optional, defaults to 50mm.""" + + LiningOffset: Optional[float] = None + """Offset to the wall. Optional, defaults to 50mm.""" + + LiningToPanelOffsetX: Optional[float] = None + """Offset from the wall. Optional, defaults to 25mm.""" + + # that way it allows you to define overall_depth constant between all panels + # and still have panels with different size: + # overall_depth = lining_depth + offset_y + # full offset from X axis = overall_depth - frame_depth. + LiningToPanelOffsetY: Optional[float] = None + """Offset from the lining. Optional, defaults to 25mm.""" + + MullionThickness: Optional[float] = None + """Mullion thickness (horizontal distance between panels). + + Applies to windows of types: DoublePanelVertical, TriplePanelBottom, TriplePanelTop, + TriplePanelLeft, TriplePanelRight. + + Optional, defaults to 50mm.""" + + FirstMullionOffset: Optional[float] = None + """Distance from the first lining to the mullion center. Optional, defaults to 300mm.""" + + SecondMullionOffset: Optional[float] = None + """Distance from the first lining to the second mullion center. + + Applies to windows of type: TriplePanelVertical. + + Optional, defaults to 450mm.""" + + TransomThickness: Optional[float] = None + """Transom thickness (vertical distance between panels), works similar way to mullions. + + Applies to windows of types:DoublePanelHorizontal, TriplePanelBottom, TriplePanelTop, + TriplePanelLeft, TriplePanelRight. + + Optional, defaults to 50mm.""" + + FirstTransomOffset: Optional[float] = None + """Optional, defaults to 300mm.""" + + SecondTransomOffset: Optional[float] = None + """ + Applies to windows of type: TriplePanelHorizontal. + Optional, defaults to 600mm.""" + + ShapeAspectStyle: None = None + """Optional. Deprecated argument.""" + + def initialize_properties(self, unit_scale: float) -> None: + # in meters + # fmt: off + default_values: dict[str, float] = dict( + LiningDepth = mm(50), + LiningThickness = mm(50), + LiningOffset = mm(50), + LiningToPanelOffsetX = mm(25), + LiningToPanelOffsetY = mm(25), + MullionThickness = mm(50), + FirstMullionOffset = mm(300), + SecondMullionOffset = mm(450), + TransomThickness = mm(50), + FirstTransomOffset = mm(300), + SecondTransomOffset = mm(600), + ) + # fmt: on + + si_conversion = 1 / unit_scale + for attr, default_value in default_values.items(): + if getattr(self, attr) is not None: + continue + setattr(self, attr, default_value * si_conversion) + + +@dataclasses.dataclass(slots=True) +class WindowPanelProperties: + FrameDepth: Optional[float] = None + """Frame thickness by Y axis. Optional, defaults to 35 mm.""" + + FrameThickness: Optional[float] = None + """Frame thickness by X axis. Optional, defaults to 35 mm.""" + + PanelPosition: None = None + """Optional, value is never used""" + + PanelOperation: None = None + """Optional, value is never used. + Defines the basic ways to describe how window panels operate.""" + + ShapeAspectStyle: None = None + """Optional. Deprecated argument.""" + + def initialize_properties(self, unit_scale: float) -> None: + # in meters + # fmt: off + default_values: dict[str, float] = dict( + FrameDepth = mm(35), + FrameThickness = mm(35), + ) + # fmt: on + + si_conversion = 1 / unit_scale + for attr, default_value in default_values.items(): + if getattr(self, attr) is not None: + continue + setattr(self, attr, default_value * si_conversion) + + +def add_window_representation( + file: ifcopenshell.file, + *, # keywords only as this API implementation is probably not final + context: ifcopenshell.entity_instance, + overall_height: Optional[float] = None, + overall_width: Optional[float] = None, + partition_type: Literal[ + "SINGLE_PANEL", + "DOUBLE_PANEL_HORIZONTAL", + "DOUBLE_PANEL_VERTICAL", + "TRIPLE_PANEL_BOTTOM", + "TRIPLE_PANEL_HORIZONTAL", + "TRIPLE_PANEL_LEFT", + "TRIPLE_PANEL_RIGHT", + "TRIPLE_PANEL_TOP", + "TRIPLE_PANEL_VERTICAL", + ] = "SINGLE_PANEL", + lining_properties: Optional[Union[WindowLiningProperties, dict[str, Any]]] = None, + panel_properties: Optional[list[Union[WindowPanelProperties, dict[str, Any]]]] = None, + unit_scale: Optional[float] = None, +) -> ifcopenshell.entity_instance: + """units in usecase_settings expected to be in ifc project units + + :param context: IfcGeometricRepresentationContext for the representation. + :type context: ifcopenshell.entity_instance + :param overall_height: Overall window height. Defaults to 0.9m. + :type overall_height: float, optional + :param overall_width: Overall window width. Defaults to 0.6m. + :type overall_width: float, optional + :param partition_type: Type of the window. Defaults to SINGLE_PANEL. + :type partition_type: str, optional + :param lining_properties: WindowLiningProperties or a dictionary to create one. + See WindowLiningProperties description for details. + :type lining_properties: Union[WindowLiningProperties, dict[str, Any]]] + :param panel_properties: A list of WindowPanelProperties or dictionaries to create one. + See WindowPanelProperties description for details. + :type panel_properties: list[Union[WindowPanelProperties, dict[str, Any]]]] + :param unit_scale: The unit scale as calculated by + ifcopenshell.util.unit.calculate_unit_scale. If not provided, it + will be automatically calculated for you. + :type unit_scale: float, optional + :return: IfcShapeRepresentation for a window. + :rtype: ifcopenshell.entity_instance + + """ usecase = Usecase() usecase.file = file # http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindow.htm # http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowTypePartitioningEnum.htm # http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowLiningProperties.htm # http://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcWindowPanelProperties.htm - usecase.settings = {"unit_scale": ifcopenshell.util.unit.calculate_unit_scale(usecase.file)} - usecase.settings.update( + # define unit_scale first as it's going to be used setting default arguments + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) if unit_scale is None else unit_scale + settings: dict[str, Any] = {"unit_scale": unit_scale} + + if lining_properties is None: + lining_properties = WindowLiningProperties() + elif not isinstance(lining_properties, WindowLiningProperties): + lining_properties = WindowLiningProperties(**lining_properties) + lining_properties.initialize_properties(unit_scale) + lining_properties = dataclasses.asdict(lining_properties) + + if panel_properties is None: + panel_properties = [WindowPanelProperties()] + + for i in range(len(panel_properties)): + properties = panel_properties[i] + if not isinstance(properties, WindowPanelProperties): + properties = WindowPanelProperties(**properties) + properties.initialize_properties(unit_scale) + panel_properties[i] = dataclasses.asdict(properties) + + settings.update( { - "context": None, # IfcGeometricRepresentationContext - # SINGLE_PANEL, DOUBLE_PANEL_HORIZONTAL, DOUBLE_PANEL_VERTICAL, - # TRIPLE_PANEL_BOTTOM, TRIPLE_PANEL_HORIZONTAL, TRIPLE_PANEL_LEFT, - # TRIPLE_PANEL_RIGHT, TRIPLE_PANEL_TOP, TRIPLE_PANEL_VERTICAL - "partition_type": "SINGLE_PANEL", - "overall_height": usecase.convert_si_to_unit(0.9), - "overall_width": usecase.convert_si_to_unit(0.6), - "lining_properties": { - "LiningDepth": usecase.convert_si_to_unit(0.050), - "LiningThickness": usecase.convert_si_to_unit(0.050), - "LiningOffset": usecase.convert_si_to_unit(0.050), # offset to the wall - # offset from the wall - "LiningToPanelOffsetX": usecase.convert_si_to_unit(0.025), - # offset from the lining - # that way it allows you to define overall_depth constant between all panels - # and still have panels with different size: - # overall_depth = lining_depth + offset_y - # full offset from X axis = overall_depth - frame_depth - "LiningToPanelOffsetY": usecase.convert_si_to_unit(0.025), - # applies to DoublePanelVertical, TriplePanelBottom, TriplePanelTop, - # TriplePanelLeft, TriplePanelRight - # mullion - horizontal distance between panels - "MullionThickness": usecase.convert_si_to_unit(0.050), - # distance from the first lining to the mullion center - "FirstMullionOffset": usecase.convert_si_to_unit(0.3), - # applies to TriplePanelVertical - # distance from the first lining to the second mullion center - "SecondMullionOffset": usecase.convert_si_to_unit(0.45), - # applies to DoublePanelHorizontal, TriplePanelBottom, TriplePanelTop, - # TriplePanelLeft, TriplePanelRight - # works similar way to mullion - "TransomThickness": usecase.convert_si_to_unit(0.050), - "FirstTransomOffset": usecase.convert_si_to_unit(0.3), - # applies to TriplePanelHorizontal - "SecondTransomOffset": usecase.convert_si_to_unit(0.6), - "ShapeAspectStyle": None, # DEPRECATED - }, - "panel_properties": [ - { - "FrameDepth": usecase.convert_si_to_unit(0.035), # by Y - "FrameThickness": usecase.convert_si_to_unit(0.035), # by X - # BOTTOM, LEFT, MIDDLE, RIGHT, TOP - "PanelPosition": ..., # NEVER USED - # defines the basic ways to describe how window panels operate - # how it's hanged, how it opens - "OperationType": None, # NEVER USED - "ShapeAspectStyle": None, # DEPRECATED - }, - ], + "context": context, + "overall_height": overall_height if overall_height is not None else usecase.convert_si_to_unit(0.9), + "overall_width": overall_width if overall_width is not None else usecase.convert_si_to_unit(0.6), + "partition_type": partition_type, + "lining_properties": lining_properties, + "panel_properties": panel_properties, } ) - for key, value in usecase_settings.items(): - usecase.settings[key] = value + usecase.settings = settings usecase.settings["panel_schema"] = DEFAULT_PANEL_SCHEMAS[usecase.settings["partition_type"]] return usecase.execute() diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/assign_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/assign_representation.py index 1df964d845..c89c6fd819 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/assign_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/assign_representation.py @@ -20,12 +20,12 @@ import ifcopenshell.api import ifcopenshell.util.element -def assign_representation(file, **usecase_settings) -> None: +def assign_representation( + file: ifcopenshell.file, product: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance +) -> None: usecase = Usecase() usecase.file = file - usecase.settings = {"product": None, "representation": None} - for key, value in usecase_settings.items(): - usecase.settings[key] = value + usecase.settings = {"product": product, "representation": representation} return usecase.execute() diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_element.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_element.py index d86e2bbca3..87d0149407 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_element.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_element.py @@ -20,16 +20,20 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.guid import ifcopenshell.util.element +from typing import Optional -def connect_element(file, **usecase_settings) -> None: +def connect_element( + file: ifcopenshell.file, + relating_element: ifcopenshell.entity_instance, + related_element: ifcopenshell.entity_instance, + description: Optional[str] = None, +) -> ifcopenshell.entity_instance: settings = { - "relating_element": None, - "related_element": None, - "description": None, + "relating_element": relating_element, + "related_element": related_element, + "description": description, } - for key, value in usecase_settings.items(): - settings[key] = value incompatible_connections = [] diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py index bab818ee64..9e18ca0f18 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py @@ -20,18 +20,24 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.guid import ifcopenshell.util.element +from typing import Optional -def connect_path(file, **usecase_settings) -> None: +def connect_path( + file: ifcopenshell.file, + relating_element: ifcopenshell.entity_instance, + related_element: ifcopenshell.entity_instance, + relating_connection: str = "NOTDEFINED", + related_connection: str = "NOTDEFINED", + description: Optional[str] = None, +) -> ifcopenshell.entity_instance: settings = { - "relating_element": None, - "related_element": None, - "relating_connection": "NOTDEFINED", - "related_connection": "NOTDEFINED", - "description": None, + "relating_element": relating_element, + "related_element": related_element, + "relating_connection": relating_connection, + "related_connection": related_connection, + "description": description, } - for key, value in usecase_settings.items(): - settings[key] = value incompatible_connections = [] for rel in settings["relating_element"].ConnectedTo: diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/create_2pt_wall.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/create_2pt_wall.py index cd508d4a3e..74c487b47e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/create_2pt_wall.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/create_2pt_wall.py @@ -22,8 +22,16 @@ import ifcopenshell.util.unit def create_2pt_wall( - file, element=None, context=None, p1=None, p2=None, elevation=None, height=None, thickness=None, is_si=True -) -> None: + file: ifcopenshell.file, + element: ifcopenshell.entity_instance, + context: ifcopenshell.entity_instance, + p1: tuple[float, float], + p2: tuple[float, float], + elevation: float, + height: float, + thickness: float, + is_si: bool = True, +) -> ifcopenshell.entity_instance: usecase = Usecase() usecase.file = file usecase.settings = { diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/disconnect_element.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/disconnect_element.py index 1e1eaaa82b..666d3d35b4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/disconnect_element.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/disconnect_element.py @@ -20,30 +20,31 @@ import ifcopenshell import ifcopenshell.util.element -def disconnect_element(file, **usecase_settings) -> None: - settings = { - "relating_element": None, - "related_element": None, - } - for key, value in usecase_settings.items(): - settings[key] = value - +def disconnect_element( + file: ifcopenshell.file, + relating_element: ifcopenshell.entity_instance, + related_element: ifcopenshell.entity_instance, +) -> None: + # TODO: arguments relating_element, related_element probably + # should be renamed to element1, element2 + # as api call doesn't really treat them as "relating" and "related" + # and just purging all connections between them incompatible_connections = [] - for rel in settings["relating_element"].ConnectedTo: - if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == settings["related_element"]: + for rel in relating_element.ConnectedTo: + if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == related_element: incompatible_connections.append(rel) - for rel in settings["relating_element"].ConnectedFrom: - if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == settings["related_element"]: + for rel in relating_element.ConnectedFrom: + if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == related_element: incompatible_connections.append(rel) - for rel in settings["related_element"].ConnectedTo: - if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == settings["relating_element"]: + for rel in related_element.ConnectedTo: + if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == relating_element: incompatible_connections.append(rel) - for rel in settings["related_element"].ConnectedFrom: - if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == settings["relating_element"]: + for rel in related_element.ConnectedFrom: + if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == relating_element: incompatible_connections.append(rel) if incompatible_connections: diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/disconnect_path.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/disconnect_path.py index 14bbaf9e7c..787f7a5633 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/disconnect_path.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/disconnect_path.py @@ -19,33 +19,36 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.util.element +from typing import Optional -def disconnect_path(file, **usecase_settings) -> None: - settings = { - "relating_element": None, - "related_element": None, - "element": None, - "connection_type": None, - } - for key, value in usecase_settings.items(): - settings[key] = value - - if settings["connection_type"] and settings["element"]: +def disconnect_path( + file: ifcopenshell.file, + element: Optional[ifcopenshell.entity_instance] = None, + connection_type: Optional[str] = None, + relating_element: Optional[ifcopenshell.entity_instance] = None, + related_element: Optional[ifcopenshell.entity_instance] = None, +) -> None: + """There are two options to use this API method: + - provide `element` (connected from) and `connection_type` that should be disconnected. + - provide connected elements to disconnect explicitly: + `relating_element` (connected from) and `related_element` (connected to) + """ + if connection_type and element: connections = [ r - for r in settings["element"].ConnectedTo - if r.is_a("IfcRelConnectsPathElements") and r.RelatingConnectionType == settings["connection_type"] + for r in element.ConnectedTo + if r.is_a("IfcRelConnectsPathElements") and r.RelatingConnectionType == connection_type ] + [ r - for r in settings["element"].ConnectedFrom - if r.is_a("IfcRelConnectsPathElements") and r.RelatedConnectionType == settings["connection_type"] + for r in element.ConnectedFrom + if r.is_a("IfcRelConnectsPathElements") and r.RelatedConnectionType == connection_type ] - else: + elif related_element: connections = [ r - for r in settings["relating_element"].ConnectedTo - if r.is_a("IfcRelConnectsPathElements") and r.RelatedElement == settings["related_element"] + for r in relating_element.ConnectedTo + if r.is_a("IfcRelConnectsPathElements") and r.RelatedElement == related_element ] for connection in set(connections): diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/edit_object_placement.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/edit_object_placement.py index 768468e03a..c71d803ce7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/edit_object_placement.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/edit_object_placement.py @@ -31,8 +31,8 @@ def edit_object_placement( file: ifcopenshell.file, product: ifcopenshell.entity_instance, matrix: Optional[NPArrayOfFloats] = None, - is_si=True, - should_transform_children=False, + is_si: bool = True, + should_transform_children: bool = False, ) -> ifcopenshell.entity_instance: usecase = Usecase() usecase.file = file diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/map_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/map_representation.py index 83e1e1e821..597463a3e5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/map_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/map_representation.py @@ -16,14 +16,15 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import ifcopenshell -def map_representation(file, **usecase_settings) -> None: + +def map_representation( + file: ifcopenshell.file, representation: ifcopenshell.entity_instance +) -> ifcopenshell.entity_instance: usecase = Usecase() usecase.file = file - usecase.settings = {"representation": None} - usecase.ifc_vertices = [] - for key, value in usecase_settings.items(): - usecase.settings[key] = value + usecase.settings = {"representation": representation} return usecase.execute() diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_boolean.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_boolean.py index 5d81203a5d..c24bcdbc1b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_boolean.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_boolean.py @@ -19,12 +19,10 @@ import ifcopenshell.util.element -def remove_boolean(file, **usecase_settings) -> None: +def remove_boolean(file: ifcopenshell.file, item: ifcopenshell.entity_instance) -> None: usecase = Usecase() usecase.file = file - usecase.settings = {"item": None} - for key, value in usecase_settings.items(): - usecase.settings[key] = value + usecase.settings = {"item": item} return usecase.execute() diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/unassign_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/unassign_representation.py index 83b1570ac6..435ea5207c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/unassign_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/unassign_representation.py @@ -20,12 +20,12 @@ import ifcopenshell.api import ifcopenshell.util.element -def unassign_representation(file, **usecase_settings) -> None: +def unassign_representation( + file: ifcopenshell.file, product: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance +) -> None: usecase = Usecase() usecase.file = file - usecase.settings = {"product": None, "representation": None} - for key, value in usecase_settings.items(): - usecase.settings[key] = value + usecase.settings = {"product": product, "representation": representation} return usecase.execute() diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/add_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/add_georeferencing.py index 5da8819e47..8b6ed48337 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/add_georeferencing.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/add_georeferencing.py @@ -16,8 +16,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import ifcopenshell -def add_georeferencing(file) -> None: + +def add_georeferencing(file: ifcopenshell.file) -> None: """Add empty georeferencing entities to a model By default, models are not georeferenced. Georeferencing requires two diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py index f4118d96e1..08d7361935 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py @@ -16,8 +16,16 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import ifcopenshell +from typing import Optional, Any -def edit_georeferencing(file, map_conversion=None, projected_crs=None, true_north=None) -> None: + +def edit_georeferencing( + file: ifcopenshell.file, + map_conversion: Optional[dict[str, Any]] = None, + projected_crs: Optional[dict[str, Any]] = None, + true_north: Optional[tuple[float, float]] = None, +) -> None: """Edits the attributes of a map conversion, projected CRS, and true north Setting the correct georeferencing parameters is a complex topic and @@ -47,7 +55,7 @@ def edit_georeferencing(file, map_conversion=None, projected_crs=None, true_nort names and values you want to edit. :type projected_crs: dict, optional :param true_north: A unitised 2D vector, where each ordinate is a float - :type true_north: list[float] + :type true_north: tuple[float, float], optional :return: None :rtype: None @@ -101,7 +109,7 @@ class Usecase: self.set_true_north() def set_true_north(self): - if self.settings["true_north"] == []: + if self.settings["true_north"] == None: return for context in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False): if context.TrueNorth: @@ -111,6 +119,8 @@ class Usecase: context.TrueNorth = self.file.create_entity("IfcDirection") direction = context.TrueNorth if self.settings["true_north"] is None: + # TODO: code will never be executed since None value + # is substituted by an empty list context.TrueNorth = self.settings["true_north"] elif context.CoordinateSpaceDimension == 2: direction.DirectionRatios = self.settings["true_north"][0:2] diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py index 3d3941ed7c..bac0f6253d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py @@ -16,8 +16,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import ifcopenshell -def remove_georeferencing(file) -> None: + +def remove_georeferencing(file: ifcopenshell.file) -> None: """Remove georeferencing data All georeferencing parameters such as projected CRS and map conversion diff --git a/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py b/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py index 2f3520c662..abb2848205 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py +++ b/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py @@ -16,13 +16,18 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +from __future__ import annotations import ifcopenshell +import ifcopenshell.util.element import ifcopenshell.util.unit import ifcopenshell.util.placement from mathutils import Matrix # For now, we depend on Blender +import bpy.types -def create_axis_curve(file, axis_curve=None, grid_axis=None) -> None: +def create_axis_curve( + file: ifcopenshell.file, axis_curve: bpy.types.Object, grid_axis: ifcopenshell.entity_instance +) -> None: """Adds curve geometry to a grid axis to represent the axis extents This currently depends on the Blender geometry kernel to function. diff --git a/src/ifcopenshell-python/ifcopenshell/api/grid/create_grid_axis.py b/src/ifcopenshell-python/ifcopenshell/api/grid/create_grid_axis.py index f089b43b98..b91d86e259 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/grid/create_grid_axis.py +++ b/src/ifcopenshell-python/ifcopenshell/api/grid/create_grid_axis.py @@ -15,9 +15,17 @@ # # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import ifcopenshell +from typing import Optional, Literal -def create_grid_axis(file, axis_tag=None, same_sense=None, uvw_axes=None, grid=None) -> None: +def create_grid_axis( + file: ifcopenshell.file, + grid: ifcopenshell.entity_instance, + axis_tag: str = "A", + same_sense: bool = True, + uvw_axes: Literal["UAxes", "VAxes", "WAxes"] = "UAxes", +) -> ifcopenshell.entity_instance: """Adds a new grid axis to a grid An IFC grid will typically have a minimum of two axes which will be @@ -66,17 +74,9 @@ def create_grid_axis(file, axis_tag=None, same_sense=None, uvw_axes=None, grid=N axis_1 = ifcopenshell.api.run("grid.create_grid_axis", model, axis_tag="1", uvw_axes="VAxes", grid=grid) """ - settings = { - "axis_tag": axis_tag or "A", - "same_sense": same_sense or True, - "uvw_axes": uvw_axes or "UAxes", # Choose which axes - "grid": grid, - } - element = file.create_entity( - "IfcGridAxis", **{"AxisTag": settings["axis_tag"], "SameSense": settings["same_sense"]} - ) - axes = list(getattr(settings["grid"], settings["uvw_axes"]) or []) + element = file.create_entity("IfcGridAxis", **{"AxisTag": axis_tag, "SameSense": same_sense}) + axes = list(getattr(grid, uvw_axes) or []) axes.append(element) - setattr(settings["grid"], settings["uvw_axes"], axes) + setattr(grid, uvw_axes, axes) return element diff --git a/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py b/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py index 51032ed52f..2e3ff1b005 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py +++ b/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py @@ -19,7 +19,7 @@ import ifcopenshell.util.element -def remove_grid_axis(file, axis=None) -> None: +def remove_grid_axis(file: ifcopenshell.file, axis: ifcopenshell.entity_instance) -> None: """Removes a grid axis from a grid :param axis: The IfcGridAxis you want to remove. @@ -43,9 +43,8 @@ def remove_grid_axis(file, axis=None) -> None: # Let's remove it! ifcopenshell.api.run("grid.remove_grid_axis", model, axis=axis_2) """ - settings = {"axis": axis} - - if len(file.get_inverse(settings["axis"].AxisCurve)) == 1: - ifcopenshell.util.element.remove_deep(file, settings["axis"].AxisCurve) - file.remove(settings["axis"].AxisCurve) - file.remove(settings["axis"]) + axis_curve = axis.AxisCurve + if len(file.get_inverse(axis_curve)) == 1: + ifcopenshell.util.element.remove_deep(file, axis_curve) + file.remove(axis_curve) + file.remove(axis) diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/add_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/add_group.py index 7a233bdcbf..44553bddcf 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/add_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/add_group.py @@ -19,9 +19,12 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.guid +from typing import Optional -def add_group(file, Name="Unnamed", Description=None) -> None: +def add_group( + file: ifcopenshell.file, Name: str = "Unnamed", Description: Optional[str] = None +) -> ifcopenshell.entity_instance: """Adds a new group An IFC group is an arbitrary collection of products, which are typically diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py index 1eb0c8d6f4..912b3c30c9 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py @@ -15,9 +15,11 @@ # # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import ifcopenshell +from typing import Any -def edit_group(file, group=None, attributes=None) -> None: +def edit_group(file: ifcopenshell.file, group: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: """Edits the attributes of an IfcGroup For more information about the attributes and data types of an @@ -26,7 +28,7 @@ def edit_group(file, group=None, attributes=None) -> None: :param group: The IfcGroup entity you want to edit :type group: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict, optional + :type attributes: dict :return: None :rtype: None @@ -38,7 +40,7 @@ def edit_group(file, group=None, attributes=None) -> None: ifcopenshell.api.run("group.edit_group", model, group=group, attributes={"Description": "All furniture and joinery included in the unit"}) """ - settings = {"group": group, "attributes": attributes or {}} + settings = {"group": group, "attributes": attributes} for name, value in settings["attributes"].items(): setattr(settings["group"], name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/remove_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/remove_group.py index 05e85f3fc0..d17806d6c3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/remove_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/remove_group.py @@ -21,7 +21,7 @@ import ifcopenshell.api import ifcopenshell.util.element -def remove_group(file, group=None) -> None: +def remove_group(file: ifcopenshell.file, group: ifcopenshell.entity_instance) -> None: """Removes a group All products assigned to the group will remain, but the relationship to diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py b/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py index 74ef54c0bb..c68992f7a3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py @@ -21,7 +21,9 @@ import ifcopenshell.api import ifcopenshell.guid -def update_group_products(file, group=None, products=None) -> None: +def update_group_products( + file: ifcopenshell.file, group: ifcopenshell.entity_instance, products: list[ifcopenshell.entity_instance] +) -> ifcopenshell.entity_instance: """Sets a group products to be an explicit list of products Any previous products assigned to that group will have their assignment diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/add_library.py b/src/ifcopenshell-python/ifcopenshell/api/library/add_library.py index fdd21c3424..00af1708e4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/add_library.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/add_library.py @@ -21,7 +21,7 @@ import ifcopenshell.util.schema import ifcopenshell.util.date -def add_library(file: ifcopenshell.entity_instance, name: str) -> ifcopenshell.entity_instance: +def add_library(file: ifcopenshell.file, name: str) -> ifcopenshell.entity_instance: """Adds a new library to the project A library is an external data source that is related to the project. It diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py index 37a57825f5..f584c341ca 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py @@ -36,7 +36,7 @@ def edit_profile_usage( :param usage: The IfcMaterialProfileSetUsage entity you want to edit :type usage: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict, optional + :type attributes: dict :return: None :rtype: None @@ -93,7 +93,7 @@ def edit_profile_usage( usecase = Usecase() usecase.file = file - usecase.settings = {"usage": usage, "attributes": attributes or {}} + usecase.settings = {"usage": usage, "attributes": attributes} return usecase.execute() diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py index e492707c44..a033030530 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py @@ -22,7 +22,7 @@ import ifcopenshell.guid def assign_product( - file: ifcopenshell.entity_instance, + file: ifcopenshell.file, relating_product: ifcopenshell.entity_instance, related_object: ifcopenshell.entity_instance, ) -> ifcopenshell.entity_instance: diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py index 79ef8b90de..5179073e70 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py @@ -21,7 +21,7 @@ from typing import Any def edit_work_schedule( - file: ifcopenshell.entity_instance, work_schedule: ifcopenshell.entity_instance, attributes: dict[str, Any] + file: ifcopenshell.file, work_schedule: ifcopenshell.entity_instance, attributes: dict[str, Any] ) -> None: """Edits the attributes of an IfcWorkSchedule diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py index 22a19e5051..05ec24adff 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py @@ -20,7 +20,7 @@ import ifcopenshell import ifcopenshell.util.element -def remove_work_plan(file: ifcopenshell.entity_instance, work_plan: ifcopenshell.entity_instance) -> None: +def remove_work_plan(file: ifcopenshell.file, work_plan: ifcopenshell.entity_instance) -> None: """Removes a work plan Note that schedules that are grouped under the work plan are not diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_textures.py b/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_textures.py index f6c8c3b0c3..48583984aa 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_textures.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_textures.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: def add_surface_textures( - file: ifcopenshell.entity_instance, + file: ifcopenshell.file, material: Optional[bpy.types.Material] = None, textures: Optional[list[dict]] = None, uv_maps: Optional[list[ifcopenshell.entity_instance]] = None, diff --git a/src/ifcopenshell-python/ifcopenshell/file.py b/src/ifcopenshell-python/ifcopenshell/file.py index ee421256c5..53513cfb05 100644 --- a/src/ifcopenshell-python/ifcopenshell/file.py +++ b/src/ifcopenshell-python/ifcopenshell/file.py @@ -24,7 +24,7 @@ import zipfile import functools import ifcopenshell from pathlib import Path -from typing import Optional, Any +from typing import Optional, Any, Union, Callable from . import ifcopenshell_wrapper from .entity_instance import entity_instance @@ -379,7 +379,7 @@ class file: return e - def __getattr__(self, attr): + def __getattr__(self, attr) -> Union[Any, Callable[..., ifcopenshell.entity_instance]]: if attr[0:6] == "create": return functools.partial(self.create_entity, attr[6:]) elif attr == "schema": diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 2430a2553a..60ac89044c 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -979,7 +979,7 @@ def get_decomposition(element: ifcopenshell.entity_instance, is_recursive=True) def get_grouped_by(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: """Retrieves all subelements of an element based on the group. - :param element: The IFC element + :param element: IfcGroup entity :type element: ifcopenshell.entity_instance :return: All subelements of the group :rtype: list[ifcopenshell.entity_instance] diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index 29ed257cc8..81ec583419 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -26,7 +26,7 @@ import ifcopenshell.util.element import ifcopenshell.util.representation import ifcopenshell.util.unit from math import cos, sin, pi, tan, radians, degrees, atan, sqrt, ceil -from typing import List, Tuple, Type, Union +from typing import Union, Optional, Literal, Any from itertools import chain from mathutils import Vector, Matrix @@ -34,7 +34,7 @@ V = lambda *x: Vector([float(i) for i in x]) sign = lambda x: x and (1, -1)[x < 0] PRECISION = 1.0e-5 -VectorTuple = Type[Tuple[float, float, float]] +VectorTuple = type[tuple[float, float, float]] "tuple of 3 `float` values" @@ -59,13 +59,17 @@ class ShapeBuilder: self.file = ifc_file def polyline( - self, points: List[Vector], closed: bool = False, position_offset: Vector = None, arc_points: List[int] = [] + self, + points: list[Vector], + closed: bool = False, + position_offset: Optional[Vector] = None, + arc_points: list[int] = [], ) -> ifcopenshell.entity_instance: """ Generate an IfcIndexedPolyCurve based on the provided points. :param points: List of 2d or 3d points - :type points: List[Vector] + :type points: list[Vector] :param closed: Whether polyline should be closed. Default is `False` :type closed: bool, optional :param position_offset: offset to be applied to all points @@ -73,7 +77,7 @@ class ShapeBuilder: :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` point's index to `arc_points` - :type arc_points: List[int], optional + :type arc_points: list[int], optional :return: IfcIndexedPolyCurve :rtype: ifcopenshell.entity_instance @@ -155,7 +159,9 @@ class ShapeBuilder: 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) -> List[Vector]: + def get_rectangle_coords( + self, size: Vector = Vector((1.0, 1.0)).freeze(), position: Optional[Vector] = None + ) -> list[Vector]: """ Get rectangle coords arranged as below: @@ -248,7 +254,7 @@ class ShapeBuilder: # 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): + def curve_between_two_points(self, points: tuple[Vector, Vector]) -> ifcopenshell.entity_instance: # > 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. @@ -268,7 +274,13 @@ class ShapeBuilder: 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): + def get_trim_points_from_mask( + self, + x_axis_radius: float, + y_axis_radius: float, + trim_points_mask: list[int], + position_offset: Optional[Vector] = None, + ) -> list[Vector]: """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). @@ -289,13 +301,13 @@ class ShapeBuilder: def create_ellipse_curve( self, - x_axis_radius, - y_axis_radius, + x_axis_radius: float, + y_axis_radius: float, position=Vector((0.0, 0.0)).freeze(), - trim_points=[], - ref_x_direction=Vector((1.0, 0.0)), - trim_points_mask=[], - ): + trim_points: list[Vector] = (), + ref_x_direction: Vector = Vector((1.0, 0.0)), + trim_points_mask: list[int] = (), + ) -> ifcopenshell.entity_instance: """ Ellipse trimming points should be specified in counter clockwise order. @@ -329,7 +341,13 @@ class ShapeBuilder: ) return trim_ellipse - def profile(self, outer_curve, name=None, inner_curves=[], profile_type="AREA"): + def profile( + self, + outer_curve: ifcopenshell.entity_instance, + name: Optional[str] = None, + inner_curves: list[ifcopenshell.entity_instance] = (), + profile_type: str = "AREA", + ) -> ifcopenshell.entity_instance: # > 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 @@ -369,7 +387,12 @@ class ShapeBuilder: profile = self.file.create_entity("IfcArbitraryClosedProfileDef", **kwargs) return profile - def translate(self, curve_or_item, translation: Vector, create_copy=False): + def translate( + self, + curve_or_item: Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]], + translation: Vector, + create_copy: bool = False, + ) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]: # > curve_or_item - could be a list of curves or items or representations # < returns translated object @@ -413,7 +436,7 @@ class ShapeBuilder: def rotate_2d_point( self, point_2d: Vector, angle=90, pivot_point: Vector = Vector((0.0, 0.0)).freeze(), counter_clockwise=False - ): + ) -> Vector: # > angle - in degrees # < rotated Vector @@ -425,12 +448,12 @@ class ShapeBuilder: def rotate( self, - curve_or_item, - angle=90, + curve_or_item: Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]], + angle: float = 90, pivot_point: Vector = Vector((0.0, 0.0)).freeze(), - counter_clockwise=False, - create_copy=False, - ): + counter_clockwise: bool = False, + create_copy: bool = False, + ) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]: # > curve_or_item - could be a list of curves or items # > angle - in degrees # < returns rotated object @@ -479,7 +502,7 @@ class ShapeBuilder: point_2d: Vector, mirror_axes: Vector = Vector((1.0, 1.0)).freeze(), mirror_point: Vector = Vector((0.0, 0.0)).freeze(), - ): + ) -> Vector: """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]) @@ -558,12 +581,12 @@ class ShapeBuilder: def mirror( self, - curve_or_item, + curve_or_item: Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]], mirror_axes: Vector = Vector((1.0, 1.0)).freeze(), mirror_point: Vector = Vector((0.0, 0.0)).freeze(), - create_copy=False, - placement_matrix=None, - ): + create_copy: bool = False, + placement_matrix: Optional[Matrix] = None, + ) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]: """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)` @@ -687,14 +710,14 @@ class ShapeBuilder: def extrude( self, - profile_or_curve, - magnitude=1.0, + profile_or_curve: ifcopenshell.entity_instance, + magnitude: float = 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, - ): + position_y_axis: Optional[Vector] = None, + ) -> ifcopenshell.entity_instance: """Extrude profile or curve to get IfcExtrudedAreaSolid. REMEMBER when handling custom axes - IFC is using RIGHT handed coordinate system. @@ -730,7 +753,9 @@ class ShapeBuilder: ) return extruded_area - def create_swept_disk_solid(self, path_curve, radius): + 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`""" if path_curve.Dim != 3: raise Exception( @@ -741,16 +766,22 @@ class ShapeBuilder: disk_solid = self.file.createIfcSweptDiskSolid(Directrix=path_curve, Radius=radius) return disk_solid - def get_representation(self, context, items, representation_type: str = None) -> ifcopenshell.entity_instance: + def get_representation( + self, + context: ifcopenshell.entity_instance, + items: Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]], + representation_type: Optional[str] = None, + ) -> ifcopenshell.entity_instance: """Create IFC representation for the specified context and items. :param context: IfcGeometricRepresentationSubContext + :type context: ifcopenshell.entity_instance :param items: could be a list or single curve/IfcExtrudedAreaSolid :param representation_type: Explicitly specified RepresentationType, defaults to `None`. If not provided it will be guessed from the items types :type representation_type: str, optional - :return: IfcRepresentation + :return: IfcShapeRepresentation :rtype: ifcopenshell.entity_instance """ if not isinstance(items, collections.abc.Iterable): @@ -779,11 +810,11 @@ class ShapeBuilder: ) return representation - def deep_copy(self, element): + def deep_copy(self, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: return ifcopenshell.util.element.copy_deep(self.file, element) # UTILITIES - def extrude_kwargs(self, axis): + def extrude_kwargs(self, axis: Literal["Y", "X", "Z"]) -> dict[str, Vector]: """Shortcut to get kwargs for `ShapeBuilder.extrude` to extrude by some axis. It assumes you have 2D profile in: @@ -814,7 +845,9 @@ class ShapeBuilder: "extrusion_vector": Vector((0, 0, 1)), } - def rotate_extrusion_kwargs_by_z(self, kwargs, angle, counter_clockwise=False): + def rotate_extrusion_kwargs_by_z( + self, kwargs: dict[str, Any], angle: float, counter_clockwise: bool = False + ) -> dict[str, Vector]: """shortcut to rotate extrusion kwargs by z axis `kwargs` expected to have `position_x_axis` and `position_z_axis` keys @@ -829,7 +862,7 @@ class ShapeBuilder: kwargs["position_z_axis"].rotate(rot) return kwargs - def get_polyline_coords(self, polyline): + def get_polyline_coords(self, polyline: ifcopenshell.entity_instance) -> list[Vector]: """polyline should be either `IfcIndexedPolyCurve` or `IfcPolyline`""" coords = None if polyline.is_a("IfcIndexedPolyCurve"): @@ -838,7 +871,7 @@ class ShapeBuilder: coords = [p.Coordinates for p in polyline.Points] return coords - def set_polyline_coords(self, polyline, coords): + def set_polyline_coords(self, polyline: ifcopenshell.entity_instance, coords: list[Vector]) -> None: """polyline should be either `IfcIndexedPolyCurve` or `IfcPolyline`""" if polyline.is_a("IfcIndexedPolyCurve"): polyline.Points.CoordList = coords @@ -846,7 +879,14 @@ class ShapeBuilder: for i, co in enumerate(coords): polyline.Points[i].Coordinates = co - def get_simple_2dcurve_data(self, coords, fillets=[], fillet_radius=[], closed=True, create_ifc_curve=None): + def get_simple_2dcurve_data( + self, + coords: list[Vector], + fillets: list[int] = (), + fillet_radius: list[float] = (), + closed: bool = True, + create_ifc_curve: bool = False, + ) -> tuple[list[Vector], list[tuple[int, int], Union[ifcopenshell.entity_instance, None]]]: """ 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. @@ -957,8 +997,14 @@ class ShapeBuilder: return (points, segments, ifc_curve) def create_z_profile_lips_curve( - self, FirstFlangeWidth, SecondFlangeWidth, Depth, Girth, WallThickness, FilletRadius - ): + self, + FirstFlangeWidth: float, + SecondFlangeWidth: float, + Depth: float, + Girth: float, + WallThickness: float, + FilletRadius: float, + ) -> ifcopenshell.entity_instance: x1 = FirstFlangeWidth x2 = SecondFlangeWidth y = Depth / 2 @@ -996,7 +1042,9 @@ class ShapeBuilder: return ifc_curve - def create_transition_arc_ifc(self, width, height, create_ifc_curve=False): + def create_transition_arc_ifc( + self, width: str, height: str, create_ifc_curve: bool = False + ) -> tuple[list[Vector], list[tuple[int, int], Union[ifcopenshell.entity_instance, None]]]: # create an arc in the rectangle with specified width and height # if it's not possible to make a complete arc # it will create arc with longest radius possible @@ -1028,7 +1076,7 @@ class ShapeBuilder: ) return points, segments, transition_arc - def polygonal_face_set(self, points, faces): + def polygonal_face_set(self, points: list[Vector], faces: list[[list[int]]]) -> ifcopenshell.entity_instance: """ > `points` - list of points @@ -1048,8 +1096,14 @@ class ShapeBuilder: return face_set def extrude_face_set( - self, points, magnitude: float, extrusion_vector=V(0, 0, 1).freeze(), offset=None, start_cap=True, end_cap=True - ): + self, + points: list[Vector], + magnitude: float, + extrusion_vector: Vector = V(0, 0, 1).freeze(), + offset: Optional[Vector] = None, + start_cap: bool = True, + end_cap: bool = True, + ) -> ifcopenshell.entity_instance: """ Method to extrude by creating face sets rather than creating IfcExtrudedAreaSolid. @@ -1057,18 +1111,20 @@ class ShapeBuilder: to assure CorrectItemsForType. :param points: list of points, assuming they form consecutive closed polyline. + :type points: list[Vector] :param magnitude: extrusion magnitude - :param type: float + :type magnitude: float :param extrusion_vector: extrusion direction, by default it's extruding by Z+ axis - :param type: Vector, optional + :type extrusion_vector: Vector, optional :param offset: offset from the points - :param type: Vector, optional + :type offset: Vector, optional :param start_cap: if True, create start cap, by default it's True - :param type: bool, optional + :type start_cap: bool, optional :param end_cap: if True, create end cap, by default it's True - :param type: bool, optional + :type end_cap: bool, optional :return: IfcPolygonalFaceSet + :rtype: ifcopenshell.entity_instance """ # prevent mutating arguments, deepcopy doesn't work diff --git a/src/ifcopenshell-python/test/api/pset/test_remove_pset.py b/src/ifcopenshell-python/test/api/pset/test_remove_pset.py index 1685f158e8..134bf1b9a7 100644 --- a/src/ifcopenshell-python/test/api/pset/test_remove_pset.py +++ b/src/ifcopenshell-python/test/api/pset/test_remove_pset.py @@ -18,6 +18,7 @@ import test.bootstrap import ifcopenshell.api +import ifcopenshell.util.element class TestRemovePset(test.bootstrap.IFC4):