diff --git a/.github/workflows/ci-ifcsverchok-build.yml b/.github/workflows/ci-ifcsverchok-build.yml
new file mode 100644
index 0000000000..637adc4441
--- /dev/null
+++ b/.github/workflows/ci-ifcsverchok-build.yml
@@ -0,0 +1,53 @@
+name: Publish-ifcsverchok
+
+on:
+ push:
+ paths:
+ - '.github/workflows/ci-ifcsverchok-build.yml'
+ - 'src/ifcsverchok/*'
+ branches:
+ - v0.7.0
+
+env:
+ major: 0
+ minor: 0
+ name: ifcsverchok
+
+jobs:
+ activate:
+ runs-on: ubuntu-latest
+ if: |
+ github.repository == 'IfcOpenShell/IfcOpenShell'
+ steps:
+ - name: Set env
+ run: echo ok go
+
+ build:
+ needs: activate
+ name: ifcsverchok
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2 # https://github.com/actions/setup-python
+ with:
+ architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
+ python-version: '3.11'
+ - run: echo ${{ env.DATE }}
+ - name: Get current date
+ id: date
+ run: echo "::set-output name=date::$(date +'%y%m%d')"
+ - name: Compile
+ run: |
+ cd src/ifcsverchok
+ make dist
+ - name: Upload Zip file to release
+ uses: svenstaro/upload-release-action@v2
+ with:
+ repo_token: ${{ secrets.GITHUB_TOKEN }}
+ file: src/ifcsverchok/dist/ifcsverchok-${{steps.date.outputs.date}}.zip
+ asset_name: ifcsverchok-${{steps.date.outputs.date}}.zip
+ tag: "ifcsverchok-${{steps.date.outputs.date}}"
+ overwrite: true
+ body: "ifcsverchok build for ${{steps.date.outputs.date}}"
diff --git a/aws/lambda/Dockerfile b/aws/lambda/Dockerfile
index fcffa1e87b..8076692e81 100644
--- a/aws/lambda/Dockerfile
+++ b/aws/lambda/Dockerfile
@@ -12,7 +12,7 @@ RUN apt-get -y update && apt-get -y install unzip curl
# Install AWS Lambda runtime interface client
RUN pip install --target ${FUNCTION_DIR} awslambdaric
-# Set the IfcOpenShell build version (check available builds at: https://blenderbim.org/docs-python/ifcopenshell-python/installation.html)
+# Set the IfcOpenShell build version (check available builds at: https://docs.ifcopenshell.org/ifcopenshell-python/installation.html)
ARG IFC_OPENSHELL_BUILD="39-v0.7.0-476ab50"
# Download and extract IfcOpenShell
diff --git a/choco/blenderbim/blenderbim.nuspec b/choco/blenderbim/blenderbim.nuspec
index d76e86a8ce..bafb86e381 100644
--- a/choco/blenderbim/blenderbim.nuspec
+++ b/choco/blenderbim/blenderbim.nuspec
@@ -15,7 +15,7 @@
https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.7.0/COPYING
true
https://github.com/IfcOpenShell/IfcOpenShell
- https://blenderbim.org/docs/
+ https://docs.blenderbim.org/
https://github.com/IfcOpenShell/IfcOpenShell/issues
blender bim blenderbim ifc python opensource foss
diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py
index a809831f3a..02173e2681 100644
--- a/src/blenderbim/blenderbim/bim/import_ifc.py
+++ b/src/blenderbim/blenderbim/bim/import_ifc.py
@@ -857,7 +857,11 @@ class IfcImporter:
print("Done creating geometry")
def create_spatial_elements(self) -> None:
- self.create_generic_elements(self.spatial_elements, unselectable=True)
+ if bpy.context.preferences.addons["blenderbim"].preferences.spatial_elements_unselectable:
+ self.create_generic_elements(self.spatial_elements, unselectable=True)
+ else:
+ self.create_generic_elements(self.spatial_elements, unselectable=False)
+
def create_elements(self) -> None:
self.create_generic_elements(self.elements)
diff --git a/src/blenderbim/blenderbim/bim/module/attribute/operator.py b/src/blenderbim/blenderbim/bim/module/attribute/operator.py
index 89b32597be..a86ff15e21 100644
--- a/src/blenderbim/blenderbim/bim/module/attribute/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/attribute/operator.py
@@ -47,10 +47,18 @@ class EnableEditingAttributes(bpy.types.Operator):
obj = bpy.data.objects.get(self.obj)
elif self.obj_type == "Material":
obj = bpy.data.materials.get(self.obj)
- oprops = obj.BIMObjectProperties
props = obj.BIMAttributeProperties
props.attributes.clear()
+ element = tool.Ifc.get_entity(obj)
+ has_inherited_predefined_type = False
+ if not element.is_a("IfcTypeObject") and (element_type := ifcopenshell.util.element.get_type(element)):
+ # Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818
+ has_inherited_predefined_type = ifcopenshell.util.element.get_predefined_type(element_type) not in (
+ "NOTDEFINED",
+ None,
+ )
+
def callback(name, prop, data):
if name in ("RefLatitude", "RefLongitude"):
new = props.attributes.add()
@@ -62,10 +70,11 @@ class EnableEditingAttributes(bpy.types.Operator):
new.string_value = "" if new.is_null else json.dumps(data[name])
blenderbim.bim.helper.add_attribute_description(new)
new.description += " The degrees, minutes and seconds should follow this format : [12,34,56]"
+ if name in ("PredefinedType", "ObjectType") and has_inherited_predefined_type:
+ props.attributes.remove(len(props.attributes) - 1)
+ return True
- blenderbim.bim.helper.import_attributes2(
- tool.Ifc.get().by_id(oprops.ifc_definition_id), props.attributes, callback=callback
- )
+ blenderbim.bim.helper.import_attributes2(element, props.attributes, callback=callback)
props.is_editing_attributes = True
return {"FINISHED"}
diff --git a/src/blenderbim/blenderbim/bim/module/classification/operator.py b/src/blenderbim/blenderbim/bim/module/classification/operator.py
index 84f2f89599..623aa59770 100644
--- a/src/blenderbim/blenderbim/bim/module/classification/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/classification/operator.py
@@ -20,6 +20,8 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.api
+import ifcopenshell.util.classification
+import ifcopenshell.util.element
import blenderbim.tool as tool
import blenderbim.bim.helper
from blenderbim.bim.ifc import IfcStore
@@ -87,7 +89,7 @@ class AddManualClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
reference = ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
- product=product,
+ products=[product],
classification=classification,
identification="X",
name="Unnamed",
@@ -292,6 +294,7 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
active_reference = tool.Ifc.get().by_id(self.reference)
identification = active_reference[1]
+ elements_by_references: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]] = {}
for obj in objects:
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
@@ -300,12 +303,16 @@ class RemoveClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
if (identification and reference[1] == identification) or (
not identification and reference == active_reference
):
- ifcopenshell.api.run(
- "classification.remove_reference",
- tool.Ifc.get(),
- reference=reference,
- product=element,
- )
+ elements_by_references.setdefault(reference, []).append(element)
+
+ if elements_by_references:
+ for reference, products in elements_by_references.items():
+ ifcopenshell.api.run(
+ "classification.remove_reference",
+ tool.Ifc.get(),
+ reference=reference,
+ products=products,
+ )
class EditClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
@@ -357,15 +364,18 @@ class AddClassificationReference(bpy.types.Operator, tool.Ifc.Operator):
classification = element
break
- for obj in objects:
- ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context)
- if not ifc_definition_id:
- continue
+ ifc_file = tool.Ifc.get()
+ products = [
+ ifc_file.by_id(ifc_definition_id)
+ for obj in objects
+ if (ifc_definition_id := tool.Blender.get_obj_ifc_definition_id(obj, self.obj_type, context))
+ ]
+ if products:
ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
reference=IfcStore.classification_file.by_id(self.reference),
- product=tool.Ifc.get().by_id(ifc_definition_id),
+ products=products,
classification=classification,
)
@@ -413,7 +423,7 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator):
reference = ifcopenshell.api.run(
"classification.add_reference",
tool.Ifc.get(),
- product=element,
+ products=[element],
classification=classification,
identification=bsdd_classification.reference_code,
name=bsdd_classification.name,
diff --git a/src/blenderbim/blenderbim/bim/module/constraint/operator.py b/src/blenderbim/blenderbim/bim/module/constraint/operator.py
index 6ab81a4695..f461554c49 100644
--- a/src/blenderbim/blenderbim/bim/module/constraint/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/constraint/operator.py
@@ -114,7 +114,7 @@ class EditObjective(bpy.types.Operator):
ifcopenshell.api.run(
"constraint.edit_objective",
self.file,
- **{"objective": self.file.by_id(props.active_constraint_id), "attributes": attributes}
+ **{"objective": self.file.by_id(props.active_constraint_id), "attributes": attributes},
)
bpy.ops.bim.load_objectives()
return {"FINISHED"}
@@ -179,19 +179,17 @@ class AssignConstraint(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
- for obj in objs:
- obj_id = obj.BIMObjectProperties.ifc_definition_id
- if not obj_id:
- continue
+ products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
+ if products:
ifcopenshell.api.run(
"constraint.assign_constraint",
self.file,
**{
- "product": self.file.by_id(obj_id),
+ "products": products,
"constraint": self.file.by_id(self.constraint),
- }
+ },
)
return {"FINISHED"}
@@ -207,18 +205,16 @@ class UnassignConstraint(bpy.types.Operator):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
- for obj in objs:
- obj_id = obj.BIMObjectProperties.ifc_definition_id
- if not obj_id:
- continue
+ products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)]
+ if products:
ifcopenshell.api.run(
"constraint.unassign_constraint",
self.file,
**{
- "product": self.file.by_id(obj_id),
+ "products": products,
"constraint": self.file.by_id(self.constraint),
- }
+ },
)
return {"FINISHED"}
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py
index 794877525b..1d5e751062 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py
@@ -2570,8 +2570,16 @@ class EnableEditingElementFilter(bpy.types.Operator, Operator):
def _execute(self, context):
obj = bpy.context.scene.camera
- if obj:
- obj.data.BIMCameraProperties.filter_mode = self.filter_mode
+ if not obj:
+ return
+ obj.data.BIMCameraProperties.filter_mode = self.filter_mode
+ element = tool.Ifc.get_entity(obj)
+ if query := ifcopenshell.util.element.get_pset(element, "EPset_Drawing", self.filter_mode.title()):
+ filter_groups = tool.Search.get_filter_groups(f"drawing_{self.filter_mode.lower()}")
+ try:
+ tool.Search.import_filter_query(query, filter_groups)
+ except:
+ pass
class EditElementFilter(bpy.types.Operator, Operator):
diff --git a/src/blenderbim/blenderbim/bim/module/drawing/prop.py b/src/blenderbim/blenderbim/bim/module/drawing/prop.py
index eedfefaee8..7773d41930 100644
--- a/src/blenderbim/blenderbim/bim/module/drawing/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/drawing/prop.py
@@ -66,6 +66,11 @@ def get_location_hint(self, context):
def update_diagram_scale(self, context):
+ if not context.scene.camera or context.scene.camera.data != self.id_data:
+ return
+ element = tool.Ifc.get_entity(context.scene.camera)
+ if not element:
+ return
try:
element = (
tool.Ifc.get()
@@ -87,6 +92,11 @@ def update_diagram_scale(self, context):
def update_is_nts(self, context):
+ if not context.scene.camera or context.scene.camera.data != self.id_data:
+ return
+ element = tool.Ifc.get_entity(context.scene.camera)
+ if not element:
+ return
try:
element = (
tool.Ifc.get()
diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py
index 06a0fc04c3..1f24392af2 100644
--- a/src/blenderbim/blenderbim/bim/module/material/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/material/ui.py
@@ -175,7 +175,7 @@ class BIM_PT_object_material(Panel):
if ObjectMaterialData.data["type_material"]:
row = self.layout.row(align=True)
- row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="FILE_PARENT")
+ row.label(text="Inherited Material: " + ObjectMaterialData.data["type_material"], icon="CON_CHILDOF")
if ObjectMaterialData.data["material_class"]:
return self.draw_material_ui()
diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py
index ceafc1fa70..ec1008cd33 100644
--- a/src/blenderbim/blenderbim/bim/module/project/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py
@@ -25,6 +25,8 @@ classes = (
operator.AppendLibraryElement,
operator.AppendLibraryElementByQuery,
operator.AssignLibraryDeclaration,
+ operator.BIM_OT_load_clipping_planes,
+ operator.BIM_OT_save_clipping_planes,
operator.ChangeLibraryElement,
operator.CreateClippingPlane,
operator.CreateProject,
diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py
index 8e96df2dcc..7449f8bcad 100644
--- a/src/blenderbim/blenderbim/bim/module/project/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/project/operator.py
@@ -39,6 +39,8 @@ from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.ui import IFCFileSelector
from blenderbim.bim import import_ifc
from blenderbim.bim import export_ifc
+from collections import defaultdict
+import json
from math import radians
from pathlib import Path
from mathutils import Vector, Matrix
@@ -1999,3 +2001,61 @@ class FlipClippingPlane(bpy.types.Operator):
obj.rotation_euler[0] += radians(180)
context.view_layer.update()
return {"FINISHED"}
+
+
+CLIPPING_PLANES_FILE_NAME = "ClippingPlanes.json" # TODO un-hardcode :=
+
+
+class BIM_OT_save_clipping_planes(bpy.types.Operator):
+ bl_idname = "bim.save_clipping_planes"
+ bl_label = "Save Clipping Planes"
+ bl_description = "Save Clipping Planes to Disk"
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ if IfcStore.path:
+ return context.scene.BIMProjectProperties.clipping_planes
+ cls.poll_message_set("Please Save The IFC File")
+
+ def execute(self, context):
+ clipping_planes_to_serialize = defaultdict(dict)
+ clipping_planes = context.scene.BIMProjectProperties.clipping_planes
+ for clipping_plane in clipping_planes:
+ obj = clipping_plane.obj
+ name = obj.name
+ clipping_planes_to_serialize[name]["location"] = obj.location[0:3]
+ clipping_planes_to_serialize[name]["rotation"] = obj.rotation_euler[0:3]
+ with open(Path(IfcStore.path).with_name(CLIPPING_PLANES_FILE_NAME), "w") as file:
+ json.dump(clipping_planes_to_serialize, file, indent=4)
+ return {"FINISHED"}
+
+
+class BIM_OT_load_clipping_planes(bpy.types.Operator):
+ bl_idname = "bim.load_clipping_planes"
+ bl_label = "Load Clipping Planes"
+ bl_description = "Load Clipping Planes from Disk"
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ if filepath := IfcStore.path:
+ if Path(filepath).with_name(CLIPPING_PLANES_FILE_NAME).exists():
+ return True
+ else:
+ cls.poll_message_set(f"No Clipping Planes File in Folder {filepath}")
+ else:
+ cls.poll_message_set("Please Save The IFC File")
+
+ def execute(self, context):
+ bpy.data.batch_remove(context.scene.BIMProjectProperties.clipping_planes_objs)
+ context.scene.BIMProjectProperties.clipping_planes.clear()
+ with open(Path(IfcStore.path).with_name(CLIPPING_PLANES_FILE_NAME), "r") as file:
+ clipping_planes_dict = json.load(file)
+ for name, values in clipping_planes_dict.items():
+ bpy.ops.bim.create_clipping_plane()
+ obj = context.scene.BIMProjectProperties.clipping_planes_objs[-1]
+ obj.name = name
+ obj.location = values["location"]
+ obj.rotation_euler = values["rotation"]
+ return {"FINISHED"}
diff --git a/src/blenderbim/blenderbim/bim/module/pset/data.py b/src/blenderbim/blenderbim/bim/module/pset/data.py
index 415ae1f5d9..0c3093f3a5 100644
--- a/src/blenderbim/blenderbim/bim/module/pset/data.py
+++ b/src/blenderbim/blenderbim/bim/module/pset/data.py
@@ -69,6 +69,7 @@ class ObjectPsetsData(Data):
@classmethod
def load(cls):
cls.data = {
+ "is_occurrence": cls.is_occurrence(),
"psets": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object), psets_only=True),
"inherited_psets": cls.inherited_psets(),
"pset_name": cls.pset_name(),
@@ -76,6 +77,10 @@ class ObjectPsetsData(Data):
}
cls.is_loaded = True
+ @classmethod
+ def is_occurrence(cls):
+ return not tool.Ifc.get_entity(bpy.context.active_object).is_a("IfcTypeObject")
+
@classmethod
def inherited_psets(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
@@ -126,11 +131,16 @@ class ObjectQtosData(Data):
@classmethod
def load(cls):
cls.data = {
+ "is_occurrence": cls.is_occurrence(),
"qtos": cls.psetqtos(tool.Ifc.get_entity(bpy.context.active_object), qtos_only=True),
"inherited_qsets": cls.inherited_qsets(),
}
cls.is_loaded = True
+ @classmethod
+ def is_occurrence(cls):
+ return not tool.Ifc.get_entity(bpy.context.active_object).is_a("IfcTypeObject")
+
@classmethod
def inherited_qsets(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
diff --git a/src/blenderbim/blenderbim/bim/module/pset/ui.py b/src/blenderbim/blenderbim/bim/module/pset/ui.py
index 445016ba50..8a715e4718 100644
--- a/src/blenderbim/blenderbim/bim/module/pset/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/pset/ui.py
@@ -208,12 +208,15 @@ class BIM_PT_object_psets(Panel):
draw_psetqto_ui(context, 0, {}, props, self.layout, "Object")
if ObjectPsetsData.data["psets"]:
- self.layout.label(text="Instance:")
+ if ObjectPsetsData.data["is_occurrence"]:
+ self.layout.label(text="Occurrence Properties:")
+ else:
+ self.layout.label(text="Type Properties:")
for pset in ObjectPsetsData.data["psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object")
if ObjectPsetsData.data["inherited_psets"]:
- self.layout.label(text="Type:")
+ self.layout.label(text="Inherited Type Properties:", icon="CON_CHILDOF")
for pset in ObjectPsetsData.data["inherited_psets"]:
draw_psetqto_ui(context, pset["id"], pset, props, self.layout, "Object", allow_removing=False)
@@ -252,12 +255,15 @@ class BIM_PT_object_qtos(Panel):
draw_psetqto_ui(context, 0, {}, props, self.layout, "Object")
if ObjectQtosData.data["qtos"]:
- self.layout.label(text="Instance:")
+ if ObjectQtosData.data["is_occurrence"]:
+ self.layout.label(text="Occurrence Quantities:")
+ else:
+ self.layout.label(text="Type Quantities:")
for qto in ObjectQtosData.data["qtos"]:
draw_psetqto_ui(context, qto["id"], qto, props, self.layout, "Object")
if ObjectQtosData.data["inherited_qsets"]:
- self.layout.label(text="Type:")
+ self.layout.label(text="Inherited Type Quantities:", icon="CON_CHILDOF")
for qset in ObjectQtosData.data["inherited_qsets"]:
draw_psetqto_ui(context, qset["id"], qset, props, self.layout, "Object", allow_removing=False)
diff --git a/src/blenderbim/blenderbim/bim/module/root/data.py b/src/blenderbim/blenderbim/bim/module/root/data.py
index 6b91e18ab8..18f7fbffcd 100644
--- a/src/blenderbim/blenderbim/bim/module/root/data.py
+++ b/src/blenderbim/blenderbim/bim/module/root/data.py
@@ -42,6 +42,7 @@ class IfcClassData:
cls.data["contexts"] = cls.contexts()
cls.data["has_entity"] = cls.has_entity()
cls.data["name"] = cls.name()
+ cls.data["has_inherited_predefined_type"] = cls.has_inherited_predefined_type()
cls.data["ifc_class"] = cls.ifc_class()
cls.data["ifc_predefined_types"] = cls.ifc_predefined_types()
cls.data["can_reassign_class"] = cls.can_reassign_class()
@@ -162,6 +163,16 @@ class IfcClassData:
name += f"[{predefined_type}]"
return name
+ @classmethod
+ def has_inherited_predefined_type(cls):
+ element = tool.Ifc.get_entity(bpy.context.view_layer.objects.active)
+ if not element:
+ return
+ if element_type := ifcopenshell.util.element.get_type(element):
+ # Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818
+ return ifcopenshell.util.element.get_predefined_type(element_type) not in ("NOTDEFINED", None)
+ return False
+
@classmethod
def ifc_class(cls):
element = tool.Ifc.get_entity(bpy.context.view_layer.objects.active)
diff --git a/src/blenderbim/blenderbim/bim/module/root/ui.py b/src/blenderbim/blenderbim/bim/module/root/ui.py
index b80c733ee5..a57bec2e40 100644
--- a/src/blenderbim/blenderbim/bim/module/root/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/root/ui.py
@@ -61,7 +61,10 @@ class BIM_PT_class(Panel):
self.layout.prop(context.scene.BIMRootProperties, "relating_class_object", icon="COPYDOWN")
else:
row = self.layout.row(align=True)
- row.label(text=IfcClassData.data["name"])
+ row.label(
+ text=IfcClassData.data["name"],
+ icon="CON_CHILDOF" if IfcClassData.data["has_inherited_predefined_type"] else "NONE",
+ )
row.operator("bim.select_ifc_class", text="", icon="RESTRICT_SELECT_OFF")
row.operator("bim.unlink_object", icon="UNLINKED", text="")
if IfcClassData.data["can_reassign_class"]:
diff --git a/src/blenderbim/blenderbim/bim/module/search/operator.py b/src/blenderbim/blenderbim/bim/module/search/operator.py
index 9fee78e1ac..6ffe447470 100644
--- a/src/blenderbim/blenderbim/bim/module/search/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/search/operator.py
@@ -235,7 +235,7 @@ class LoadSearch(Operator, tool.Ifc.Operator):
def _execute(self, context):
filter_groups = tool.Search.get_filter_groups(self.module)
group = tool.Ifc.get().by_id(int(context.scene.BIMSearchProperties.saved_searches))
- query = tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups)
+ tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups)
def draw(self, context):
props = context.scene.BIMSearchProperties
diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py
index bed2349191..7654062959 100644
--- a/src/blenderbim/blenderbim/bim/ui.py
+++ b/src/blenderbim/blenderbim/bim/ui.py
@@ -131,6 +131,8 @@ class BIM_PT_section_with_cappings(Panel):
box = layout.box()
header = box.row(align=True)
header.label(text="Clipping Planes")
+ header.operator("bim.save_clipping_planes", text="", icon="EXPORT")
+ header.operator("bim.load_clipping_planes", text="", icon="IMPORT")
header.operator("bim.create_clipping_plane", text="", icon="ADD")
box.template_list(
@@ -150,7 +152,7 @@ class BIM_PT_section_with_cappings(Panel):
class BIM_UL_clipping_plane(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
- if item:
+ if item and item.obj:
obj = item.obj
row = layout.row(align=True)
row.prop(obj, "name", text="", emboss=False)
@@ -202,6 +204,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
name="Should Make A Cha-Ching Sound When Project Costs Updates", default=False
)
lock_grids_on_import: BoolProperty(name="Should Lock Grids By Default", default=True)
+ spatial_elements_unselectable: BoolProperty(name="Should Make Spatial Elements Unselectable By Default", default=True)
decorations_colour: bpy.props.FloatVectorProperty(
name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
)
@@ -283,6 +286,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row.prop(self, "should_play_chaching_sound")
row = layout.row()
row.prop(self, "lock_grids_on_import")
+ row = layout.row()
+ row.prop(self, "spatial_elements_unselectable")
+
+
row = layout.row()
row.prop(context.scene.BIMProjectProperties, "should_disable_undo_on_save")
diff --git a/src/blenderbim/blenderbim/core/brick.py b/src/blenderbim/blenderbim/core/brick.py
index c16b341b1c..58e7afed18 100644
--- a/src/blenderbim/blenderbim/core/brick.py
+++ b/src/blenderbim/blenderbim/core/brick.py
@@ -74,7 +74,7 @@ def assign_brick_reference(ifc, brick, element=None, library=None, brick_uri=Non
if not reference:
reference = ifc.run("library.add_reference", library=library)
ifc.run("library.edit_reference", reference=reference, attributes=brick.export_brick_attributes(brick_uri))
- ifc.run("library.assign_reference", product=element, reference=reference)
+ ifc.run("library.assign_reference", products=[element], reference=reference)
project = brick.get_brickifc_project()
if not project:
project = brick.add_brickifc_project(brick.get_namespace(brick_uri))
diff --git a/src/blenderbim/blenderbim/core/document.py b/src/blenderbim/blenderbim/core/document.py
index ce6dc374c6..ce58e1edf4 100644
--- a/src/blenderbim/blenderbim/core/document.py
+++ b/src/blenderbim/blenderbim/core/document.py
@@ -109,8 +109,8 @@ def remove_document(ifc, document_tool, document=None):
def assign_document(ifc, product=None, document=None):
- ifc.run("document.assign_document", product=product, document=document)
+ ifc.run("document.assign_document", products=[product], document=document)
def unassign_document(ifc, product=None, document=None):
- ifc.run("document.unassign_document", product=product, document=document)
+ ifc.run("document.unassign_document", products=[product], document=document)
diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py
index f8039488f2..cccdc16ad1 100644
--- a/src/blenderbim/blenderbim/core/drawing.py
+++ b/src/blenderbim/blenderbim/core/drawing.py
@@ -241,7 +241,7 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None):
attributes = {"Identification": "X", "Name": drawing_name, "Scope": "DRAWING"}
ifc.run("document.edit_information", information=information, attributes=attributes)
ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri})
- ifc.run("document.assign_document", product=element, document=reference)
+ ifc.run("document.assign_document", products=[element], document=reference)
drawing.import_drawings()
@@ -265,7 +265,7 @@ def duplicate_drawing(ifc, drawing_tool, drawing=None, should_duplicate_annotati
ifc.run("group.assign_group", group=new_group, products=[new_annotation])
old_reference = drawing_tool.get_drawing_document(new_drawing)
- ifc.run("document.unassign_document", product=new_drawing, document=old_reference)
+ ifc.run("document.unassign_document", products=[new_drawing], document=old_reference)
information = ifc.run("document.add_information")
uri = drawing_tool.get_default_drawing_path(drawing_name)
@@ -276,7 +276,7 @@ def duplicate_drawing(ifc, drawing_tool, drawing=None, should_duplicate_annotati
attributes = {"Identification": "X", "Name": drawing_name, "Scope": "DRAWING"}
ifc.run("document.edit_information", information=information, attributes=attributes)
ifc.run("document.edit_reference", reference=reference, attributes={"Location": uri})
- ifc.run("document.assign_document", product=new_drawing, document=reference)
+ ifc.run("document.assign_document", products=[new_drawing], document=reference)
drawing_tool.import_drawings()
return new_drawing
diff --git a/src/blenderbim/blenderbim/core/library.py b/src/blenderbim/blenderbim/core/library.py
index eba4fc770a..6b5ce7e767 100644
--- a/src/blenderbim/blenderbim/core/library.py
+++ b/src/blenderbim/blenderbim/core/library.py
@@ -83,8 +83,8 @@ def edit_library_reference(ifc, library):
def assign_library_reference(ifc, obj=None, reference=None):
- ifc.run("library.assign_reference", product=ifc.get_entity(obj), reference=reference)
+ ifc.run("library.assign_reference", products=[ifc.get_entity(obj)], reference=reference)
def unassign_library_reference(ifc, obj=None, reference=None):
- ifc.run("library.unassign_reference", product=ifc.get_entity(obj), reference=reference)
+ ifc.run("library.unassign_reference", products=[ifc.get_entity(obj)], reference=reference)
diff --git a/src/blenderbim/blenderbim/core/resource.py b/src/blenderbim/blenderbim/core/resource.py
index dd3875a04f..63524cd03c 100644
--- a/src/blenderbim/blenderbim/core/resource.py
+++ b/src/blenderbim/blenderbim/core/resource.py
@@ -195,7 +195,7 @@ def add_usage_constraint(ifc, resource_tool, resource=None, reference_path=None)
},
)
ifc.run("constraint.add_metric_reference", metric=metric, reference_path=reference_path)
- ifc.run("constraint.assign_constraint", product=resource, constraint=objective)
+ ifc.run("constraint.assign_constraint", products=[resource], constraint=objective)
def remove_usage_constraint(ifc, resource_tool, resource, reference_path):
@@ -206,7 +206,7 @@ def remove_usage_constraint(ifc, resource_tool, resource, reference_path):
reference = resource_tool.get_metric_reference(metric, is_deep=True)
if reference == reference_path:
ifc.run("constraint.remove_metric", metric=metric)
- ifc.run("constraint.unassign_constraint", product=resource, constraint=constraint)
+ ifc.run("constraint.unassign_constraint", products=[resource], constraint=constraint)
ifc.run("constraint.remove_constraint", constraint=constraint)
diff --git a/src/blenderbim/blenderbim/core/spatial.py b/src/blenderbim/blenderbim/core/spatial.py
index 57d627eaae..171642900a 100644
--- a/src/blenderbim/blenderbim/core/spatial.py
+++ b/src/blenderbim/blenderbim/core/spatial.py
@@ -32,7 +32,7 @@ def reference_structure(
element: Optional[ifcopenshell.entity_instance] = None,
) -> Union[ifcopenshell.entity_instance, None]:
if spatial.can_reference(structure, element):
- return ifc.run("spatial.reference_structure", product=element, relating_structure=structure)
+ return ifc.run("spatial.reference_structure", products=[element], relating_structure=structure)
def dereference_structure(
@@ -42,7 +42,7 @@ def dereference_structure(
element: Optional[ifcopenshell.entity_instance] = None,
) -> None:
if spatial.can_reference(structure, element):
- return ifc.run("spatial.dereference_structure", product=element, relating_structure=structure)
+ return ifc.run("spatial.dereference_structure", products=[element], relating_structure=structure)
def assign_container(
diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py
index ca2552b17d..d1d7e9649a 100644
--- a/src/blenderbim/blenderbim/tool/drawing.py
+++ b/src/blenderbim/blenderbim/tool/drawing.py
@@ -733,7 +733,6 @@ class Drawing(blenderbim.core.tool.Drawing):
obj.BIMObjectProperties.ifc_definition_id = ifc_definition_id
-
@classmethod
def import_drawings(cls):
props = bpy.context.scene.DocProperties
@@ -1776,7 +1775,6 @@ class Drawing(blenderbim.core.tool.Drawing):
element_obj_names = set()
for element in filtered_elements:
obj = tool.Ifc.get_object(element)
- element_obj_names.add(obj.name)
current_representation = tool.Geometry.get_active_representation(obj)
if current_representation:
subcontext = current_representation.ContextOfItems
@@ -1805,10 +1803,14 @@ class Drawing(blenderbim.core.tool.Drawing):
# Don't hide IfcAnnotations as some of them might exist without representations
if has_context or element.is_a("IfcAnnotation"):
- # Note that render visibility is only set on drawing generation time for speed.
- obj.hide_set(False)
+ element_obj_names.add(obj.name)
- [obj.hide_set(False) for obj in bpy.context.view_layer.objects if obj.name not in element_obj_names]
+ # Note that render visibility is only set on drawing generation time for speed.
+ [
+ obj.hide_set(False) # Show the object
+ for obj in bpy.context.view_layer.objects
+ if obj.name in element_obj_names or not tool.Ifc.get_entity(obj)
+ ]
cls.import_camera_props(drawing, camera)
diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py
index 7fb10700f2..0a3ff34fdf 100644
--- a/src/blenderbim/blenderbim/tool/geometry.py
+++ b/src/blenderbim/blenderbim/tool/geometry.py
@@ -91,6 +91,17 @@ class Geometry(blenderbim.core.tool.Geometry):
if element.is_a("IfcRelSpaceBoundary"):
ifcopenshell.api.run("boundary.remove_boundary", tool.Ifc.get(), boundary=element)
return bpy.data.objects.remove(obj)
+ if element.is_a("IfcGridAxis"):
+ is_last_axis = False
+ # Deleting the last W axis is OK
+ if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
+ (grid := element.PartOfV) and len(grid[0].VAxes) == 1
+ ):
+ is_last_axis = True
+ if is_last_axis:
+ return
+ ifcopenshell.api.run("grid.remove_grid_axis", tool.Ifc.get(), axis=element)
+ return bpy.data.objects.remove(obj)
collection = obj.BIMObjectProperties.collection
if collection:
diff --git a/src/blenderbim/docs/users/other_addons.rst b/src/blenderbim/docs/users/other_addons.rst
index 88d4c4ff7a..7ae799a6fb 100644
--- a/src/blenderbim/docs/users/other_addons.rst
+++ b/src/blenderbim/docs/users/other_addons.rst
@@ -29,7 +29,9 @@ Some of these add-ons are not shipped with Blender:
- `Sverchok `__ - Sverchok is a visual
programming add-on for Blender that allows you to generate parametric
geometry, create scripts for non-programmers, model solids from FreeCAD, and
- much more.
+ much more. There is also
+ `IfcSverchok `__
+ that adds IFC features to Sverchok.
- `BlenderGIS `__ - BlenderGIS lets you
import GIS data, grab elevation data from the web, and generate TINs from
survey points and contours.
diff --git a/src/blenderbim/test/core/test_brick.py b/src/blenderbim/test/core/test_brick.py
index 157a363682..e410fbe9b9 100644
--- a/src/blenderbim/test/core/test_brick.py
+++ b/src/blenderbim/test/core/test_brick.py
@@ -124,21 +124,21 @@ class TestAssignBrickReference:
ifc.run("library.add_reference", library="library").should_be_called().will_return("reference")
brick.export_brick_attributes("brick_uri").should_be_called().will_return("attributes")
ifc.run("library.edit_reference", reference="reference", attributes="attributes").should_be_called()
- ifc.run("library.assign_reference", product="element", reference="reference").should_be_called()
+ ifc.run("library.assign_reference", products=["element"], reference="reference").should_be_called()
brick.get_brickifc_project().should_be_called().will_return("project")
brick.add_brickifc_reference("brick_uri", "element", "project").should_be_called()
subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick_uri")
def test_assigning_to_an_existing_reference(self, ifc, brick):
brick.get_library_brick_reference("library", "brick_uri").should_be_called().will_return("reference")
- ifc.run("library.assign_reference", product="element", reference="reference").should_be_called()
+ ifc.run("library.assign_reference", products=["element"], reference="reference").should_be_called()
brick.get_brickifc_project().should_be_called().will_return("project")
brick.add_brickifc_reference("brick_uri", "element", "project").should_be_called()
subject.assign_brick_reference(ifc, brick, element="element", library="library", brick_uri="brick_uri")
def test_adding_a_brickifc_project_if_it_doesnt_exist(self, ifc, brick):
brick.get_library_brick_reference("library", "brick_uri").should_be_called().will_return("reference")
- ifc.run("library.assign_reference", product="element", reference="reference").should_be_called()
+ ifc.run("library.assign_reference", products=["element"], reference="reference").should_be_called()
brick.get_brickifc_project().should_be_called().will_return(None)
brick.get_namespace("brick_uri").should_be_called().will_return("namespace")
brick.add_brickifc_project("namespace").should_be_called().will_return("project")
@@ -277,4 +277,4 @@ class TestSetBrickListRoot:
class TestRemoveBrickRelation:
def test_run(self, brick):
brick.remove_relation("brick_uri", "predicate", "object").should_be_called()
- subject.remove_brick_relation(brick, brick_uri="brick_uri", predicate="predicate", object="object")
\ No newline at end of file
+ subject.remove_brick_relation(brick, brick_uri="brick_uri", predicate="predicate", object="object")
diff --git a/src/blenderbim/test/core/test_document.py b/src/blenderbim/test/core/test_document.py
index 708c899e48..9811c887a8 100644
--- a/src/blenderbim/test/core/test_document.py
+++ b/src/blenderbim/test/core/test_document.py
@@ -133,11 +133,11 @@ class TestRemoveDocument:
class TestAssignDocument:
def test_run(self, ifc):
- ifc.run("document.assign_document", product="product", document="document").should_be_called()
+ ifc.run("document.assign_document", products=["product"], document="document").should_be_called()
subject.assign_document(ifc, product="product", document="document")
class TestUnassignDocument:
def test_run(self, ifc):
- ifc.run("document.unassign_document", product="product", document="document").should_be_called()
+ ifc.run("document.unassign_document", products=["product"], document="document").should_be_called()
subject.unassign_document(ifc, product="product", document="document")
diff --git a/src/blenderbim/test/core/test_drawing.py b/src/blenderbim/test/core/test_drawing.py
index b07d0f8368..a2b6608f5d 100644
--- a/src/blenderbim/test/core/test_drawing.py
+++ b/src/blenderbim/test/core/test_drawing.py
@@ -365,7 +365,7 @@ class TestAddDrawing:
attributes={"Identification": "X", "Name": "name", "Scope": "DRAWING"},
).should_be_called()
ifc.run("document.edit_reference", reference="reference", attributes={"Location": "uri"}).should_be_called()
- ifc.run("document.assign_document", product="element", document="reference").should_be_called()
+ ifc.run("document.assign_document", products=["element"], document="reference").should_be_called()
drawing.import_drawings().should_be_called()
subject.add_drawing(ifc, collector, drawing, target_view="target_view", location_hint="location_hint")
@@ -391,7 +391,7 @@ class TestDuplicateDrawing:
ifc.run("group.assign_group", group="new_group", products=["new_annotation"]).should_be_called()
drawing.get_drawing_document("new_drawing").should_be_called().will_return("old_reference")
- ifc.run("document.unassign_document", product="new_drawing", document="old_reference").should_be_called()
+ ifc.run("document.unassign_document", products=["new_drawing"], document="old_reference").should_be_called()
ifc.run("document.add_information").should_be_called().will_return("information")
ifc.run("document.add_reference", information="information").should_be_called().will_return("reference")
@@ -405,7 +405,7 @@ class TestDuplicateDrawing:
ifc.run(
"document.edit_reference", reference="reference", attributes={"Location": "drawing_path"}
).should_be_called()
- ifc.run("document.assign_document", product="new_drawing", document="reference").should_be_called()
+ ifc.run("document.assign_document", products=["new_drawing"], document="reference").should_be_called()
drawing.import_drawings().should_be_called()
subject.duplicate_drawing(ifc, drawing, drawing="drawing", should_duplicate_annotations=True)
diff --git a/src/blenderbim/test/core/test_library.py b/src/blenderbim/test/core/test_library.py
index 2809722e81..4722cde5c0 100644
--- a/src/blenderbim/test/core/test_library.py
+++ b/src/blenderbim/test/core/test_library.py
@@ -114,12 +114,12 @@ class TestEditLibraryReference:
class TestAssignLibraryReference:
def test_run(self, ifc):
ifc.get_entity("obj").should_be_called().will_return("product")
- ifc.run("library.assign_reference", product="product", reference="reference").should_be_called()
+ ifc.run("library.assign_reference", products=["product"], reference="reference").should_be_called()
subject.assign_library_reference(ifc, obj="obj", reference="reference")
class TestUnassignLibraryReference:
def test_run(self, ifc):
ifc.get_entity("obj").should_be_called().will_return("product")
- ifc.run("library.unassign_reference", product="product", reference="reference").should_be_called()
+ ifc.run("library.unassign_reference", products=["product"], reference="reference").should_be_called()
subject.unassign_library_reference(ifc, obj="obj", reference="reference")
diff --git a/src/blenderbim/test/core/test_spatial.py b/src/blenderbim/test/core/test_spatial.py
index b77e95dc59..9e01aefde4 100644
--- a/src/blenderbim/test/core/test_spatial.py
+++ b/src/blenderbim/test/core/test_spatial.py
@@ -23,14 +23,14 @@ from test.core.bootstrap import ifc, collector, spatial
class TestReferenceStructure:
def test_run(self, ifc, spatial):
spatial.can_reference("structure", "element").should_be_called().will_return(True)
- ifc.run("spatial.reference_structure", product="element", relating_structure="structure").should_be_called()
+ ifc.run("spatial.reference_structure", products=["element"], relating_structure="structure").should_be_called()
subject.reference_structure(ifc, spatial, structure="structure", element="element")
class TestDereferenceStructure:
def test_run(self, ifc, spatial):
spatial.can_reference("structure", "element").should_be_called().will_return(True)
- ifc.run("spatial.dereference_structure", product="element", relating_structure="structure").should_be_called()
+ ifc.run("spatial.dereference_structure", products=["element"], relating_structure="structure").should_be_called()
subject.dereference_structure(ifc, spatial, structure="structure", element="element")
diff --git a/src/blenderbim/test/tool/test_document.py b/src/blenderbim/test/tool/test_document.py
index 3ef56744e0..8b22ce19d0 100644
--- a/src/blenderbim/test/tool/test_document.py
+++ b/src/blenderbim/test/tool/test_document.py
@@ -18,6 +18,7 @@
import bpy
import ifcopenshell
+import ifcopenshell.api
import blenderbim.core.tool
import blenderbim.tool as tool
from test.bim.bootstrap import NewFile
@@ -188,7 +189,7 @@ class TestImportReferences(NewFile):
assert len(props.documents) == 1
assert props.documents[0].ifc_definition_id == reference.id()
assert props.documents[0].name == "Unnamed"
- assert props.documents[0].identification == "*"
+ assert props.documents[0].identification == "X"
assert props.documents[0].is_information is False
diff --git a/src/ifcbimtester/CITATION.cff b/src/ifcbimtester/CITATION.cff
index d2d6ca6abb..a7222b5a28 100644
--- a/src/ifcbimtester/CITATION.cff
+++ b/src/ifcbimtester/CITATION.cff
@@ -11,7 +11,7 @@ authors:
- name: "IfcOpenShell contributors"authors:
repository-code: >-
https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/ifcbimtester
-url: 'https://blenderbim.org/docs-python/bimtester.html'
+url: 'https://docs.ifcopenshell.org/bimtester.html'
abstract: Wrapper for Gherkin based unit testing for IFC models
keywords:
- Gherkin
diff --git a/src/ifcopenshell-python/docs/ifccsv.rst b/src/ifcopenshell-python/docs/ifccsv.rst
index 76742b3ec7..3c9f93b766 100644
--- a/src/ifcopenshell-python/docs/ifccsv.rst
+++ b/src/ifcopenshell-python/docs/ifccsv.rst
@@ -153,7 +153,7 @@ interface to access the IfcOpenShell utilities.
1. Install the BlenderBIM Add-on by following the `BlenderBIM Add-on
installation documentation
- `_.
+ `_.
2. Launch Blender. Change to the **Scene Properties** tab in the **Properties
Panel**. Scroll down to the **IFC Collaboration > IFC CSV Import / Export**
diff --git a/src/ifcopenshell-python/docs/ifcdiff.rst b/src/ifcopenshell-python/docs/ifcdiff.rst
index b8b5332566..1f82d4d400 100644
--- a/src/ifcopenshell-python/docs/ifcdiff.rst
+++ b/src/ifcopenshell-python/docs/ifcdiff.rst
@@ -88,7 +88,7 @@ interface to access the IfcOpenShell utilities.
1. Install the BlenderBIM Add-on by following the `BlenderBIM Add-on
installation documentation
- `_.
+ `_.
2. Launch Blender. Change to the **Scene Properties** tab in the **Properties
Panel**. Scroll down to the **IFC Quality Control > IFC Diff** panel.
diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst
index 1801ccf75e..77f71fbf34 100644
--- a/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst
+++ b/src/ifcopenshell-python/docs/ifcopenshell-python/installation.rst
@@ -227,7 +227,7 @@ The BlenderBIM Add-on is available either as a stable build or a daily build.
1. Install the BlenderBIM Add-on by following the `BlenderBIM Add-on
installation documentation
- `_.
+ `_.
2. Launch Blender. On the top left of the Viewport panel, click the **Editor
Type** icon to change the viewport into a **Python Console**.
@@ -261,7 +261,7 @@ and run your script using the **Text > Run Script** menu or by clicking on the
Blender. This can help when learning how to write scripts as you can double
check the results of your scripts with what you see in the graphical
interface. `Read more
- `_.
+ `_.
From source with precompiled binaries
-------------------------------------
diff --git a/src/ifcopenshell-python/docs/ifcsverchok/installation.rst b/src/ifcopenshell-python/docs/ifcsverchok/installation.rst
index 775ab2bcb0..b49ff5e600 100644
--- a/src/ifcopenshell-python/docs/ifcsverchok/installation.rst
+++ b/src/ifcopenshell-python/docs/ifcsverchok/installation.rst
@@ -12,7 +12,7 @@ Packaged installation
IfcSverchok is packaged like a regular Blender add-on, so installation is the
same as any other Blender add-on. `Download IfcSverchok here
-`__.
+`__.
Like all Blender add-ons, they can be installed using ``Edit > Preferences >
Addons > Install > Choose Downloaded ZIP > Enable Add-on Checkbox``. You can
diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py
index 8f7b82351c..f5a56279c0 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py
@@ -87,6 +87,36 @@ ARGUMENTS_DEPRECATION = {
"material.unassign_material": partial(
batching_argument_deprecation, prev_argument="product", new_argument="products"
),
+ "classification.add_reference": partial(
+ batching_argument_deprecation, prev_argument="product", new_argument="products"
+ ),
+ "classification.remove_reference": partial(
+ batching_argument_deprecation, prev_argument="product", new_argument="products"
+ ),
+ "library.assign_reference": partial(
+ batching_argument_deprecation, prev_argument="product", new_argument="products"
+ ),
+ "library.unassign_reference": partial(
+ batching_argument_deprecation, prev_argument="product", new_argument="products"
+ ),
+ "document.assign_document": partial(
+ batching_argument_deprecation, prev_argument="product", new_argument="products"
+ ),
+ "document.unassign_document": partial(
+ batching_argument_deprecation, prev_argument="product", new_argument="products"
+ ),
+ "spatial.reference_structure": partial(
+ batching_argument_deprecation, prev_argument="product", new_argument="products"
+ ),
+ "spatial.dereference_structure": partial(
+ batching_argument_deprecation, prev_argument="product", new_argument="products"
+ ),
+ "constraint.assign_constraint": partial(
+ batching_argument_deprecation, prev_argument="product", new_argument="products"
+ ),
+ "constraint.unassign_constraint": partial(
+ batching_argument_deprecation, prev_argument="product", new_argument="products"
+ ),
}
diff --git a/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py b/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py
index 072427057a..42365ca802 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py
@@ -64,7 +64,8 @@ class Usecase:
self.settings["product"].PredefinedType = "USERDEFINED"
elif hasattr(self.settings["product"], "ObjectType"):
relating_type = ifcopenshell.util.element.get_type(self.settings["product"])
- if relating_type and relating_type.PredefinedType != "NOTDEFINED":
+ # Allow for None due to https://github.com/buildingSMART/IFC4.3.x-development/issues/818
+ if relating_type and relating_type.PredefinedType not in ("NOTDEFINED", None):
self.settings["product"].ObjectType = None
self.settings["product"].PredefinedType = None
elif (
diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py b/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py
index 0682ab7565..d59a4e9343 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py
@@ -19,10 +19,11 @@
import ifcopenshell
import ifcopenshell.util.schema
import ifcopenshell.util.date
+from typing import Union
class Usecase:
- def __init__(self, file, classification=None):
+ def __init__(self, file: ifcopenshell.file, classification: Union[str, ifcopenshell.entity_instance]):
"""Adds a new classification system to the project
External classification systems such as Uniclass or Omniclass are
@@ -81,7 +82,7 @@ class Usecase:
"classification": classification,
}
- def execute(self):
+ def execute(self) -> ifcopenshell.entity_instance:
if isinstance(self.settings["classification"], str):
classification = self.file.createIfcClassification(Name=self.settings["classification"])
self.relate_to_project(classification)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py
index 320825e335..9057f7a25f 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py
@@ -17,12 +17,24 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell
+import ifcopenshell.api
+import ifcopenshell.util.element
import ifcopenshell.util.schema
+from typing import Optional, Union
class Usecase:
- def __init__(self, file, product=None, reference=None, identification=None, name=None, classification=None, is_lightweight=True):
- """Adds a new classification reference and assigns it to a product
+ def __init__(
+ self,
+ file: ifcopenshell.file,
+ products: list[ifcopenshell.entity_instance],
+ reference: Optional[ifcopenshell.entity_instance] = None,
+ identification: Optional[str] = None,
+ name: Optional[str] = None,
+ classification: Optional[ifcopenshell.entity_instance] = None,
+ is_lightweight=True,
+ ):
+ """Adds a new classification reference and assigns it to the list of products
A classification reference is a single entry such as "Pr_12_23_34" that
is part of an external classification system (such as Uniclass or
@@ -33,7 +45,7 @@ class Usecase:
resources such as profiles, documents, libraries, and so on.
Classification references can be added in two ways. Option 1) specify a
- custom arbitrary reference, where you have the manually specify the
+ custom arbitrary reference, where you have to manually specify the
identification (e.g. "Pr_12_23_45") and name (e.g. "Door Products").
Option 2) add a reference from an IFC classification library. The latter
is preferred if you are using a common classification system such as
@@ -52,13 +64,13 @@ class Usecase:
assigned to both the type and an occurrence, then the assignment at the
occurrence will override the type classification.
- :param product: The IFC object, property, or resource you want to
+ :param product: The list of IFC objects, properties, or resources you want to
associate the classification reference to.
- :type product: ifcopenshell.entity_instance.entity_instance
+ :type product: list[ifcopenshell.entity_instance.entity_instance]
:param reference: The classification reference entity taken from an
IFC classification library. If you supply this parameter, you will
use option 2.
- :type product: ifcopenshell.entity_instance.entity_instance, optional
+ :type reference: ifcopenshell.entity_instance.entity_instance, optional
:param identification: If you choose option 1 and do not specify a
reference, you may manually specify an identification code. The code
is typically a short identifier and may have punctuation to separate
@@ -70,7 +82,7 @@ class Usecase:
:param classification: The IfcClassification entity in your IFC model
(not the library, if you are doing option 2) that the reference is
part of.
- :type product: ifcopenshell.entity_instance.entity_instance
+ :type classification: ifcopenshell.entity_instance.entity_instance
:param is_lightweight: If you are doing option 2, choose whether or not
to only add that particular reference (lighweight) or also add all
of its parent references in the classification hierarchy (not
@@ -81,8 +93,12 @@ class Usecase:
is generally unnecessary. Using lightweight classifications are
recommended and is the default.
:type is_lightweight: bool, optional
+
+ :raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements.
+
:return: The newly added IfcClassificationReference
- :rtype: ifcopenshell.entity_instance.entity_instance
+ or `None` if `products` was empty list.
+ :rtype: Union[ifcopenshell.entity_instance.entity_instance, None]
Example:
@@ -93,7 +109,7 @@ class Usecase:
classification = ifcopenshell.api.run("classification.add_classification",
model, classification="MyCustomClassification")
ifcopenshell.api.run("classification.add_reference", model,
- product=wall_type, classification=classification,
+ products=[wall_type], classification=classification,
identification="W_01", name="Interior Walls")
# Option 2: adding a popular classification from a library
@@ -104,12 +120,12 @@ class Usecase:
reference = [r for r in library.by_type("IfcClassificationReference")
if r.Identification == "XYZ"][0]
ifcopenshell.api.run("classification.add_reference", model,
- product=wall_type, classification=classification,
+ products=[wall_type], classification=classification,
reference=reference)
"""
self.file = file
self.settings = {
- "product": product,
+ "products": products,
"reference": reference,
"identification": identification,
"name": name,
@@ -117,8 +133,27 @@ class Usecase:
"is_lightweight": is_lightweight,
}
- def execute(self):
- self.is_rooted = self.settings["product"].is_a("IfcRoot")
+ def execute(self) -> Union[ifcopenshell.entity_instance, None]:
+ if not self.settings["products"]:
+ return
+
+ if self.settings["reference"]:
+ referenced = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"])
+ if set(self.settings["products"]).issubset(referenced):
+ # nothing to do, all elements already have this reference assigned
+ return self.settings["reference"]
+
+ self.rooted_products: set[ifcopenshell.entity_instance] = set()
+ self.non_rooted_products: set[ifcopenshell.entity_instance] = set()
+ for product in self.settings["products"]:
+ if product.is_a("IfcRoot"):
+ self.rooted_products.add(product)
+ else:
+ self.non_rooted_products.add(product)
+
+ if self.non_rooted_products and self.file.schema == "IFC2X3":
+ raise TypeError(f"Cannot add reference to non-IfcRoot element in IFC2X3: {self.non_rooted_products}.")
+
if self.settings["reference"]:
return self.add_from_library()
return self.add_from_identification()
@@ -134,14 +169,10 @@ class Usecase:
else:
reference.Identification = self.settings["identification"]
- relationship = self.get_existing_relationship(reference)
- if relationship:
- self.add_to_existing_relationship(relationship)
- else:
- self.add_new_relationship(reference)
+ self.update_relationships(reference)
return reference
- def add_from_library(self):
+ def add_from_library(self) -> ifcopenshell.entity_instance:
if hasattr(self.settings["reference"], "ItemReference"):
identification = self.settings["reference"].ItemReference # IFC2X3
else:
@@ -155,8 +186,9 @@ class Usecase:
old_referenced_source = self.settings["reference"].ReferencedSource
self.settings["reference"].ReferencedSource = None
else:
+ classification_name = self.settings["classification"].Name
existing_classification = [
- c for c in self.file.by_type("IfcClassification") if c.Name == self.settings["classification"].Name
+ c for c in self.file.by_type("IfcClassification") if c.Name == classification_name
]
reference = migrator.migrate(self.settings["reference"], self.file)
@@ -174,15 +206,10 @@ class Usecase:
for element in to_delete:
self.file.remove(element)
- relationship = self.get_existing_relationship(reference)
- if relationship:
- self.add_to_existing_relationship(relationship)
- else:
- self.add_new_relationship(reference)
-
+ self.update_relationships(reference)
return reference
- def get_existing_reference(self, identification):
+ def get_existing_reference(self, identification: Optional[str] = None) -> Union[ifcopenshell.entity_instance, None]:
for reference in self.file.by_type("IfcClassificationReference"):
if self.file.schema == "IFC2X3":
if reference.ItemReference == identification:
@@ -191,39 +218,39 @@ class Usecase:
if reference.Identification == identification:
return reference
- def add_new_relationship(self, reference):
- if self.is_rooted:
- self.file.create_entity(
- "IfcRelAssociatesClassification",
- GlobalId=ifcopenshell.guid.new(),
- RelatedObjects=[self.settings["product"]],
- RelatingClassification=reference,
- )
- else:
- self.file.create_entity(
- "IfcExternalReferenceRelationship",
- RelatingReference=reference,
- RelatedResourceObjects=[self.settings["product"]],
- )
-
- def add_to_existing_relationship(self, rel):
- if self.is_rooted:
- related_objects = set(rel.RelatedObjects)
- related_objects.add(self.settings["product"])
- rel.RelatedObjects = list(related_objects)
- else:
- related_objects = set(rel.RelatedResourceObjects)
- related_objects.add(self.settings["product"])
- rel.RelatedResourceObjects = list(related_objects)
-
- def get_existing_relationship(self, reference):
- if self.is_rooted:
+ def update_relationships(self, reference: ifcopenshell.entity_instance) -> None:
+ root_rel, non_root_rel = None, None
+ if self.rooted_products:
if self.file.schema == "IFC2X3":
for rel in self.file.by_type("IfcRelAssociatesClassification"):
if rel.RelatingClassification == reference:
- return rel
- elif reference.ClassificationRefForObjects:
- return reference.ClassificationRefForObjects[0]
- elif self.file.schema != "IFC2X3":
- if reference.ExternalReferenceForResources:
- return reference.ExternalReferenceForResources[0]
+ root_rel = rel
+ break
+ else:
+ root_rel = next(iter(reference.ClassificationRefForObjects), None)
+
+ if root_rel:
+ related_objects = set(root_rel.RelatedObjects) | self.rooted_products
+ root_rel.RelatedObjects = list(related_objects)
+ ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": root_rel})
+ else:
+ self.file.create_entity(
+ "IfcRelAssociatesClassification",
+ OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file),
+ GlobalId=ifcopenshell.guid.new(),
+ RelatedObjects=list(self.rooted_products),
+ RelatingClassification=reference,
+ )
+
+ if self.non_rooted_products:
+ # NOTE: Only Ifc4+. Ifc2x3 is already handled by raising TypeError
+ non_root_rel = next(iter(reference.ExternalReferenceForResources), None)
+ if non_root_rel:
+ related_objects = set(non_root_rel.RelatedResourceObjects) | self.non_rooted_products
+ non_root_rel.RelatedResourceObjects = list(related_objects)
+ else:
+ self.file.create_entity(
+ "IfcExternalReferenceRelationship",
+ RelatingReference=reference,
+ RelatedResourceObjects=list(self.non_rooted_products),
+ )
diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py
index 48b55b3740..d4e8d802a6 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py
@@ -17,12 +17,18 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell
+import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
- def __init__(self, file, reference=None, product=None):
- """Removes a classification reference from a product
+ def __init__(
+ self,
+ file: ifcopenshell.file,
+ reference: ifcopenshell.entity_instance,
+ products: list[ifcopenshell.entity_instance],
+ ):
+ """Removes a classification reference from the list of products
If the classification reference is no longer associated to any products,
the classification reference itself is also removed.
@@ -30,9 +36,12 @@ class Usecase:
:param reference: The IfcClassificationReference entity of the
relationship you want to remove.
:type reference: ifcopenshell.entity_instance.entity_instance
- :param product: The object entity of the relationship you want to
+ :param product: The list fo object entities of the relationship you want to
remove.
- :type reference: ifcopenshell.entity_instance.entity_instance
+ :type product: list[ifcopenshell.entity_instance.entity_instance]
+
+ :raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements.
+
:return: None
:rtype: None
@@ -44,42 +53,75 @@ class Usecase:
classification = ifcopenshell.api.run("classification.add_classification",
model, classification="MyCustomClassification")
reference = ifcopenshell.api.run("classification.add_reference", model,
- product=wall_type, classification=classification,
+ products=[wall_type], classification=classification,
identification="W_01", name="Interior Walls")
ifcopenshell.api.run("classification.remove_reference", model,
- reference=reference, product=wall_type)
+ reference=reference, products=[wall_type])
"""
self.file = file
- self.settings = {"reference": reference, "product": product}
+ self.settings = {"reference": reference, "products": products}
- def execute(self):
- if self.settings["product"].is_a("IfcRoot"):
- for rel in self.file.by_type("IfcRelAssociatesClassification"):
- if rel.RelatingClassification == self.settings["reference"] and rel.RelatedObjects:
- if self.settings["product"] in rel.RelatedObjects:
- related_objects = list(rel.RelatedObjects)
- related_objects.remove(self.settings["product"])
- if len(related_objects):
- rel.RelatedObjects = related_objects
- else:
- history = rel.OwnerHistory
- self.file.remove(rel)
- if history:
- ifcopenshell.util.element.remove_deep2(self.file, history)
- else:
- for rel in self.file.by_type("IfcExternalReferenceRelationship"):
- if rel.RelatingReference == self.settings["reference"] and rel.RelatedResourceObjects:
- if self.settings["product"] in rel.RelatedResourceObjects:
- related_objects = list(rel.RelatedResourceObjects)
- related_objects.remove(self.settings["product"])
- if len(related_objects):
- rel.RelatedResourceObjects = related_objects
- else:
- self.file.remove(rel)
+ def execute(self) -> None:
+ is_ifc2x3 = self.file.schema == "IFC2X3"
+ products = set(self.settings["products"])
+ referenced = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"])
+ products -= products.difference(referenced)
+
+ # all products are already unassigned from a reference
+ if not products:
+ return
+
+ rooted_products: set[ifcopenshell.entity_instance] = set()
+ non_rooted_products: set[ifcopenshell.entity_instance] = set()
+ for product in self.settings["products"]:
+ if product.is_a("IfcRoot"):
+ rooted_products.add(product)
+ else:
+ non_rooted_products.add(product)
+
+ if non_rooted_products and is_ifc2x3:
+ raise TypeError(f"Cannot add reference to non-IfcRoot element in IFC2X3: {non_rooted_products}.")
+
+ if rooted_products:
+ reference_rels: set[ifcopenshell.entity_instance] = set()
+ for product in rooted_products:
+ reference_rels.update(product.HasAssociations)
+
+ reference_rels = {
+ rel
+ for rel in reference_rels
+ if rel.is_a("IfcRelAssociatesClassification")
+ and rel.RelatingClassification == self.settings["reference"]
+ }
+
+ for rel in reference_rels:
+ related_objects = set(rel.RelatedObjects) - rooted_products
+ if related_objects:
+ rel.RelatedObjects = list(related_objects)
+ ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
+ else:
+ history = rel.OwnerHistory
+ self.file.remove(rel)
+ if history:
+ ifcopenshell.util.element.remove_deep2(self.file, history)
+
+ if non_rooted_products:
+ reference_rels: set[ifcopenshell.entity_instance] = set()
+ for product in non_rooted_products:
+ rels = getattr(product, "HasExternalReferences", None)
+ if rels is None:
+ rels = getattr(product, "HasExternalReference", [])
+ reference_rels.update(rels)
+
+ reference_rels = {rel for rel in reference_rels if rel.RelatingReference == self.settings["reference"]}
+ for rel in reference_rels:
+ related_objects = set(rel.RelatedResourceObjects) - non_rooted_products
+ if related_objects:
+ rel.RelatedResourceObjects = list(related_objects)
+ else:
+ self.file.remove(rel)
# TODO: we only handle lightweight classifications here
- if (
- not self.settings["reference"].ClassificationRefForObjects
- and not self.settings["reference"].ExternalReferenceForResources
- ):
+ referenced_elements = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"])
+ if not referenced_elements:
self.file.remove(self.settings["reference"])
diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py
index 1d77a7bb74..759fab9a5e 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py
@@ -17,11 +17,18 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell
+import ifcopenshell.api
+from typing import Union
class Usecase:
- def __init__(self, file, product=None, constraint=None):
- """Assigns a constraint to a product
+ def __init__(
+ self,
+ file: ifcopenshell.file,
+ products: list[ifcopenshell.entity_instance],
+ constraint: ifcopenshell.entity_instance,
+ ):
+ """Assigns a constraint to a list of products
This assigns a relationship between a product and a constraint, so that
when a product's properties and quantities do not match the requirements
@@ -31,36 +38,58 @@ class Usecase:
constraints are inherited from the type. This way, it is not necessary
to create lots of constraint assignments.
- :param product: The product the constraint applies to. This is anything
+ :param products: The list of products the constraint applies to. This is anything
which can have properties or quantities.
- :type product: ifcopenshell.entity_instance.entity_instance
+ :type products: list[ifcopenshell.entity_instance.entity_instance]
:param constraint: The IfcObjective constraint
:type constraint: ifcopenshell.entity_instance.entity_instance
:return: The new or updated IfcRelAssociatesConstraint relationship
+ or `None` if `products` was an empty list.
:rtype: ifcopenshell.entity_instance.entity_instance
"""
self.file = file
self.settings = {
- "product": product,
+ "products": products,
"constraint": constraint,
}
- def execute(self):
- rel = self.get_constraint_rel()
- related_objects = set(rel.RelatedObjects) if rel.RelatedObjects else set()
- related_objects.add(self.settings["product"])
- rel.RelatedObjects = list(related_objects)
- return rel
+ def execute(self) -> Union[ifcopenshell.entity_instance, None]:
+ products = set(self.settings["products"])
+ if not products:
+ return
+
+ self.constraint = self.settings["constraint"]
+
+ rels = self.get_constraint_rels()
+ related_objects = set()
+ for rel in rels:
+ related_objects.update(rel.RelatedObjects)
+
+ products_to_assign = products - related_objects
+ if not products_to_assign:
+ return rels[0]
+
+ rel = next(iter(rels), None)
+
+ if rel:
+ related_objects = set(rel.RelatedObjects) | products_to_assign
+ rel.RelatedObjects = list(related_objects)
+ ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
+ return rel
- def get_constraint_rel(self):
- for rel in self.file.by_type("IfcRelAssociatesConstraint"):
- if rel.RelatingConstraint == self.settings["constraint"]:
- return rel
return self.file.create_entity(
"IfcRelAssociatesConstraint",
**{
"GlobalId": ifcopenshell.guid.new(),
- # TODO: owner history
- "RelatingConstraint": self.settings["constraint"],
+ "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
+ "RelatingConstraint": self.constraint,
+ "RelatedObjects": list(products_to_assign),
}
)
+
+ def get_constraint_rels(self) -> list[ifcopenshell.entity_instance]:
+ rels = []
+ for rel in self.file.get_inverse(self.constraint):
+ if rel.is_a("IfcRelAssociatesConstraint"):
+ rels.append(rel)
+ return rels
diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py
index 66b3bc94b9..dbc1e1b7fb 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py
@@ -17,18 +17,24 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell
+import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
- def __init__(self, file, product=None, constraint=None):
- """Unassigns a constraint to a product
+ def __init__(
+ self,
+ file: ifcopenshell.file,
+ products: list[ifcopenshell.entity_instance],
+ constraint: ifcopenshell.entity_instance,
+ ):
+ """Unassigns a constraint from a list of products
The constraint will not be deleted and is available to be assigned to
other products.
- :param product: The product the constraint applies to.
- :type product: ifcopenshell.entity_instance.entity_instance
+ :param products: The list of products the constraint applies to.
+ :type products: list[ifcopenshell.entity_instance.entity_instance]
:param constraint: The IfcObjective constraint
:type constraint: ifcopenshell.entity_instance.entity_instance
:return: None
@@ -36,14 +42,42 @@ class Usecase:
"""
self.file = file
self.settings = {
- "product": product,
+ "products": products,
"constraint": constraint,
}
def execute(self):
- for rel in self.settings["product"].HasAssociations:
- if rel.is_a("IfcRelAssociatesConstraint") and rel.RelatingConstraint == self.settings["constraint"]:
- history = rel.OwnerHistory
- self.file.remove(rel)
- if history:
- ifcopenshell.util.element.remove_deep2(self.file, history)
+ products = set(self.settings["products"])
+ if not products:
+ return
+
+ self.constraint = self.settings["constraint"]
+ rels = self.get_constraint_rels()
+ related_objects = set()
+ for rel in rels:
+ related_objects.update(rel.RelatedObjects)
+
+ if not related_objects.intersection(products):
+ return
+
+ for rel in rels:
+ related_objects = set(rel.RelatedObjects)
+ if not related_objects.intersection(products):
+ continue
+ related_objects -= products
+ if related_objects:
+ rel.RelatedObjects = list(related_objects)
+ ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
+ continue
+
+ history = rel.OwnerHistory
+ self.file.remove(rel)
+ if history:
+ ifcopenshell.util.element.remove_deep2(self.file, history)
+
+ def get_constraint_rels(self) -> list[ifcopenshell.entity_instance]:
+ rels = []
+ for rel in self.file.get_inverse(self.constraint):
+ if rel.is_a("IfcRelAssociatesConstraint"):
+ rels.append(rel)
+ return rels
diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py b/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py
index 39cdb109d6..bcec5da606 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py
@@ -69,10 +69,12 @@ class Usecase:
def execute(self) -> ifcopenshell.entity_instance:
if self.file.schema == "IFC2X3":
- reference = self.file.create_entity("IfcDocumentReference")
+ reference = self.file.create_entity("IfcDocumentReference", ItemReference="X")
if self.settings["information"]:
references = list(self.settings["information"].DocumentReferences or [])
references.append(reference)
self.settings["information"].DocumentReferences = references
return reference
- return self.file.create_entity("IfcDocumentReference", ReferencedDocument=self.settings["information"])
+ return self.file.create_entity(
+ "IfcDocumentReference", ReferencedDocument=self.settings["information"], Identification="X"
+ )
diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py b/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py
index c7f98e4517..f67cbe890a 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py
@@ -17,11 +17,19 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell
+import ifcopenshell.api
+import ifcopenshell.util.element
+from typing import Union
class Usecase:
- def __init__(self, file, product=None, document=None):
- """Assigns a document to a product
+ def __init__(
+ self,
+ file: ifcopenshell.file,
+ products: list[ifcopenshell.entity_instance],
+ document: ifcopenshell.entity_instance,
+ ):
+ """Assigns a document to a list of products
An object may be assigned to zero, one, or multiple documents. Almost
any object or property may be assigned to a document, though typically
@@ -32,14 +40,16 @@ class Usecase:
consistent with other external relationships (such as classification
systems or libraries).
- :param product: The object to associate the document to. This could be
+ :param product: The list of objects to associate the document to. This could be
almost any sensible object in IFC.
- :type product: ifcopenshell.entity_instance.entity_instance
+ :type product: list[ifcopenshell.entity_instance.entity_instance]
:param document: The IfcDocumentReference to associate to, or
alternatively an IfcDocumentInformation, though this is not
recommended.
:type document: ifcopenshell.entity_instance.entity_instance
:return: The IfcRelAssociatesDocument relationship
+ or `None` if `products` was an empty list or all products were
+ already assigned to the `document`.
:rtype: ifcopenshell.entity_instance.entity_instance
Example:
@@ -54,42 +64,51 @@ class Usecase:
reference = ifcopenshell.api.run("document.add_reference", model, information=document)
# Let's imagine storey represents an IfcBuildingStorey for the ground floor
- ifcopenshell.api.run("document.assign_document", model, product=storey, document=reference)
+ ifcopenshell.api.run("document.assign_document", model, products=[storey], document=reference)
"""
self.file = file
self.settings = {
- "product": product,
+ "products": products,
"document": document,
}
- def execute(self):
- rel = self.get_document_rel()
- related_objects = set(rel.RelatedObjects) if rel.RelatedObjects else set()
- related_objects.add(self.settings["product"])
- rel.RelatedObjects = list(related_objects)
+ def execute(self) -> Union[ifcopenshell.entity_instance, None]:
+ # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference?
+ # NOTE: reuses code from `library.assign_reference`
+
+ referenced_elements = ifcopenshell.util.element.get_referenced_elements(self.settings["document"])
+ products: set[ifcopenshell.entity_instance] = set(self.settings["products"])
+ products = products - referenced_elements
+
+ if not products:
+ return
- def get_document_rel(self):
if self.file.schema == "IFC2X3":
- for rel in self.file.by_type("IfcRelAssociatesDocument"):
- if rel.RelatingDocument == self.settings["document"]:
- return rel
+ rel = next(
+ (
+ r
+ for r in self.file.by_type("IfcRelAssociatesDocument")
+ if r.RelatingDocument == self.settings["document"]
+ ),
+ None,
+ )
else:
- if (
- hasattr(self.settings["document"], "DocumentRefForObjects")
- and self.settings["document"].DocumentRefForObjects
- ):
- return self.settings["document"].DocumentRefForObjects[0]
- elif (
- hasattr(self.settings["document"], "DocumentInfoForObjects")
- and self.settings["document"].DocumentInfoForObjects
- ):
- return self.settings["document"].DocumentInfoForObjects[0]
+ ifc_class = self.settings["document"].is_a()
+ if ifc_class == "IfcDocumentReference":
+ rel = next(iter(self.settings["document"].DocumentRefForObjects), None)
+ elif ifc_class == "IfcDocumentInformation":
+ rel = next(iter(self.settings["document"].DocumentInfoForObjects), None)
- return self.file.create_entity(
- "IfcRelAssociatesDocument",
- **{
- "GlobalId": ifcopenshell.guid.new(),
- "OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
- "RelatingDocument": self.settings["document"],
- }
- )
+ if not rel:
+ return self.file.create_entity(
+ "IfcRelAssociatesDocument",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file),
+ RelatedObjects=list(products),
+ RelatingDocument=self.settings["document"],
+ )
+
+ related_objects = set(rel.RelatedObjects) | products
+ rel.RelatedObjects = list(related_objects)
+ ifcopenshell.api.run("owner.update_owner_history", self.file, element=rel)
+ return rel
diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py b/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py
index 5a08c13262..dd43573e65 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py
@@ -17,16 +17,22 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell
+import ifcopenshell.api
import ifcopenshell.util.element
class Usecase:
- def __init__(self, file, product=None, document=None):
- """Unassigns a document and a product association
+ def __init__(
+ self,
+ file: ifcopenshell.file,
+ products: list[ifcopenshell.entity_instance],
+ document: ifcopenshell.entity_instance,
+ ):
+ """Unassigns a document and an association to the list of products
- :param product: The object that the document reference or information is
+ :param product: The list of objects that the document reference or information is
related to.
- :type product: ifcopenshell.entity_instance.entity_instance
+ :type product: list[ifcopenshell.entity_instance.entity_instance]
:param document: The IfcDocumentReference (typically) or in rare cases
the IfcDocumentInformation that is associated with the product
:type document: ifcopenshell.entity_instance.entity_instance
@@ -45,24 +51,39 @@ class Usecase:
reference = ifcopenshell.api.run("document.add_reference", model, information=document)
# Let's imagine storey represents an IfcBuildingStorey for the ground floor
- ifcopenshell.api.run("document.assign_document", model, product=storey, document=reference)
+ ifcopenshell.api.run("document.assign_document", model, products=[storey], document=reference)
# Now let's change our mind and remove the association
- ifcopenshell.api.run("document.unassign_document", model, product=storey, document=reference)
+ ifcopenshell.api.run("document.unassign_document", model, products=[storey], document=reference)
"""
self.file = file
self.settings = {
- "product": product,
+ "products": products,
"document": document,
}
def execute(self):
- for rel in self.settings["product"].HasAssociations:
- if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument == self.settings["document"]:
- if len(rel.RelatedObjects) == 1:
- history = rel.OwnerHistory
- self.file.remove(rel)
- if history:
- ifcopenshell.util.element.remove_deep2(self.file, history)
- else:
- rel.RelatedObjects = [o for o in rel.RelatedObjects if o != self.settings["product"]]
+ # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference?
+ # NOTE: reuses code from `library.un assign_reference`
+
+ reference_rels: set[ifcopenshell.entity_instance] = set()
+ products = set(self.settings["products"])
+ for product in products:
+ reference_rels.update(product.HasAssociations)
+
+ reference_rels = {
+ rel
+ for rel in reference_rels
+ if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument == self.settings["document"]
+ }
+
+ for rel in reference_rels:
+ related_objects = set(rel.RelatedObjects) - products
+ if related_objects:
+ rel.RelatedObjects = list(related_objects)
+ ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
+ else:
+ history = rel.OwnerHistory
+ self.file.remove(rel)
+ if history:
+ ifcopenshell.util.element.remove_deep2(self.file, history)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py
index 9e34626e4a..77554e1f7f 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py
@@ -26,7 +26,7 @@ class Usecase:
surveyor, and a third-party digital engineer with expertise in IFC to
moderate. For more information, read the BlenderBIM Add-on documentation
for Georeferencing:
- https://blenderbim.org/docs/users/georeferencing.html
+ https://docs.blenderbim.org/users/georeferencing.html
For more information about the attributes and data types of an
IfcMapConversion, consult the IFC documentation.
diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py
index a1f3f83d17..8ee4a21eb5 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py
@@ -17,23 +17,30 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell
+import ifcopenshell.api
+import ifcopenshell.util.element
+from typing import Union
class Usecase:
- def __init__(self, file, product=None, reference=None):
- """Associates a product with a library reference
+ def __init__(
+ self, file: ifcopenshell.file, products: ifcopenshell.entity_instance, reference: ifcopenshell.entity_instance
+ ):
+ """Associates a list products with a library reference
A product may be associated with zero, one, or many references across
multiple libraries. See ifcopenshell.api.library.add_reference for more
detail about how references work.
- :param product: The IfcProduct you want to associate with the reference
- :type product: ifcopenshell.entity_instance.entity_instance
+ :param products: The list of IfcProducts you want to associate with the reference
+ :type products: list[ifcopenshell.entity_instance.entity_instance]
:param reference: The IfcLibraryReference you want the product to be
associated with.
:type reference: ifcopenshell.entity_instance.entity_instance
:return: The IfcRelAssociatesLibrary relationship entity
- :rtype: ifcopenshell.entity_instance.entity_instance
+ or `None` if `products` was an empty list or all products were
+ already assigned to the `reference`.
+ :rtype: Union[ifcopenshell.entity_instance.entity_instance, None]
Example:
@@ -51,40 +58,46 @@ class Usecase:
ifc_class="IfcUnitaryEquipment", predefined_type="AIRHANDLER")
# And now assign the IFC model's AHU with its Brickschema counterpart
- ifcopenshell.api.run("library.assign_reference", model, reference=reference, product=ahu)
+ ifcopenshell.api.run("library.assign_reference", model, reference=reference, products=[ahu])
"""
self.file = file
self.settings = {
- "product": product,
+ "products": products,
"reference": reference,
}
- def execute(self):
+ def execute(self) -> Union[ifcopenshell.entity_instance, None]:
+ # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference?
+
+ referenced_elements = ifcopenshell.util.element.get_referenced_elements(self.settings["reference"])
+ products: set[ifcopenshell.entity_instance] = set(self.settings["products"])
+ products = products - referenced_elements
+
+ if not products:
+ return
+
if self.file.schema == "IFC2X3":
- rels = self.get_ifc2x3_rels()
+ rel = next(
+ (
+ r
+ for r in self.file.by_type("IfcRelAssociatesLibrary")
+ if r.RelatingLibrary == self.settings["reference"]
+ ),
+ None,
+ )
else:
- rels = self.settings["reference"].LibraryRefForObjects
- if not rels:
+ rel = next(iter(self.settings["reference"].LibraryRefForObjects), None)
+
+ if not rel:
return self.file.create_entity(
"IfcRelAssociatesLibrary",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file),
- RelatedObjects=[self.settings["product"]],
+ RelatedObjects=list(products),
RelatingLibrary=self.settings["reference"],
)
- for rel in rels:
- if self.settings["product"] in rel.RelatedObjects:
- return rel
-
- rel = rels[0]
- related_objects = list(rel.RelatedObjects)
- related_objects.append(self.settings["product"])
- rel.RelatedObjects = related_objects
+ related_objects = set(rel.RelatedObjects) | products
+ rel.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", self.file, element=rel)
return rel
-
- def get_ifc2x3_rels(self):
- return [
- r for r in self.file.by_type("IfcRelAssociatesLibrary") if r.RelatingLibrary == self.settings["reference"]
- ]
diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py
index 639a3dd35b..b650ffba75 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py
@@ -18,18 +18,24 @@
import ifcopenshell
import ifcopenshell.util.element
+import ifcopenshell.api
class Usecase:
- def __init__(self, file, reference=None, product=None):
- """Unassigns a product from a reference
+ def __init__(
+ self,
+ file: ifcopenshell.file,
+ reference: ifcopenshell.entity_instance,
+ products: list[ifcopenshell.entity_instance],
+ ):
+ """Unassigns a product of products from a reference
If the product isn't assigned to the reference, nothing will happen.
:param reference: The IfcLibraryReference to unassign from
:type reference: ifcopenshell.entity_instance.entity_instance
- :param product: A IfcProduct element to unassign from the reference
- :type product: ifcopenshell.entity_instance.entity_instance
+ :param products: A list of IfcProduct elements to unassign from the reference
+ :type products: list[ifcopenshell.entity_instance.entity_instance]
:return: None
:rtype: None
@@ -49,27 +55,36 @@ class Usecase:
ifc_class="IfcUnitaryEquipment", predefined_type="AIRHANDLER")
# And now assign the IFC model's AHU with its Brickschema counterpart
- ifcopenshell.api.run("library.assign_reference", model, reference=reference, product=ahu)
+ ifcopenshell.api.run("library.assign_reference", model, reference=reference, products=[ahu])
# Let's change our mind and unassign it.
- ifcopenshell.api.run("library.unassign_reference", model, reference=reference, product=ahu)
+ ifcopenshell.api.run("library.unassign_reference", model, reference=reference, products=[ahu])
"""
self.file = file
- self.settings = {"reference": reference, "product": product}
+ self.settings = {"reference": reference, "products": products}
def execute(self):
- rels = self.settings["reference"].LibraryRefForObjects
- if not rels:
- return
- for rel in rels:
- if self.settings["product"] in rel.RelatedObjects:
- if len(rel.RelatedObjects) == 1:
- history = rel.OwnerHistory
- self.file.remove(rel)
- if history:
- ifcopenshell.util.element.remove_deep2(self.file, history)
- continue
- related_objects = list(rel.RelatedObjects)
- related_objects.remove(self.settings["product"])
- rel.RelatedObjects = related_objects
+ # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference?
+
+ reference_rels: set[ifcopenshell.entity_instance] = set()
+ products = set(self.settings["products"])
+ for product in products:
+ reference_rels.update(product.HasAssociations)
+
+ reference_rels = {
+ rel
+ for rel in reference_rels
+ if rel.is_a("IfcRelAssociatesLibrary") and rel.RelatingLibrary == self.settings["reference"]
+ }
+
+ for rel in reference_rels:
+ related_objects = set(rel.RelatedObjects) - products
+ if related_objects:
+ rel.RelatedObjects = list(related_objects)
+ ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
+ else:
+ history = rel.OwnerHistory
+ self.file.remove(rel)
+ if history:
+ ifcopenshell.util.element.remove_deep2(self.file, history)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py b/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py
index 5427dd9c23..6c0e6a456f 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py
@@ -38,7 +38,7 @@ def get_application(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instanc
if not app and ifc.schema == "IFC2X3":
raise Exception(
"Please create an application to continue. See the owner.create_owner_history docs for more info."
- "https://blenderbim.org/docs-python/autoapi/ifcopenshell/api/owner/create_owner_history/index.html"
+ "https://docs.ifcopenshell.org/autoapi/ifcopenshell/api/owner/create_owner_history/index.html"
)
return (app or [None])[0]
@@ -58,7 +58,7 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None
if not pao and ifc.schema == "IFC2X3":
raise Exception(
"Please create a user to continue. See the owner.create_owner_history docs for more info."
- "https://blenderbim.org/docs-python/autoapi/ifcopenshell/api/owner/create_owner_history/index.html"
+ "https://docs.ifcopenshell.org/autoapi/ifcopenshell/api/owner/create_owner_history/index.html"
)
return (pao or [None])[0]
diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py
index 4a62532762..00cb94d8b3 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py
@@ -22,11 +22,16 @@ import ifcopenshell.util.element
class Usecase:
- def __init__(self, file, product=None, relating_structure=None):
- """Dereferences the a product and space
+ def __init__(
+ self,
+ file: ifcopenshell.file,
+ products: list[ifcopenshell.entity_instance],
+ relating_structure: ifcopenshell.entity_instance,
+ ):
+ """Dereferences a list of products and space
- :param product: The physical IfcElement that exists in the space.
- :type product: ifcopenshell.entity_instance.entity_instance
+ :param products: The list of physical IfcElements that exists in the space.
+ :type products: list[ifcopenshell.entity_instance.entity_instance]
:param relating_structure: The IfcSpatialStructureElement element, such
as IfcBuilding, IfcBuildingStorey, or IfcSpace that the element
exists in.
@@ -59,23 +64,24 @@ class Usecase:
ifcopenshell.api.run("spatial.assign_container", model, products=[column], relating_structure=storey1)
# And referenced in the others
- ifcopenshell.api.run("spatial.reference_structure", model, product=column, relating_structure=storey2)
- ifcopenshell.api.run("spatial.reference_structure", model, product=column, relating_structure=storey3)
+ ifcopenshell.api.run("spatial.reference_structure", model, products=[column], relating_structure=storey2)
+ ifcopenshell.api.run("spatial.reference_structure", model, products=[column], relating_structure=storey3)
# Actually, it only goes up to storey 2.
- ifcopenshell.api.run("spatial.dereference_structure", model, product=column, relating_structure=storey3)
+ ifcopenshell.api.run("spatial.dereference_structure", model, products=[column], relating_structure=storey3)
"""
self.file = file
- self.settings = {"product": product, "relating_structure": relating_structure}
+ self.settings = {"products": products, "relating_structure": relating_structure}
- def execute(self):
- for rel in self.settings["product"].ReferencedInStructures:
- if rel.RelatingStructure != self.settings["relating_structure"]:
+ def execute(self) -> None:
+ products = set(self.settings["products"])
+ for rel in self.settings["relating_structure"].ReferencesElements:
+ related_elements = set(rel.RelatedElements)
+ if not related_elements.intersection(products):
continue
- related_elements = list(rel.RelatedElements)
- related_elements.remove(self.settings["product"])
+ related_elements = related_elements - products
if related_elements:
- rel.RelatedElements = related_elements
+ rel.RelatedElements = list(related_elements)
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
else:
history = rel.OwnerHistory
diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py
index 187ce20afe..6d8915828a 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py
@@ -18,11 +18,18 @@
import ifcopenshell
import ifcopenshell.api
+import ifcopenshell.util.element
+from typing import Union
class Usecase:
- def __init__(self, file, product=None, relating_structure=None):
- """Denote that a product is related to a spatial structure
+ def __init__(
+ self,
+ file: ifcopenshell.file,
+ products: list[ifcopenshell.entity_instance],
+ relating_structure: ifcopenshell.entity_instance,
+ ):
+ """Denote that a list products is related to a list of spatial structures
This is similar to ifcopenshell.api.spatial.assign_container, except
that containment can only occur between a product and a single spatial
@@ -39,13 +46,15 @@ class Usecase:
Referencing is non-hierarchical, so a door may be referenced in multiple
spaces simultaneously.
- :param product: The physical IfcElement that exists in the space.
- :type product: ifcopenshell.entity_instance.entity_instance
+ :param products: The list of physical IfcElements that exists in the space.
+ :type products: list[ifcopenshell.entity_instance.entity_instance]
:param relating_structure: The IfcSpatialStructureElement element, such
as IfcBuilding, IfcBuildingStorey, or IfcSpace that the element
exists in.
+ :type relating_structure: ifcopenshell.entity_instance.entity_instance
:return: The IfcRelReferencedInSpatialStructure relationship instance
- :rtype: ifcopenshell.entity_instance.entity_instance
+ or `None` if `products` was an empty list.
+ :rtype: Union[ifcopenshell.entity_instance, None]
Example:
@@ -73,37 +82,43 @@ class Usecase:
ifcopenshell.api.run("spatial.assign_container", model, products=[column], relating_structure=storey1)
# And referenced in the others
- ifcopenshell.api.run("spatial.reference_structure", model, product=column, relating_structure=storey2)
- ifcopenshell.api.run("spatial.reference_structure", model, product=column, relating_structure=storey3)
+ ifcopenshell.api.run(
+ "spatial.reference_structure", model, products=[column], relating_structure=[storey2, storey3]
+ )
"""
self.file = file
self.settings = {
- "product": product,
+ "products": products,
"relating_structure": relating_structure,
}
- def execute(self):
- referenced_in_structures = self.settings["product"].ReferencedInStructures
- references_elements = self.settings["relating_structure"].ReferencesElements
+ def execute(self) -> Union[ifcopenshell.entity_instance, None]:
+ structure = self.settings["relating_structure"]
+ products = set(self.settings["products"])
- for rel in referenced_in_structures:
- if rel.RelatingStructure == self.settings["relating_structure"]:
- return
+ if not products:
+ return
- if references_elements:
- related_elements = list(references_elements[0].RelatedElements)
- related_elements.append(self.settings["product"])
- references_elements[0].RelatedElements = related_elements
- ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": references_elements[0]})
- else:
- references_elements = self.file.create_entity(
+ referenced = ifcopenshell.util.element.get_structure_referenced_elements(structure)
+ products_to_assign = products - referenced
+ rel = next(iter(structure.ReferencesElements), None)
+
+ if not products_to_assign:
+ return rel
+
+ if rel is None:
+ rel = self.file.create_entity(
"IfcRelReferencedInSpatialStructure",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
- "RelatedElements": [self.settings["product"]],
- "RelatingStructure": self.settings["relating_structure"],
+ "RelatedElements": list(products_to_assign),
+ "RelatingStructure": structure,
}
)
+ else:
+ related_elements = set(rel.RelatedElements) | products_to_assign
+ rel.RelatedElements = list(related_elements)
+ ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
- return references_elements
+ return rel
diff --git a/src/ifcopenshell-python/ifcopenshell/entity_instance.py b/src/ifcopenshell-python/ifcopenshell/entity_instance.py
index 39a703ae51..00e441d9ac 100644
--- a/src/ifcopenshell-python/ifcopenshell/entity_instance.py
+++ b/src/ifcopenshell-python/ifcopenshell/entity_instance.py
@@ -30,7 +30,7 @@ import functools
import subprocess
import sys
import time
-from typing import Union, Any, Callable, TypeVar
+from typing import Union, Any, Callable, TypeVar, overload
from . import ifcopenshell_wrapper
from . import settings
@@ -317,15 +317,25 @@ class entity_instance(object):
return self.wrapped_data.to_string(valid_spf)
- def is_a(self, *args) -> Union[str, bool]:
+ @overload
+ def is_a(self) -> str: ...
+ @overload
+ def is_a(self, ifc_class: str) -> bool: ...
+ @overload
+ def is_a(self, with_schema: bool) -> str: ...
+ def is_a(self, *args: Union[str, bool]) -> Union[str, bool]:
"""Return the IFC class name of an instance, or checks if an instance belongs to a class.
The check will also return true if a parent class name is provided.
:param args: If specified, is a case insensitive IFC class name to check
- :type args: string
+ or if specified as a boolean then will define whether
+ returned IFC class name should include schema name
+ (e.g. "IFC4.IfcWall" if `True` and "IfcWall" if `False`).
+ If omitted will act as `False`.
+ :type args: Union[str, bool]
:returns: Either the name of the class, or a boolean if it passes the check
- :rtype: string|bool
+ :rtype: Union[str, bool]
Example:
diff --git a/src/ifcopenshell-python/ifcopenshell/util/classification.py b/src/ifcopenshell-python/ifcopenshell/util/classification.py
index 12e82139cb..42208a90e7 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/classification.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/classification.py
@@ -23,10 +23,10 @@ from typing import Optional
def get_references(element: ifcopenshell.entity_instance, should_inherit=True) -> set[ifcopenshell.entity_instance]:
results = set()
if not element.is_a("IfcRoot"):
- if hasattr(element, "HasExternalReferences"):
- return {r.RelatingReference for r in element.HasExternalReferences or []}
- elif hasattr(element, "HasExternalReference"): # Seriously, IFC?
- return {r.RelatingReference for r in element.HasExternalReference or []}
+ if (references := getattr(element, "HasExternalReferences", None)) is not None or (
+ references := getattr(element, "HasExternalReference", None)
+ ) is not None:
+ return {r.RelatingReference for r in references}
if should_inherit:
element_type = ifcopenshell.util.element.get_type(element)
if element_type and element_type != element:
diff --git a/src/ifcopenshell-python/ifcopenshell/util/constraint.py b/src/ifcopenshell-python/ifcopenshell/util/constraint.py
index b2a38d6060..b8f6aac14f 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/constraint.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/constraint.py
@@ -18,20 +18,59 @@
#
#
-def get_constraints(product):
+import ifcopenshell
+from typing import Union
+
+
+def get_constraints(product: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
+ """
+ Retrieves the constraints assigned to the `product`.
+
+ :param product: The IFC element.
+ :type product: ifcopenshell.entity_instance.entity_instance
+ :return: List of assigned constraints.
+ :rtype: list[ifcopenshell.entity_instance.entity_instance]
+ """
constraints = []
for rel in product.HasAssociations or []:
if rel.is_a("IfcRelAssociatesConstraint"):
constraints.append(rel.RelatingConstraint)
return constraints
-def get_metrics(constraint):
+
+def get_constrained_elements(constraint: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]:
+ """
+ Retrieves the elements constrained by a `constraint`.
+
+ :param product: The IFC element.
+ :type product: ifcopenshell.entity_instance.entity_instance
+ :return: Set of elements constrained by a `constrant`.
+ :rtype: set[ifcopenshell.entity_instance.entity_instance]
+ """
+ elements = set()
+ for rel in constraint.file.get_inverse(constraint):
+ if rel.is_a("IfcRelAssociatesConstraint"):
+ elements.update(rel.RelatedObjects)
+ return elements
+
+
+def get_metrics(constraint: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
+ """
+ Retrieves the list of nested constraints for a IfcObjective `constraint`.
+
+ :param product: IfcObjective constraint.
+ :type product: ifcopenshell.entity_instance.entity_instance
+ :return: List of nested constraints.
+ :rtype: list[ifcopenshell.entity_instance.entity_instance]
+ """
+
metrics = []
for metric in constraint.BenchmarkValues or []:
metrics.append(metric)
return metrics
-def get_metric_reference(metric, is_deep=True):
+
+def get_metric_reference(metric: ifcopenshell.entity_instance, is_deep=True):
def get_reference_Attribute(ref, path):
if ref:
if is_deep:
@@ -47,7 +86,10 @@ def get_metric_reference(metric, is_deep=True):
reference = metric.ReferencePath
return get_reference_Attribute(reference, "")
-def get_metric_constraints(resource, attribute):
+
+def get_metric_constraints(
+ resource: ifcopenshell.entity_instance, attribute
+) -> Union[list[ifcopenshell.entity_instance], None]:
metrics = []
for constraint in get_constraints(resource) or []:
for metric in get_metrics(constraint) or []:
@@ -60,15 +102,14 @@ def get_metric_constraints(resource, attribute):
return metrics
return None
-def is_hard_constraint(metric):
- if metric.ConstraintGrade == "HARD" and metric.Benchmark == "EQUALTO":
- return True
-def is_attribute_locked(product, attribute):
+def is_hard_constraint(metric: ifcopenshell.entity_instance) -> bool:
+ return metric.ConstraintGrade == "HARD" and metric.Benchmark == "EQUALTO"
+
+
+def is_attribute_locked(product: ifcopenshell.entity_instance, attribute) -> bool:
is_locked = False
- metrics = get_metric_constraints(
- product, attribute
- )
+ metrics = get_metric_constraints(product, attribute)
for metric in metrics or []:
if is_hard_constraint(metric):
is_locked = True
diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py
index 6c084b57ad..f8b8791787 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/element.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/element.py
@@ -20,16 +20,17 @@ from __future__ import annotations
import ifcopenshell
import ifcopenshell.util.element
from typing import Any, Callable, Optional, Union, Literal, overload
+from collections import namedtuple
def get_pset(
element: ifcopenshell.entity_instance,
name: str,
prop: Optional[str] = None,
- psets_only=False,
- qtos_only=False,
- should_inherit=True,
- verbose=False,
+ psets_only: bool = False,
+ qtos_only: bool = False,
+ should_inherit: bool = True,
+ verbose: bool = False,
) -> Union[Any, dict[str, Any]]:
"""Retrieve a single property set or single property
@@ -429,8 +430,7 @@ def get_predefined_type(element: ifcopenshell.entity_instance) -> str:
element = ifcopenshell.by_type("IfcWall")[0]
predefined_type = ifcopenshell.util.element.get_predefined_type(element)
"""
- element_type = get_type(element)
- if element_type:
+ if element_type := get_type(element):
predefined_type = getattr(element_type, "PredefinedType", None)
if predefined_type == "USERDEFINED" or not predefined_type:
predefined_type = getattr(element_type, "ElementType", ...)
@@ -563,7 +563,9 @@ def get_material(
return get_material(relating_type, should_skip_usage)
-def get_materials(element: ifcopenshell.entity_instance, should_inherit=True) -> list[ifcopenshell.entity_instance]:
+def get_materials(
+ element: ifcopenshell.entity_instance, should_inherit: bool = True
+) -> list[ifcopenshell.entity_instance]:
"""Gets individual materials of an element
If the element has a material set, the individual materials of that set are
@@ -826,7 +828,7 @@ def get_layers(
def get_container(
- element: ifcopenshell.entity_instance, should_get_direct=False, ifc_class: Optional[str] = None
+ element: ifcopenshell.entity_instance, should_get_direct: bool = False, ifc_class: Optional[str] = None
) -> ifcopenshell.entity_instance:
"""
Retrieves the spatial structure container of an element.
@@ -902,6 +904,27 @@ def get_referenced_structures(element: ifcopenshell.entity_instance) -> list[ifc
return [r.RelatingStructure for r in getattr(element, "ReferencedInStructures", [])]
+def get_structure_referenced_elements(structure: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]:
+ """Retreives a set of elements referenced by a structure
+
+ :param structure: IfcSpatialElement
+ :type element: ifcopenshell.entity_instance.entity_instance
+ :return: A set of referenced elements, IfcSpatialReferenceSelect
+ :rtype: set[ifcopenshell.entity_instance.entity_instance]
+
+ Example:
+
+ .. code:: python
+
+ element = file.by_type("IfcBuildingStorey")[0]
+ print(ifcopenshell.util.element.get_structure_referenced_elements(element))
+ """
+ referenced = set()
+ for rel in structure.ReferencesElements:
+ referenced.update(rel.RelatedElements)
+ return referenced
+
+
def get_decomposition(element: ifcopenshell.entity_instance, is_recursive=True) -> list[ifcopenshell.entity_instance]:
"""
Retrieves all subelements of an element based on the spatial decomposition
@@ -1092,6 +1115,66 @@ def get_components(element: ifcopenshell.entity_instance, include_ports=False) -
return is_decomposed_by[0].RelatedObjects
+ReferenceData = namedtuple("ReferenceData", "inverse_attribute, rel_class, relating_element_attribute")
+
+# References below are omitted because they do not introduce
+# any additional referenced objects besides the objects
+# from their supertype IfcExternalReference
+# - IfcExternallyDefinedHatchStyle
+# - IfcExternallyDefinedSurfaceStyle
+# - IfcExternallyDefinedTextFont
+
+REFERENCE_TYPES: dict[str, ReferenceData] = {
+ "IfcClassificationReference": ReferenceData(
+ "ClassificationRefForObjects",
+ "IfcRelAssociatesClassification",
+ "RelatingClassification",
+ ),
+ "IfcDocumentReference": ReferenceData("DocumentRefForObjects", "IfcRelAssociatesDocument", "RelatingDocument"),
+ "IfcLibraryReference": ReferenceData("LibraryRefForObjects", "IfcRelAssociatesLibrary", "RelatingLibrary"),
+}
+
+
+def get_referenced_elements(reference: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]:
+ """Get all elements with assigned `reference`
+
+ :param reference: IfcExternalReference subtype reference
+ :type reference: ifcopenshell.entity_instance.entity_instance
+ :return: The elements with assigned `reference`
+ :rtype: set[ifcopenshell.entity_instance.entity_instance]
+
+ Example:
+
+ .. code:: python
+
+ reference = file.by_type("IfcClassificationReference")[0]
+ elements = ifcopenshell.util.element.get_referenced_elements(reference)
+ """
+
+ related_objects: set[ifcopenshell.entity_instance] = set()
+ ifc_file = reference.file
+ ifc_class = reference.is_a()
+
+ if ifc_file.schema == "IFC2X3":
+ reference_data = REFERENCE_TYPES.get(ifc_class)
+ if reference_data:
+ for rel in ifc_file.by_type(reference_data.rel_class):
+ if getattr(rel, reference_data.relating_element_attribute) == reference:
+ related_objects.update(rel.RelatedObjects)
+
+ else:
+ # IfcExternalReference
+ for external_rel in reference.ExternalReferenceForResources:
+ related_objects.update(external_rel.RelatedResourceObjects)
+
+ reference_data = REFERENCE_TYPES.get(ifc_class)
+ if reference_data:
+ for rel in getattr(reference, reference_data.inverse_attribute):
+ related_objects.update(rel.RelatedObjects)
+
+ return related_objects
+
+
def replace_attribute(element: ifcopenshell.entity_instance, old: Any, new: Any) -> None:
for i, attribute_value in enumerate(element):
if has_element_reference(attribute_value, old):
diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape.py b/src/ifcopenshell-python/ifcopenshell/util/shape.py
index 4aaf6a08fe..798000005a 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/shape.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/shape.py
@@ -17,6 +17,7 @@
# along with IfcOpenShell. If not, see .
import shapely
+import shapely.ops
import numpy as np
import ifcopenshell.util.element
import ifcopenshell.util.placement
diff --git a/src/ifcopenshell-python/pyproject.toml b/src/ifcopenshell-python/pyproject.toml
index 89a55a2c69..fc1bff6807 100644
--- a/src/ifcopenshell-python/pyproject.toml
+++ b/src/ifcopenshell-python/pyproject.toml
@@ -15,7 +15,7 @@ classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
]
-dependencies = ["mathutils", "shapely", "numpy", "isodate", "dateutil", "lark"]
+dependencies = ["mathutils", "shapely", "numpy", "isodate", "python-dateutil", "lark"]
[project.urls]
"Homepage" = "http://ifcopenshell.org"
diff --git a/src/ifcopenshell-python/test/api/classification/test_add_classification_reference.py b/src/ifcopenshell-python/test/api/classification/test_add_reference.py
similarity index 52%
rename from src/ifcopenshell-python/test/api/classification/test_add_classification_reference.py
rename to src/ifcopenshell-python/test/api/classification/test_add_reference.py
index 7cf5722b8f..ed52b05d35 100644
--- a/src/ifcopenshell-python/test/api/classification/test_add_classification_reference.py
+++ b/src/ifcopenshell-python/test/api/classification/test_add_reference.py
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see .
+import pytest
import test.bootstrap
import ifcopenshell.api
import ifcopenshell.util.classification
@@ -23,85 +24,127 @@ import ifcopenshell.util.classification
class TestAddReference(test.bootstrap.IFC4):
def test_adding_a_reference(self):
+ is_ifc2x3 = self.file.schema == "IFC2X3"
+
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name")
ifcopenshell.api.run(
"classification.add_reference",
self.file,
- product=element,
+ products=[element, element2],
identification="X",
name="Foobar",
classification=result,
)
references = list(ifcopenshell.util.classification.get_references(element))
assert len(references) == 1
- assert references[0].Identification == "X"
+ assert getattr(references[0], "ItemReference" if is_ifc2x3 else "Identification") == "X"
assert references[0].Name == "Foobar"
assert references[0].ReferencedSource == self.file.by_type("IfcClassification")[0]
- element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
- ifcopenshell.api.run(
- "classification.add_reference",
- self.file,
- product=element2,
- identification="X",
- name="Foobar",
- classification=result,
+ references2 = list(ifcopenshell.util.classification.get_references(element))
+ assert len(references2) == 1
+ assert getattr(references2[0], "ItemReference" if is_ifc2x3 else "Identification") == "X"
+ assert references2[0].Name == "Foobar"
+ assert references2[0] == references[0]
+
+ rel = next(
+ rel
+ for rel in self.file.by_type("IfcRelAssociatesClassification")
+ if rel.RelatingClassification == references[0]
)
- assert list(ifcopenshell.util.classification.get_references(element2))[0].Identification == "X"
- assert list(ifcopenshell.util.classification.get_references(element2))[0].Name == "Foobar"
- assert list(ifcopenshell.util.classification.get_references(element2))[0] == references[0]
+ assert len(rel.RelatedObjects) == 2
def test_adding_a_library_based_reference(self):
+ is_ifc2x3 = self.file.schema == "IFC2X3"
+
library = ifcopenshell.file()
classification = library.createIfcClassification(Name="Name")
reference = library.createIfcClassificationReference(Identification="1", ReferencedSource=classification)
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
result = ifcopenshell.api.run("classification.add_classification", self.file, classification=classification)
ifcopenshell.api.run(
"classification.add_reference",
self.file,
- product=element,
+ products=[element, element2],
reference=reference,
classification=result,
)
references = list(ifcopenshell.util.classification.get_references(element))
assert len(references) == 1
- assert references[0].Identification == "1"
+ assert getattr(references[0], "ItemReference" if is_ifc2x3 else "Identification") == "1"
assert references[0].ReferencedSource == self.file.by_type("IfcClassification")[0]
- def test_adding_a_reference_to_a_resource(self):
+ references2 = list(ifcopenshell.util.classification.get_references(element2))
+ assert len(references2) == 1
+ assert getattr(references2[0], "ItemReference" if is_ifc2x3 else "Identification") == "1"
+ assert references2[0].ReferencedSource == self.file.by_type("IfcClassification")[0]
+ assert references[0] == references2[0]
+
+ rel = next(
+ rel
+ for rel in self.file.by_type("IfcRelAssociatesClassification")
+ if rel.RelatingClassification == references[0]
+ )
+ assert len(rel.RelatedObjects) == 2
+
+ def test_adding_a_reference_to_a_resource_and_to_a_root(self):
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
element = self.file.createIfcMaterial()
+ element2 = self.file.createIfcCostValue()
+ element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name")
- ifcopenshell.api.run(
- "classification.add_reference",
- self.file,
- product=element,
- identification="X",
- name="Foobar",
- classification=result,
- )
+
+ if self.file.schema == "IFC2X3":
+ with pytest.raises(TypeError):
+ ifcopenshell.api.run(
+ "classification.add_reference",
+ self.file,
+ products=[element, element2, element3],
+ identification="X",
+ name="Foobar",
+ classification=result,
+ )
+ return
+ else:
+ ifcopenshell.api.run(
+ "classification.add_reference",
+ self.file,
+ products=[element, element2, element3],
+ identification="X",
+ name="Foobar",
+ classification=result,
+ )
+
references = list(ifcopenshell.util.classification.get_references(element))
assert len(references) == 1
assert references[0].Identification == "X"
assert references[0].Name == "Foobar"
assert references[0].ReferencedSource == self.file.by_type("IfcClassification")[0]
- element2 = self.file.createIfcCostValue()
- ifcopenshell.api.run(
- "classification.add_reference",
- self.file,
- product=element2,
- identification="X",
- name="Foobar",
- classification=result,
- )
- assert list(ifcopenshell.util.classification.get_references(element2))[0].Identification == "X"
- assert list(ifcopenshell.util.classification.get_references(element2))[0].Name == "Foobar"
- assert list(ifcopenshell.util.classification.get_references(element2))[0] == references[0]
+ references2 = list(ifcopenshell.util.classification.get_references(element2))
+ assert references2[0].Identification == "X"
+ assert references2[0].Name == "Foobar"
+ assert references2[0] == references[0]
+
+ references3 = list(ifcopenshell.util.classification.get_references(element3))
+ assert references3[0].Identification == "X"
+ assert references3[0].Name == "Foobar"
+ assert references3[0] == references[0]
assert len(self.file.by_type("IfcExternalReferenceRelationship")[0].RelatedResourceObjects) == 2
+ rel = next(
+ rel
+ for rel in self.file.by_type("IfcRelAssociatesClassification")
+ if rel.RelatingClassification == references[0]
+ )
+ assert len(rel.RelatedObjects) == 1
+
+
+class TestAddReferenceIFC2X3(test.bootstrap.IFC2X3, TestAddReference):
+ pass
diff --git a/src/ifcopenshell-python/test/api/classification/test_remove_classification.py b/src/ifcopenshell-python/test/api/classification/test_remove_classification.py
index 3652d6e8c8..fb3537ddf2 100644
--- a/src/ifcopenshell-python/test/api/classification/test_remove_classification.py
+++ b/src/ifcopenshell-python/test/api/classification/test_remove_classification.py
@@ -34,7 +34,7 @@ class TestRemoveClassification(test.bootstrap.IFC4):
ifcopenshell.api.run(
"classification.add_reference",
self.file,
- product=element,
+ products=[element],
identification="X",
name="Foobar",
classification=result,
@@ -51,7 +51,7 @@ class TestRemoveClassification(test.bootstrap.IFC4):
ifcopenshell.api.run(
"classification.add_reference",
self.file,
- product=element,
+ products=[element],
identification="X",
name="Foobar",
classification=result,
diff --git a/src/ifcopenshell-python/test/api/classification/test_remove_classification_reference.py b/src/ifcopenshell-python/test/api/classification/test_remove_reference.py
similarity index 55%
rename from src/ifcopenshell-python/test/api/classification/test_remove_classification_reference.py
rename to src/ifcopenshell-python/test/api/classification/test_remove_reference.py
index 597317f3bb..8e03fdef4f 100644
--- a/src/ifcopenshell-python/test/api/classification/test_remove_classification_reference.py
+++ b/src/ifcopenshell-python/test/api/classification/test_remove_reference.py
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see .
+import pytest
import test.bootstrap
import ifcopenshell.api
import ifcopenshell.util.classification
@@ -25,58 +26,80 @@ class TestRemoveReference(test.bootstrap.IFC4):
def test_removing_a_reference(self):
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name")
reference = ifcopenshell.api.run(
"classification.add_reference",
self.file,
- product=element,
+ products=[element, element2],
identification="X",
name="Foobar",
classification=result,
)
- ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference)
+ ifcopenshell.api.run(
+ "classification.remove_reference", self.file, products=[element, element2], reference=reference
+ )
assert len(ifcopenshell.util.classification.get_references(element)) == 0
+ assert len(ifcopenshell.util.classification.get_references(element2)) == 0
assert len(self.file.by_type("IfcClassificationReference")) == 0
- def test_removing_a_reference_from_a_resource(self):
+ def test_removing_a_reference_from_a_resource_and_from_a_root(self):
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
element = self.file.createIfcMaterial()
+ element2 = self.file.createIfcCostValue()
+ element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name")
- reference = ifcopenshell.api.run(
- "classification.add_reference",
- self.file,
- product=element,
- identification="X",
- name="Foobar",
- classification=result,
+ if self.file.schema == "IFC2X3":
+ with pytest.raises(TypeError):
+ reference = ifcopenshell.api.run(
+ "classification.add_reference",
+ self.file,
+ products=[element, element2, element3],
+ identification="X",
+ name="Foobar",
+ classification=result,
+ )
+ return
+ else:
+ reference = ifcopenshell.api.run(
+ "classification.add_reference",
+ self.file,
+ products=[element, element2, element3],
+ identification="X",
+ name="Foobar",
+ classification=result,
+ )
+
+ ifcopenshell.api.run(
+ "classification.remove_reference", self.file, products=[element, element2, element3], reference=reference
)
- ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference)
assert len(ifcopenshell.util.classification.get_references(element)) == 0
+ assert len(ifcopenshell.util.classification.get_references(element2)) == 0
+ assert len(ifcopenshell.util.classification.get_references(element3)) == 0
assert len(self.file.by_type("IfcClassificationReference")) == 0
def test_retaining_the_reference_if_still_in_use(self):
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
- element = self.file.createIfcMaterial()
- element2 = self.file.createIfcMaterial()
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name")
reference = ifcopenshell.api.run(
"classification.add_reference",
self.file,
- product=element,
- identification="X",
- name="Foobar",
- classification=result,
- )
- reference2 = ifcopenshell.api.run(
- "classification.add_reference",
- self.file,
- product=element2,
+ products=[element, element2, element3],
identification="X",
name="Foobar",
classification=result,
)
assert len(self.file.by_type("IfcClassificationReference")) == 1
- ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference)
+ ifcopenshell.api.run(
+ "classification.remove_reference", self.file, products=[element, element2], reference=reference
+ )
assert len(self.file.by_type("IfcClassificationReference")) == 1
- ifcopenshell.api.run("classification.remove_reference", self.file, product=element2, reference=reference2)
+ ifcopenshell.api.run("classification.remove_reference", self.file, products=[element3], reference=reference)
assert len(self.file.by_type("IfcClassificationReference")) == 0
+
+
+class TestRemoveReferenceIFC2X3(test.bootstrap.IFC2X3, TestRemoveReference):
+ pass
diff --git a/src/ifcopenshell-python/test/api/constraint/test_assign_constraint.py b/src/ifcopenshell-python/test/api/constraint/test_assign_constraint.py
new file mode 100644
index 0000000000..f881ae9651
--- /dev/null
+++ b/src/ifcopenshell-python/test/api/constraint/test_assign_constraint.py
@@ -0,0 +1,64 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2022 Dion Moult
+#
+# This file is part of IfcOpenShell.
+#
+# IfcOpenShell is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcOpenShell is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcOpenShell. If not, see .
+
+import pytest
+import test.bootstrap
+import ifcopenshell.api
+import ifcopenshell.util.constraint
+
+
+class TestAssignConstraint(test.bootstrap.IFC4):
+ def test_assign_a_constraint(self):
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ constraint = ifcopenshell.api.run("constraint.add_objective", self.file)
+ ifcopenshell.api.run(
+ "constraint.assign_constraint", self.file, products=[element, element2], constraint=constraint
+ )
+ assert ifcopenshell.util.constraint.get_constrained_elements(constraint) == {element, element2}
+ assert len(self.file.by_type("IfcRelAssociatesConstraint")) == 1
+
+ def test_doing_nothing_if_the_constraint_is_already_assigned(self):
+ constraint = ifcopenshell.api.run("constraint.add_objective", self.file)
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "constraint.assign_constraint", self.file, products=[element, element2], constraint=constraint
+ )
+ total_elements = len([e for e in self.file])
+ ifcopenshell.api.run(
+ "constraint.assign_constraint", self.file, products=[element, element2], constraint=constraint
+ )
+ assert len([e for e in self.file]) == total_elements
+
+ def test_that_old_relationships_are_updated_if_they_still_contain_elements(self):
+ constraint = ifcopenshell.api.run("constraint.add_objective", self.file)
+ element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run("constraint.assign_constraint", self.file, products=[element1], constraint=constraint)
+ rel = self.file.by_type("IfcRelAssociatesConstraint")[0]
+
+ element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "constraint.assign_constraint", self.file, products=[element2, element3], constraint=constraint
+ )
+ assert len(rel.RelatedObjects) == 3
+
+
+class TestAssignConstraintIFC2X3(test.bootstrap.IFC2X3, TestAssignConstraint):
+ pass
diff --git a/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py b/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py
new file mode 100644
index 0000000000..0d8a21b8bf
--- /dev/null
+++ b/src/ifcopenshell-python/test/api/constraint/test_unassign_constraint.py
@@ -0,0 +1,67 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2022 Dion Moult
+#
+# This file is part of IfcOpenShell.
+#
+# IfcOpenShell is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcOpenShell is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcOpenShell. If not, see .
+
+import pytest
+import test.bootstrap
+import ifcopenshell.api
+import ifcopenshell.util.constraint
+
+
+class TestUnassignConstraint(test.bootstrap.IFC4):
+ def test_unassigning_a_constraint(self):
+ constraint = ifcopenshell.api.run("constraint.add_objective", self.file)
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "constraint.assign_constraint", self.file, products=[element, element2], constraint=constraint
+ )
+ ifcopenshell.api.run(
+ "constraint.unassign_constraint", self.file, products=[element, element2], constraint=constraint
+ )
+ assert ifcopenshell.util.constraint.get_constrained_elements(element) == set()
+ assert len(self.file.by_type("IfcRelAssociatesConstraint")) == 0
+
+ def test_doing_nothing_if_no_constraint(self):
+ constraint = ifcopenshell.api.run("constraint.add_objective", self.file)
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "constraint.unassign_constraint", self.file, products=[element, element2], constraint=constraint
+ )
+ assert ifcopenshell.util.constraint.get_constrained_elements(element) == set()
+ assert ifcopenshell.util.constraint.get_constrained_elements(element2) == set()
+
+ def test_updating_the_rel_when_a_reference_is_removed_with_multipled_elements(self):
+ constraint = ifcopenshell.api.run("constraint.add_objective", self.file)
+ element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run("constraint.assign_constraint", self.file, products=[element1], constraint=constraint)
+ rel = self.file.by_type("IfcRelAssociatesConstraint")[0]
+
+ ifcopenshell.api.run(
+ "constraint.assign_constraint", self.file, products=[element2, element3], constraint=constraint
+ )
+ ifcopenshell.api.run(
+ "constraint.unassign_constraint", self.file, products=[element1, element2], constraint=constraint
+ )
+ assert rel.RelatedObjects == (element3,)
+
+
+class TestUnassignConstraintIFC2X3(test.bootstrap.IFC2X3, TestUnassignConstraint):
+ pass
diff --git a/src/ifcopenshell-python/test/api/document/test_assign_document.py b/src/ifcopenshell-python/test/api/document/test_assign_document.py
index 6c76f844b4..32d584ac51 100644
--- a/src/ifcopenshell-python/test/api/document/test_assign_document.py
+++ b/src/ifcopenshell-python/test/api/document/test_assign_document.py
@@ -18,22 +18,25 @@
import test.bootstrap
import ifcopenshell.api
+import ifcopenshell.util.element
class TestAssignDocument(test.bootstrap.IFC4):
def test_assigning_a_document(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
reference = ifcopenshell.api.run("document.add_reference", self.file, information=None)
- ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference)
+ ifcopenshell.api.run("document.assign_document", self.file, products=[element], document=reference)
assert element.HasAssociations[0].RelatingDocument == reference
+ assert ifcopenshell.util.element.get_referenced_elements(reference) == {element}
def test_assigning_multiple_documents(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
reference = ifcopenshell.api.run("document.add_reference", self.file, information=None)
- ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference)
- ifcopenshell.api.run("document.assign_document", self.file, product=element2, document=reference)
+ ifcopenshell.api.run("document.assign_document", self.file, products=[element, element2], document=reference)
assert len(self.file.by_type("IfcRelAssociatesDocument")) == 1
- assert element.HasAssociations[0].RelatingDocument == reference
- assert element2.HasAssociations[0].RelatingDocument == reference
- assert element.HasAssociations[0] == element.HasAssociations[0]
+ assert ifcopenshell.util.element.get_referenced_elements(reference) == {element, element2}
+
+
+class TestAssignDocumentIFC2X3(test.bootstrap.IFC2X3, TestAssignDocument):
+ pass
diff --git a/src/ifcopenshell-python/test/api/document/test_remove_reference.py b/src/ifcopenshell-python/test/api/document/test_remove_reference.py
index e5f5892172..a1b3c260e2 100644
--- a/src/ifcopenshell-python/test/api/document/test_remove_reference.py
+++ b/src/ifcopenshell-python/test/api/document/test_remove_reference.py
@@ -35,7 +35,7 @@ class TestRemoveReference(test.bootstrap.IFC4):
wall = self.file.createIfcWall()
information = ifcopenshell.api.run("document.add_information", self.file, parent=None)
reference = ifcopenshell.api.run("document.add_reference", self.file, information=information)
- ifcopenshell.api.run("document.assign_document", self.file, product=wall, document=reference)
+ ifcopenshell.api.run("document.assign_document", self.file, products=[wall], document=reference)
assert len(self.file.by_type("IfcRelAssociatesDocument")) == 2
ifcopenshell.api.run("document.remove_reference", self.file, reference=reference)
assert len(self.file.by_type("IfcDocumentReference")) == 0
diff --git a/src/ifcopenshell-python/test/api/document/test_unassign_document.py b/src/ifcopenshell-python/test/api/document/test_unassign_document.py
index 8f227dd2b2..bce6c8d6a1 100644
--- a/src/ifcopenshell-python/test/api/document/test_unassign_document.py
+++ b/src/ifcopenshell-python/test/api/document/test_unassign_document.py
@@ -18,23 +18,29 @@
import test.bootstrap
import ifcopenshell.api
+import ifcopenshell.util.element
class TestUnassignDocument(test.bootstrap.IFC4):
def test_unassigning_a_document(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
reference = ifcopenshell.api.run("document.add_reference", self.file, information=None)
- ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference)
- ifcopenshell.api.run("document.unassign_document", self.file, product=element, document=reference)
+ ifcopenshell.api.run("document.assign_document", self.file, products=[element], document=reference)
+ ifcopenshell.api.run("document.unassign_document", self.file, products=[element], document=reference)
assert not element.HasAssociations
assert not len(self.file.by_type("IfcRelAssociatesDocument"))
def test_unassigning_a_document_used_by_multiple_entities(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
reference = ifcopenshell.api.run("document.add_reference", self.file, information=None)
- ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference)
- ifcopenshell.api.run("document.assign_document", self.file, product=element2, document=reference)
- ifcopenshell.api.run("document.unassign_document", self.file, product=element, document=reference)
- assert not element.HasAssociations
- assert element2.HasAssociations[0].RelatingDocument == reference
+ ifcopenshell.api.run(
+ "document.assign_document", self.file, products=[element, element2, element3], document=reference
+ )
+ ifcopenshell.api.run("document.unassign_document", self.file, products=[element, element2], document=reference)
+ assert ifcopenshell.util.element.get_referenced_elements(reference) == {element3}
+
+
+class TestUnassignDocumentIFC2X3(test.bootstrap.IFC2X3, TestUnassignDocument):
+ pass
diff --git a/src/ifcopenshell-python/test/api/library/test_assign_reference.py b/src/ifcopenshell-python/test/api/library/test_assign_reference.py
index 1133b592d6..550df3940f 100644
--- a/src/ifcopenshell-python/test/api/library/test_assign_reference.py
+++ b/src/ifcopenshell-python/test/api/library/test_assign_reference.py
@@ -20,39 +20,48 @@ import test.bootstrap
import ifcopenshell.api
+def validate_ifc_file(ifc_file: ifcopenshell.file, use_json=True):
+ import ifcopenshell
+ import ifcopenshell.validate
+
+ if use_json:
+ logger = ifcopenshell.validate.json_logger()
+ else:
+ import logging
+
+ logger = logging.getLogger("validate")
+ logger.setLevel(logging.DEBUG)
+
+ ifcopenshell.validate.validate(ifc_file, logger, express_rules=True)
+ if use_json:
+ if logger.statements:
+ from pprint import pprint
+
+ pprint(logger.statements)
+ else:
+ print("IFC is completely valid.")
+
+
class TestAssignReference(test.bootstrap.IFC4):
def test_assigning_a_reference(self):
reference = self.file.createIfcLibraryReference()
product = self.file.createIfcWall()
product2 = self.file.createIfcWall()
- ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference)
- assert reference.LibraryRefForObjects[0].RelatedObjects == (product,)
- ifcopenshell.api.run("library.assign_reference", self.file, product=product2, reference=reference)
- assert reference.LibraryRefForObjects[0].RelatedObjects == (product, product2)
+ product3 = self.file.createIfcWall()
+ ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference)
+ rel = self.file.by_type("IfcRelAssociatesLibrary")[0]
+ assert rel.RelatedObjects == (product,)
+ ifcopenshell.api.run("library.assign_reference", self.file, products=[product2, product3], reference=reference)
+ assert set(rel.RelatedObjects) == set((product, product2, product3))
def test_not_assigning_twice(self):
reference = self.file.createIfcLibraryReference()
product = self.file.createIfcWall()
- ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference)
- ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference)
- assert reference.LibraryRefForObjects[0].RelatedObjects == (product,)
-
-
-class TestAssignReferenceIFC2X3(test.bootstrap.IFC2X3):
- def test_assigning_a_reference(self):
- reference = self.file.createIfcLibraryReference()
- product = self.file.createIfcWall()
- product2 = self.file.createIfcWall()
- ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference)
+ ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference)
+ ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference)
rel = self.file.by_type("IfcRelAssociatesLibrary")[0]
assert rel.RelatedObjects == (product,)
- ifcopenshell.api.run("library.assign_reference", self.file, product=product2, reference=reference)
- assert rel.RelatedObjects == (product, product2)
- def test_not_assigning_twice(self):
- reference = self.file.createIfcLibraryReference()
- product = self.file.createIfcWall()
- ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference)
- ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference)
- rel = self.file.by_type("IfcRelAssociatesLibrary")[0]
- assert rel.RelatedObjects == (product,)
+
+class TestAssignReferenceIFC2X3(test.bootstrap.IFC2X3, TestAssignReference):
+ pass
diff --git a/src/ifcopenshell-python/test/api/library/test_remove_reference.py b/src/ifcopenshell-python/test/api/library/test_remove_reference.py
index 26a9106411..a98ed2f083 100644
--- a/src/ifcopenshell-python/test/api/library/test_remove_reference.py
+++ b/src/ifcopenshell-python/test/api/library/test_remove_reference.py
@@ -24,7 +24,7 @@ class TestRemoveReference(test.bootstrap.IFC4):
def test_removing_a_reference(self):
reference = self.file.createIfcLibraryReference()
product = self.file.createIfcWall()
- ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference)
+ ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference)
ifcopenshell.api.run("library.remove_reference", self.file, reference=reference)
assert len(self.file.by_type("IfcLibraryReference")) == 0
assert len(self.file.by_type("IfcRelAssociatesLibrary")) == 0
diff --git a/src/ifcopenshell-python/test/api/library/test_unassign_reference.py b/src/ifcopenshell-python/test/api/library/test_unassign_reference.py
index 900dcdcc12..8962bb686a 100644
--- a/src/ifcopenshell-python/test/api/library/test_unassign_reference.py
+++ b/src/ifcopenshell-python/test/api/library/test_unassign_reference.py
@@ -18,12 +18,20 @@
import test.bootstrap
import ifcopenshell.api
+import ifcopenshell.util.element
class TestUnassignReference(test.bootstrap.IFC4):
def test_unassigning_a_reference(self):
reference = self.file.createIfcLibraryReference()
- product = self.file.createIfcWall()
- ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference)
- ifcopenshell.api.run("library.unassign_reference", self.file, product=product, reference=reference)
+ products = [self.file.createIfcWall() for i in range(3)]
+ ifcopenshell.api.run("library.assign_reference", self.file, products=products, reference=reference)
+ ifcopenshell.api.run("library.unassign_reference", self.file, products=products[:1], reference=reference)
+ assert ifcopenshell.util.element.get_referenced_elements(reference) == set(products[1:])
+
+ ifcopenshell.api.run("library.unassign_reference", self.file, products=products[1:], reference=reference)
+ assert ifcopenshell.util.element.get_referenced_elements(reference) == set()
assert len(self.file.by_type("IfcRelAssociatesLibrary")) == 0
+
+class TestUnassignReferenceIFC2X3(test.bootstrap.IFC2X3, TestUnassignReference):
+ pass
diff --git a/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py b/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py
index 1cf2f3108e..474f8bbcaf 100644
--- a/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py
+++ b/src/ifcopenshell-python/test/api/spatial/test_dereference_structure.py
@@ -26,28 +26,42 @@ class TestDereferenceStructure(test.bootstrap.IFC4):
def test_removing_a_container(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding")
subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
- ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element)
- ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element)
+ subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "spatial.reference_structure", self.file, products=[subelement, subelement2], relating_structure=element
+ )
+ ifcopenshell.api.run(
+ "spatial.dereference_structure", self.file, products=[subelement, subelement2], relating_structure=element
+ )
assert ifcopenshell.util.element.get_referenced_structures(subelement) == []
+ assert len(self.file.by_type("IfcRelReferencedInSpatialStructure")) == 0
def test_doing_nothing_if_no_container(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding")
subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
- ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element)
+ subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "spatial.dereference_structure", self.file, products=[subelement, subelement2], relating_structure=element
+ )
assert ifcopenshell.util.element.get_referenced_structures(subelement) == []
+ assert ifcopenshell.util.element.get_referenced_structures(subelement2) == []
def test_updating_the_rel_when_a_reference_is_removed_with_multipled_elements(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding")
subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
- ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement1, relating_structure=element)
- ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement2, relating_structure=element)
- ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement1, relating_structure=element)
- assert self.file.by_type("IfcRelReferencedInSpatialStructure")[0].RelatedElements == (subelement2,)
+ subelement3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "spatial.reference_structure", self.file, products=[subelement1], relating_structure=element
+ )
+ ifcopenshell.api.run(
+ "spatial.reference_structure", self.file, products=[subelement2, subelement3], relating_structure=element
+ )
+ ifcopenshell.api.run(
+ "spatial.dereference_structure", self.file, products=[subelement1, subelement2], relating_structure=element
+ )
+ assert element.ReferencesElements[0].RelatedElements == (subelement3,)
- def test_deleting_the_rel_when_a_container_is_removed_with_no_elements(self):
- element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding")
- subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
- ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element)
- ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element)
- assert len(self.file.by_type("IfcRelReferencedInSpatialStructure")) == 0
+
+class TestDereferenceStructureIFC2X3(test.bootstrap.IFC2X3, TestDereferenceStructure):
+ pass
diff --git a/src/ifcopenshell-python/test/api/spatial/test_reference_structure.py b/src/ifcopenshell-python/test/api/spatial/test_reference_structure.py
index 21b6b0e8e8..0ef22d708a 100644
--- a/src/ifcopenshell-python/test/api/spatial/test_reference_structure.py
+++ b/src/ifcopenshell-python/test/api/spatial/test_reference_structure.py
@@ -26,25 +26,39 @@ class TestReferenceStructure(test.bootstrap.IFC4):
def test_referencing_a_structure(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding")
subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
- rel = ifcopenshell.api.run(
- "spatial.reference_structure", self.file, product=subelement, relating_structure=element
+ subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "spatial.reference_structure", self.file, products=[subelement, subelement2], relating_structure=element
)
- assert ifcopenshell.util.element.get_referenced_structures(subelement) == [element]
- assert rel.is_a("IfcRelReferencedInSpatialStructure")
+ assert ifcopenshell.util.element.get_structure_referenced_elements(element) == {subelement, subelement2}
def test_doing_nothing_if_the_structure_is_already_referenced(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding")
subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
- ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element)
+ subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "spatial.reference_structure", self.file, products=[subelement, subelement2], relating_structure=element
+ )
total_elements = len([e for e in self.file])
- ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement, relating_structure=element)
+ ifcopenshell.api.run(
+ "spatial.reference_structure", self.file, products=[subelement, subelement2], relating_structure=element
+ )
assert len([e for e in self.file]) == total_elements
def test_that_old_relationships_are_updated_if_they_still_contain_elements(self):
- element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding")
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding")
subelement1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "spatial.reference_structure", self.file, products=[subelement1], relating_structure=element
+ )
subelement2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
- ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement1, relating_structure=element1)
- ifcopenshell.api.run("spatial.reference_structure", self.file, product=subelement2, relating_structure=element1)
+ subelement3 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "spatial.reference_structure", self.file, products=[subelement2, subelement3], relating_structure=element
+ )
rel = subelement1.ReferencedInStructures[0]
- assert len(rel.RelatedElements) == 2
+ assert len(rel.RelatedElements) == 3
+
+
+class TestReferenceStructureIFC2X3(test.bootstrap.IFC2X3, TestReferenceStructure):
+ pass
diff --git a/src/ifcopenshell-python/test/api/test_api.py b/src/ifcopenshell-python/test/api/test_api.py
index 779d1238f2..72d20392f3 100644
--- a/src/ifcopenshell-python/test/api/test_api.py
+++ b/src/ifcopenshell-python/test/api/test_api.py
@@ -18,6 +18,8 @@
import test.bootstrap
import ifcopenshell.api
+import ifcopenshell.util.classification
+import ifcopenshell.util.constraint
import ifcopenshell.util.element
import ifcopenshell.util.system
from datetime import datetime
@@ -186,3 +188,126 @@ class TestTemporarySupportForDeprecatedAPIArguments(test.bootstrap.IFC4):
assert len(self.file.by_type("IfcRelAssociatesMaterial")) == 0
assert len(self.file.by_type("IfcWall")) == 1
assert len(self.file.by_type("IfcMaterial")) == 1
+
+ @deprecation_check
+ def test_adding_a_reference(self):
+ ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name")
+ ifcopenshell.api.run(
+ "classification.add_reference",
+ self.file,
+ product=element,
+ identification="X",
+ name="Foobar",
+ classification=result,
+ )
+ references = list(ifcopenshell.util.classification.get_references(element))
+ assert len(references) == 1
+ assert references[0].Identification == "X"
+ assert references[0].Name == "Foobar"
+ assert references[0].ReferencedSource == self.file.by_type("IfcClassification")[0]
+
+ element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "classification.add_reference",
+ self.file,
+ product=element2,
+ identification="X",
+ name="Foobar",
+ classification=result,
+ )
+ assert list(ifcopenshell.util.classification.get_references(element2))[0].Identification == "X"
+ assert list(ifcopenshell.util.classification.get_references(element2))[0].Name == "Foobar"
+ assert list(ifcopenshell.util.classification.get_references(element2))[0] == references[0]
+
+ @deprecation_check
+ def test_removing_a_reference(self):
+ ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name")
+ reference = ifcopenshell.api.run(
+ "classification.add_reference",
+ self.file,
+ products=[element],
+ identification="X",
+ name="Foobar",
+ classification=result,
+ )
+ ifcopenshell.api.run("classification.remove_reference", self.file, product=element, reference=reference)
+ assert len(ifcopenshell.util.classification.get_references(element)) == 0
+ assert len(self.file.by_type("IfcClassificationReference")) == 0
+
+ @deprecation_check
+ def test_assigning_a_reference(self):
+ reference = self.file.createIfcLibraryReference()
+ product = self.file.createIfcWall()
+ product2 = self.file.createIfcWall()
+ ifcopenshell.api.run("library.assign_reference", self.file, product=product, reference=reference)
+ assert reference.LibraryRefForObjects[0].RelatedObjects == (product,)
+ ifcopenshell.api.run("library.assign_reference", self.file, product=product2, reference=reference)
+ assert set(reference.LibraryRefForObjects[0].RelatedObjects) == set((product, product2))
+
+ @deprecation_check
+ def test_unassigning_a_reference(self):
+ reference = self.file.createIfcLibraryReference()
+ product = self.file.createIfcWall()
+ ifcopenshell.api.run("library.assign_reference", self.file, products=[product], reference=reference)
+ ifcopenshell.api.run("library.unassign_reference", self.file, product=product, reference=reference)
+ assert len(self.file.by_type("IfcRelAssociatesLibrary")) == 0
+
+ @deprecation_check
+ def test_assigning_a_document(self):
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ reference = ifcopenshell.api.run("document.add_reference", self.file, information=None)
+ ifcopenshell.api.run("document.assign_document", self.file, product=element, document=reference)
+ assert element.HasAssociations[0].RelatingDocument == reference
+
+ @deprecation_check
+ def test_unassigning_a_document(self):
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ reference = ifcopenshell.api.run("document.add_reference", self.file, information=None)
+ ifcopenshell.api.run("document.assign_document", self.file, products=[element], document=reference)
+ ifcopenshell.api.run("document.unassign_document", self.file, product=element, document=reference)
+ assert not element.HasAssociations
+ assert not len(self.file.by_type("IfcRelAssociatesDocument"))
+
+ @deprecation_check
+ def test_referencing_a_structure(self):
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding")
+ subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ rel = ifcopenshell.api.run(
+ "spatial.reference_structure", self.file, product=subelement, relating_structure=element
+ )
+ assert ifcopenshell.util.element.get_referenced_structures(subelement) == [element]
+ assert rel.is_a("IfcRelReferencedInSpatialStructure")
+
+ @deprecation_check
+ def test_removing_a_container(self):
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding")
+ subelement = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "spatial.reference_structure", self.file, products=[subelement], relating_structure=element
+ )
+ ifcopenshell.api.run("spatial.dereference_structure", self.file, product=subelement, relating_structure=element)
+ assert ifcopenshell.util.element.get_referenced_structures(subelement) == []
+
+ @deprecation_check
+ def test_assign_a_constraint(self):
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ constraint = ifcopenshell.api.run("constraint.add_objective", self.file)
+ ifcopenshell.api.run("constraint.assign_constraint", self.file, product=element, constraint=constraint)
+ assert ifcopenshell.util.constraint.get_constrained_elements(constraint) == {element}
+
+ @deprecation_check
+ def test_unassigning_a_constraint(self):
+ constraint = ifcopenshell.api.run("constraint.add_objective", self.file)
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run(
+ "constraint.assign_constraint", self.file, product=element, constraint=constraint
+ )
+ ifcopenshell.api.run(
+ "constraint.unassign_constraint", self.file, product=element, constraint=constraint
+ )
+ assert ifcopenshell.util.constraint.get_constrained_elements(element) == set()
+ assert len(self.file.by_type("IfcRelAssociatesConstraint")) == 0
diff --git a/src/ifcopenshell-python/test/util/test_classification.py b/src/ifcopenshell-python/test/util/test_classification.py
index d394f8056b..7fe054e1e3 100644
--- a/src/ifcopenshell-python/test/util/test_classification.py
+++ b/src/ifcopenshell-python/test/util/test_classification.py
@@ -34,14 +34,14 @@ class TestGetReferences(test.bootstrap.IFC4):
ifcopenshell.api.run(
"classification.add_reference",
self.file,
- product=element,
+ products=[element],
reference=reference1,
classification=classification,
)
ifcopenshell.api.run(
"classification.add_reference",
self.file,
- product=element,
+ products=[element],
reference=reference2,
classification=classification,
)
@@ -54,7 +54,7 @@ class TestGetReferences(test.bootstrap.IFC4):
ifcopenshell.api.run(
"classification.add_reference",
self.file,
- product=element,
+ products=[element],
identification="X",
name="Foobar",
classification=result,
@@ -74,14 +74,14 @@ class TestGetReferences(test.bootstrap.IFC4):
ifcopenshell.api.run(
"classification.add_reference",
self.file,
- product=element,
+ products=[element],
reference=reference1,
classification=classification,
)
ifcopenshell.api.run(
"classification.add_reference",
self.file,
- product=element_type,
+ products=[element_type],
reference=reference2,
classification=classification,
)
@@ -103,14 +103,14 @@ class TestGetReferences(test.bootstrap.IFC4):
ifcopenshell.api.run(
"classification.add_reference",
self.file,
- product=element,
+ products=[element],
reference=reference1,
classification=classification,
)
ifcopenshell.api.run(
"classification.add_reference",
self.file,
- product=element_type,
+ products=[element_type],
reference=reference2,
classification=classification,
)
diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py
index c8a63e299d..ccb548a076 100644
--- a/src/ifcopenshell-python/test/util/test_element.py
+++ b/src/ifcopenshell-python/test/util/test_element.py
@@ -291,6 +291,16 @@ class TestGetPredefinedTypeIFC4(test.bootstrap.IFC4):
element_type.ProcessType = "FOOBAR"
assert subject.get_predefined_type(element) == "FOOBAR"
+ def test_getting_an_element_type_predefined_type(self):
+ element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType")
+ element_type.PredefinedType = "PARTITIONING"
+ assert subject.get_predefined_type(element_type) == "PARTITIONING"
+
+ def test_getting_an_element_type_null_predefined_type(self):
+ element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType")
+ element_type.PredefinedType = "NOTDEFINED"
+ assert subject.get_predefined_type(element_type) == "NOTDEFINED"
+
class TestGetTypeIFC4(test.bootstrap.IFC4):
def test_getting_the_type_of_a_product(self):
@@ -352,12 +362,16 @@ class TestGetMaterial(test.bootstrap.IFC4):
def test_getting_a_material_layer_set_of_a_product(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
- rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet")
+ rel = ifcopenshell.api.run(
+ "material.assign_material", self.file, products=[element], type="IfcMaterialLayerSet"
+ )
assert subject.get_material(element) == rel.RelatingMaterial
def test_getting_a_material_profile_set_of_a_product(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
- rel = ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialProfileSet")
+ rel = ifcopenshell.api.run(
+ "material.assign_material", self.file, products=[element], type="IfcMaterialProfileSet"
+ )
assert subject.get_material(element) == rel.RelatingMaterial
def test_getting_a_material_layer_set_usage_of_a_product(self):
@@ -512,7 +526,9 @@ class TestGetElementsByMaterial(test.bootstrap.IFC4):
material_set = ifcopenshell.api.run("material.add_material_set", self.file, set_type="IfcMaterialProfileSet")
ifcopenshell.api.run("material.add_profile", self.file, profile_set=material_set, material=material)
ifcopenshell.api.run("material.assign_material", self.file, products=[element_type], material=material_set)
- ifcopenshell.api.run("material.assign_material", self.file, products=[element], type="IfcMaterialProfileSetUsage")
+ ifcopenshell.api.run(
+ "material.assign_material", self.file, products=[element], type="IfcMaterialProfileSetUsage"
+ )
usage = self.file.by_type("IfcMaterialProfileSetUsage")[0]
assert subject.get_elements_by_material(self.file, material) == {element, element_type}
assert subject.get_elements_by_material(self.file, material_set) == {element, element_type}
@@ -703,13 +719,33 @@ class TestGetReferencedStructures(test.bootstrap.IFC4):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
assert subject.get_referenced_structures(element) == []
building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding")
- ifcopenshell.api.run("spatial.reference_structure", self.file, product=element, relating_structure=building)
+ ifcopenshell.api.run("spatial.reference_structure", self.file, products=[element], relating_structure=building)
assert subject.get_referenced_structures(element) == [building]
building2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding")
- ifcopenshell.api.run("spatial.reference_structure", self.file, product=element, relating_structure=building2)
+ ifcopenshell.api.run("spatial.reference_structure", self.file, products=[element], relating_structure=building2)
assert subject.get_referenced_structures(element) == [building, building2]
+class TestGetReferencedStructuresIFC2X3(test.bootstrap.IFC2X3, TestGetReferencedStructures):
+ pass
+
+
+class TestGetStructureReferencedElements(test.bootstrap.IFC4):
+ def test_getting_references_of_an_element(self):
+ building = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcBuilding")
+ assert subject.get_structure_referenced_elements(building) == set()
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run("spatial.reference_structure", self.file, products=[element], relating_structure=building)
+ assert subject.get_structure_referenced_elements(building) == {element}
+ element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ ifcopenshell.api.run("spatial.reference_structure", self.file, products=[element2], relating_structure=building)
+ assert subject.get_structure_referenced_elements(building) == {element, element2}
+
+
+class TestGetStructureReferencedElementsIFC2X3(test.bootstrap.IFC2X3, TestGetStructureReferencedElements):
+ pass
+
+
class TestGetDecompositionIFC4(test.bootstrap.IFC4):
def test_getting_decomposed_subelements_of_an_element(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcElementAssembly")
@@ -769,6 +805,50 @@ class TestGetNestIFC2X3(test.bootstrap.IFC2X3, TestGetNestIFC4):
pass
+class TestGetReferencedElements(test.bootstrap.IFC4):
+ # TODO: test other references:
+ # IfcExternallyDefinedHatchStyle
+ # IfcExternallyDefinedSurfaceStyle
+ # IfcExternallyDefinedTextFont
+ def test_get_elements_referenced_by_classification_reference(self):
+ ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
+ elements = [ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")]
+ if self.file.schema != "IFC2X3":
+ elements.append(self.file.create_entity("IfcCostValue"))
+ result = ifcopenshell.api.run("classification.add_classification", self.file, classification="Name")
+ reference = ifcopenshell.api.run(
+ "classification.add_reference",
+ self.file,
+ products=elements,
+ identification="X",
+ name="Foobar",
+ classification=result,
+ )
+ assert subject.get_referenced_elements(reference) == set(elements)
+
+ def test_get_elements_referenced_by_library_reference(self):
+ reference = self.file.createIfcLibraryReference()
+ elements = [
+ ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"),
+ ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"),
+ ]
+ ifcopenshell.api.run("library.assign_reference", self.file, reference=reference, products=elements)
+ assert subject.get_referenced_elements(reference) == set(elements)
+
+ def test_get_elements_referenced_by_document_reference(self):
+ reference = ifcopenshell.api.run("document.add_reference", self.file, information=None)
+ elements = [
+ ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"),
+ ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall"),
+ ]
+ ifcopenshell.api.run("document.assign_document", self.file, document=reference, products=elements)
+ assert subject.get_referenced_elements(reference) == set(elements)
+
+
+class TestGetReferencedElementsIFC2X3(test.bootstrap.IFC2X3, TestGetReferencedElements):
+ pass
+
+
class TestReplaceAttributeIFC4(test.bootstrap.IFC4):
def test_replacing_an_elements_attribute(self):
element = self.file.createIfcWall("foo")
diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py
index d8ba4bb174..571511d39f 100644
--- a/src/ifcopenshell-python/test/util/test_selector.py
+++ b/src/ifcopenshell-python/test/util/test_selector.py
@@ -225,7 +225,7 @@ class TestFilterElements(test.bootstrap.IFC4):
ifcopenshell.api.run(
"classification.add_reference",
self.file,
- product=element,
+ products=[element],
identification="X",
name="Foobar",
classification=result,
diff --git a/src/ifcsverchok/README.md b/src/ifcsverchok/README.md
index cdc605f17b..e2c5944dfa 100644
--- a/src/ifcsverchok/README.md
+++ b/src/ifcsverchok/README.md
@@ -3,9 +3,9 @@
N.B.! IfcSverchok nodes are WIP. You can experience Blender crashes while using them, especially if you're undoing (ctrl/cmd-Z) something in the node tree.
-## Packaged installation[](https://blenderbim.org/docs-python/ifcsverchok/installation.html#packaged-installation "Permalink to this headline")
+## Packaged installation[](https://docs.ifcopenshell.org/ifcsverchok/installation.html#packaged-installation "Permalink to this headline")
-IfcSverchok is packaged like a regular Blender add-on, so installation is the same as any other Blender add-on. [Download IfcSverchok here](https://blenderbim.org/builds/ifcsverchok-230823.zip).
+IfcSverchok is packaged like a regular Blender add-on, so installation is the same as any other Blender add-on. [Download IfcSverchok here](https://github.com/IfcOpenShell/IfcOpenShell/releases/download/ifcsverchok-240417/ifcsverchok-240417.zip).
Like all Blender add-ons, they can be installed using `Edit > Preferences > Addons > Install > Choose Downloaded ZIP > Enable Add-on Checkbox`. You can enable add-ons permanently by using `Save User Settings` from the Addons menu.
diff --git a/src/ifcsverchok/__init__.py b/src/ifcsverchok/__init__.py
index 66a9563c29..8b45c3f712 100644
--- a/src/ifcsverchok/__init__.py
+++ b/src/ifcsverchok/__init__.py
@@ -32,6 +32,26 @@ import importlib
import logging
logger = logging.getLogger("sverchok.ifc")
+
+
+def ensure_addons_are_enabled(*addon_names: str) -> None:
+ errors = []
+ for addon_name in addon_names:
+ try:
+ module = importlib.import_module(addon_name)
+ # `__addon_enabled__` is not present if addon wasn't enabled before
+ if not getattr(module, "__addon_enabled__", False):
+ errors.append(f"- Addon {addon_name} appears to be disabled, it should be enabled before IFC Sverchok.")
+ except ModuleNotFoundError:
+ errors.append(f"- Addon {addon_name} is not installed.")
+
+ if errors:
+ raise Exception("Some issues were found trying to enable IFC Sverchok:\n" + "\n".join(errors))
+
+
+ensure_addons_are_enabled("blenderbim", "sverchok")
+
+
from sverchok.ui.nodeview_space_menu import add_node_menu
@@ -69,36 +89,15 @@ def nodes_index():
]
-node_categories = [
- {
- "IFC": [
- "SvIfcCreateFile",
- "SvIfcReadFile",
- "SvIfcWriteFile",
- "SvIfcCreateEntity",
- "SvIfcCreateShape",
- "SvIfcReadEntity",
- "SvIfcPickIfcClass",
- "SvIfcById",
- "SvIfcByGuid",
- "SvIfcByType",
- "SvIfcByQuery",
- "SvIfcAdd",
- "SvIfcAddPset",
- "SvIfcAddSpatialElement",
- "SvIfcRemove",
- "SvIfcGenerateGuid",
- "SvIfcGetProperty",
- "SvIfcGetAttribute",
- "SvIfcSelectBlenderObjects",
- "SvIfcApi",
- "SvIfcBMeshToIfcRepr",
- "SvIfcSverchokToIfcRepr",
- "SvIfcCreateProject",
- "SvIfcQuickProjectSetup",
- ]
- }
-]
+def make_node_categories() -> list[dict[str, list[str]]]:
+ node_categories = [{}]
+ for category, nodes in nodes_index():
+ nodes = [node_name for idname, node_name in nodes]
+ node_categories[0][category] = nodes
+ return node_categories
+
+
+node_categories = make_node_categories()
def make_node_list():
diff --git a/src/ifctester/README.md b/src/ifctester/README.md
index 5b3487675e..f145f00804 100644
--- a/src/ifctester/README.md
+++ b/src/ifctester/README.md
@@ -4,6 +4,23 @@ With **IfcTester**, you can author and read **Information Delivery Specification
## How to use it
+### Command line use
+
+.. code-block:: bash
+
+ # run console reporter
+ python -m ifctester example.ids example.ifc
+ python -m ifctester example.ids example.ifc -r Html -o report.html
+
+Available flags:
+
+- ``-r`` / ``--reporter``: The reporting method to view audit results. Availabe reporters: Console, Txt, Json, Html, Ods, Bcf
+- ``--no-color``: Disable colour output (supported by Console reporting).
+- ``--excel-safe``: Make sure exported ODS is safely exported for Excel.
+- ``-o`` / ``--output``: Output file (supported for all types of reporting except Console).
+
+### Code example
+
```python
import ifcopenshell
from ifctester import ids, reporter
diff --git a/src/ifctester/ifctester/__main__.py b/src/ifctester/ifctester/__main__.py
index c5c70cd9c2..a49c4c1094 100644
--- a/src/ifctester/ifctester/__main__.py
+++ b/src/ifctester/ifctester/__main__.py
@@ -31,10 +31,13 @@ parser.add_argument(
"-r", "--reporter", type=str, help="The reporting method to view audit results", default="Console"
)
parser.add_argument(
- "--no-color", help="Disable colour output supported by Console reporting", action="store_true"
+ "--no-color", help="Disable colour output (supported by Console reporting)", action="store_true"
)
parser.add_argument(
- "-o", "--output", help="Output file supported by Json reporting"
+ "--excel-safe", help="Make sure exported ODS is safely exported for Excel", action="store_true"
+)
+parser.add_argument(
+ "-o", "--output", help="Output file (supported for all types of reporting except Console)"
)
args = parser.parse_args()
@@ -56,7 +59,7 @@ elif args.reporter == "Json":
elif args.reporter == "Html":
engine = reporter.Html(specs)
elif args.reporter == "Ods":
- engine = reporter.Ods(specs)
+ engine = reporter.Ods(specs, excel_safe=args.excel_safe)
elif args.reporter == "Bcf":
engine = reporter.Bcf(specs)
diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py
index 2048d1b307..867e06b36c 100644
--- a/src/ifctester/ifctester/facet.py
+++ b/src/ifctester/ifctester/facet.py
@@ -24,7 +24,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.classification
from functools import lru_cache
from xmlschema.validators import identities
-from typing import Union, Optional, Any, Literal, TYPE_CHECKING
+from typing import Union, Optional, Any, Literal, TYPE_CHECKING, TypedDict
from logging import Logger
if TYPE_CHECKING:
@@ -61,12 +61,17 @@ def get_psets(element):
Cardinality = Literal["required", "optional", "prohibited"]
+class FacetFailure(TypedDict):
+ element: ifcopenshell.entity_instance
+ reason: str
+
+
class Facet:
cardinality: Cardinality
def __init__(self, *parameters):
self.status = None
- self.failures = []
+ self.failures: list[FacetFailure] = []
for i, name in enumerate(self.parameters):
setattr(self, name.replace("@", ""), parameters[i])
@@ -105,7 +110,7 @@ class Facet:
clause_type: str,
specification: Optional[Specification] = None,
requirement: Optional[Facet] = None,
- ):
+ ) -> str:
if clause_type == "applicability":
templates = self.applicability_templates
elif clause_type == "requirement":
diff --git a/src/ifctester/ifctester/ids.py b/src/ifctester/ifctester/ids.py
index da71c87c96..4703b0445a 100644
--- a/src/ifctester/ifctester/ids.py
+++ b/src/ifctester/ifctester/ids.py
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcTester. If not, see .
+from __future__ import annotations
import os
import datetime
import ifcopenshell
@@ -34,14 +35,19 @@ from .facet import (
get_pset,
get_psets,
Cardinality,
+ FacetFailure,
)
-from typing import List, Optional, Union
+from typing import List, Optional, Union, overload, Literal
cwd = os.path.dirname(os.path.realpath(__file__))
schema = None
-def open(filepath, validate=False):
+@overload
+def open(filepath: str, validate: Literal[False] = False) -> Ids: ...
+@overload
+def open(filepath: str, validate: Literal[True]) -> None: ...
+def open(filepath: str, validate=False) -> Union[Ids, None]:
if validate:
get_schema().validate(filepath)
return Ids().parse(
@@ -265,11 +271,11 @@ class Specification:
if self.maxOccurs != 0: # This is a required or optional specification
if not is_pass:
self.failed_entities.add(element)
- facet.failures.append({"element": element, "reason": str(result)})
+ facet.failures.append(FacetFailure(element=element, reason=str(result)))
else: # This is a prohibited specification
if is_pass:
self.failed_entities.add(element)
- facet.failures.append({"element": element, "reason": str(result)})
+ facet.failures.append(FacetFailure(element=element, reason=str(result)))
self.status = True
for facet in self.requirements:
diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py
index ea77e6a35f..90f7fd1c36 100644
--- a/src/ifctester/ifctester/reporter.py
+++ b/src/ifctester/ifctester/reporter.py
@@ -16,7 +16,9 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcTester. If not, see .
+from __future__ import annotations
import os
+import re
import sys
import math
import logging
@@ -24,12 +26,15 @@ import datetime
import ifcopenshell
import ifcopenshell.util.unit
import ifcopenshell.util.element
+from .ids import Specification, Ids
+from .facet import Facet, FacetFailure
+from typing import TypedDict, Union, Literal, Optional
cwd = os.path.dirname(os.path.realpath(__file__))
class Reporter:
- def __init__(self, ids):
+ def __init__(self, ids: Ids):
self.ids = ids
def report(self, ids):
@@ -42,8 +47,79 @@ class Reporter:
pass
+ResultsPercent = Union[int, Literal["N/A"]]
+
+
+class Results(TypedDict):
+ title: str
+ date: str
+ filepath: str
+ filename: str
+ specifications: list[ResultsSpecification]
+ status: bool
+ total_specifications: int
+ total_specifications: int
+ total_specifications_pass: int
+ total_specifications_fail: int
+ percent_specifications_pass: ResultsPercent
+ total_requirements: int
+ total_requirements_pass: int
+ total_requirements_fail: int
+ percent_requirements_pass: ResultsPercent
+ total_checks: int
+ total_checks_pass: int
+ total_checks_fail: int
+ percent_checks_pass: ResultsPercent
+
+
+class ResultsSpecification(TypedDict):
+ name: str
+ description: str
+ instructions: str
+ status: bool
+ total_applicable: int
+ total_applicable_pass: int
+ total_applicable_fail: int
+ percent_applicable_pass: ResultsPercent
+ total_checks: int
+ total_checks_pass: int
+ total_checks_fail: int
+ percent_checks_pass: ResultsPercent
+ required: bool
+ applicability: list[str]
+ requirements: list[ResultsRequirement]
+
+
+class ResultsRequirement(TypedDict):
+ description: str
+ status: bool
+ failed_entities: list[ResultsFailedEntity]
+ total_applicable: int
+ total_pass: int
+ total_fail: int
+ percent_pass: ResultsPercent
+
+
+# use different syntax because of the "class" key
+ResultsFailedEntity = TypedDict(
+ "ResultsFailedEntity",
+ {
+ "reason": str,
+ "element": str,
+ "element_type": str,
+ "class": str,
+ "predefined_type": str,
+ "name": Union[str, None],
+ "description": Union[str, None],
+ "id": int,
+ "global_id": Union[str, None],
+ "tag": Union[str, None],
+ },
+)
+
+
class Console(Reporter):
- def __init__(self, ids, use_colour=True):
+ def __init__(self, ids: Ids, use_colour=True):
super().__init__(ids)
self.use_colour = use_colour
self.colours = {
@@ -59,14 +135,14 @@ class Console(Reporter):
"reverse": "\033[;7m",
}
- def report(self):
+ def report(self) -> None:
self.set_style("bold", "blue")
self.print(self.ids.info.get("title", "Untitled IDS"))
for specification in self.ids.specifications:
self.report_specification(specification)
self.set_style("reset")
- def report_specification(self, specification):
+ def report_specification(self, specification: Specification) -> None:
if specification.status is True:
self.set_style("bold", "green")
self.print("[PASS] ", end="")
@@ -113,7 +189,7 @@ class Console(Reporter):
self.print(" " * 12 + f"... {len(requirement.failures)} in total ...")
self.set_style("reset")
- def report_reason(self, failure):
+ def report_reason(self, failure: FacetFailure) -> None:
is_bold = False
for substring in failure["reason"].split('"'):
if is_bold:
@@ -126,11 +202,11 @@ class Console(Reporter):
self.print(" - " + str(failure["element"]))
self.set_style("reset")
- def set_style(self, *colours):
+ def set_style(self, *colours: str):
if self.use_colour:
sys.stdout.write("".join([self.colours[c] for c in colours]))
- def print(self, txt, end=None):
+ def print(self, txt: str, end: Optional[str] = None):
if end is not None:
print(txt, end=end)
else:
@@ -138,14 +214,14 @@ class Console(Reporter):
class Txt(Console):
- def __init__(self, ids):
+ def __init__(self, ids: Ids):
super().__init__(ids, use_colour=False)
self.text = ""
- def print(self, txt, end=None):
- self.text += txt + "\n" if end is None else txt
+ def print(self, txt: str, end: Optional[str] = None):
+ self.text += txt + "\n" if end is None else end
- def to_string(self):
+ def to_string(self) -> None:
print(self.text)
def to_file(self, filepath: str) -> None:
@@ -154,11 +230,11 @@ class Txt(Console):
class Json(Reporter):
- def __init__(self, ids):
+ def __init__(self, ids: Ids):
super().__init__(ids)
- self.results = {}
+ self.results = Results()
- def report(self):
+ def report(self) -> Results:
self.results["title"] = self.ids.info.get("title", "Untitled IDS")
self.results["date"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.results["filepath"] = self.ids.filepath
@@ -203,7 +279,7 @@ class Json(Reporter):
)
return self.results
- def report_specification(self, specification):
+ def report_specification(self, specification: Specification) -> ResultsSpecification:
applicability = [a.to_string("applicability") for a in specification.applicability]
total_applicable = len(specification.applicable_entities)
total_checks = 0
@@ -216,57 +292,60 @@ class Json(Reporter):
total_checks += total_applicable
total_checks_pass += total_pass
requirements.append(
- {
- "description": requirement.to_string("requirement", specification, requirement),
- "status": requirement.status,
- "failed_entities": self.report_failed_entities(requirement),
- "total_applicable": total_applicable,
- "total_pass": total_pass,
- "total_fail": total_fail,
- "percent_pass": percent_pass,
- }
+ ResultsRequirement(
+ description=requirement.to_string("requirement", specification, requirement),
+ status=requirement.status,
+ failed_entities=self.report_failed_entities(requirement),
+ total_applicable=total_applicable,
+ total_pass=total_pass,
+ total_fail=total_fail,
+ percent_pass=percent_pass,
+ )
)
total_applicable_pass = total_applicable - len(specification.failed_entities)
percent_applicable_pass = (
math.floor((total_applicable_pass / total_applicable) * 100) if total_applicable else "N/A"
)
percent_checks_pass = math.floor((total_checks_pass / total_checks) * 100) if total_checks else "N/A"
- return {
- "name": specification.name,
- "description": specification.description,
- "instructions": specification.instructions,
- "status": specification.status,
- "total_applicable": total_applicable,
- "total_applicable_pass": total_applicable_pass,
- "total_applicable_fail": total_applicable - total_applicable_pass,
- "percent_applicable_pass": percent_applicable_pass,
- "total_checks": total_checks,
- "total_checks_pass": total_checks_pass,
- "total_checks_fail": total_checks - total_checks_pass,
- "percent_checks_pass": percent_checks_pass,
- "required": specification.minOccurs != 0,
- "applicability": applicability,
- "requirements": requirements,
- }
- def report_failed_entities(self, requirement):
+ return ResultsSpecification(
+ name=specification.name,
+ description=specification.description,
+ instructions=specification.instructions,
+ status=specification.status,
+ total_applicable=total_applicable,
+ total_applicable_pass=total_applicable_pass,
+ total_applicable_fail=total_applicable - total_applicable_pass,
+ percent_applicable_pass=percent_applicable_pass,
+ total_checks=total_checks,
+ total_checks_pass=total_checks_pass,
+ total_checks_fail=total_checks - total_checks_pass,
+ percent_checks_pass=percent_checks_pass,
+ required=specification.minOccurs != 0,
+ applicability=applicability,
+ requirements=requirements,
+ )
+
+ def report_failed_entities(self, requirement: Facet) -> list[ResultsFailedEntity]:
return [
- {
- "reason": f["reason"],
- "element": str(f["element"]),
- "element_type": str(ifcopenshell.util.element.get_type(f["element"])),
- "class": f["element"].is_a(),
- "predefined_type": ifcopenshell.util.element.get_predefined_type(f["element"]),
- "name": getattr(f["element"], "Name", None),
- "description": getattr(f["element"], "Description", None),
- "id": f["element"].id(),
- "global_id": getattr(f["element"], "GlobalId", None),
- "tag": getattr(f["element"], "Tag", None),
- }
+ ResultsFailedEntity(
+ {
+ "reason": f["reason"],
+ "element": str(f["element"]),
+ "element_type": str(ifcopenshell.util.element.get_type(f["element"])),
+ "class": f["element"].is_a(),
+ "predefined_type": ifcopenshell.util.element.get_predefined_type(f["element"]),
+ "name": getattr(f["element"], "Name", None),
+ "description": getattr(f["element"], "Description", None),
+ "id": f["element"].id(),
+ "global_id": getattr(f["element"], "GlobalId", None),
+ "tag": getattr(f["element"], "Tag", None),
+ }
+ )
for f in requirement.failures
]
- def to_string(self):
+ def to_string(self) -> str:
import json
return json.dumps(self.results)
@@ -279,11 +358,10 @@ class Json(Reporter):
class Html(Json):
- def __init__(self, ids):
+ def __init__(self, ids: Ids):
super().__init__(ids)
- self.results = {}
- def report(self):
+ def report(self) -> None:
super().report()
entity_limit = 100
for spec in self.results["specifications"]:
@@ -294,7 +372,7 @@ class Html(Json):
requirement["total_entities"] = total
requirement["total_omitted"] = total - entity_limit
- def to_string(self):
+ def to_string(self) -> str:
import pystache
with open(os.path.join(cwd, "templates", "report.html"), "r") as file:
@@ -309,15 +387,42 @@ class Html(Json):
class Ods(Json):
- def __init__(self, ids):
+ def __init__(self, ids: Ids, excel_safe=False):
super().__init__(ids)
+ self.excel_safe = excel_safe
self.colours = {
"h": "cccccc", # Header
"p": "97cc64", # Pass
"f": "fb5a3e", # Fail
"t": "ffffff", # Regular text
}
- self.results = {}
+
+ def excel_safe_spreadsheet_name(self, name: str) -> str:
+ if not self.excel_safe:
+ return name
+
+ warning = (
+ f'WARNING. Sheet name "{name}" is not valid for Excel and will be changed. '
+ "See: https://support.microsoft.com/en-us/office/rename-a-worksheet-3f1f7148-ee83-404d-8ef0-9ff99fbad1f9"
+ )
+
+ if not name or name == "History":
+ print(warning)
+ return "placeholder spreadsheet name"
+
+ if name.startswith("'") or name.endswith("'"):
+ print(warning)
+ name = name.strip("'")
+
+ pattern = r"[\\\/\?\*\:\[\]]"
+ if re.search(pattern, name):
+ name = re.sub(pattern, "", name)
+ print(warning)
+
+ if len(name) > 31:
+ name = name[:31]
+ print(warning)
+ return name
def to_file(self, filepath: str) -> None:
from odf.opendocument import OpenDocumentSpreadsheet
@@ -334,7 +439,7 @@ class Ods(Json):
self.doc.automaticstyles.addElement(style)
self.cell_formats[key] = style
- table = Table(name=self.results["title"])
+ table = Table(name=self.excel_safe_spreadsheet_name(self.results["title"]))
tr = TableRow()
for header in ["Specification", "Status", "Total Pass", "Total Checks", "Percentage Pass"]:
tc = TableCell(valuetype="string", stylename="h")
@@ -371,7 +476,7 @@ class Ods(Json):
for specification in self.results["specifications"]:
if specification["status"]:
continue
- table = Table(name=specification["name"])
+ table = Table(name=self.excel_safe_spreadsheet_name(specification["name"]))
tr = TableRow()
for header in [
"Requirement",
@@ -419,12 +524,12 @@ class Ods(Json):
table.addElement(tr)
self.doc.spreadsheet.addElement(table)
- self.doc.save(filepath, addsuffix=filepath.lower().endswith(".ods"))
+ self.doc.save(filepath, addsuffix=not filepath.lower().endswith(".ods"))
class Bcf(Json):
- def report_failed_entities(self, requirement):
- return [{"reason": f["reason"], "element": f["element"]} for f in requirement.failures]
+ def report_failed_entities(self, requirement: Facet) -> list[FacetFailure]:
+ return [FacetFailure(f) for f in requirement.failures]
def to_file(self, filepath: str) -> None:
import numpy as np
diff --git a/src/ifctester/test/test_facet.py b/src/ifctester/test/test_facet.py
index bd89a00946..ddee91ab36 100644
--- a/src/ifctester/test/test_facet.py
+++ b/src/ifctester/test/test_facet.py
@@ -706,24 +706,24 @@ class TestClassification:
element0 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall")
element1 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcSlab")
ifcopenshell.api.run(
- "classification.add_reference", ifc, product=element1, reference=ref1, classification=system_a
+ "classification.add_reference", ifc, products=[element1], reference=ref1, classification=system_a
)
element11 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcColumn")
ifcopenshell.api.run(
- "classification.add_reference", ifc, product=element11, reference=ref11, classification=system_a
+ "classification.add_reference", ifc, products=[element11], reference=ref11, classification=system_a
)
element22 = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcBeam")
ifcopenshell.api.run(
"classification.add_reference",
ifc,
- product=element22,
+ products=[element22],
reference=ref22,
classification=system_a,
is_lightweight=False,
)
material = ifc.createIfcMaterial(Name="Material")
ifcopenshell.api.run(
- "classification.add_reference", ifc, product=material, reference=ref1, classification=system_a
+ "classification.add_reference", ifc, products=[material], reference=ref1, classification=system_a
)
facet = Classification(system="Foobar")
@@ -810,15 +810,15 @@ class TestClassification:
wall_type = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWallType")
ifcopenshell.api.run("type.assign_type", ifc, related_objects=[wall], relating_type=wall_type)
ifcopenshell.api.run(
- "classification.add_reference", ifc, product=wall, reference=ref11, classification=system_a
+ "classification.add_reference", ifc, products=[wall], reference=ref11, classification=system_a
)
ifcopenshell.api.run(
- "classification.add_reference", ifc, product=wall_type, reference=ref22, classification=system_a
+ "classification.add_reference", ifc, products=[wall_type], reference=ref22, classification=system_a
)
system_b = ifcopenshell.api.run("classification.add_classification", ifc, classification=system_b)
ifcopenshell.api.run(
- "classification.add_reference", ifc, product=wall_type, reference=refx, classification=system_b
+ "classification.add_reference", ifc, products=[wall_type], reference=refx, classification=system_b
)
facet = Classification(system="Foobar", value="11")
diff --git a/src/opencdeserver/README.md b/src/opencdeserver/README.md
index 2623000fbc..87d2fb8147 100644
--- a/src/opencdeserver/README.md
+++ b/src/opencdeserver/README.md
@@ -250,7 +250,7 @@ https://technical.buildingsmart.org/standards/ifc/ifc-schema-specifications/
https://www.sciencedirect.com/science/article/pii/S0926580523000389
- IFCOpenShell documentation
-https://blenderbim.org/docs-python/
+https://docs.ifcopenshell.org/
### Frontend