Compare commits

..

24 Commits

Author SHA1 Message Date
Andrej730 a2ee920a5f fix group.update_group_products to work with multiple rels 2024-05-15 17:18:34 +05:00
Andrej730 fdbe74a432 maintain snake in api calls
there were only two methods using camel case for arguments
2024-05-15 17:18:34 +05:00
Andrej730 4e39fb3edd fix issue adding layers (name is not optional in ifc) 2024-05-15 17:18:34 +05:00
Andrej730 c2049246cd fix issue adding classification failing to use a None value for the date 2024-05-15 17:18:34 +05:00
Andrej730 335e2b7a78 ifc2x3 tests 2024-05-15 17:18:34 +05:00
Andrej730 bb8e84e5ec typing 2024-05-15 17:18:33 +05:00
Andrej730 2fa30be0d0 small optimization for da1bdc802
On large projects predict dense mesh stage can take 20s+ and reusing attribute value can save up to half of this time.
Using indices also helps but it's not that significant and sometimes it's the same time as using attribute names.
2024-05-14 18:19:13 +05:00
Bruno Perdigão c50149ad87 added 'product' parameter to a few usages of 'pset.remove_pset' after ebd03e9 2024-05-14 17:48:05 +05:00
Dion Moult da1bdc802d Accommodate invalid models coming from Cadwork 2024-05-14 16:16:18 +10:00
Andrej730 a9cba30da6 ifc2x3 tests
removed part of test_append_two_type_products_sharing_the_same_material_indirectly_via_a_material_set for ifc2x3 compatibility and removed part is already tested in test_append_two_type_products_sharing_the_same_material_with_properties
2024-05-13 18:01:18 +05:00
Andrej730 3665dda87c library.remove_library, remove_reference to support ifc2x3 2024-05-13 18:01:18 +05:00
Andrej730 f2696d5352 avoid confusing TypeErrors from api calls
After ab5ea4c85 it was always throwing wrong singature errors like below even if TypeError was caused by some internal issues inside API - it was adding couple extra steps to traceback making errors more noisy.

TypeError: Incorrect function arguments provided for library.edit_library
attribute 'VersionDate' for entity 'IFC2X3.IfcLibraryInformation' is expecting value of type 'ENTITY INSTANCE', got 'str'.. You specified args (<ifcopenshell.file.file object at 0x0000027EB8E6BCD0>,) and settings {'library': #1=IfcLibraryInformation('Name','Version',$,$,$), 'attributes': {'Name': 'Name', 'Version': 'Version', 'VersionDate': 'VersionDate', 'Location': 'Location', 'Description': 'Description'}}
E
Correct signature is (file: ifcopenshell.file.file, library: ifcopenshell.entity_instance.entity_instance, attributes: dict[str, typing.Any]) -> None
See help(ifcopenshell.api.library.edit_library) for documentation.
2024-05-13 18:01:18 +05:00
Andrej730 5e394e1576 library.edit_library - support datetime for VersionDate attribute 2024-05-13 18:01:18 +05:00
Andrej730 ebd03e9290 typing 2024-05-13 18:01:17 +05:00
Andrej730 c3bfd7354f pset.add_pset - throw an error if entity doesn't support adding a pset 2024-05-13 18:01:17 +05:00
Andrej730 1d046eaadf material.remove_material and copy_material to handle ifc2x3 props 2024-05-13 18:01:17 +05:00
Andrej730 2f554db54b owner.remove_person to remove IfcInventory in ifc2x3 2024-05-13 18:01:17 +05:00
Andrej730 54b4cef25e fix errors appending related materials in ifc2x3
it wasn't processing materials properties correctly because it was expecting material to have class IfcMaterialDefinition
2024-05-13 18:01:17 +05:00
Andrej730 c6106e6636 fix edit_pset error for editing material properties in ifc2x3 2024-05-13 18:01:17 +05:00
Andrej730 865dbab3ec fix error removing array pset after c50d1ea2f 2024-05-13 18:01:17 +05:00
Andrej730 76e6c63a01 ifc2x3 tests - prevent using removed user/application
When some test was creating an element and then removing it, it would also remove user and application as they wasn't used anywhere else.
`ifcopenshell.util.element.remove_deep2(file, history)` we use in every api for element deletion can possibly remove user and application which can be unsafe if `get_user` is returning some specific entity that then will become invalid.

Also fixed tests breaking due ifcownerhistory and user/application appearing in ifc2x3.
2024-05-13 18:01:16 +05:00
Gorgious56 3320accc7a The error box is now drawn in red to draw attention to it 2024-05-13 12:00:01 +02:00
Gorgious56 ac686db298 Prevent error when user deletes all spatial structure elements and the spatial manager panel is expanded 2024-05-13 11:57:48 +02:00
Dion Moult aca26c7597 Fix #4659. 2024-05-12 17:16:50 +10:00
179 changed files with 2053 additions and 976 deletions
+7 -3
View File
@@ -608,7 +608,9 @@ class IfcImporter:
threshold = 10000 # Just from experience.
faces = [len(e.CfsFaces) for e in self.file.by_type("IfcClosedShell")]
# The check for CfsFaces/Faces/CoordIndex accommodates invalid data from Cadwork
# 0 IfcClosedShell.CfsFaces
faces = [len(faces) for e in self.file.by_type("IfcClosedShell") if (faces := e[0])]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
return
@@ -616,12 +618,14 @@ class IfcImporter:
if self.file.schema == "IFC2X3":
return
faces = [len(e.Faces) for e in self.file.by_type("IfcPolygonalFaceSet")]
# 2 IfcPolygonalFaceSet.Faces
faces = [len(faces) for e in self.file.by_type("IfcPolygonalFaceSet") if (faces := e[2])]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
return
faces = [len(e.CoordIndex) for e in self.file.by_type("IfcTriangulatedFaceSet")]
# 3 IfcTriangulatedFaceSet.CoordIndex
faces = [len(index) for e in self.file.by_type("IfcTriangulatedFaceSet") if (index := e[3])]
if faces and max(faces) > threshold:
self.ifc_import_settings.should_use_native_meshes = True
@@ -100,7 +100,7 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, Operator):
pset = ifcopenshell.util.element.get_pset(element, 'BBIM_Linked_Aggregate')
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
class BIM_OT_enable_editing_aggregate(bpy.types.Operator, Operator):
@@ -938,7 +938,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
pset = ifcopenshell.util.element.get_pset(new[0], "BBIM_Linked_Aggregate")
if pset:
pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=new[0],pset=pset)
if new[0].is_a("IfcElementAssembly"):
linked_aggregate_group = [
@@ -1050,7 +1050,7 @@ class DuplicateMoveLinkedAggregate(bpy.types.Operator):
if self.group_name in product_groups_name:
return
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.group_name)
linked_aggregate_group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.group_name)
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=[element], group=linked_aggregate_group)
def custom_incremental_naming_for_element_assembly(old_to_new):
@@ -83,17 +83,8 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Data"))[self.item]
props = obj.BIMArrayProperties
relating_obj = props.relating_array_object
if relating_obj:
element = tool.Ifc.get_entity(relating_obj)
parent_globalid = ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Parent")
parent_element = tool.Ifc.get().by_guid(parent_globalid)
data = json.loads(ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array", "Data"))[self.item]
else:
data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Array", "Data"))[self.item]
props.count = data["count"]
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
props.x = data["x"] * si_conversion
@@ -102,9 +93,7 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator):
props.use_local_space = data.get("use_local_space", False)
props.sync_children = data.get("sync_children", False)
props.method = data.get("method", "OFFSET")
props.is_editing = self.item
return {"FINISHED"}
@@ -148,10 +137,6 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator):
tool.Blender.Modifier.Array.set_children_lock_state(element, self.item, True)
tool.Blender.Modifier.Array.constrain_children_to_parent(element)
#clears the relating_array_object so it doesn't show again next time
props.relating_array_object = None
return {"FINISHED"}
@@ -208,7 +193,7 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator):
pset = tool.Ifc.get().by_id(pset["id"])
if len(data) == 1:
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
else:
del data[self.item]
data = tool.Ifc.get().createIfcText(json.dumps(data))
@@ -644,6 +644,6 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMDoorProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -87,21 +87,6 @@ def update_type_page(self, context):
AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types()
def update_relating_array_from_object(self, context):
bpy.ops.bim.enable_editing_array(item=self.is_editing)
return
def is_object_array_applicable(self, obj):
element = tool.Ifc.get_entity(obj)
if not element:
return False
return ifcopenshell.util.element.get_pset(element, "BBIM_Array")
class BIMModelProperties(PropertyGroup):
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
relating_type_id: bpy.props.EnumProperty(
@@ -218,14 +203,6 @@ class BIMArrayProperties(PropertyGroup):
description="Regenerate all children based on the parent object",
default=False,
)
relating_array_object: bpy.props.PointerProperty(
type=bpy.types.Object,
name="Copy Array Properties",
update=update_relating_array_from_object,
poll=is_object_array_applicable,
)
class BIMStairProperties(PropertyGroup):
@@ -535,5 +535,5 @@ class RemoveRailing(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMRailingProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Railing")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -757,7 +757,7 @@ class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMRoofProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Roof")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -337,6 +337,6 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMStairProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -223,8 +223,6 @@ class BIM_PT_array(bpy.types.Panel):
row = col.row(align=True)
row.prop(props, "z")
row.operator("bim.input_cursor_z_array", icon="CURSOR", text="")
row = col.row(align=True)
row.prop(props, "relating_array_object", icon="COPYDOWN")
else:
row = box.row(align=True)
name = f"{array['count']} Items ({array.get('method', 'OFFSET').capitalize()})"
@@ -590,6 +590,6 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
obj.BIMWindowProperties.is_editing = False
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"}
@@ -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
@@ -214,7 +215,7 @@ class SaveSearch(Operator, tool.Ifc.Operator):
group = group[0]
group.Description = description
else:
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.name, description=description)
if results:
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=list(results), group=group)
@@ -367,7 +368,7 @@ class SaveColourscheme(Operator, tool.Ifc.Operator):
description = json.dumps(
{"type": "BBIM_Search", "colourscheme": colourscheme, "colourscheme_query": query}
)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), name=self.name, description=description)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -44,6 +44,8 @@ def update_elevation(self, context):
def update_active_container_index(self, context):
if self.active_container_index < 0:
return
self.active_container_id = self.containers[self.active_container_index].ifc_definition_id
self.container_name = self.containers[self.active_container_index].name
self.elevation = self.containers[self.active_container_index].elevation
@@ -116,7 +116,7 @@ class BIM_PT_SpatialManager(Panel):
self.props = context.scene.BIMSpatialManagerProperties
row = self.layout.row()
row.operator("bim.load_container_manager", icon="FILE_REFRESH", text="Load Spatial Structure")
if self.props.active_container_index < len(self.props.containers):
if 0 <= self.props.active_container_index < len(self.props.containers):
ifc_definition_id = self.props.containers[self.props.active_container_index].ifc_definition_id
row = self.layout.row()
row.alignment = "RIGHT"
@@ -134,7 +134,7 @@ class BIM_PT_SpatialManager(Panel):
"active_container_index",
)
row = self.layout.row()
if self.props.active_container_index < len(self.props.containers):
if 0 <= self.props.active_container_index < len(self.props.containers):
row.prop(self.props, "container_name", text="")
row.prop(self.props, "elevation", text="")
op = row.operator("bim.edit_container_attributes", icon="CHECKMARK", text="Apply")
+1
View File
@@ -406,6 +406,7 @@ class BIM_PT_tabs(Panel):
if blenderbim.last_error:
box = self.layout.box()
box.alert=True
row = box.row(align=True)
row.label(text="BlenderBIM experienced an error :(", icon="ERROR")
row.operator("bim.close_error", text="", icon="CANCEL")
+1 -1
View File
@@ -47,7 +47,7 @@ def add_representation(
data = geometry.get_object_data(obj)
if not data and ifc_representation_class != "IfcTextLiteral":
raise IncompatibleRepresentationError()
return
representation = ifc.run(
"geometry.add_representation",
+1 -1
View File
@@ -551,7 +551,7 @@ class Model(blenderbim.core.tool.Model):
data = tool.Ifc.get().createIfcText(json.dumps(data))
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"Data": data})
else:
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), pset=pset)
ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset)
@classmethod
def get_flow_segment_axis(cls, obj):
+1 -1
View File
@@ -1630,7 +1630,7 @@ class Sequence(blenderbim.core.tool.Sequence):
group.Description = json.dumps(description)
else:
description = json.dumps({"type": "BBIM_AnimationColorScheme", "colourscheme": colour_scheme})
group = tool.Ifc.run("group.add_group", Name=name, Description=description)
group = tool.Ifc.run("group.add_group", name=name, description=description)
return group[0]
@classmethod
@@ -67,6 +67,20 @@ def batching_argument_deprecation(
return (replace_usecase or usecase_path, settings)
def renamed_arguments_deprecation(
usecase_path: str, settings: dict, arguments_remapped: dict[str, str]
) -> tuple[str, dict]:
for prev_argument, new_argument in arguments_remapped.items():
if prev_argument in settings:
print(
f"WARNING. `{prev_argument}` argument is deprecated for API method "
f'"{usecase_path}" and should be replaced with `{new_argument}`.'
)
settings = settings | {new_argument: settings[prev_argument]}
settings.pop(prev_argument)
return (usecase_path, settings)
ARGUMENTS_DEPRECATION = {
"spatial.assign_container": partial(
batching_argument_deprecation, prev_argument="product", new_argument="products"
@@ -143,6 +157,10 @@ ARGUMENTS_DEPRECATION = {
"project.unassign_declaration": partial(
batching_argument_deprecation, prev_argument="definition", new_argument="definitions"
),
"group.add_group": partial(
renamed_arguments_deprecation, arguments_remapped={"Name": "name", "Description": "description"}
),
"layer.add_layer": partial(renamed_arguments_deprecation, arguments_remapped={"Name": "name"}),
}
@@ -324,8 +342,18 @@ def wrap_usecase(usecase_path, usecase):
try:
result = usecase(*args, **settings)
except TypeError as e:
msg = f"Incorrect function arguments provided for {usecase_path}\n{str(e)}. You specified args {args} and settings {settings}\n\nCorrect signature is {inspect.signature(usecase)}\nSee help(ifcopenshell.api.{usecase_path}) for documentation."
except NotImplementedError as e:
if not e.args[0].startswith(f"{usecase.__name__}()"):
# signature errors typically start with function name
# e.g. "TypeError: edit_library() got an unexpected keyword argument 'test'"
# otherwise it's an error inside api call and we shouldn't get in the way
raise e
msg = (
f"Incorrect function arguments provided for {usecase_path}\n{str(e)}. "
f"You specified args {args} and settings {settings}\n\n"
f"Correct signature is {inspect.signature(usecase)}\n"
f"See help(ifcopenshell.api.{usecase_path}) for documentation."
)
raise TypeError(msg) from e
if should_run_listeners:
@@ -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()
@@ -110,7 +110,9 @@ class Usecase:
"IfcCalendarDate", **ifcopenshell.util.date.datetime2ifc(edition_date, "IfcCalendarDate")
)
else:
result.EditionDate = ifcopenshell.util.date.datetime2ifc(edition_date, "IfcDate")
if edition_date:
edition_date = ifcopenshell.util.date.datetime2ifc(edition_date, "IfcDate")
result.EditionDate = edition_date
self.relate_to_project(result)
@@ -16,8 +16,17 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import 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:
@@ -16,8 +16,11 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import 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)
@@ -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
@@ -16,13 +16,13 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import 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)
@@ -16,13 +16,13 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import 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)
@@ -17,9 +17,14 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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,
@@ -16,27 +16,51 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __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()
@@ -16,13 +16,15 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __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()
@@ -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"],
@@ -17,26 +17,43 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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()
@@ -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()
@@ -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()
@@ -16,11 +16,12 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __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()
@@ -17,22 +17,32 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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()
@@ -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()
@@ -16,11 +16,14 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __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()
@@ -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()
@@ -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 = []
@@ -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:
@@ -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 = {
@@ -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:
@@ -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):
@@ -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
@@ -16,14 +16,15 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import 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()
@@ -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()
@@ -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()
@@ -16,8 +16,10 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import 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
@@ -16,8 +16,16 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import 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]
@@ -16,8 +16,10 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import 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
@@ -16,13 +16,18 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from __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.
@@ -15,9 +15,17 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import 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
@@ -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)
@@ -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
@@ -34,8 +37,8 @@ def add_group(file, Name="Unnamed", Description=None) -> None:
:param Name: The name of the group. Defaults to "Unnamed"
:type Name: str, optional
:param Description: The description of the purpose of the group.
:type Description: str, optional
:param description: The description of the purpose of the group.
:type description: str, optional
:return: The newly created IfcGroup
:rtype: ifcopenshell.entity_instance
@@ -43,11 +46,11 @@ def add_group(file, Name="Unnamed", Description=None) -> None:
.. code:: python
ifcopenshell.api.run("group.add_group", model, Name="Unit 1A")
ifcopenshell.api.run("group.add_group", model, name="Unit 1A")
"""
settings = {
"Name": Name or "Unnamed",
"Description": Description,
"name": name or "Unnamed",
"description": description,
}
return file.create_entity(
@@ -55,7 +58,7 @@ def add_group(file, Name="Unnamed", Description=None) -> None:
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", file),
"Name": settings["Name"],
"Description": settings["Description"],
"Name": settings["name"],
"Description": settings["description"],
}
)
@@ -42,7 +42,7 @@ def assign_group(
.. code:: python
group = ifcopenshell.api.run("group.add_group", model, Name="Furniture")
group = ifcopenshell.api.run("group.add_group", model, name="Furniture")
ifcopenshell.api.run("group.assign_group", model,
products=model.by_type("IfcFurniture"), group=group)
"""
@@ -15,9 +15,11 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import 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
@@ -34,11 +36,11 @@ def edit_group(file, group=None, attributes=None) -> None:
.. code:: python
group = ifcopenshell.api.run("group.add_group", model, Name="Unit 1A")
group = ifcopenshell.api.run("group.add_group", model, name="Unit 1A")
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)
@@ -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
@@ -36,7 +36,7 @@ def remove_group(file, group=None) -> None:
.. code:: python
group = ifcopenshell.api.run("group.add_group", model, Name="Unit 1A")
group = ifcopenshell.api.run("group.add_group", model, name="Unit 1A")
ifcopenshell.api.run("group.remove_group", model, group=group)
"""
settings = {"group": group}
@@ -39,7 +39,7 @@ def unassign_group(
.. code:: python
group = ifcopenshell.api.run("group.add_group", model, Name="Furniture")
group = ifcopenshell.api.run("group.add_group", model, name="Furniture")
furniture = model.by_type("IfcFurniture")
ifcopenshell.api.run("group.assign_group", model, products=furniture, group=group)
@@ -19,9 +19,12 @@
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.guid
import ifcopenshell.util.element
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
@@ -38,7 +41,7 @@ def update_group_products(file, group=None, products=None) -> None:
.. code:: python
group = ifcopenshell.api.run("group.add_group", model, Name="Furniture")
group = ifcopenshell.api.run("group.add_group", model, name="Furniture")
ifcopenshell.api.run("group.update_group_products", model,
products=model.by_type("IfcFurniture"), group=group)
"""
@@ -58,11 +61,17 @@ def update_group_products(file, group=None, products=None) -> None:
}
)
else:
# assumes 1:1 cardinality, will need to be updated to reflect IFC4 changes
# where the cardinality is 0:? - vulevukusej
rel = settings["group"].IsGroupedBy[0]
existing_sub_groups = [g for g in rel.RelatedObjects if g.is_a("IfcGroup")]
rels = settings["group"].IsGroupedBy
objects = set(settings["products"])
for rel in rels:
objects.update([g for g in rel.RelatedObjects if g.is_a("IfcGroup")])
to_purge = rels[1:]
rel.RelatedObjects = settings["products"]
for g in existing_sub_groups:
rel.RelatedObjects.add(g)
for rel in to_purge:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
rels[0].RelatedObjects = list(objects)
return rels[0]
@@ -15,9 +15,11 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Optional
def add_layer(file, Name=None) -> None:
def add_layer(file: ifcopenshell.file, name: str = "Unnamed") -> ifcopenshell.entity_instance:
"""Adds a new layer
An IFC layer is like a CAD layer. Portions of an object's geometry
@@ -32,15 +34,13 @@ def add_layer(file, Name=None) -> None:
Some software that are still based on layers, such as Tekla or ArchiCAD
may also use this layer information for filtering.
:param Name: The name of the layer. Defaults to "Unnamed".
:type Name: str, optional
:param name: The name of the layer. Defaults to "Unnamed".
:type name: str, optional
:return: The newly created IfcPresentationLayerAssignment element
:rtype: ifcopenshell.entity_instance
Example:
ifcopenshell.api.run("layer.add_layer", model, Name="AI-WALL-FULL-DIMS-N")
ifcopenshell.api.run("layer.add_layer", model, name="AI-WALL-FULL-DIMS-N")
"""
settings = {"Name": Name or "Unnamed"}
return file.create_entity("IfcPresentationLayerAssignment", Name=settings["Name"])
return file.create_entity("IfcPresentationLayerAssignment", Name=name)
@@ -59,7 +59,7 @@ def assign_layer(
ifcopenshell.api.run("geometry.edit_object_placement", model, product=wall)
# Now let's create a layer that contains walls
layer = ifcopenshell.api.run("layer.add_layer", model, Name="AI-WALL")
layer = ifcopenshell.api.run("layer.add_layer", model, name="AI-WALL")
# And assign our wall representation item (in this example, there is
# only one item) to the layer.
@@ -15,9 +15,11 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Any
def edit_layer(file, layer=None, attributes=None) -> None:
def edit_layer(file: ifcopenshell.file, layer: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
"""Edits the attributes of an IfcPresentationLayerAssignment
For more information about the attributes and data types of an
@@ -26,7 +28,7 @@ def edit_layer(file, layer=None, attributes=None) -> None:
:param layer: The IfcPresentationLayerAssignment entity you want to edit
:type layer: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:type attributes: dict
:return: None
:rtype: None
@@ -34,11 +36,11 @@ def edit_layer(file, layer=None, attributes=None) -> None:
.. code:: python
layer = ifcopenshell.api.run("layer.add_layer", model, Name="AI-WALL")
layer = ifcopenshell.api.run("layer.add_layer", model, name="AI-WALL")
ifcopenshell.api.run("layer.edit_layer", model,
layer=layer, attributes={"Description": "All walls, based on the AIA standard."})
"""
settings = {"layer": layer, "attributes": attributes or {}}
settings = {"layer": layer, "attributes": attributes}
for name, value in settings["attributes"].items():
setattr(settings["layer"], name, value)
@@ -15,9 +15,10 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
def remove_layer(file, layer=None) -> None:
def remove_layer(file: ifcopenshell.file, layer: ifcopenshell.entity_instance) -> None:
"""Removes a layer
All representation items assigned to the layer will remain, but the
@@ -32,9 +33,7 @@ def remove_layer(file, layer=None) -> None:
.. code:: python
layer = ifcopenshell.api.run("layer.add_layer", model, Name="AI-WALL")
layer = ifcopenshell.api.run("layer.add_layer", model, name="AI-WALL")
ifcopenshell.api.run("layer.remove_layer", model, layer=layer)
"""
settings = {"layer": layer}
file.remove(settings["layer"])
file.remove(layer)
@@ -56,7 +56,7 @@ def unassign_layer(
ifcopenshell.api.run("geometry.edit_object_placement", model, product=wall)
# Now let's create a layer that contains walls
layer = ifcopenshell.api.run("layer.add_layer", model, Name="AI-WALL")
layer = ifcopenshell.api.run("layer.add_layer", model, name="AI-WALL")
# And assign our wall representation item (in this example, there is
# only one item) to the layer.
@@ -21,7 +21,7 @@ import ifcopenshell.util.schema
import ifcopenshell.util.date
def add_library(file, name=None) -> None:
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
@@ -60,6 +60,4 @@ def add_library(file, name=None) -> None:
ifcopenshell.api.run("library.add_library", model, name="Brickschema")
"""
settings = {"name": name}
return file.create_entity("IfcLibraryInformation", Name=settings["name"])
return file.create_entity("IfcLibraryInformation", Name=name)
@@ -15,9 +15,13 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.util.date
import datetime
from typing import Any
def edit_library(file, library=None, attributes=None) -> None:
def edit_library(file: ifcopenshell.file, library: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
"""Edits the attributes of an IfcLibraryInformation
For more information about the attributes and data types of an
@@ -26,7 +30,7 @@ def edit_library(file, library=None, attributes=None) -> None:
:param library: The IfcLibraryInformation entity you want to edit
:type library: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:type attributes: dict
:return: None
:rtype: None
@@ -39,7 +43,16 @@ def edit_library(file, library=None, attributes=None) -> None:
attributes={"Description": "A Brickschema TTL including only mechanical distribution systems."})
"""
settings = {"library": library, "attributes": attributes or {}}
if "VersionDate" in attributes:
dt = attributes["VersionDate"]
if isinstance(dt, datetime.datetime):
if file.schema != "IFC2X3":
dt = ifcopenshell.util.date.datetime2ifc(dt, "IfcDateTime")
else:
calendar_date = ifcopenshell.util.date.datetime2ifc(dt, "IfcCalendarDate")
dt = file.create_entity("IfcCalendarDate", **calendar_date)
attributes = attributes.copy()
attributes["VersionDate"] = dt
for name, value in settings["attributes"].items():
setattr(settings["library"], name, value)
for name, value in attributes.items():
setattr(library, name, value)
@@ -15,9 +15,13 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Any
def edit_reference(file, reference=None, attributes=None) -> None:
def edit_reference(
file: ifcopenshell.file, reference: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcLibraryReference
For more information about the attributes and data types of an
@@ -26,7 +30,7 @@ def edit_reference(file, reference=None, attributes=None) -> None:
:param reference: The IfcLibraryReference 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
@@ -40,7 +44,7 @@ def edit_reference(file, reference=None, attributes=None) -> None:
ifcopenshell.api.run("library.edit_reference", model,
reference=reference, attributes={"Identification": "http://example.org/digitaltwin#AHU01"})
"""
settings = {"reference": reference, "attributes": attributes or {}}
settings = {"reference": reference, "attributes": attributes}
for name, value in settings["attributes"].items():
setattr(settings["reference"], name, value)
@@ -20,7 +20,7 @@ import ifcopenshell
import ifcopenshell.util.element
def remove_library(file, library=None) -> None:
def remove_library(file: ifcopenshell.file, library: ifcopenshell.entity_instance) -> None:
"""Removes a library
All references along with their relationships will also be removed. Any
@@ -38,14 +38,23 @@ def remove_library(file, library=None) -> None:
library = ifcopenshell.api.run("library.add_library", model, name="Brickschema")
ifcopenshell.api.run("library.remove_library", model, library=library)
"""
settings = {"library": library}
for reference in set(settings["library"].HasLibraryReferences or []):
file.remove(reference)
file.remove(settings["library"])
for rel in file.by_type("IfcRelAssociatesLibrary"):
if not rel.RelatingLibrary:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
if file.schema != "IFC2X3":
rels = []
for reference in set(library.HasLibraryReferences):
rels.extend(reference.LibraryRefForObjects)
file.remove(reference)
rels.extend(library.LibraryInfoForObjects)
file.remove(library)
else:
for reference in set(library.LibraryReference or []):
file.remove(reference)
file.remove(library)
# RelatingLibrary could either be library itself or library reference we removed
rels = [rel for rel in file.by_type("IfcRelAssociatesLibrary") if rel.RelatingLibrary is None]
for rel in rels:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
@@ -40,11 +40,14 @@ def remove_reference(file: ifcopenshell.file, reference: ifcopenshell.entity_ins
# Let's change our mind and remove it.
ifcopenshell.api.run("library.remove_reference", model, reference=reference)
"""
settings = {"reference": reference}
if file.schema != "IFC2X3":
rels = reference.LibraryRefForObjects
else:
rels = [rel for rel in file.by_type("IfcRelAssociatesLibrary") if rel.RelatingLibrary == reference]
for rel in settings["reference"].LibraryRefForObjects:
for rel in rels:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
file.remove(settings["reference"])
file.remove(reference)
@@ -15,9 +15,12 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
def add_constituent(file, constituent_set=None, material=None) -> None:
def add_constituent(
file: ifcopenshell.file, constituent_set: ifcopenshell.entity_instance, material: ifcopenshell.entity_instance
) -> ifcopenshell.entity_instance:
"""Adds a new constituent to a constituent set
A constituent describes how a portion of an object is made out of a
@@ -15,9 +15,12 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
def add_layer(file, layer_set=None, material=None) -> None:
def add_layer(
file: ifcopenshell.file, layer_set: ifcopenshell.entity_instance, material: ifcopenshell.entity_instance
) -> ifcopenshell.entity_instance:
"""Adds a new layer to a layer set
A layer represents a portion of material within a layered build up,
@@ -19,7 +19,9 @@
import ifcopenshell
def add_list_item(file, material_list=None, material=None) -> None:
def add_list_item(
file: ifcopenshell.file, material_list: ifcopenshell.entity_instance, material: ifcopenshell.entity_instance
) -> None:
"""Adds a new material in a list of materials
In IFC2X3, if you wanted an object to have multiple materials (i.e. a
@@ -15,9 +15,13 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Optional
def add_material(file, name=None, category=None) -> None:
def add_material(
file: ifcopenshell.file, name: Optional[str] = None, category: Optional[str] = None
) -> ifcopenshell.entity_instance:
"""Adds a new material
A material in IFC represents a physical material, such as timber, steel,
@@ -48,7 +52,7 @@ def add_material(file, name=None, category=None) -> None:
:param name: The name of the material, typically tagged in a finishes
drawing or schedule.
:type name: str
:type name: str, optional
:param category: The category of the material.
:type category: str, optional
:return: The newly created IfcMaterial
@@ -15,9 +15,12 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
def add_material_set(file, name="Unnamed", set_type="IfcMaterialConstituentSet") -> None:
def add_material_set(
file: ifcopenshell.file, name: str = "Unnamed", set_type: str = "IfcMaterialConstituentSet"
) -> ifcopenshell.entity_instance:
"""Adds a new material set
IFC allows you to state that objects are made out of multiple materials.
@@ -19,7 +19,9 @@
import ifcopenshell.util.representation
def assign_profile(file, material_profile=None, profile=None) -> None:
def assign_profile(
file: ifcopenshell.file, material_profile: ifcopenshell.entity_instance, profile: ifcopenshell.entity_instance
) -> None:
"""Changes the profile curve of a material profile item in a profile set
In addition to changing the profile curve, it will also change the
@@ -94,7 +96,8 @@ def assign_profile(file, material_profile=None, profile=None) -> None:
class Usecase:
def execute(self):
file: ifcopenshell.file
def execute(self) -> None:
# TODO: handle composite profiles
old_profile = self.settings["material_profile"].Profile
self.settings["material_profile"].Profile = self.settings["profile"]
@@ -117,7 +120,7 @@ class Usecase:
# TODO: check remove deep
self.file.remove(old_profile)
def change_profile(self, element):
def change_profile(self, element: ifcopenshell.entity_instance) -> None:
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
return
@@ -20,7 +20,7 @@ import ifcopenshell
import ifcopenshell.util.element
def copy_material(file, material=None) -> None:
def copy_material(file: ifcopenshell.file, material: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
"""Copies a material
All material psets and styles are copied. The copied material is not
@@ -48,11 +48,23 @@ def copy_material(file, material=None) -> None:
if inverse.is_a("IfcMaterialProperties"):
# Properties must not be shared between objects for convenience of authoring
inverse = ifcopenshell.util.element.copy(file, inverse)
properties = []
for pset in inverse.Properties:
properties.append(ifcopenshell.util.element.copy_deep(file, pset))
inverse.Properties = properties
inverse.Material = new
props_attribute = "Properties"
if file.schema == "IFC2X3":
if not inverse.is_a("IfcExtendedMaterialProperties"):
continue
props_attribute = "ExtendedProperties"
props = getattr(inverse, props_attribute)
if not props:
continue
copied_props = []
for pset in props:
copied_props.append(ifcopenshell.util.element.copy_deep(file, pset))
setattr(inverse, props_attribute, copied_props)
elif inverse.is_a("IfcMaterialDefinitionRepresentation"):
inverse = ifcopenshell.util.element.copy_deep(
file, inverse, exclude=["IfcRepresentationContext", "IfcMaterial"]
@@ -15,9 +15,11 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Any
def edit_assigned_material(file, element=None, attributes=None) -> None:
def edit_assigned_material(file: ifcopenshell.file, element: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
"""Edits the attributes of an IfcMaterial
For more information about the attributes and data types of an
@@ -26,7 +28,7 @@ def edit_assigned_material(file, element=None, attributes=None) -> None:
:param element: The IfcMaterial entity you want to edit
:type element: 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_assigned_material(file, element=None, attributes=None) -> None:
ifcopenshell.api.run("material.edit_assigned_material", model,
element=concrete, attributes={"Description": "40MPA concrete with broom finish"})
"""
settings = {"element": element, "attributes": attributes or {}}
settings = {"element": element, "attributes": attributes}
for name, value in settings["attributes"].items():
setattr(settings["element"], name, value)
@@ -15,9 +15,16 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Optional, Any
def edit_constituent(file, constituent=None, attributes=None, material=None) -> None:
def edit_constituent(
file: ifcopenshell.file,
constituent: ifcopenshell.entity_instance,
attributes: Optional[dict[str, Any]] = None,
material: Optional[ifcopenshell.entity_instance] = None,
) -> None:
"""Edits the attributes of an IfcMaterialConstituent
For more information about the attributes and data types of an
@@ -15,9 +15,16 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Optional, Any
def edit_layer(file, layer=None, attributes=None, material=None) -> None:
def edit_layer(
file: ifcopenshell.file,
layer: ifcopenshell.entity_instance,
attributes: Optional[dict[str, Any]] = None,
material: Optional[ifcopenshell.entity_instance] = None,
) -> None:
"""Edits the attributes of an IfcMaterialLayer
For more information about the attributes and data types of an
@@ -15,9 +15,11 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Any
def edit_layer_usage(file, usage=None, attributes=None) -> None:
def edit_layer_usage(file: ifcopenshell.file, usage: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
"""Edits the attributes of an IfcMaterialLayerSetUsage
This is typically used to change the offset from the reference line to
@@ -29,7 +31,7 @@ def edit_layer_usage(file, usage=None, attributes=None) -> None:
:param usage: The IfcMaterialLayerSetUsage 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
@@ -73,7 +75,7 @@ def edit_layer_usage(file, usage=None, attributes=None) -> None:
ifcopenshell.api.run("material.edit_layer_usage", model,
usage=rel.RelatingMaterial, attributes={"OffsetFromReferenceLine": 200})
"""
settings = {"usage": usage, "attributes": attributes or {}}
settings = {"usage": usage, "attributes": attributes}
for name, value in settings["attributes"].items():
setattr(settings["usage"], name, value)
@@ -15,12 +15,12 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Any
def edit_material(file, material=None, attributes=None) -> None:
def edit_material(file: ifcopenshell.file, material: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
"""Edits the attributes of an IfcMaterial"""
settings = {"material": material, "attributes": attributes or {}}
for name, value in settings["attributes"].items():
setattr(settings["material"], name, value)
for name, value in attributes.items():
setattr(material, name, value)
@@ -17,7 +17,17 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
def edit_profile(file, profile=None, attributes=None, profile_def=None, material=None) -> None:
from typing import Any, Optional
import ifcopenshell
def edit_profile(
file: ifcopenshell.file,
profile: ifcopenshell.entity_instance,
attributes: Optional[dict[str, Any]] = None,
profile_def: Optional[ifcopenshell.entity_instance] = None,
material: Optional[ifcopenshell.entity_instance] = None,
) -> None:
"""Edits the attributes of an IfcMaterialProfile
For more information about the attributes and data types of an
@@ -15,12 +15,14 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.geom
import ifcopenshell.util.representation
from typing import Any
def edit_profile_usage(file, usage=None, attributes=None) -> None:
def edit_profile_usage(
file: ifcopenshell.file, usage: ifcopenshell.entity_instance, attributes: dict[str, Any]
) -> None:
"""Edits the attributes of an IfcMaterialProfileSetUsage
This is typically used to change the cardinal point of the profile.
@@ -34,7 +36,7 @@ def edit_profile_usage(file, usage=None, attributes=None) -> None:
: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
@@ -91,7 +93,7 @@ def edit_profile_usage(file, usage=None, attributes=None) -> None:
usecase = Usecase()
usecase.file = file
usecase.settings = {"usage": usage, "attributes": attributes or {}}
usecase.settings = {"usage": usage, "attributes": attributes}
return usecase.execute()
@@ -15,9 +15,10 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
def remove_constituent(file, constituent=None) -> None:
def remove_constituent(file: ifcopenshell.file, constituent: ifcopenshell.entity_instance) -> None:
"""Removes a constituent from a constituent set
Note that it is invalid to have zero items in a set, so you should leave
@@ -15,9 +15,10 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
def remove_layer(file, layer=None) -> None:
def remove_layer(file: ifcopenshell.file, layer: ifcopenshell.entity_instance) -> None:
"""Removes a layer from a layer set
Note that it is invalid to have zero items in a set, so you should leave
@@ -19,7 +19,9 @@
import ifcopenshell
def remove_list_item(file, material_list=None, material_index=0) -> None:
def remove_list_item(
file: ifcopenshell.file, material_list: ifcopenshell.entity_instance, material_index: int = 0
) -> None:
"""Removes an item in an material list
Note that it is invalid to have zero items in a list, so you should leave
@@ -20,7 +20,7 @@ import ifcopenshell
import ifcopenshell.util.element
def remove_material(file, material=None) -> None:
def remove_material(file: ifcopenshell.file, material: ifcopenshell.entity_instance) -> None:
"""Removes a material
If the material is used in a material set, the corresponding layer,
@@ -62,7 +62,13 @@ def remove_material(file, material=None) -> None:
if history:
ifcopenshell.util.element.remove_deep2(file, history)
elif inverse.is_a("IfcMaterialProperties"):
for prop in inverse.Properties or []:
if file.schema != "IFC2X3":
props = inverse.Properties
else:
# only IfcExtendedMaterialProperties have properties in IFC2X3
props = getattr(inverse, "ExtendedProperties", None)
props = props or []
for prop in props:
file.remove(prop)
file.remove(inverse)
elif inverse.is_a("IfcMaterialDefinitionRepresentation"):
@@ -20,7 +20,7 @@ import ifcopenshell
import ifcopenshell.util.element
def remove_material_set(file, material=None) -> None:
def remove_material_set(file: ifcopenshell.file, material: ifcopenshell.entity_instance) -> None:
"""Removes a material set
All set items, such as layers, profiles, or constituents will also be
@@ -21,7 +21,7 @@ import ifcopenshell
import ifcopenshell.util.element
def remove_profile(file, profile=None) -> None:
def remove_profile(file: ifcopenshell.file, profile: ifcopenshell.entity_instance) -> None:
"""Removes a profile item from a profile set
Note that it is invalid to have zero items in a set, so you should leave
@@ -15,9 +15,12 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
def reorder_set_item(file, material_set=None, old_index=0, new_index=0) -> None:
def reorder_set_item(
file: ifcopenshell.file, material_set: ifcopenshell.entity_instance, old_index: int = 0, new_index: int = 0
) -> None:
"""Reorders an item in a material set
In some material sets, the order have meaning, like in a layer set. In
@@ -21,15 +21,15 @@ import ifcopenshell.api
import ifcopenshell.util.element
def change_nest(file, item=None, new_parent=None) -> None:
def change_nest(
file: ifcopenshell.file, item: ifcopenshell.entity_instance, new_parent: ifcopenshell.entity_instance
) -> None:
"""Assigns a cost item to a new parent cost item"""
settings = {"item": item, "new_parent": new_parent}
if not settings["item"].Nests:
if not item.Nests:
return
nests = settings["item"].Nests[0]
nests = item.Nests[0]
related_objects = list(nests.RelatedObjects)
related_objects.remove(settings["item"])
related_objects.remove(item)
if related_objects:
nests.RelatedObjects = related_objects
ifcopenshell.api.run("owner.update_owner_history", file, **{"element": nests})
@@ -41,6 +41,6 @@ def change_nest(file, item=None, new_parent=None) -> None:
ifcopenshell.api.run(
"nest.assign_object",
file,
related_objects=[settings["item"]],
relating_object=settings["new_parent"],
related_objects=[item],
relating_object=new_parent,
)
@@ -15,19 +15,18 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
def reorder_nesting(file, item=None, old_index=0, new_index=0) -> None:
def reorder_nesting(
file: ifcopenshell.file, item: ifcopenshell.entity_instance, old_index: int = 0, new_index: int = 0
) -> None:
"""Reorders an item in a nesting set"""
settings = {"item": item, "old_index": old_index, "new_index": new_index}
if not settings["item"].Nests:
if not item.Nests:
return
nesting_set = settings["item"].Nests[0]
if not settings["old_index"]:
old_index = nesting_set.RelatedObjects.index(settings["item"])
else:
old_index = settings["old_index"]
nesting_set = item.Nests[0]
if not old_index:
old_index = nesting_set.RelatedObjects.index(item)
items = list(getattr(nesting_set, "RelatedObjects") or [])
items.insert(settings["new_index"], items.pop(old_index))
items.insert(new_index, items.pop(old_index))
setattr(nesting_set, "RelatedObjects", items)
@@ -19,9 +19,14 @@
import ifcopenshell
import ifcopenshell.api
from typing import Literal
def add_actor(file, actor=None, ifc_class="IfcActor") -> None:
def add_actor(
file: ifcopenshell.file,
actor: ifcopenshell.entity_instance,
ifc_class: Literal["IfcActor", "IfcOccupant"] = "IfcActor",
) -> ifcopenshell.entity_instance:
"""Adds a new actor
An actor is a person or an organisation who has a responsibility or role
@@ -15,9 +15,12 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
def add_address(file, assigned_object=None, ifc_class="IfcPostalAddress") -> None:
def add_address(
file: ifcopenshell.file, assigned_object: ifcopenshell.entity_instance, ifc_class: str = "IfcPostalAddress"
) -> ifcopenshell.entity_instance:
"""Add a new telecom or postal address to an organisation or person
A person or organisation may have associated contact details such as
@@ -17,15 +17,16 @@
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.api
from typing import Optional
def add_application(
file,
application_developer=None,
version=None,
application_full_name="IfcOpenShell",
application_identifier="IfcOpenShell",
) -> None:
file: ifcopenshell.file,
application_developer: Optional[ifcopenshell.entity_instance] = None,
version: Optional[str] = None,
application_full_name: str = "IfcOpenShell",
application_identifier: str = "IfcOpenShell",
) -> ifcopenshell.entity_instance:
"""Adds a new application
IFC data may be associated with an authoring application to identify
@@ -46,6 +47,8 @@ def add_application(
:param application_identifier: An identification string for the
application intended for computers to read.
:type application_identifier: str, optional
:return: The newly created IfcApplication
:rtype: ifcopenshell.entity_instance
Example:
@@ -23,7 +23,7 @@ def add_person(
identification: str = "HSeldon",
family_name: str = "Seldon",
given_name: str = "Hari",
) -> None:
) -> ifcopenshell.entity_instance:
"""Adds a new person
Persons are used to identify a legal or liable representative of an
@@ -15,9 +15,10 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
def add_role(file, assigned_object=None, role="ARCHITECT") -> None:
def add_role(file: ifcopenshell.file, assigned_object: ifcopenshell.entity_instance, role: str = "ARCHITECT") -> ifcopenshell.entity_instance:
"""Adds and assigns a new role
People and organisations must play one or more roles on a project. Roles
@@ -32,7 +33,7 @@ def add_role(file, assigned_object=None, role="ARCHITECT") -> None:
be assigned to.
:type assigned_object: ifcopenshell.entity_instance
:param role: The type of role, taken from the IFC documentation for
IfcActorRole, or a custom name.
IfcActorRole, or a custom name. Defaults to "ARCHITECT".
:type role: str, optional
:return: The newly created IfcActorRole
:rtype: ifcopenshell.entity_instance
@@ -21,7 +21,9 @@ import ifcopenshell.api
import ifcopenshell.guid
def assign_actor(file, relating_actor=None, related_object=None) -> None:
def assign_actor(
file: ifcopenshell.file, relating_actor: ifcopenshell.entity_instance, related_object: ifcopenshell.entity_instance
) -> ifcopenshell.entity_instance:
"""Assigns an actor to an object
An actor may be assigned to objects which implies that the actor is
@@ -80,7 +82,7 @@ def assign_actor(file, relating_actor=None, related_object=None) -> None:
if settings["related_object"].HasAssignments:
for rel in settings["related_object"].HasAssignments:
if rel.is_a("IfcRelAssignsToActor") and rel.RelatingActor == settings["relating_actor"]:
return
return rel
rel = None
@@ -15,9 +15,11 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Any
def edit_actor(file, actor=None, attributes=None) -> None:
def edit_actor(file: ifcopenshell.file, actor: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
"""Edits the attributes of an IfcActor
For more information about the attributes and data types of an
@@ -26,7 +28,7 @@ def edit_actor(file, actor=None, attributes=None) -> None:
:param actor: The IfcActor entity you want to edit
:type actor: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
:type attributes: dict, optional
:type attributes: dict
:return: None
:rtype: None
@@ -47,7 +49,7 @@ def edit_actor(file, actor=None, attributes=None) -> None:
ifcopenshell.api.run("actor.edit_actor", model,
actor=actor, attributes={"Description": "Responsible for buildings A, B, and C."})
"""
settings = {"actor": actor, "attributes": attributes or {}}
settings = {"actor": actor, "attributes": attributes}
for name, value in settings["attributes"].items():
setattr(settings["actor"], name, value)
@@ -15,9 +15,11 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
from typing import Any
def edit_address(file, address=None, attributes=None) -> None:
def edit_address(file: ifcopenshell.file, address: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
"""Edits the attributes of an IfcAddress
For more information about the attributes and data types of an
@@ -26,7 +28,7 @@ def edit_address(file, address=None, attributes=None) -> None:
:param address: The IfcAddress entity you want to edit
:type address: 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 +51,7 @@ def edit_address(file, address=None, attributes=None) -> None:
"ElectronicMailAddresses": ["bobthebuilder@example.com"],
"WWWHomePageURL": "https://thinkmoult.com"})
"""
settings = {"address": address, "attributes": attributes or {}}
settings = {"address": address, "attributes": attributes}
for name, value in settings["attributes"].items():
setattr(settings["address"], name, value)

Some files were not shown because too many files have changed in this diff Show More