diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml
new file mode 100644
index 0000000000..57a4d8f707
--- /dev/null
+++ b/.github/workflows/build_all.yml
@@ -0,0 +1,21 @@
+name: Dispatch Build IfcOpenShell
+
+on:
+ workflow_dispatch:
+
+jobs:
+ trigger-workflows:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ workflow:
+ - 'Build IfcOpenShell Linux'
+ - 'Build IfcOpenShell Linux ARM'
+ - 'Build IfcOpenShell OSX'
+ - 'Build IfcOpenShell WASM / Pyodide'
+ - 'Build IfcOpenShell Windows'
+ steps:
+ - name: Trigger binary build workflows
+ uses: benc-uk/workflow-dispatch@v1
+ with:
+ workflow: ${{ matrix.workflow }}
diff --git a/.github/workflows/build_osx.yml b/.github/workflows/build_osx.yml
index d70cf0930c..4b968c8516 100644
--- a/.github/workflows/build_osx.yml
+++ b/.github/workflows/build_osx.yml
@@ -128,10 +128,8 @@ jobs:
#
# To force the link and overwrite all conflicting files:
# brew link --overwrite python@3.13
- brew link --overwrite python@3.12
- brew link --overwrite python@3.13
# https://github.com/rust-lang/rustup/pull/3989/files
- brew install --overwrite awscli
+ brew install --overwrite awscli | true
- name: Upload .zip archives to S3
run: |
diff --git a/nix/build-all.py b/nix/build-all.py
index cb821a5d6d..5f2f2f0a79 100644
--- a/nix/build-all.py
+++ b/nix/build-all.py
@@ -350,7 +350,7 @@ def git_clone_or_pull_repository(clone_url, target_dir, revision=None):
run([git, "clone", "--recursive", clone_url, target_dir])
else:
logger.info(f"directory '{target_dir}' already cloned. Pulling latest changes.")
- run([git, "-C", target_dir, "fetch", "--all", "--tags"])
+ run([git, "-C", target_dir, "fetch", "--all", "--tags", "--force"])
# detect whether we are on a branch and pull
if run([git, "rev-parse", "--abbrev-ref", "HEAD"], cwd=target_dir) != "HEAD":
diff --git a/src/bonsai/bonsai/bim/__init__.py b/src/bonsai/bonsai/bim/__init__.py
index 3755a8d12a..5985459e2a 100644
--- a/src/bonsai/bonsai/bim/__init__.py
+++ b/src/bonsai/bonsai/bim/__init__.py
@@ -115,10 +115,8 @@ classes = [
operator.ReloadIfcFile,
operator.RemoveIfcFile,
operator.RevertClippingPlaneCut,
- operator.SelectDataDir,
- operator.SelectCacheDir,
+ operator.SelectDir,
operator.SelectIfcFile,
- operator.SelectSchemaDir,
operator.SelectURIAttribute,
operator.SetTab,
operator.SwitchTab,
diff --git a/src/bonsai/bonsai/bim/export_ifc.py b/src/bonsai/bonsai/bim/export_ifc.py
index deafc5b3ff..84173a9ec5 100644
--- a/src/bonsai/bonsai/bim/export_ifc.py
+++ b/src/bonsai/bonsai/bim/export_ifc.py
@@ -132,7 +132,8 @@ class IfcExporter:
bpy.ops.bim.update_representation(obj=obj.name)
def has_changed_materials(self, obj: bpy.types.Object) -> bool:
- checksum = obj.data.BIMMeshProperties.material_checksum
+ mprops = tool.Geometry.get_mesh_props(obj.data)
+ checksum = mprops.material_checksum
return checksum != tool.Geometry.get_material_checksum(obj)
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py
index d65017243f..4ed12d74f0 100644
--- a/src/bonsai/bonsai/bim/handler.py
+++ b/src/bonsai/bonsai/bim/handler.py
@@ -60,7 +60,7 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
if props.is_renaming:
props.is_renmaing = False
return
- IfcStore.get_file().by_id(ifc_definition_id).Name = obj.name
+ tool.Ifc.get().by_id(ifc_definition_id).Name = obj.name
refresh_ui_data()
return
@@ -71,7 +71,7 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) -
obj.BIMObjectProperties.is_renaming = False
return
- element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id)
+ element = tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id)
if "/" in obj.name:
object_name = obj.name
element_name = obj.name.split("/", 1)[1]
@@ -221,35 +221,38 @@ def refresh_ui_data():
if isinstance(tool.Ifc.get(), ifcopenshell.sqlite):
tool.Ifc.get().clear_cache()
- bpy.context.scene.DocProperties.should_draw_decorations = bpy.context.scene.DocProperties.should_draw_decorations
+ props = tool.Drawing.get_document_props()
+ props.should_draw_decorations = props.should_draw_decorations
if bpy.context.scene.WebProperties.is_connected:
tool.Web.send_webui_data()
@persistent
-def loadIfcStore(scene):
+def loadIfcStore(scene: bpy.types.Scene) -> None:
IfcStore.purge()
refresh_ui_data()
- if not IfcStore.get_file():
+ if not tool.Ifc.get():
return
- IfcStore.get_schema()
+ tool.Ifc.schema()
IfcStore.relink_all_objects()
@persistent
-def undo_post(scene):
- if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction:
- IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
- IfcStore.undo(until_key=bpy.context.scene.BIMProperties.last_transaction)
+def undo_post(scene: bpy.types.Scene) -> None:
+ props = tool.Blender.get_bim_props()
+ if IfcStore.last_transaction != props.last_transaction:
+ IfcStore.last_transaction = props.last_transaction
+ IfcStore.undo(until_key=props.last_transaction)
refresh_ui_data()
tool.Ifc.rebuild_element_maps()
@persistent
-def redo_post(scene):
- if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction:
- IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction
- IfcStore.redo(until_key=bpy.context.scene.BIMProperties.last_transaction)
+def redo_post(scene: bpy.types.Scene) -> None:
+ props = tool.Blender.get_bim_props()
+ if IfcStore.last_transaction != props.last_transaction:
+ IfcStore.last_transaction = props.last_transaction
+ IfcStore.redo(until_key=props.last_transaction)
refresh_ui_data()
tool.Ifc.rebuild_element_maps()
@@ -282,7 +285,7 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None
return pao
-def viewport_shading_changed_callback(area):
+def viewport_shading_changed_callback(area: bpy.types.Area) -> None:
shading = area.spaces.active.shading.type
if shading == "RENDERED":
bpy.context.scene.BIMStylesProperties.active_style_type = "External"
@@ -340,10 +343,11 @@ def load_post(scene):
tool.Blender.setup_tabs()
if tool.Ifc.get() and bpy.data.is_saved:
- bpy.context.scene.BIMProperties.has_blend_warning = True
+ props = tool.Blender.get_bim_props()
+ props.has_blend_warning = True
# Bonsai overlays
- georeference_props = bpy.context.scene.BIMGeoreferenceProperties
+ georeference_props = tool.Georeference.get_georeference_props()
aggregate_props = bpy.context.scene.BIMAggregateProperties
nest_props = bpy.context.scene.BIMNestProperties
model_props = tool.Model.get_model_props()
diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py
index a8b25bca1a..3ac994739a 100644
--- a/src/bonsai/bonsai/bim/helper.py
+++ b/src/bonsai/bonsai/bim/helper.py
@@ -27,12 +27,12 @@ import ifcopenshell.util.element
import ifcopenshell.util.unit
from ifcopenshell.util.doc import get_attribute_doc, get_predefined_type_doc, get_property_doc
import bonsai.tool as tool
-from bonsai.bim.ifc import IfcStore
from typing import Optional, Callable, Any, Union, Iterable, TYPE_CHECKING
if TYPE_CHECKING:
import bonsai.bim.prop
from bonsai.bim.prop import Attribute
+ from bonsai.bim.module.search.prop import BIMFilterGroup
# ImportCallback return values:
# - None - property should be imported by default workflow
@@ -119,7 +119,8 @@ def import_attributes(
data: dict[str, Any],
callback: Optional[ImportCallback] = None,
) -> None:
- for attribute in IfcStore.get_schema().declaration_by_name(ifc_class).all_attributes():
+ schema = tool.Ifc.schema()
+ for attribute in schema.declaration_by_name(ifc_class).all_attributes():
import_attribute(attribute, props, data, callback=callback)
@@ -372,11 +373,16 @@ def convert_property_group_from_si(property_group: bpy.types.PropertyGroup, skip
setattr(property_group, prop_name, prop_value)
-def draw_filter(layout: bpy.types.UILayout, filter_groups, data, module: str) -> None:
+def draw_filter(
+ layout: bpy.types.UILayout,
+ filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup],
+ data,
+ module: str,
+) -> None:
if not data.is_loaded:
data.load()
- sprops = bpy.context.scene.BIMSearchProperties
+ sprops = tool.Search.get_search_props()
if tool.Ifc.get():
row = layout.row(align=True)
diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py
index 1c5d7800b1..35b40b7b3c 100644
--- a/src/bonsai/bonsai/bim/ifc.py
+++ b/src/bonsai/bonsai/bim/ifc.py
@@ -105,7 +105,8 @@ class IfcStore:
@staticmethod
def get_file():
if IfcStore.file is None:
- IfcStore.path = cast(str, bpy.context.scene.BIMProperties.ifc_file)
+ props = tool.Blender.get_bim_props()
+ IfcStore.path = props.ifc_file
# Interpret relative paths as relative to .blend file.
if IfcStore.path and not os.path.isabs(IfcStore.path):
IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path))
@@ -119,10 +120,11 @@ class IfcStore:
@staticmethod
def get_cache():
if IfcStore.cache is None and IfcStore.path:
+ props = tool.Blender.get_bim_props()
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
- os.makedirs(bpy.context.scene.BIMProperties.cache_dir, exist_ok=True)
- IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5")
+ os.makedirs(props.cache_dir, exist_ok=True)
+ IfcStore.cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
cache_path = Path(IfcStore.cache_path)
cache_settings = ifcopenshell.geom.settings()
serializer_settings = ifcopenshell.geom.serializer_settings()
@@ -162,7 +164,8 @@ class IfcStore:
assert IfcStore.file
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
- new_cache_path = os.path.join(bpy.context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5")
+ props = tool.Blender.get_bim_props()
+ new_cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5")
IfcStore.cache = None
try:
shutil.move(IfcStore.cache_path, new_cache_path)
@@ -178,6 +181,7 @@ class IfcStore:
if not os.path.isfile(path):
return
extension = path.split(".")[-1]
+ props = tool.Project.get_project_props()
if extension.lower() == "ifczip":
with tempfile.TemporaryDirectory() as unzipped_path:
with zipfile.ZipFile(path, "r") as zip_ref:
@@ -187,7 +191,7 @@ class IfcStore:
return
elif extension.lower() == "ifcxml":
IfcStore.file = ifcopenshell.file(ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(path))
- elif bpy.context.scene.BIMProjectProperties.should_stream:
+ elif props.should_stream:
IfcStore.file = ifcopenshell.open(path, should_stream=True)
else:
IfcStore.file = ifcopenshell.open(path)
@@ -195,9 +199,8 @@ class IfcStore:
@staticmethod
def get_schema() -> ifcopenshell.ifcopenshell_wrapper.schema_definition:
if IfcStore.file is None:
- IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(
- bpy.context.scene.BIMProjectProperties.export_schema
- )
+ props = tool.Project.get_project_props()
+ IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(props.export_schema)
elif IfcStore.schema is None:
IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(IfcStore.file.schema_identifier)
return IfcStore.schema
@@ -247,7 +250,7 @@ class IfcStore:
# refactor this class and deprecate usage of IfcStore in favour of
# tools.
if not isinstance(obj, (bpy.types.Object, bpy.types.Material)):
- obj.BIMMeshProperties.ifc_definition_id = element.id()
+ tool.Geometry.get_mesh_props(obj).ifc_definition_id = element.id()
return
existing_obj = IfcStore.id_map.get(element.id(), None)
@@ -414,7 +417,8 @@ class IfcStore:
method: Literal["EXECUTE", "INVOKE", "MODAL"] = "EXECUTE",
) -> set[str]:
bonsai.last_actions.append({"type": "operator", "name": operator.bl_idname})
- bpy.context.scene.BIMProperties.is_dirty = True
+ props = tool.Blender.get_bim_props()
+ props.is_dirty = True
# Modals don't nest, and Blender handles the loop that continuously calls modal()
is_top_level_operator = not bool(IfcStore.current_transaction) or (method == "MODAL")
@@ -493,7 +497,8 @@ class IfcStore:
) -> None:
key = getattr(operator, "transaction_key", None)
data = getattr(operator, "transaction_data", None)
- bpy.context.scene.BIMProperties.last_transaction = key
+ props = tool.Blender.get_bim_props()
+ props.last_transaction = key
IfcStore.last_transaction = key
rollback = rollback or getattr(operator, "rollback", lambda data: True)
commit = commit or getattr(operator, "commit", lambda data: True)
diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py
index d2bdc2ed70..2a3a5f97a2 100644
--- a/src/bonsai/bonsai/bim/import_ifc.py
+++ b/src/bonsai/bonsai/bim/import_ifc.py
@@ -69,6 +69,9 @@ class MaterialCreator:
if isinstance(mesh, bpy.types.Curve):
return
+ if mesh.get("has_layer_styles", None) == True:
+ return
+
self.mesh = mesh
self.obj = obj
if element.is_a("IfcTypeProduct"):
@@ -90,7 +93,8 @@ class MaterialCreator:
return # Already has materials assign to the representation itself
# Otherwise, we need to check for material styles on the element, since
# create_shape on types only works on representations.
- context = tool.Ifc.get().by_id(self.mesh.BIMMeshProperties.ifc_definition_id).ContextOfItems
+ mprops = tool.Geometry.get_mesh_props(self.mesh)
+ context = tool.Ifc.get().by_id(mprops.ifc_definition_id).ContextOfItems
for material in ifcopenshell.util.element.get_materials(element):
if style := ifcopenshell.util.representation.get_material_style(material, context):
self.mesh["ios_materials"] = (style.id(),)
@@ -218,7 +222,7 @@ class IfcImporter:
self.progress = 0
self.material_creator = MaterialCreator(ifc_import_settings, self)
- classes_to_wireframe_str = bpy.context.scene.DocProperties.classes_to_wireframe
+ classes_to_wireframe_str = tool.Drawing.get_document_props().classes_to_wireframe
self.classes_to_wireframe_list = [word.strip() for word in classes_to_wireframe_str.split(",")]
def profile_code(self, message: str) -> None:
@@ -434,7 +438,7 @@ class IfcImporter:
return False
def calculate_model_offset(self) -> None:
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if self.ifc_import_settings.false_origin_mode == "MANUAL":
tool.Loader.set_manual_blender_offset(self.file)
elif self.ifc_import_settings.false_origin_mode == "AUTOMATIC":
@@ -878,17 +882,19 @@ class IfcImporter:
def load_file(self):
self.ifc_import_settings.logger.info("loading file %s", self.ifc_import_settings.input_file)
- if not bpy.context.scene.BIMProperties.ifc_file:
- bpy.context.scene.BIMProperties.ifc_file = self.ifc_import_settings.input_file
- self.file = IfcStore.get_file()
+ props = tool.Blender.get_bim_props()
+ if not props.ifc_file:
+ props.ifc_file = self.ifc_import_settings.input_file
+ self.file = tool.Ifc.get()
def calculate_unit_scale(self):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
tool.Loader.set_unit_scale(self.unit_scale)
- def set_units(self):
+ def set_units(self) -> None:
if not (assignment := self.file.by_type("IfcProject")[0].UnitsInContext):
return # Geometry is optional in IFC
+ props = tool.Blender.get_bim_props()
for unit in assignment.Units:
if unit.is_a("IfcNamedUnit") and unit.UnitType == "LENGTHUNIT":
if unit.is_a("IfcSIUnit"):
@@ -908,19 +914,19 @@ class IfcImporter:
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "AREAUNIT":
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
try:
- bpy.context.scene.BIMProperties.area_unit = "{}{}".format(
+ props.area_unit = "{}{}".format(
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
)
except: # Probably an invalid unit.
- bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
+ props.area_unit = "SQUARE_METRE"
elif unit.is_a("IfcNamedUnit") and unit.UnitType == "VOLUMEUNIT":
name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower()
try:
- bpy.context.scene.BIMProperties.volume_unit = "{}{}".format(
+ props.volume_unit = "{}{}".format(
unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name
)
except: # Probably an invalid unit.
- bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
+ props.volume_unit = "CUBIC_METRE"
def create_project(self):
project = self.file.by_type("IfcProject")[0]
@@ -964,10 +970,10 @@ class IfcImporter:
tool.Collector.assign(obj, should_clean_users_collection=False)
def is_curve_annotation(self, element: ifcopenshell.entity_instance) -> bool:
- object_type = element.ObjectType
+ object_type = ifcopenshell.util.element.get_predefined_type(element)
return (
object_type in tool.Drawing.ANNOTATION_TYPES_DATA
- and tool.Drawing.ANNOTATION_TYPES_DATA[object_type][3] == "curve"
+ and tool.Drawing.ANNOTATION_TYPES_DATA[object_type].data_type == "curve"
)
def get_drawing_group(self, element):
@@ -1056,12 +1062,13 @@ class IfcImporter:
else:
mesh["has_cartesian_point_offset"] = False
- return tool.Loader.convert_geometry_to_mesh(
+ mesh = tool.Loader.convert_geometry_to_mesh(
geometry,
mesh,
verts=verts,
load_indexed_maps=self.ifc_import_settings.load_indexed_maps,
)
+ return tool.Loader.slice_layerset_mesh(element, mesh)
except:
self.ifc_import_settings.logger.error("Could not create mesh for %s", element)
import traceback
@@ -1069,9 +1076,10 @@ class IfcImporter:
print(traceback.format_exc())
def set_default_context(self):
+ rprops = tool.Root.get_root_props()
for subcontext in self.file.by_type("IfcGeometricRepresentationSubContext"):
if subcontext.ContextIdentifier == "Body":
- bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id())
+ rprops.contexts = str(subcontext.id())
break
def link_element(self, element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None:
diff --git a/src/bonsai/bonsai/bim/module/aggregate/operator.py b/src/bonsai/bonsai/bim/module/aggregate/operator.py
index c2e0cb16c2..c324037b89 100644
--- a/src/bonsai/bonsai/bim/module/aggregate/operator.py
+++ b/src/bonsai/bonsai/bim/module/aggregate/operator.py
@@ -23,7 +23,6 @@ import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.aggregate as core
import bonsai.core.spatial
-from bonsai.bim.ifc import IfcStore
class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator):
diff --git a/src/bonsai/bonsai/bim/module/aggregate/ui.py b/src/bonsai/bonsai/bim/module/aggregate/ui.py
index 7b9503f530..74389c723d 100644
--- a/src/bonsai/bonsai/bim/module/aggregate/ui.py
+++ b/src/bonsai/bonsai/bim/module/aggregate/ui.py
@@ -39,7 +39,7 @@ class BIM_PT_aggregate(Panel):
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
- if not IfcStore.get_element(props.ifc_definition_id):
+ if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
return False
@@ -120,9 +120,9 @@ class BIM_PT_linked_aggregate(Panel):
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
- if not IfcStore.get_element(props.ifc_definition_id):
+ if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
- if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
+ if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
return False
return True
diff --git a/src/bonsai/bonsai/bim/module/attribute/operator.py b/src/bonsai/bonsai/bim/module/attribute/operator.py
index 3282c66ec5..2b279895e3 100644
--- a/src/bonsai/bonsai/bim/module/attribute/operator.py
+++ b/src/bonsai/bonsai/bim/module/attribute/operator.py
@@ -26,7 +26,6 @@ import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.core.attribute as core
import bonsai.core.spatial
-from bonsai.bim.ifc import IfcStore
def get_objs_for_operation(operator_properties, context):
@@ -117,7 +116,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
obj = tool.Blender.get_active_object(is_selected=False)
if not (element := tool.Ifc.get_entity(obj)):
return
diff --git a/src/bonsai/bonsai/bim/module/attribute/prop.py b/src/bonsai/bonsai/bim/module/attribute/prop.py
index 207fd927fd..fd2dab7cd2 100644
--- a/src/bonsai/bonsai/bim/module/attribute/prop.py
+++ b/src/bonsai/bonsai/bim/module/attribute/prop.py
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see .
import bpy
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
diff --git a/src/bonsai/bonsai/bim/module/attribute/ui.py b/src/bonsai/bonsai/bim/module/attribute/ui.py
index 6f0f5a5599..0fce2b07d4 100644
--- a/src/bonsai/bonsai/bim/module/attribute/ui.py
+++ b/src/bonsai/bonsai/bim/module/attribute/ui.py
@@ -18,7 +18,6 @@
import bonsai.bim.helper
from bpy.types import Panel
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.attribute.data import AttributesData
import bonsai.tool as tool
diff --git a/src/bonsai/bonsai/bim/module/augin/operator.py b/src/bonsai/bonsai/bim/module/augin/operator.py
index 965279e9b7..b578beb554 100644
--- a/src/bonsai/bonsai/bim/module/augin/operator.py
+++ b/src/bonsai/bonsai/bim/module/augin/operator.py
@@ -113,7 +113,7 @@ class AuginCreateNewModel(bpy.types.Operator):
context.scene.collection.objects.link(cam_obj)
context.scene.camera = cam_obj
- tmpdir = tempfile.mkdtemp()
+ tmpdir = tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None)
thumb_path = os.path.join(tmpdir, "thumb.png")
context.scene.render.image_settings.file_format = "PNG"
context.scene.render.filepath = thumb_path
@@ -122,15 +122,16 @@ class AuginCreateNewModel(bpy.types.Operator):
context.scene.render.image_settings.file_format = old_file_format
context.scene.render.filepath = old_filepath
- client.upload_file(context.scene.BIMProperties.ifc_file, result["s3_bucket"], result["model_path"])
+ bim_props = tool.Blender.get_bim_props()
+ client.upload_file(bim_props.ifc_file, result["s3_bucket"], result["model_path"])
client.upload_file(thumb_path, result["s3_bucket"], result["thumb_path"])
# Notify done
url = "https://server.auge.pro.br/API/v3/augin_rest.php/files_uploaded"
payload = {
"user_token": props.token,
- "ifc_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file),
- "model_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file),
+ "ifc_filesize": os.path.getsize(bim_props.ifc_file),
+ "model_filesize": os.path.getsize(bim_props.ifc_file),
"thumb_filesize": os.path.getsize(thumb_path),
"model_upload_path": result["model_path"],
"thumb_upload_path": result["thumb_path"],
diff --git a/src/bonsai/bonsai/bim/module/augin/ui.py b/src/bonsai/bonsai/bim/module/augin/ui.py
index 0da722f7a6..65ca3ac7f9 100644
--- a/src/bonsai/bonsai/bim/module/augin/ui.py
+++ b/src/bonsai/bonsai/bim/module/augin/ui.py
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see .
import bpy.types
+import bonsai.tool as tool
class BIM_PT_augin(bpy.types.Panel):
@@ -47,7 +48,8 @@ class BIM_PT_augin(bpy.types.Panel):
row = layout.row()
row.label(text="Logged in as " + props.username)
- if not context.scene.BIMProperties.ifc_file:
+ bim_props = tool.Blender.get_bim_props()
+ if not bim_props.ifc_file:
row = layout.row()
row.label(text="No IFC Found")
return
diff --git a/src/bonsai/bonsai/bim/module/bcf/operator.py b/src/bonsai/bonsai/bim/module/bcf/operator.py
index bfcc40b8a9..0c145d98f8 100644
--- a/src/bonsai/bonsai/bim/module/bcf/operator.py
+++ b/src/bonsai/bonsai/bim/module/bcf/operator.py
@@ -43,7 +43,6 @@ import bonsai.tool as tool
import bonsai.bim.module.bcf.prop as bcf_prop
import bonsai.bim.module.bcf.bcfstore as bcfstore
from pathlib import Path
-from bonsai.bim.ifc import IfcStore
from math import radians, degrees, atan, tan, cos, sin
from mathutils import Vector, Matrix, Euler, geometry
from xsdata.models.datatype import XmlDateTime
@@ -1206,7 +1205,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
return True
def execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
bcfxml = bcfstore.BcfStore.get_bcfxml()
assert bcfxml
@@ -1316,7 +1315,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
[0, 0, 0, 1],
)
)
- props = context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
matrix = ifcopenshell.util.geolocation.global2local(
@@ -1355,7 +1354,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
objs: list[bpy.types.Object] = []
for global_id in exception_global_ids:
- obj = IfcStore.get_element(global_id)
+ obj = tool.Ifc.get_object_by_identifier(global_id)
if obj and context.view_layer.objects.get(obj.name):
assert isinstance(obj, bpy.types.Object)
objs.append(obj)
@@ -1414,7 +1413,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
return
bpy.ops.object.select_all(action="DESELECT")
for global_id in selected_global_ids:
- obj = IfcStore.get_element(global_id)
+ obj = tool.Ifc.get_object_by_identifier(global_id)
if obj:
obj.select_set(True)
obj.hide_set(False)
@@ -1427,7 +1426,7 @@ class ActivateBcfViewpoint(bpy.types.Operator):
for acomponent in acoloring.component:
global_id_colours.setdefault(acomponent.ifc_guid, acoloring.color)
for global_id, color in global_id_colours.items():
- obj = IfcStore.get_element(global_id)
+ obj = tool.Ifc.get_object_by_identifier(global_id)
if obj:
obj.color = self.hex_to_rgb(color)
diff --git a/src/bonsai/bonsai/bim/module/boundary/operator.py b/src/bonsai/bonsai/bim/module/boundary/operator.py
index e3bc51683c..75cf32cb66 100644
--- a/src/bonsai/bonsai/bim/module/boundary/operator.py
+++ b/src/bonsai/bonsai/bim/module/boundary/operator.py
@@ -25,6 +25,7 @@ import mathutils
import numpy as np
import multiprocessing
import ifcopenshell.api
+import ifcopenshell.api.boundary
import ifcopenshell.geom
import ifcopenshell.util.unit
import ifcopenshell.util.shape
@@ -113,7 +114,7 @@ class Loader:
bm.edges.new((verts[-1], verts[0]))
bm.to_mesh(mesh)
bm.free()
- mesh.BIMMeshProperties.ifc_definition_id = surface.id()
+ tool.Ifc.link(surface, mesh)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
matrix = mathutils.Matrix(
ifcopenshell.util.placement.get_axis2placement(surface.BasisSurface.Position).tolist()
@@ -416,7 +417,7 @@ class UpdateBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
tool.Boundary.move_origin_to_space_origin(context.active_object)
settings = tool.Boundary.get_assign_connection_geometry_settings(context.active_object)
- ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
+ ifcopenshell.api.boundary.assign_connection_geometry(tool.Ifc.get(), **settings)
return {"FINISHED"}
diff --git a/src/bonsai/bonsai/bim/module/boundary/ui.py b/src/bonsai/bonsai/bim/module/boundary/ui.py
index e3c7d8936f..e7332d50c6 100644
--- a/src/bonsai/bonsai/bim/module/boundary/ui.py
+++ b/src/bonsai/bonsai/bim/module/boundary/ui.py
@@ -18,7 +18,6 @@
import bpy
from bpy.types import Panel, UIList
-from bonsai.bim.ifc import IfcStore
import bonsai.tool as tool
from bonsai.bim.module.boundary.data import SpaceBoundariesData
@@ -34,7 +33,7 @@ class BIM_PT_SceneBoundaries(Panel):
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ return tool.Ifc.get()
def draw(self, context):
row = self.layout.row(align=True)
@@ -58,9 +57,9 @@ class BIM_PT_Boundary(Panel):
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
- if not IfcStore.get_element(props.ifc_definition_id):
+ if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
- entity = IfcStore.get_file().by_id(props.ifc_definition_id)
+ entity = tool.Ifc.get().by_id(props.ifc_definition_id)
return entity.is_a("IfcRelSpaceBoundary")
def draw(self, context):
@@ -134,7 +133,7 @@ class BIM_PT_SpaceBoundaries(Panel):
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
- if not IfcStore.get_element(props.ifc_definition_id):
+ if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
element = tool.Ifc.get_entity(context.active_object)
for ifc_class in ("IfcSpace", "IfcExternalSpatialElement"):
diff --git a/src/bonsai/bonsai/bim/module/bsdd/ui.py b/src/bonsai/bonsai/bim/module/bsdd/ui.py
index ac3e633d8a..046bc46fec 100644
--- a/src/bonsai/bonsai/bim/module/bsdd/ui.py
+++ b/src/bonsai/bonsai/bim/module/bsdd/ui.py
@@ -18,7 +18,6 @@
import bonsai.tool as tool
from bpy.types import Panel, UIList
-from bonsai.bim.ifc import IfcStore
class BIM_PT_bsdd(Panel):
diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py
index 854b34bb7d..4b70d04df1 100644
--- a/src/bonsai/bonsai/bim/module/cad/workspace.py
+++ b/src/bonsai/bonsai/bim/module/cad/workspace.py
@@ -69,10 +69,12 @@ class CadTool(WorkSpaceTool):
("bim.cad_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
)
- def draw_settings(context, layout, workspace_tool):
+ def draw_settings(
+ context: bpy.types.Context, layout: bpy.types.UILayout, workspace_tool: bpy.types.WorkSpaceTool
+ ) -> None:
ui_context = str(context.region.type)
obj = context.active_object
- if not obj or not obj.data:
+ if not obj or not (data := obj.data):
return
is_profile = tool.Geometry.is_profile_object_active()
if is_profile:
@@ -122,7 +124,10 @@ class CadTool(WorkSpaceTool):
row = row if ui_context == "TOOL_HEADER" else layout.row(align=True)
add_layout_hotkey_operator(row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__, ui_context)
- elif hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "AXIS":
+ elif (
+ isinstance(data, tool.Geometry.TYPES_WITH_MESH_PROPERTIES)
+ and tool.Geometry.get_mesh_props(data).subshape_type == "AXIS"
+ ):
add_header_apply_button(
layout, "Edit Axis", "bim.edit_extrusion_axis", "bim.disable_editing_extrusion_axis", ui_context
)
@@ -141,7 +146,7 @@ class CadTool(WorkSpaceTool):
if (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["pset_data"]
- and context.active_object.BIMRailingProperties.is_editing_path
+ and obj.BIMRailingProperties.is_editing_path
):
add_header_apply_button(
layout,
@@ -154,7 +159,7 @@ class CadTool(WorkSpaceTool):
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["pset_data"]
- and context.active_object.BIMRoofProperties.is_editing_path
+ and obj.BIMRoofProperties.is_editing_path
):
add_header_apply_button(
layout, "Edit Roof Path", "bim.finish_editing_roof_path", "bim.cancel_editing_roof_path", ui_context
@@ -252,15 +257,27 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion)
def hotkey_S_Q(self):
- element = tool.Ifc.get_entity(bpy.context.active_object)
- if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE":
+ obj = bpy.context.active_object
+
+ if not obj:
+ return
+
+ if not tool.Geometry.has_mesh_properties(data := obj.data):
+ return
+
+ element = tool.Ifc.get_entity(obj)
+ if not element:
+ return
+
+ mprops = tool.Geometry.get_mesh_props(data)
+ if mprops.subshape_type == "PROFILE":
if element.is_a("IfcProfileDef"):
bpy.ops.bim.edit_arbitrary_profile()
elif element.is_a("IfcRelSpaceBoundary"):
bpy.ops.bim.edit_boundary_geometry()
else:
bpy.ops.bim.edit_extrusion_profile()
- elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS":
+ elif mprops.subshape_type == "AXIS":
bpy.ops.bim.edit_extrusion_axis()
def hotkey_S_R(self):
diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py
index 020aeba55e..c3d87e4759 100644
--- a/src/bonsai/bonsai/bim/module/clash/operator.py
+++ b/src/bonsai/bonsai/bim/module/clash/operator.py
@@ -300,7 +300,7 @@ class SelectIfcClashResults(bpy.types.Operator):
def execute(self, context):
# TODO refactor into new clash results system
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
with open(self.filepath) as f:
clash_sets = json.load(f)
@@ -321,8 +321,9 @@ class SelectIfcClashResults(bpy.types.Operator):
ifc_file = ""
for scene in obj.users_scene:
- if scene.BIMProperties.ifc_file:
- ifc_file = scene.BIMProperties.ifc_file
+ bim_props = tool.Blender.get_bim_props(scene)
+ if bim_props.ifc_file:
+ ifc_file = bim_props.ifc_file
if scene.library:
break
@@ -478,7 +479,7 @@ class SelectSmartGroup(bpy.types.Operator):
@classmethod
def poll(cls, context):
- return IfcStore.get_file() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group
+ return tool.Ifc.get() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group
def execute(self, context):
selected_smart_group = context.scene.BIMClashProperties.active_smart_group
diff --git a/src/bonsai/bonsai/bim/module/classification/operator.py b/src/bonsai/bonsai/bim/module/classification/operator.py
index 199fa2d61b..fa5e914880 100644
--- a/src/bonsai/bonsai/bim/module/classification/operator.py
+++ b/src/bonsai/bonsai/bim/module/classification/operator.py
@@ -201,7 +201,8 @@ class EnableEditingClassification(bpy.types.Operator):
def execute(self, context):
def callback(name, prop, data):
if name == "ReferenceTokens":
- new = bpy.context.scene.BIMGeoreferenceProperties.projected_crs.add()
+ geo_props = tool.Georeference.get_georeference_props()
+ new = geo_props.projected_crs.add()
new.name = name
new.data_type = "string"
new.is_null = data[name] is None
@@ -239,7 +240,7 @@ class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator):
classification: bpy.props.IntProperty()
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"classification.remove_classification",
tool.Ifc.get(),
diff --git a/src/bonsai/bonsai/bim/module/classification/ui.py b/src/bonsai/bonsai/bim/module/classification/ui.py
index 87257fbaaa..c171ff3cd6 100644
--- a/src/bonsai/bonsai/bim/module/classification/ui.py
+++ b/src/bonsai/bonsai/bim/module/classification/ui.py
@@ -21,7 +21,6 @@ import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.bim.module.classification.prop as classification_prop
from bpy.types import Panel, UIList
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.classification.data import (
ClassificationsData,
ClassificationReferencesData,
@@ -124,7 +123,7 @@ class ReferenceUI:
self.sprops = context.scene.BIMClassificationProperties
self.bprops = context.scene.BIMBSDDProperties
self.props = context.scene.BIMClassificationReferenceProperties
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
self.draw_add_ui(context)
diff --git a/src/bonsai/bonsai/bim/module/constraint/operator.py b/src/bonsai/bonsai/bim/module/constraint/operator.py
index deed4d6335..3a66b074c7 100644
--- a/src/bonsai/bonsai/bim/module/constraint/operator.py
+++ b/src/bonsai/bonsai/bim/module/constraint/operator.py
@@ -23,7 +23,6 @@ import ifcopenshell.api.constraint
import ifcopenshell.util.attribute
import bonsai.bim.helper
import bonsai.tool as tool
-from bonsai.bim.ifc import IfcStore
class LoadObjectives(bpy.types.Operator):
@@ -84,7 +83,7 @@ class AddObjective(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
- result = ifcopenshell.api.run("constraint.add_objective", IfcStore.get_file())
+ result = ifcopenshell.api.run("constraint.add_objective", tool.Ifc.get())
bpy.ops.bim.load_objectives()
bpy.ops.bim.enable_editing_constraint(constraint=result.id())
return {"FINISHED"}
@@ -116,7 +115,7 @@ class RemoveConstraint(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMConstraintProperties
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"constraint.remove_constraint", self.file, **{"constraint": self.file.by_id(self.constraint)}
)
diff --git a/src/bonsai/bonsai/bim/module/constraint/ui.py b/src/bonsai/bonsai/bim/module/constraint/ui.py
index f7d530a757..76f4c4a1bb 100644
--- a/src/bonsai/bonsai/bim/module/constraint/ui.py
+++ b/src/bonsai/bonsai/bim/module/constraint/ui.py
@@ -16,8 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+import bonsai.tool as tool
from bpy.types import Panel, UIList
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.constraint.data import ConstraintsData, ObjectConstraintsData
@@ -33,7 +33,7 @@ class BIM_PT_constraints(Panel):
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ return tool.Ifc.get()
def draw(self, context):
if not ConstraintsData.is_loaded:
@@ -81,7 +81,7 @@ class BIM_PT_object_constraints(Panel):
def poll(cls, context):
if not context.active_object:
return False
- if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
+ if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
@@ -93,7 +93,7 @@ class BIM_PT_object_constraints(Panel):
self.oprops = obj.BIMObjectProperties
self.sprops = context.scene.BIMConstraintProperties
self.props = obj.BIMObjectConstraintProperties
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
self.draw_add_ui()
diff --git a/src/bonsai/bonsai/bim/module/cost/prop.py b/src/bonsai/bonsai/bim/module/cost/prop.py
index 092f429121..abe035fdb6 100644
--- a/src/bonsai/bonsai/bim/module/cost/prop.py
+++ b/src/bonsai/bonsai/bim/module/cost/prop.py
@@ -19,7 +19,6 @@
import bpy
import ifcopenshell.api
import bonsai.tool as tool
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.classification.data import CostClassificationsData
from bonsai.bim.module.cost.data import CostSchedulesData, CostItemRatesData, CostItemQuantitiesData
from bonsai.bim.prop import StrProperty, Attribute
@@ -83,7 +82,7 @@ def update_cost_item_identification(self, context):
props = context.scene.BIMCostProperties
if not props.is_cost_update_enabled or self.identification == "XXX":
return
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"cost.edit_cost_item",
self.file,
@@ -98,7 +97,7 @@ def update_cost_item_name(self, context):
props = context.scene.BIMCostProperties
if not props.is_cost_update_enabled or self.name == "Unnamed":
return
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"cost.edit_cost_item",
self.file,
diff --git a/src/bonsai/bonsai/bim/module/cost/ui.py b/src/bonsai/bonsai/bim/module/cost/ui.py
index 67d14437ed..387998e9ac 100644
--- a/src/bonsai/bonsai/bim/module/cost/ui.py
+++ b/src/bonsai/bonsai/bim/module/cost/ui.py
@@ -21,7 +21,6 @@ import bonsai.bim.helper
import bonsai.bim.module.cost.prop as CostProp
import bonsai.tool as tool
from bpy.types import Panel, UIList
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.cost.data import CostSchedulesData
from typing import Any
@@ -37,7 +36,7 @@ class BIM_PT_cost_schedules(Panel):
@classmethod
def poll(cls, context):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
diff --git a/src/bonsai/bonsai/bim/module/covetool/operator.py b/src/bonsai/bonsai/bim/module/covetool/operator.py
index 0bafcafa4f..1d64dd958e 100644
--- a/src/bonsai/bonsai/bim/module/covetool/operator.py
+++ b/src/bonsai/bonsai/bim/module/covetool/operator.py
@@ -20,8 +20,8 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.util.element
+import bonsai.tool as tool
from math import degrees, atan2
-from bonsai.bim.ifc import IfcStore
from .api import Api
@@ -92,7 +92,7 @@ class RunAnalysis(bpy.types.Operator):
bl_label = "Run Analysis"
def execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
self.inputs = {
"floors": [],
"walls": [],
diff --git a/src/bonsai/bonsai/bim/module/csv/operator.py b/src/bonsai/bonsai/bim/module/csv/operator.py
index 7d59bc5a92..cd9e784bf2 100644
--- a/src/bonsai/bonsai/bim/module/csv/operator.py
+++ b/src/bonsai/bonsai/bim/module/csv/operator.py
@@ -27,7 +27,6 @@ import ifcopenshell
import ifcopenshell.util.selector
import bonsai.tool as tool
import bonsai.bim.module.drawing.scheduler as scheduler
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.handler import refresh_ui_data
from typing import TYPE_CHECKING
from collections import Counter
@@ -212,7 +211,7 @@ class ExportIfcCsv(bpy.types.Operator):
props = context.scene.CsvProperties
self.filepath = bpy.path.ensure_ext(self.filepath, f".{props.format}")
if props.should_load_from_memory:
- ifc_file = IfcStore.get_file()
+ ifc_file = tool.Ifc.get()
else:
ifc_file = ifcopenshell.open(props.csv_ifc_file)
results = ifcopenshell.util.selector.filter_elements(
@@ -314,7 +313,7 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
props = context.scene.CsvProperties
if props.should_load_from_memory:
- ifc_file = IfcStore.get_file()
+ ifc_file = tool.Ifc.get()
else:
ifc_file = ifcopenshell.open(props.csv_ifc_file)
ifc_csv = ifccsv.IfcCsv()
diff --git a/src/bonsai/bonsai/bim/module/csv/ui.py b/src/bonsai/bonsai/bim/module/csv/ui.py
index 0d4d6ef11e..6aa41177b0 100644
--- a/src/bonsai/bonsai/bim/module/csv/ui.py
+++ b/src/bonsai/bonsai/bim/module/csv/ui.py
@@ -17,8 +17,8 @@
# along with Bonsai. If not, see .
import bonsai.bim.helper
+import bonsai.tool as tool
from bpy.types import Panel
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.search.data import SearchData
@@ -37,7 +37,7 @@ class BIM_PT_ifccsv(Panel):
scene = context.scene
props = scene.CsvProperties
- if IfcStore.get_file():
+ if tool.Ifc.get():
row = layout.row(align=True)
row.prop(props, "should_load_from_memory")
row.operator("bim.import_csv_attributes", icon="IMPORT", text="")
@@ -49,7 +49,7 @@ class BIM_PT_ifccsv(Panel):
row.operator("bim.export_csv_attributes", icon="EXPORT", text="")
row.prop(props, "should_show_settings", icon="PREFERENCES", text="")
- if not IfcStore.get_file() or not props.should_load_from_memory:
+ if not tool.Ifc.get() or not props.should_load_from_memory:
row = layout.row(align=True)
row.prop(props, "csv_ifc_file")
row.operator("bim.select_csv_ifc_file", icon="FILE_FOLDER", text="")
diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py
index 33100956ef..4b16c3fb12 100644
--- a/src/bonsai/bonsai/bim/module/debug/operator.py
+++ b/src/bonsai/bonsai/bim/module/debug/operator.py
@@ -78,10 +78,10 @@ class PrintIfcFile(bpy.types.Operator):
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ return tool.Ifc.get()
def execute(self, context):
- print(IfcStore.get_file().wrapped_data.to_string())
+ print(tool.Ifc.get().wrapped_data.to_string())
return {"FINISHED"}
@@ -101,13 +101,14 @@ class ConvertToBlender(bpy.types.Operator):
if tool.Geometry.has_mesh_properties(data):
if data.library:
continue
- data.BIMMeshProperties.ifc_definition_id = 0
+ tool.Geometry.get_mesh_props(data).ifc_definition_id = 0
for material in bpy.data.materials:
if material.library:
continue
tool.Ifc.unlink(obj=material)
- context.scene.BIMProperties.ifc_file = ""
- context.scene.BIMDebugProperties.attributes.clear()
+ bim_props = tool.Blender.get_bim_props()
+ bim_props.ifc_file = ""
+ tool.Debug.get_debug_props().attributes.clear()
IfcStore.purge()
bonsai.bim.handler.refresh_ui_data()
return {"FINISHED"}
@@ -160,7 +161,8 @@ class ProfileImportIFC(bpy.types.Operator):
if not tool.Ifc.get():
cls.poll_message_set("No IFC file loaded.")
return False
- if not context.scene.BIMProperties.ifc_file:
+ bim_props = tool.Blender.get_bim_props()
+ if not bim_props.ifc_file:
cls.poll_message_set("Current IFC file is not saved.")
return False
return True
@@ -184,10 +186,10 @@ class CreateAllShapes(bpy.types.Operator):
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ return tool.Ifc.get()
def execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
elements = self.file.by_type("IfcElement") + self.file.by_type("IfcSpace")
total = len(elements)
@@ -256,7 +258,7 @@ class CreateShapeFromStepId(bpy.types.Operator):
logger = logging.getLogger("ImportIFC")
self.ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
self.file = tool.Ifc.get()
- element = self.file.by_id(self.step_id or int(context.scene.BIMDebugProperties.step_id))
+ element = self.file.by_id(self.step_id or int(tool.Debug.get_debug_props().step_id))
settings = ifcopenshell.geom.settings()
settings.set("keep-bounding-boxes", True)
if self.should_include_curves:
@@ -309,7 +311,7 @@ class RewindInspector(bpy.types.Operator):
bl_description = "Rewind the Inspector to the previously inspected element"
def execute(self, context):
- props = context.scene.BIMDebugProperties
+ props = tool.Debug.get_debug_props()
total_breadcrumbs = len(props.step_id_breadcrumb)
if total_breadcrumbs < 2:
return {"FINISHED"}
@@ -328,13 +330,13 @@ class InspectFromStepId(bpy.types.Operator):
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ return tool.Ifc.get()
def execute(self, context):
- self.file = IfcStore.get_file()
- debug_props = context.scene.BIMDebugProperties
+ self.file = tool.Ifc.get()
+ debug_props = tool.Debug.get_debug_props()
debug_props.active_step_id = self.step_id
- crumb = context.scene.BIMDebugProperties.step_id_breadcrumb.add()
+ crumb = debug_props.step_id_breadcrumb.add()
crumb.name = str(self.step_id)
element = self.file.by_id(self.step_id)
debug_props.attributes.clear()
@@ -385,7 +387,7 @@ class InspectFromObject(bpy.types.Operator):
if (
(data := obj.data)
and tool.Geometry.has_mesh_properties(data)
- and (ifc_id := data.BIMMeshProperties.ifc_definition_id)
+ and (ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id)
):
return ifc_id
@@ -422,7 +424,7 @@ class PrintObjectPlacement(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
- placement = ifcopenshell.util.placement.get_local_placement(IfcStore.get_file().by_id(self.step_id))
+ placement = ifcopenshell.util.placement.get_local_placement(tool.Ifc.get().by_id(self.step_id))
if self.create_empty_object:
bpy.ops.object.empty_add(type="ARROWS")
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -438,7 +440,8 @@ class ParseExpress(bpy.types.Operator):
bl_label = "Parse Express"
def execute(self, context):
- core.parse_express(tool.Debug, context.scene.BIMDebugProperties.express_file)
+ props = tool.Debug.get_debug_props()
+ core.parse_express(tool.Debug, props.express_file)
bonsai.bim.handler.refresh_ui_data()
return {"FINISHED"}
@@ -452,8 +455,9 @@ class SelectExpressFile(bpy.types.Operator):
filter_glob: bpy.props.StringProperty(default="*.exp", options={"HIDDEN"})
def execute(self, context):
+ props = tool.Debug.get_debug_props()
if os.path.exists(self.filepath) and "exp" in os.path.splitext(self.filepath)[1]:
- context.scene.BIMDebugProperties.express_file = self.filepath
+ props.express_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
@@ -497,7 +501,7 @@ class PrintUnusedElementStats(bpy.types.Operator):
ignore_styled_items: bpy.props.BoolProperty(name="Ignore Styled Items", default=True)
def execute(self, context):
- props = context.scene.BIMDebugProperties
+ props = tool.Debug.get_debug_props()
# ignore some classes that could have zero 0 inverse references by their nature
ignore_classes = []
if self.ignore_contexts:
@@ -543,7 +547,7 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
return True
def _execute(self, context):
- props = context.scene.BIMDebugProperties
+ props = tool.Debug.get_debug_props()
if props.ifc_class_purge:
purged_elements = core.purge_unused_elements(tool.Ifc, tool.Debug, props.ifc_class_purge)
self.report({"INFO"}, f"{purged_elements} unused elements found and removed.")
@@ -803,7 +807,7 @@ class DebugActiveDrawing(bpy.types.Operator):
)
def execute(self, context: bpy.types.Context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
drawing_item = props.drawings[props.active_drawing_index]
drawing = tool.Ifc.get().by_id(drawing_item.ifc_definition_id)
diff --git a/src/bonsai/bonsai/bim/module/debug/prop.py b/src/bonsai/bonsai/bim/module/debug/prop.py
index 01076ccde3..5a28b22f79 100644
--- a/src/bonsai/bonsai/bim/module/debug/prop.py
+++ b/src/bonsai/bonsai/bim/module/debug/prop.py
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+import bpy
from bonsai.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
@@ -28,6 +29,9 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
+from typing import TYPE_CHECKING, Literal, get_args
+
+DisplayType = Literal["BOUNDS", "WIRE", "SOLID", "TEXTURED"]
class BIMDebugProperties(PropertyGroup):
@@ -41,14 +45,23 @@ class BIMDebugProperties(PropertyGroup):
inverse_references: CollectionProperty(name="Inverse References", type=Attribute)
express_file: StringProperty(name="Express File")
display_type: EnumProperty(
- items=[
- ("BOUNDS", "Bounds", ""),
- ("WIRE", "Wire", ""),
- ("SOLID", "Solid", ""),
- ("TEXTURED", "Textured", ""),
- ],
+ items=[(display_type, display_type.capitalize(), "") for display_type in get_args(DisplayType)],
name="Display Type",
default="BOUNDS",
)
ifc_class_purge: StringProperty(name="Unused Elements IFC Class", default="")
package_name: StringProperty(name="Package Name", default="")
+
+ if TYPE_CHECKING:
+ step_id: int
+ number_of_polygons: int
+ percentile_of_polygons: int
+ active_step_id: int
+ step_id_breadcrumb: bpy.types.bpy_prop_collection_idprop[StrProperty]
+ attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
+ inverse_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
+ inverse_references: bpy.types.bpy_prop_collection_idprop[Attribute]
+ express_file: str
+ display_type: str
+ ifc_class_purge: str
+ package_name: str
diff --git a/src/bonsai/bonsai/bim/module/debug/ui.py b/src/bonsai/bonsai/bim/module/debug/ui.py
index 3611920095..c2c43bd4bb 100644
--- a/src/bonsai/bonsai/bim/module/debug/ui.py
+++ b/src/bonsai/bonsai/bim/module/debug/ui.py
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see .
import bpy
+import bonsai.tool as tool
from bpy.types import Panel
@@ -32,10 +33,11 @@ class BIM_PT_debug(Panel):
def draw(self, context):
layout = self.layout
- props = context.scene.BIMDebugProperties
+ props = tool.Debug.get_debug_props()
+ bim_props = tool.Blender.get_bim_props()
row = self.layout.row(align=True)
- row.prop(context.scene.BIMProperties, "ifc_file", text="")
+ row.prop(bim_props, "ifc_file", text="")
row.operator("bim.validate_ifc_file", icon="CHECKMARK", text="")
row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="")
@@ -87,27 +89,25 @@ class BIM_PT_debug(Panel):
row.prop(props, "step_id", text="")
row = layout.split(factor=0.7, align=True)
- row.operator("bim.select_high_polygon_meshes").threshold = context.scene.BIMDebugProperties.number_of_polygons
+ row.operator("bim.select_high_polygon_meshes").threshold = props.number_of_polygons
row.prop(props, "number_of_polygons", text="")
row = layout.split(factor=0.7, align=True)
- row.operator("bim.select_highest_polygon_meshes").percentile = (
- context.scene.BIMDebugProperties.percentile_of_polygons
- )
+ row.operator("bim.select_highest_polygon_meshes").percentile = props.percentile_of_polygons
row.prop(props, "percentile_of_polygons", text="")
row = layout.split(factor=0.5, align=True)
row.prop(props, "display_type", text="")
- row.operator("bim.override_display_type").display = context.scene.BIMDebugProperties.display_type
+ row.operator("bim.override_display_type").display = props.display_type
layout.operator("bim.purge_unused_representations")
row = layout.row(align=True)
- row.prop(context.scene.BIMDebugProperties, "ifc_class_purge", text="")
+ row.prop(props, "ifc_class_purge", text="")
row.operator("bim.purge_unused_elements_by_class", text="Purge Orphaned", icon="TRASH")
row.operator("bim.print_unused_elements_stats", text="", icon="INFO")
- if context.active_object and context.active_object.data:
- mprops = context.active_object.data.BIMMeshProperties
+ if context.active_object and (data := context.active_object.data):
+ mprops = tool.Geometry.get_mesh_props(data)
row = layout.row()
row.operator("bim.get_representation_ifc_parameters")
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
@@ -123,7 +123,7 @@ class BIM_PT_debug(Panel):
row.operator("bim.rewind_inspector", icon="FRAME_PREV", text="")
row.prop(props, "active_step_id", text="")
row = layout.row(align=True)
- row.operator("bim.inspect_from_step_id").step_id = context.scene.BIMDebugProperties.active_step_id
+ row.operator("bim.inspect_from_step_id").step_id = props.active_step_id
row.operator("bim.inspect_from_object")
if props.attributes:
diff --git a/src/bonsai/bonsai/bim/module/diff/operator.py b/src/bonsai/bonsai/bim/module/diff/operator.py
index ead5dd6e04..dc9e981b19 100644
--- a/src/bonsai/bonsai/bim/module/diff/operator.py
+++ b/src/bonsai/bonsai/bim/module/diff/operator.py
@@ -70,8 +70,9 @@ class VisualiseDiff(bpy.types.Operator):
ifc_file = ""
for scene in obj.users_scene:
- if scene.BIMProperties.ifc_file:
- ifc_file = scene.BIMProperties.ifc_file
+ bim_props = tool.Blender.get_bim_props(scene)
+ if bim_props.ifc_file:
+ ifc_file = bim_props.ifc_file
if scene.library:
break
@@ -257,8 +258,9 @@ class SelectDiffObjects(bpy.types.Operator):
ifc_file = ""
for scene in obj.users_scene:
- if scene.BIMProperties.ifc_file:
- ifc_file = scene.BIMProperties.ifc_file
+ bim_props = tool.Blender.get_bim_props(scene)
+ if bim_props.ifc_file:
+ ifc_file = bim_props.ifc_file
if scene.library:
break
diff --git a/src/bonsai/bonsai/bim/module/diff/ui.py b/src/bonsai/bonsai/bim/module/diff/ui.py
index b147f884d1..34e66df0f5 100644
--- a/src/bonsai/bonsai/bim/module/diff/ui.py
+++ b/src/bonsai/bonsai/bim/module/diff/ui.py
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see .
from bpy.types import Panel
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.diff.data import DiffData
import bonsai.bim.helper
import bonsai.tool as tool
diff --git a/src/bonsai/bonsai/bim/module/document/data.py b/src/bonsai/bonsai/bim/module/document/data.py
index 0a75605b5c..5cde82e499 100644
--- a/src/bonsai/bonsai/bim/module/document/data.py
+++ b/src/bonsai/bonsai/bim/module/document/data.py
@@ -52,7 +52,7 @@ class DocumentData:
@classmethod
def parent_document(cls):
- props = bpy.context.scene.BIMDocumentProperties
+ props = tool.Document.get_document_props()
if len(props.breadcrumbs):
parent = tool.Ifc.get().by_id(int(props.breadcrumbs[-1].name))
if tool.Ifc.get_schema() == "IFC2X3":
diff --git a/src/bonsai/bonsai/bim/module/document/operator.py b/src/bonsai/bonsai/bim/module/document/operator.py
index 70c3401fb0..2ed1a0a8d5 100644
--- a/src/bonsai/bonsai/bim/module/document/operator.py
+++ b/src/bonsai/bonsai/bim/module/document/operator.py
@@ -24,7 +24,6 @@ import ifcopenshell.util.element
import bonsai.bim.handler
import bonsai.tool as tool
import bonsai.core.document as core
-from bonsai.bim.ifc import IfcStore
class LoadProjectDocuments(bpy.types.Operator):
@@ -116,7 +115,7 @@ class EditDocument(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
- props = context.scene.BIMDocumentProperties
+ props = tool.Document.get_document_props()
core.edit_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(props.active_document_id))
diff --git a/src/bonsai/bonsai/bim/module/document/prop.py b/src/bonsai/bonsai/bim/module/document/prop.py
index 2f8d32408f..ef4fb29ab9 100644
--- a/src/bonsai/bonsai/bim/module/document/prop.py
+++ b/src/bonsai/bonsai/bim/module/document/prop.py
@@ -30,6 +30,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
+from typing import TYPE_CHECKING
def update_document_name(self: "Document", context: bpy.types.Context) -> None:
@@ -57,6 +58,11 @@ class Document(PropertyGroup):
)
ifc_definition_id: IntProperty(name="IFC Definition ID")
+ if TYPE_CHECKING:
+ identification: str
+ is_information: bool
+ ifc_definition_id: int
+
class BIMDocumentProperties(PropertyGroup):
document_attributes: CollectionProperty(name="Document Attributes", type=Attribute)
@@ -65,3 +71,11 @@ class BIMDocumentProperties(PropertyGroup):
breadcrumbs: CollectionProperty(name="Breadcrumbs", type=StrProperty)
active_document_index: IntProperty(name="Active Document Index")
is_editing: BoolProperty(name="Is Editing", default=False)
+
+ if TYPE_CHECKING:
+ document_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
+ active_document_id: int
+ documents: bpy.types.bpy_prop_collection_idprop[Document]
+ breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty]
+ active_document_index: int
+ is_editing: bool
diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py
index d2cf2e7ab6..4bd935c8f6 100644
--- a/src/bonsai/bonsai/bim/module/document/ui.py
+++ b/src/bonsai/bonsai/bim/module/document/ui.py
@@ -16,8 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+import bonsai.tool as tool
from bpy.types import Panel, UIList
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData
@@ -33,13 +33,13 @@ class BIM_PT_documents(Panel):
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ return tool.Ifc.get()
def draw(self, context):
if not DocumentData.is_loaded:
DocumentData.load()
- self.props = context.scene.BIMDocumentProperties
+ self.props = tool.Document.get_document_props()
row = self.layout.row(align=True)
row.label(text="{} Documents Found".format(DocumentData.data["total_information"]), icon="FILE")
@@ -92,7 +92,7 @@ class BIM_PT_object_documents(Panel):
def poll(cls, context):
if not context.active_object:
return False
- if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
+ if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
@@ -102,8 +102,8 @@ class BIM_PT_object_documents(Panel):
obj = context.active_object
self.oprops = obj.BIMObjectProperties
- self.props = context.scene.BIMDocumentProperties
- self.file = IfcStore.get_file()
+ self.props = tool.Document.get_document_props()
+ self.file = tool.Ifc.get()
self.draw_add_ui()
diff --git a/src/bonsai/bonsai/bim/module/drawing/annotation.py b/src/bonsai/bonsai/bim/module/drawing/annotation.py
index 8bc9ee46d5..9f02989384 100644
--- a/src/bonsai/bonsai/bim/module/drawing/annotation.py
+++ b/src/bonsai/bonsai/bim/module/drawing/annotation.py
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+from __future__ import annotations
import os
import bpy
import math
@@ -125,7 +126,9 @@ class Annotator:
return obj
@staticmethod
- def get_annotation_obj(drawing: ifcopenshell.entity_instance, object_type: str, data_type: str) -> bpy.types.Object:
+ def get_annotation_obj(
+ drawing: ifcopenshell.entity_instance, object_type: str, data_type: tool.Drawing.ANNOTATION_DATA_TYPE
+ ) -> bpy.types.Object:
camera = tool.Ifc.get_object(drawing)
co1, _, _, _ = Annotator.get_placeholder_coords(camera)
matrix_world = tool.Drawing.get_camera_matrix(camera)
@@ -147,11 +150,17 @@ class Annotator:
collection.objects.link(obj)
return obj
- if object_type != "ANGLE":
- for obj in collection.objects:
- element = tool.Ifc.get_entity(obj)
- if element and element.ObjectType == object_type and obj.type == object_type.upper():
- return obj
+ # TODO: remove as outdated?
+ # Is reusing the same objects preventing the creation of new annotations.
+ # if object_type != "ANGLE":
+ # for obj in collection.objects:
+ # element = tool.Ifc.get_entity(obj)
+ # if (
+ # element
+ # and ifcopenshell.util.element.get_predefined_type(element) == object_type
+ # and obj.type == data_type.upper()
+ # ):
+ # return obj
if data_type == "mesh":
data = bpy.data.meshes.new(object_type)
diff --git a/src/bonsai/bonsai/bim/module/drawing/data.py b/src/bonsai/bonsai/bim/module/drawing/data.py
index be252a29c9..30fda54828 100644
--- a/src/bonsai/bonsai/bim/module/drawing/data.py
+++ b/src/bonsai/bonsai/bim/module/drawing/data.py
@@ -88,7 +88,8 @@ class SheetsData:
project = tool.Ifc.get().by_type("IfcProject")[0]
titleblocks_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir")
if not titleblocks_dir:
- titleblocks_dir = bpy.context.scene.DocProperties.titleblocks_dir
+ props = tool.Drawing.get_document_props()
+ titleblocks_dir = props.titleblocks_dir
titleblocks_dir = tool.Ifc.resolve_uri(titleblocks_dir)
if os.path.exists(titleblocks_dir):
files.extend([str(f.stem) for f in Path(titleblocks_dir).glob("*.svg")])
@@ -120,23 +121,25 @@ class DrawingsData:
@classmethod
def location_hint(cls):
- if bpy.context.scene.DocProperties.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
+ props = tool.Drawing.get_document_props()
+ if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
results = [("0", "Origin", "")]
results.extend(
[(str(s.id()), s.Name or "Unnamed", "") for s in tool.Ifc.get().by_type("IfcBuildingStorey")]
)
return results
- elif bpy.context.scene.DocProperties.target_view in ["MODEL_VIEW"]:
+ elif props.target_view in ["MODEL_VIEW"]:
return [(h.upper(), h, "") for h in ["Orthographic", "Perspective"]]
return [(h.upper(), h, "") for h in ["North", "South", "East", "West"]]
@classmethod
def active_drawing_pset_data(cls):
ifc_file = tool.Ifc.get()
- drawing_id = bpy.context.scene.DocProperties.active_drawing_id
+ props = tool.Drawing.get_document_props()
+ drawing_id = props.active_drawing_id
if drawing_id == 0:
return {}
- drawing = ifc_file.by_id(bpy.context.scene.DocProperties.active_drawing_id)
+ drawing = ifc_file.by_id(drawing_id)
return ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
@@ -361,7 +364,11 @@ class DecoratorData:
element = tool.Ifc.get_entity(obj)
supported_object_types = ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS")
- if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in supported_object_types:
+ if (
+ not element
+ or not element.is_a("IfcAnnotation")
+ or ifcopenshell.util.element.get_predefined_type(element) not in supported_object_types
+ ):
return None
dimension_style = "arrow"
diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py
index e8fba1914d..9c26c2b1c5 100644
--- a/src/bonsai/bonsai/bim/module/drawing/decoration.py
+++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py
@@ -423,7 +423,8 @@ class BaseDecorator:
# font_size = 16 <-- this is a good default
# TODO: need to synchronize it better with svg
- magic_font_scale = bpy.context.scene.DocProperties.magic_font_scale
+ props = tool.Drawing.get_document_props()
+ magic_font_scale = props.magic_font_scale
font_size_px = int(magic_font_scale * mm_to_px) * font_size_mm / 2.5
pos = pos - line_no * font_size_px * rotation_matrix[1]
@@ -976,6 +977,7 @@ class FallDecorator(BaseDecorator):
# same function as in svgwriter.py
def get_label_text():
element = tool.Ifc.get_entity(obj)
+ assert element
B, A = [v.co.xyz for v in spline_points[:2]]
rise = abs(A.z - B.z)
O = A.copy()
@@ -988,13 +990,14 @@ class FallDecorator(BaseDecorator):
angle = 90
# ues SLOPE_ANGLE as default
- if element.ObjectType in ("FALL", "SLOPE_ANGLE"):
+ object_type = ifcopenshell.util.element.get_predefined_type(element)
+ if object_type in ("FALL", "SLOPE_ANGLE"):
return f"{angle}°"
- elif element.ObjectType == "SLOPE_FRACTION":
+ elif object_type == "SLOPE_FRACTION":
if angle == 90:
return "-"
return f"{self.format_value(context, rise)} / {self.format_value(context, run)}"
- elif element.ObjectType == "SLOPE_PERCENT":
+ elif object_type == "SLOPE_PERCENT":
if angle == 90:
return "-"
return f"{round(angle_tg * 100)} %"
@@ -2022,7 +2025,8 @@ class DecorationsHandler:
for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.decorators[object_type] = self.decorators["FALL"]
self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"]
- if drawing_font := bpy.context.scene.DocProperties.drawing_font:
+ props = tool.Drawing.get_document_props()
+ if drawing_font := props.drawing_font:
drawing_font_path = tool.Blender.get_data_dir_path(Path("fonts") / drawing_font)
if drawing_font_path.is_file():
font_id = blf.load(drawing_font_path.__str__())
@@ -2045,7 +2049,7 @@ class DecorationsHandler:
if not element.is_a("IfcAnnotation"):
continue
- object_type: Union[str, None] = element.ObjectType
+ object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element)
if object_type == "DRAWING":
continue
diff --git a/src/bonsai/bonsai/bim/module/drawing/gizmos.py b/src/bonsai/bonsai/bim/module/drawing/gizmos.py
index 8e4f9bb97e..952ea0cb96 100644
--- a/src/bonsai/bonsai/bim/module/drawing/gizmos.py
+++ b/src/bonsai/bonsai/bim/module/drawing/gizmos.py
@@ -19,6 +19,7 @@
import bpy
import blf
import gpu
+import bonsai.tool as tool
from bpy import types
from mathutils import Vector
from mathutils import geometry
@@ -478,21 +479,25 @@ class ExtrusionWidget(types.GizmoGroup):
bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"}
@classmethod
- def poll(cls, ctx):
- obj = ctx.object
+ def poll(cls, context):
+ obj = context.active_object
return (
obj
- and obj.type == "MESH"
- and obj.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth") is not None
+ and (data := obj.data)
+ and isinstance(data, bpy.types.Mesh)
+ and tool.Geometry.get_mesh_props(data).ifc_parameters.get("IfcExtrudedAreaSolid/Depth") is not None
)
- def setup(self, ctx):
- target = ctx.object
- prop = target.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
+ def setup(self, context: bpy.types.Context) -> None:
+ target = context.object
+ assert target
+ mesh = target.data
+ assert isinstance(mesh, bpy.types.Mesh)
+ prop = tool.Geometry.get_mesh_props(mesh).ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
basis = target.matrix_world.normalized()
- theme = ctx.preferences.themes[0].user_interface
- scale_value = self.get_scale_value(ctx.scene.unit_settings.system, ctx.scene.unit_settings.length_unit)
+ theme = context.preferences.themes[0].user_interface
+ scale_value = self.get_scale_value(context.scene.unit_settings.system, context.scene.unit_settings.length_unit)
# setup handle
gz = self.handle = self.gizmos.new("BIM_GT_uglydot_3d")
@@ -521,23 +526,26 @@ class ExtrusionWidget(types.GizmoGroup):
# gz.use_draw_modal = True
# gz.target_set_prop('value', target.demo, 'depth')
- def refresh(self, ctx):
+ def refresh(self, context: bpy.types.Context) -> None:
"""updating gizmos"""
- target = ctx.object
+ target = context.active_object
basis = target.matrix_world.normalized()
self.handle.matrix_basis = basis
self.guides.matrix_basis = basis
- def update(self, ctx):
+ def update(self, context: bpy.types.Context) -> None:
"""updating object"""
bpy.ops.bim.update_parametric_representation()
- target = ctx.object
- prop = target.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
+ target = context.active_object
+ assert target
+ mesh = target.data
+ assert isinstance(mesh, bpy.types.Mesh)
+ prop = tool.Geometry.get_mesh_props(mesh).ifc_parameters.get("IfcExtrudedAreaSolid/Depth")
self.handle.target_set_prop("offset", prop, "value")
self.guides.target_set_prop("depth", prop, "value")
@staticmethod
- def get_scale_value(system, length_unit):
+ def get_scale_value(system: str, length_unit: str) -> float:
scale_value = 1
if system == "METRIC":
if length_unit == "KILOMETERS":
diff --git a/src/bonsai/bonsai/bim/module/drawing/handler.py b/src/bonsai/bonsai/bim/module/drawing/handler.py
index abfc8bb261..d1622d1908 100644
--- a/src/bonsai/bonsai/bim/module/drawing/handler.py
+++ b/src/bonsai/bonsai/bim/module/drawing/handler.py
@@ -24,7 +24,8 @@ from bpy.app.handlers import persistent
@persistent
def load_post(*args):
- if bpy.context.scene.DocProperties.should_draw_decorations:
+ props = tool.Drawing.get_document_props()
+ if props.should_draw_decorations:
decoration.DecorationsHandler.install(bpy.context)
else:
decoration.DecorationsHandler.uninstall()
@@ -35,9 +36,11 @@ def depsgraph_update_pre_handler(scene):
set_active_camera_resolution(scene)
-def set_active_camera_resolution(scene):
- if not scene.camera or "/" not in scene.camera.name or not scene.DocProperties.drawings:
+def set_active_camera_resolution(scene: bpy.types.Scene) -> None:
+ props = tool.Drawing.get_document_props()
+ if not scene.camera or "/" not in scene.camera.name or not props.drawings:
return
+ assert isinstance(scene.camera.data, bpy.types.Camera)
props = scene.camera.data.BIMCameraProperties
ortho_scale = max((props.width, props.height))
aspect_ratio = props.width / props.height
@@ -60,5 +63,3 @@ def set_active_camera_resolution(scene):
scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x = int(raster_x)
scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y = int(raster_y)
-
- current_drawing = scene.DocProperties.drawings[scene.DocProperties.current_drawing_index]
diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py
index 5bd1661bfa..c63fd3d910 100644
--- a/src/bonsai/bonsai/bim/module/drawing/helper.py
+++ b/src/bonsai/bonsai/bim/module/drawing/helper.py
@@ -19,8 +19,10 @@
import bpy
import math
import mathutils.geometry
+import ifcopenshell
import bonsai.tool as tool
from mathutils import Vector
+from typing import Union
# Code taken and updated from https://blenderartists.org/t/detecting-intersection-of-bounding-boxes/457520/2
@@ -135,6 +137,9 @@ def format_distance(
scaleFactor = bpy.context.scene.unit_settings.scale_length
unit_system = bpy.context.scene.unit_settings.system
unit_length = bpy.context.scene.unit_settings.length_unit
+ area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(
+ ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT")
+ )
value *= scaleFactor
@@ -225,7 +230,24 @@ def format_distance(
if add_inches or frac:
tx_dist += '"'
else:
- tx_dist = str("%1.3f" % (value * toInches / inPerFoot)) + " sq. ft."
+ fmt = "%1.3f"
+ sq_feet = round(value * toInches / inPerFoot, 4)
+ tx_dist = ""
+ if area_unit_symbol == " ft2":
+ fmt += area_unit_symbol
+ tx_dist = fmt % sq_feet
+ if area_unit_symbol == " in2":
+ sq_inch = sq_feet * 144
+ fmt += area_unit_symbol
+ tx_dist = fmt % sq_inch
+ if area_unit_symbol == " yd2":
+ sq_yard = sq_feet / 9
+ fmt += area_unit_symbol
+ tx_dist = fmt % sq_yard
+ if area_unit_symbol == " mi2":
+ sq_mile = sq_feet / 27878400
+ fmt += area_unit_symbol
+ tx_dist = fmt % sq_mile
# METRIC FORMATTING
elif unit_system == "METRIC":
@@ -287,16 +309,38 @@ def format_distance(
d_mm = value * (1000)
tx_dist = fmt % d_mm
if isArea:
- tx_dist += s_code
+ if area_unit_symbol == " m2":
+ if decimal_places is None:
+ fmt = "%1.3f"
+ if hide_units is False:
+ fmt += area_unit_symbol
+ tx_dist = fmt % value
+ if area_unit_symbol == " cm2":
+ if decimal_places is None:
+ fmt = "%1.1f"
+ if hide_units is False:
+ fmt += area_unit_symbol
+ d_cm = value * (10000)
+ tx_dist = fmt % d_cm
+ if area_unit_symbol == " mm2":
+ if decimal_places is None:
+ fmt = "%1.0f"
+ if hide_units is False:
+ fmt += area_unit_symbol
+ d_cm = value * (1000000)
+ tx_dist = fmt % d_cm
+
else:
tx_dist = fmt % value
return tx_dist
-def get_active_drawing(scene):
+def get_active_drawing(
+ scene: bpy.types.Scene,
+) -> Union[tuple[bpy.types.Collection, bpy.types.Camera], tuple[None, None]]:
"""Get active drawing collection and camera"""
- props = scene.DocProperties
+ props = tool.Drawing.get_document_props()
try:
camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id))
return camera.BIMObjectProperties.collection, camera
diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py
index 950d701e4a..3528e4dc36 100644
--- a/src/bonsai/bonsai/bim/module/drawing/operator.py
+++ b/src/bonsai/bonsai/bim/module/drawing/operator.py
@@ -134,19 +134,19 @@ class AddDrawing(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Add a drawing view to the IFC project"
def _execute(self, context):
- self.props = context.scene.DocProperties
- hint = self.props.location_hint
- if self.props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
+ props = tool.Drawing.get_document_props()
+ hint = props.location_hint
+ if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]:
hint = int(hint)
core.add_drawing(
tool.Ifc,
tool.Collector,
tool.Drawing,
- target_view=self.props.target_view,
+ target_view=props.target_view,
location_hint=hint,
)
try:
- drawing = tool.Ifc.get().by_id(self.props.active_drawing_id)
+ drawing = tool.Ifc.get().by_id(props.active_drawing_id)
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
except:
pass
@@ -162,7 +162,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -176,7 +176,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
row.prop(self, "should_duplicate_annotations")
def _execute(self, context):
- self.props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
core.duplicate_drawing(
tool.Ifc,
tool.Drawing,
@@ -184,7 +184,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
should_duplicate_annotations=self.should_duplicate_annotations,
)
try:
- drawing = tool.Ifc.get().by_id(self.props.active_drawing_id)
+ drawing = tool.Ifc.get().by_id(props.active_drawing_id)
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
except:
pass
@@ -244,7 +244,7 @@ class CreateDrawing(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
- self.props = context.scene.DocProperties
+ self.props = tool.Drawing.get_document_props()
active_drawing_id = context.scene.camera.BIMObjectProperties.ifc_definition_id
if self.print_all:
@@ -261,7 +261,7 @@ class CreateDrawing(bpy.types.Operator):
self.camera = context.scene.camera
self.camera_element = tool.Ifc.get_entity(self.camera)
self.camera_document = tool.Drawing.get_drawing_document(self.camera_element)
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
with profile("Drawing generation process"):
with profile("Initialize drawing generation process"):
@@ -385,7 +385,7 @@ class CreateDrawing(bpy.types.Operator):
obj.hide_render = obj.name not in visible_object_names
context.scene.render.filepath = str(Path(svg_path).with_suffix(".png"))
- drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index]
+ drawing_style = self.props.drawing_styles[self.cprops.active_drawing_style_index]
if drawing_style.render_type == "DEFAULT":
bpy.ops.render.render(write_still=True)
@@ -712,9 +712,11 @@ class CreateDrawing(bpy.types.Operator):
}
cached_linework -= edited_guids
- files = {context.scene.BIMProperties.ifc_file: tool.Ifc.get()}
+ bim_props = tool.Blender.get_bim_props()
+ files = {bim_props.ifc_file: tool.Ifc.get()}
- for link in context.scene.BIMProjectProperties.links:
+ props = tool.Project.get_project_props()
+ for link in props.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
files[link.name] = IfcStore.session_files[link.name]
@@ -729,7 +731,7 @@ class CreateDrawing(bpy.types.Operator):
# Don't use draw.main() just whilst we're prototyping and experimenting
# TODO: hash paths are never used
ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest()
- ifc_cache_path = os.path.join(context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5")
+ ifc_cache_path = os.path.join(bim_props.cache_dir, f"{ifc_hash}.h5")
self.serialiser.setFile(ifc)
drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc)
@@ -1141,7 +1143,8 @@ class CreateDrawing(bpy.types.Operator):
try:
return tool.Ifc.get().by_guid(guid)
except:
- for link in bpy.context.scene.BIMProjectProperties.links:
+ props = tool.Project.get_project_props()
+ for link in props.links:
if link.name not in IfcStore.session_files:
IfcStore.session_files[link.name] = ifcopenshell.open(link.name)
try:
@@ -1367,7 +1370,11 @@ class CreateDrawing(bpy.types.Operator):
elements = list(elements | filtered_drawing_annotations)
annotations = sorted(
- elements, key=lambda a: (tool.Drawing.get_annotation_z_index(a), 1 if a.ObjectType == "TEXT" else 0)
+ elements,
+ key=lambda a: (
+ tool.Drawing.get_annotation_z_index(a),
+ 1 if ifcopenshell.util.element.get_predefined_type(a) == "TEXT" else 0,
+ ),
)
precision = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "MetricPrecision")
@@ -1458,7 +1465,8 @@ class AddSheet(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Add a sheet to the project"
def _execute(self, context):
- core.add_sheet(tool.Ifc, tool.Drawing, titleblock=context.scene.DocProperties.titleblock)
+ props = tool.Drawing.get_document_props()
+ core.add_sheet(tool.Ifc, tool.Drawing, titleblock=props.titleblock)
class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator):
@@ -1474,7 +1482,7 @@ class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator):
cls.poll_message_set("Not implemented yet.")
return False
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -1483,7 +1491,7 @@ class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
pass
"""
- self.props = context.scene.DocProperties
+ self.props = tool.Drawing.get_document_props()
core.duplicate_sheet(
tool.Ifc,
tool.Drawing,
@@ -1508,7 +1516,7 @@ class OpenLayout(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
- self.props = context.scene.DocProperties
+ self.props = tool.Drawing.get_document_props()
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
sheet_builder = sheeter.SheetBuilder()
sheet_builder.update_sheet_drawing_sizes(sheet)
@@ -1530,7 +1538,8 @@ class SelectAllSheets(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
- for sheet in context.scene.DocProperties.sheets:
+ props = tool.Drawing.get_document_props()
+ for sheet in props.sheets:
if sheet.is_selected != self.select_all:
sheet.is_selected = self.select_all
return {"FINISHED"}
@@ -1550,7 +1559,7 @@ class OpenSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_sheet_item(is_sheet=True):
cls.poll_message_set("No sheet selected.")
return False
@@ -1564,7 +1573,7 @@ class OpenSheet(bpy.types.Operator, tool.Ifc.Operator):
return self.execute(context)
def execute(self, context):
- self.props = context.scene.DocProperties
+ self.props = tool.Drawing.get_document_props()
svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command
if self.open_all:
@@ -1612,9 +1621,10 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
# Won't be visible in UI anyway.
- if not props.sheets or not context.scene.BIMProperties.data_dir:
+ bim_props = tool.Blender.get_bim_props()
+ if not props.sheets or not bim_props.data_dir:
return False
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
@@ -1622,8 +1632,8 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
return True
def _execute(self, context):
- props = context.scene.DocProperties
- active_drawing = tool.Drawing.get_active_drawing_item()
+ props = tool.Drawing.get_document_props()
+ active_drawing = props.drawings[props.active_drawing_index]
assert active_drawing
active_sheet = tool.Drawing.get_active_sheet(context)
@@ -1680,7 +1690,7 @@ class RemoveDrawingFromSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
active_item = tool.Drawing.get_active_sheet_item()
if active_item is None:
return False
@@ -1714,11 +1724,12 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_sheet_item(is_sheet=True):
cls.poll_message_set("No sheet selected.")
return False
- return props.sheets and context.scene.BIMProperties.data_dir
+ bim_props = tool.Blender.get_bim_props()
+ return props.sheets and bim_props.data_dir
def invoke(self, context, event):
# opening all sheets on shift+click
@@ -1731,7 +1742,7 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
scene = context.scene
- props = scene.DocProperties
+ props = tool.Drawing.get_document_props()
svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command
svg2dxf_command = tool.Blender.get_addon_preferences().svg2dxf_command
@@ -1838,7 +1849,8 @@ class SelectAllDrawings(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
- for drawing in context.scene.DocProperties.drawings:
+ props = tool.Drawing.get_document_props()
+ for drawing in props.drawings:
if drawing.is_selected != self.select_all:
drawing.is_selected = self.select_all
return {"FINISHED"}
@@ -1857,7 +1869,7 @@ class OpenDrawing(bpy.types.Operator):
@classmethod
def poll(cls, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -1871,7 +1883,7 @@ class OpenDrawing(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
- self.props = context.scene.DocProperties
+ self.props = tool.Drawing.get_document_props()
if self.open_all:
drawings = [
tool.Ifc.get().by_id(d.ifc_definition_id) for d in self.props.drawings if d.is_drawing and d.is_selected
@@ -1907,7 +1919,7 @@ class ActivateModel(bpy.types.Operator):
bl_description = "Activate the model view, hide all annotations"
def execute(self, context):
- dprops = bpy.context.scene.DocProperties
+ dprops = tool.Drawing.get_document_props()
dprops.active_drawing_id = 0
CutDecorator.uninstall()
@@ -1971,11 +1983,12 @@ class ActivateDrawingBase:
return self.execute(context)
def execute(self, context):
- if bpy.context.scene.DocProperties.is_editing_drawings == False:
+ props = tool.Drawing.get_document_props()
+ if props.is_editing_drawings == False:
bpy.ops.bim.load_drawings()
drawing = tool.Ifc.get().by_id(self.drawing)
- dprops = bpy.context.scene.DocProperties
+ dprops = tool.Drawing.get_document_props()
if self.use_quick_preview:
tool.Blender.activate_camera(tool.Drawing.import_temporary_drawing_camera(drawing))
@@ -2027,7 +2040,7 @@ class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase):
@classmethod
def poll(cls, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -2050,7 +2063,7 @@ class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase):
@classmethod
def poll(cls, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_sheet_item(reference_type="DRAWING"):
cls.poll_message_set("No drawing selected.")
return False
@@ -2066,7 +2079,8 @@ class SelectDocIfcFile(bpy.types.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
- context.scene.DocProperties.ifc_files[self.index].name = self.filepath
+ props = tool.Drawing.get_document_props()
+ props.ifc_files[self.index].name = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
@@ -2098,7 +2112,7 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -2112,12 +2126,10 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator):
return self.execute(context)
def _execute(self, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
if self.remove_all:
drawings = [
- tool.Ifc.get().by_id(d.ifc_definition_id)
- for d in context.scene.DocProperties.drawings
- if d.is_drawing and d.is_selected
+ tool.Ifc.get().by_id(d.ifc_definition_id) for d in props.drawings if d.is_drawing and d.is_selected
]
else:
if not self.drawing:
@@ -2187,7 +2199,8 @@ class ReloadDrawingStyles(bpy.types.Operator):
with open(json_path, "r") as fi:
shading_styles_json = json.load(fi)
- drawing_styles = context.scene.DocProperties.drawing_styles
+ props = tool.Drawing.get_document_props()
+ drawing_styles = props.drawing_styles
drawing_styles.clear()
styles = [style for style in shading_styles_json]
for style_name in styles:
@@ -2212,7 +2225,8 @@ class AddDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
- drawing_styles = context.scene.DocProperties.drawing_styles
+ props = tool.Drawing.get_document_props()
+ drawing_styles = props.drawing_styles
new = drawing_styles.add()
# drawing style is saved to ifc on rename
new.name = tool.Blender.ensure_unique_name("New Drawing Style", drawing_styles)
@@ -2227,7 +2241,8 @@ class RemoveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
index: bpy.props.IntProperty()
def execute(self, context):
- context.scene.DocProperties.drawing_styles.remove(self.index)
+ props = tool.Drawing.get_document_props()
+ props.drawing_styles.remove(self.index)
context.scene.camera.data.BIMCameraProperties.active_drawing_style_index = max(self.index - 1, 0)
bpy.ops.bim.save_drawing_styles_data()
return {"FINISHED"}
@@ -2283,7 +2298,8 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
index = int(self.index)
else:
index = context.scene.camera.data.BIMCameraProperties.active_drawing_style_index
- scene.DocProperties.drawing_styles[index].raster_style = json.dumps(style)
+ props = tool.Drawing.get_document_props()
+ props.drawing_styles[index].raster_style = json.dumps(style)
bpy.ops.bim.save_drawing_styles_data()
return {"FINISHED"}
@@ -2309,7 +2325,8 @@ class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator):
if not DrawingsData.is_loaded:
DrawingsData.load()
drawing_pset_data = DrawingsData.data["active_drawing_pset_data"]
- drawing_styles = context.scene.DocProperties.drawing_styles
+ props = tool.Drawing.get_document_props()
+ drawing_styles = props.drawing_styles
rel_path = drawing_pset_data["ShadingStyles"]
current_style = drawing_pset_data.get("CurrentShadingStyle", None)
@@ -2338,7 +2355,7 @@ class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator):
new_style_name = None
ifc_file = tool.Ifc.get()
- drawing = ifc_file.by_id(context.scene.DocProperties.active_drawing_id)
+ drawing = ifc_file.by_id(props.active_drawing_id)
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
ifcopenshell.api.run(
"pset.edit_pset", ifc_file, pset=pset, properties={"CurrentShadingStyle": new_style_name}
@@ -2357,17 +2374,18 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
scene = context.scene
ifc_file = tool.Ifc.get()
active_drawing_style_index = scene.camera.data.BIMCameraProperties.active_drawing_style_index
+ props = tool.Drawing.get_document_props()
- if active_drawing_style_index >= len(scene.DocProperties.drawing_styles):
+ if active_drawing_style_index >= len(props.drawing_styles):
self.report({"ERROR"}, "Could not find active drawing style")
return {"CANCELLED"}
- self.drawing_style = scene.DocProperties.drawing_styles[active_drawing_style_index]
+ self.drawing_style = props.drawing_styles[active_drawing_style_index]
self.set_raster_style(context)
self.set_query(context)
- drawing = ifc_file.by_id(scene.DocProperties.active_drawing_id)
+ drawing = ifc_file.by_id(props.active_drawing_id)
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
ifcopenshell.api.run(
"pset.edit_pset", ifc_file, pset=pset, properties={"CurrentShadingStyle": self.drawing_style.name}
@@ -2392,7 +2410,8 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
def set_query(self, context: bpy.types.Context) -> None:
self.include_global_ids = []
self.exclude_global_ids = []
- for ifc_file in context.scene.DocProperties.ifc_files:
+ props = tool.Drawing.get_document_props()
+ for ifc_file in props.ifc_files:
try:
ifc = ifcopenshell.open(ifc_file.name)
except:
@@ -2507,14 +2526,15 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
if not props.schedules:
cls.poll_message_set("No schedule selected.")
return False
- return props.schedules and props.sheets and context.scene.BIMProperties.data_dir
+ bim_props = tool.Blender.get_bim_props()
+ return props.schedules and props.sheets and bim_props.data_dir
def _execute(self, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
active_schedule = props.schedules[props.active_schedule_index]
active_sheet = tool.Drawing.get_active_sheet(context)
schedule = tool.Ifc.get().by_id(active_schedule.ifc_definition_id)
@@ -2573,14 +2593,15 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
if not props.references:
cls.poll_message_set("No reference selected.")
return False
- return props.references and props.sheets and context.scene.BIMProperties.data_dir
+ bim_props = tool.Blender.get_bim_props()
+ return props.references and props.sheets and bim_props.data_dir
def _execute(self, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
active_reference = props.references[props.active_reference_index]
active_sheet = tool.Drawing.get_active_sheet(context)
extref = tool.Ifc.get().by_id(active_reference.ifc_definition_id)
@@ -2682,7 +2703,8 @@ class AddDrawingStyleAttribute(bpy.types.Operator):
def execute(self, context):
props = context.scene.camera.data.BIMCameraProperties
- context.scene.DocProperties.drawing_styles[props.active_drawing_style_index].attributes.add()
+ dprops = tool.Drawing.get_document_props()
+ dprops.drawing_styles[props.active_drawing_style_index].attributes.add()
return {"FINISHED"}
@@ -2695,7 +2717,8 @@ class RemoveDrawingStyleAttribute(bpy.types.Operator):
def execute(self, context):
props = context.scene.camera.data.BIMCameraProperties
- context.scene.DocProperties.drawing_styles[props.active_drawing_style_index].attributes.remove(self.index)
+ dprops = tool.Drawing.get_document_props()
+ dprops.drawing_styles[props.active_drawing_style_index].attributes.remove(self.index)
return {"FINISHED"}
@@ -2979,7 +3002,7 @@ class LoadSheets(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
core.load_sheets(tool.Drawing)
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
sheets_not_found = []
for sheet_prop in props.sheets:
if not sheet_prop.is_sheet:
@@ -3009,7 +3032,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
document_type: Literal["SHEET", "TITLEBLOCK", "EMBEDDED"]
def invoke(self, context, event):
- self.props = context.scene.DocProperties
+ self.props = tool.Drawing.get_document_props()
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
if sheet.is_a("IfcDocumentInformation"):
self.document_type = "SHEET"
@@ -3023,6 +3046,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
+ props = tool.Drawing.get_document_props()
if self.document_type == "SHEET":
row = self.layout.row()
row.prop(self, "identification", text="Identification")
@@ -3030,13 +3054,13 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator):
row.prop(self, "name", text="Name")
elif self.document_type == "TITLEBLOCK":
row = self.layout.row()
- row.prop(context.scene.DocProperties, "titleblock", text="Titleblock")
+ row.prop(props, "titleblock", text="Titleblock")
elif self.document_type == "EMBEDDED":
row = self.layout.row()
row.prop(self, "identification", text="Identification")
def _execute(self, context):
- self.props = context.scene.DocProperties
+ self.props = tool.Drawing.get_document_props()
sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)
if self.document_type == "SHEET":
core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name)
@@ -3134,7 +3158,7 @@ class ExpandTargetView(bpy.types.Operator):
target_view: bpy.props.StringProperty()
def execute(self, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
for drawing in [d for d in props.drawings if d.target_view == self.target_view]:
drawing.is_expanded = True
core.load_drawings(tool.Drawing)
@@ -3150,7 +3174,7 @@ class ContractTargetView(bpy.types.Operator):
target_view: bpy.props.StringProperty()
def execute(self, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
for drawing in [d for d in props.drawings if d.target_view == self.target_view]:
drawing.is_expanded = False
core.load_drawings(tool.Drawing)
@@ -3166,7 +3190,7 @@ class ExpandSheet(bpy.types.Operator):
sheet: bpy.props.IntProperty()
def execute(self, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]:
sheet.is_expanded = True
core.load_sheets(tool.Drawing)
@@ -3182,7 +3206,7 @@ class ContractSheet(bpy.types.Operator):
sheet: bpy.props.IntProperty()
def execute(self, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]:
sheet.is_expanded = False
core.load_sheets(tool.Drawing)
@@ -3373,7 +3397,7 @@ class ConvertSVGToDXF(bpy.types.Operator):
@classmethod
def poll(cls, context):
- props = context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.")
return False
@@ -3387,14 +3411,13 @@ class ConvertSVGToDXF(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
+ props = tool.Drawing.get_document_props()
if self.convert_all:
drawings = [
- tool.Ifc.get().by_id(d.ifc_definition_id)
- for d in context.scene.DocProperties.drawings
- if d.is_drawing and d.is_selected
+ tool.Ifc.get().by_id(d.ifc_definition_id) for d in props.drawings if d.is_drawing and d.is_selected
]
else:
- drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)]
+ drawings = [tool.Ifc.get().by_id(props.drawings.get(self.view).ifc_definition_id)]
drawing_uris: list[Path] = []
drawings_not_found: list[str] = []
diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py
index 02ce195100..6c875d1cfe 100644
--- a/src/bonsai/bonsai/bim/module/drawing/prop.py
+++ b/src/bonsai/bonsai/bim/module/drawing/prop.py
@@ -76,7 +76,7 @@ def update_diagram_scale(self, context):
try:
element = (
tool.Ifc.get()
- .by_id(self.id_data.BIMMeshProperties.ifc_definition_id)
+ .by_id(tool.Geometry.get_mesh_props(self.id_data).ifc_definition_id)
.OfProductRepresentation[0]
.ShapeOfProduct[0]
)
@@ -93,7 +93,7 @@ def update_diagram_scale(self, context):
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=diagram_scale)
-def update_is_nts(self, context):
+def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> None:
if not self.update_props:
return
if not context.scene.camera or context.scene.camera.data != self.id_data:
@@ -104,7 +104,7 @@ def update_is_nts(self, context):
try:
element = (
tool.Ifc.get()
- .by_id(self.id_data.BIMMeshProperties.ifc_definition_id)
+ .by_id(tool.Geometry.get_mesh_props(self.id_data).ifc_definition_id)
.OfProductRepresentation[0]
.ShapeOfProduct[0]
)
@@ -192,8 +192,8 @@ def get_drawing_style_name(self: "DrawingStyle"):
def set_drawing_style_name(self: "DrawingStyle", new_value: str) -> None:
"""ensure the name is unique"""
- scene = bpy.context.scene
- drawing_styles = [s.name for s in scene.DocProperties.drawing_styles if s.name != self.name]
+ props = tool.Drawing.get_document_props()
+ drawing_styles = [s.name for s in props.drawing_styles if s.name != self.name]
new_value = tool.Blender.ensure_unique_name(new_value, drawing_styles)
old_value = self.name
self["name"] = new_value
diff --git a/src/bonsai/bonsai/bim/module/drawing/scheduler.py b/src/bonsai/bonsai/bim/module/drawing/scheduler.py
index d00fd1d94f..00ce2c1612 100644
--- a/src/bonsai/bonsai/bim/module/drawing/scheduler.py
+++ b/src/bonsai/bonsai/bim/module/drawing/scheduler.py
@@ -56,7 +56,7 @@ def a1_to_rc(cell):
class Scheduler:
- def schedule(self, infile, outfile):
+ def schedule(self, infile: str, outfile: str) -> None:
self.svg = svgwrite.Drawing(
outfile,
debug=False,
@@ -71,11 +71,12 @@ class Scheduler:
elif infile.endswith("xlsx"):
self.schedule_xlsx(infile, outfile)
- def parse_css(self, infile):
+ def parse_css(self, infile: str) -> None:
+ props = tool.Drawing.get_document_props()
stylesheet_path = os.path.splitext(infile)[0] + ".css"
if not os.path.exists(stylesheet_path):
- stylesheet_rel_path = getattr(bpy.context.scene.DocProperties, "schedules_stylesheet_path")
- ifc_file_path = os.path.dirname(IfcStore.path)
+ stylesheet_rel_path = props.schedules_stylesheet_path
+ ifc_file_path = os.path.dirname(tool.Ifc.get_path())
stylesheet_path = ifc_file_path + "\\" + stylesheet_rel_path
if not os.path.exists(stylesheet_path):
stylesheet_path = tool.Blender.get_data_dir_path(Path("assets") / "schedule.css")
@@ -91,7 +92,7 @@ class Scheduler:
self.svg.defs.add(self.svg.style(css))
- def schedule_xlsx(self, infile, outfile):
+ def schedule_xlsx(self, infile: str, outfile: str) -> None:
workbook = openpyxl.open(infile, data_only=True)
sheet = workbook.active
@@ -236,7 +237,7 @@ class Scheduler:
self.svg["viewBox"] = "0 0 {} {}".format(total_width, total_height)
self.svg.save(pretty=True)
- def schedule_ods(self, infile, outfile):
+ def schedule_ods(self, infile: str, outfile: str) -> None:
doc = load_ods(infile)
# useful for debugging ods
@@ -495,11 +496,11 @@ class Scheduler:
self.svg["viewBox"] = "0 0 {} {}".format(total_width, total_height)
self.svg.save(pretty=True)
- def get_style(self, style_name, styles):
+ def get_style(self, style_name: str, styles: dict) -> dict:
style = styles[style_name] if style_name else {}
return style
- def get_box_alignment(self, style):
+ def get_box_alignment(self, style: dict) -> str:
if style and "vertical-align" in style and style["vertical-align"] != "automatic":
vertical_align = style["vertical-align"]
else:
diff --git a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py
index e6dd66150a..9a249450f7 100644
--- a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py
+++ b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py
@@ -181,35 +181,35 @@ class SvgWriter:
self.decimal_places = decimal_places
for element in annotations:
obj = tool.Ifc.get_object(element)
- if not obj or element.ObjectType == "DRAWING":
+ if not obj or (object_type := ifcopenshell.util.element.get_predefined_type(element)) == "DRAWING":
continue
- elif element.ObjectType == "GRID":
+ elif object_type == "GRID":
self.draw_grid_annotation(obj)
- elif element.ObjectType == "TEXT_LEADER":
+ elif object_type == "TEXT_LEADER":
self.draw_leader_annotation(obj)
- elif element.ObjectType == "STAIR_ARROW":
+ elif object_type == "STAIR_ARROW":
self.draw_stair_annotation(obj)
- elif element.ObjectType == "DIMENSION":
+ elif object_type == "DIMENSION":
self.draw_dimension_annotations(obj)
- elif element.ObjectType == "ANGLE":
+ elif object_type == "ANGLE":
self.draw_angle_annotations(obj)
- elif element.ObjectType == "RADIUS":
+ elif object_type == "RADIUS":
self.draw_radius_annotations(obj)
- elif element.ObjectType == "DIAMETER":
+ elif object_type == "DIAMETER":
self.draw_diameter_annotations(obj)
- elif element.ObjectType == "ELEVATION":
+ elif object_type == "ELEVATION":
self.draw_elevation_annotation(obj)
- elif element.ObjectType == "SECTION":
+ elif object_type == "SECTION":
self.draw_section_annotation(obj)
- elif element.ObjectType == "BREAKLINE":
+ elif object_type == "BREAKLINE":
self.draw_break_annotations(obj)
- elif element.ObjectType == "PLAN_LEVEL":
+ elif object_type == "PLAN_LEVEL":
self.draw_plan_level_annotation(obj)
- elif element.ObjectType == "SECTION_LEVEL":
+ elif object_type == "SECTION_LEVEL":
self.draw_section_level_annotation(obj)
- elif element.ObjectType == "TEXT":
+ elif object_type == "TEXT":
self.draw_text_annotation(obj, obj.location)
- elif element.ObjectType in ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
+ elif object_type in ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"):
self.draw_fall_annotations(obj)
else:
self.draw_misc_annotation(obj)
@@ -1207,13 +1207,14 @@ class SvgWriter:
angle = 90
# ues SLOPE_ANGLE as default
- if element.ObjectType in ("FALL", "SLOPE_ANGLE"):
+ object_type = ifcopenshell.util.element.get_predefined_type(element)
+ if object_type in ("FALL", "SLOPE_ANGLE"):
return f"{angle}°"
- elif element.ObjectType == "SLOPE_FRACTION":
+ elif object_type == "SLOPE_FRACTION":
if angle == 90:
return "-"
return f"{helper.format_distance(rise, precision=self.precision, decimal_places=self.decimal_places)} / {helper.format_distance(run, precision=self.precision, decimal_places=self.decimal_places)}"
- elif element.ObjectType == "SLOPE_PERCENT":
+ elif object_type == "SLOPE_PERCENT":
if angle == 90:
return "-"
return f"{round(angle_tg * 100)} %"
diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py
index f35fc55325..6bf62ddec2 100644
--- a/src/bonsai/bonsai/bim/module/drawing/ui.py
+++ b/src/bonsai/bonsai/bim/module/drawing/ui.py
@@ -49,7 +49,7 @@ class BIM_PT_camera(Panel):
return
self.layout.use_property_split = True
- dprops = context.scene.DocProperties
+ dprops = tool.Drawing.get_document_props()
props = context.scene.camera.data.BIMCameraProperties
col = self.layout.column(align=True)
@@ -161,7 +161,7 @@ class BIM_PT_drawing_underlay(Panel):
layout.use_property_split = True
camera = context.scene.camera
assert camera
- dprops = context.scene.DocProperties
+ dprops = tool.Drawing.get_document_props()
props = camera.data.BIMCameraProperties
drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles)
@@ -229,7 +229,7 @@ class BIM_PT_drawings(Panel):
draw_project_not_saved_ui(self)
return
- self.props = context.scene.DocProperties
+ self.props = tool.Drawing.get_document_props()
if not self.props.is_editing_drawings:
row = self.layout.row(align=True)
@@ -302,7 +302,7 @@ class BIM_PT_schedules(Panel):
draw_project_not_saved_ui(self)
return
- self.props = context.scene.DocProperties
+ self.props = tool.Drawing.get_document_props()
if not self.props.is_editing_schedules:
row = self.layout.row(align=True)
@@ -352,7 +352,7 @@ class BIM_PT_references(Panel):
draw_project_not_saved_ui(self)
return
- self.props = context.scene.DocProperties
+ self.props = tool.Drawing.get_document_props()
if not self.props.is_editing_references:
row = self.layout.row(align=True)
@@ -394,7 +394,7 @@ class BIM_PT_sheets(Panel):
draw_project_not_saved_ui(self)
return
- self.props = context.scene.DocProperties
+ self.props = tool.Drawing.get_document_props()
if not self.props.is_editing_sheets:
row = self.layout.row(align=True)
@@ -601,7 +601,7 @@ class BIM_UL_drawinglist(bpy.types.UIList):
selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT"
row.prop(item, "is_selected", text="", icon=selected_icon, emboss=False)
row.prop(item, "name", text="", emboss=False)
- self.props = context.scene.DocProperties
+ self.props = tool.Drawing.get_document_props()
if (
self.props.drawings
and self.props.active_drawing_id
diff --git a/src/bonsai/bonsai/bim/module/drawing/workspace.py b/src/bonsai/bonsai/bim/module/drawing/workspace.py
index 30d57bf3db..16d26ea7e0 100644
--- a/src/bonsai/bonsai/bim/module/drawing/workspace.py
+++ b/src/bonsai/bonsai/bim/module/drawing/workspace.py
@@ -19,6 +19,7 @@
import os
import bpy
+import ifcopenshell.util.element
import bonsai.core.type
import bonsai.core.drawing as core
import bonsai.tool as tool
@@ -197,6 +198,8 @@ def create_annotation_occurrence(context):
class AnnotationToolUI:
+ layout: bpy.types.UILayout
+
@classmethod
def draw(cls, context, layout):
cls.layout = layout
@@ -224,7 +227,8 @@ class AnnotationToolUI:
@classmethod
def draw_create_object_interface(cls):
row = cls.layout.row(align=True)
- row.prop(bpy.context.scene.DocProperties, "should_draw_decorations", text="Viewport Annotations")
+ props = tool.Drawing.get_document_props()
+ row.prop(props, "should_draw_decorations", text="Viewport Annotations")
@classmethod
def draw_edit_object_interface(cls, context):
@@ -330,7 +334,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not element or not element.is_a("IfcAnnotation"):
continue
- annotation_type = element.ObjectType
+ annotation_type = ifcopenshell.util.element.get_predefined_type(element)
if annotation_type not in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
self.report({"ERROR"}, f"Annotation type {annotation_type} is not supported for readjustment.")
continue
diff --git a/src/bonsai/bonsai/bim/module/fm/ui.py b/src/bonsai/bonsai/bim/module/fm/ui.py
index 1e76ec61c3..89cd764adf 100644
--- a/src/bonsai/bonsai/bim/module/fm/ui.py
+++ b/src/bonsai/bonsai/bim/module/fm/ui.py
@@ -18,7 +18,6 @@
import bonsai.tool as tool
from bpy.types import Panel
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.fm.data import FMData
@@ -40,11 +39,11 @@ class BIM_PT_fm(Panel):
scene = context.scene
props = scene.BIMFMProperties
- if IfcStore.get_file():
+ if tool.Ifc.get():
row = layout.row()
row.prop(props, "should_load_from_memory")
- if not IfcStore.get_file() or not props.should_load_from_memory:
+ if not tool.Ifc.get() or not props.should_load_from_memory:
row = layout.row()
props.ifc_files.layout_file_select(row, "*.ifc;*.ifczip;*.ifcxml", "IFC File(s)")
diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py
index 533cdcd6bf..4e9eb9cf6f 100644
--- a/src/bonsai/bonsai/bim/module/geometry/__init__.py
+++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py
@@ -98,12 +98,14 @@ addon_keymaps = []
@persistent
-def block_scale(scene):
+def block_scale(scene: bpy.types.Scene) -> None:
+ import bonsai.tool as tool
+
if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active):
if isinstance(obj, bpy.types.Object) and obj.BIMObjectProperties.ifc_definition_id:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
- elif isinstance(obj, bpy.types.Mesh) and obj.BIMMeshProperties.ifc_definition_id:
+ elif isinstance(obj, bpy.types.Mesh) and tool.Geometry.get_mesh_props(obj).ifc_definition_id:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
diff --git a/src/bonsai/bonsai/bim/module/geometry/data.py b/src/bonsai/bonsai/bim/module/geometry/data.py
index a719d27893..2ba25df702 100644
--- a/src/bonsai/bonsai/bim/module/geometry/data.py
+++ b/src/bonsai/bonsai/bim/module/geometry/data.py
@@ -53,15 +53,16 @@ class ViewportData:
obj = bpy.context.active_object
element = tool.Ifc.get_entity(obj)
- modes = [obj_mode]
-
- if bpy.context.scene.BIMGeometryProperties.representation_obj:
+ modes: list[tuple[str, str, str, str, int]] = [obj_mode]
+ gprops = tool.Geometry.get_geometry_props()
+ if gprops.representation_obj:
modes.append(item_mode)
if not obj:
return modes
- if obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs:
+ pprops = tool.Project.get_project_props()
+ if obj in pprops.clipping_planes_objs:
pass
elif element:
if tool.Geometry.is_locked(element):
@@ -107,8 +108,9 @@ class RepresentationsData:
element = tool.Ifc.get_entity(obj)
active_representation_id = None
- if obj.data and hasattr(obj.data, "BIMMeshProperties"):
- active_representation_id = obj.data.BIMMeshProperties.ifc_definition_id
+ active_representation = tool.Geometry.get_active_representation(obj)
+ if active_representation:
+ active_representation_id = active_representation.id()
for representation in tool.Geometry.get_representations_iter(element):
representation_type = representation.RepresentationType
@@ -158,9 +160,9 @@ class RepresentationsData:
if not obj.data:
return []
element = tool.Ifc.get_entity(obj)
- if not (active_representation_id := obj.data.BIMMeshProperties.ifc_definition_id):
+ base_representation = tool.Geometry.get_active_representation(obj)
+ if not base_representation:
return [] # Maybe in profile editing mode
- base_representation = tool.Ifc.get().by_id(active_representation_id)
# shape aspects matching context of the active representation
matching_shape_aspects = []
@@ -390,7 +392,7 @@ class PlacementData:
def load(cls):
cls.data = {"has_placement": cls.has_placement()}
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
obj = bpy.context.active_object
if obj and props.has_blender_offset:
xyz = cls.original_xyz(obj)
@@ -413,7 +415,7 @@ class PlacementData:
@classmethod
def original_xyz(cls, obj):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
xyz = ifcopenshell.util.geolocation.xyz2enh(
obj.matrix_world[0][3],
obj.matrix_world[1][3],
diff --git a/src/bonsai/bonsai/bim/module/geometry/decorator.py b/src/bonsai/bonsai/bim/module/geometry/decorator.py
index 2be10a27f0..5993cc4469 100644
--- a/src/bonsai/bonsai/bim/module/geometry/decorator.py
+++ b/src/bonsai/bonsai/bim/module/geometry/decorator.py
@@ -46,12 +46,14 @@ class ItemDecorator:
obj_is_boolean: dict[str, list[ifcopenshell.entity_instance]] = {}
objs: dict[str, dict[str, list]] = {}
obj_matrix: dict[str, Matrix] = {}
- for item_obj in context.scene.BIMGeometryProperties.item_objs:
+ props = tool.Geometry.get_geometry_props()
+ for item_obj in props.item_objs:
if obj := item_obj.obj:
obj: bpy.types.Object
objs[obj.name] = cls.get_obj_data(obj)
obj_is_selected[obj.name] = obj.select_get()
- item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ item = tool.Geometry.get_active_representation(obj)
+ assert item
obj_is_boolean[obj.name] = [i for i in tool.Ifc.get().get_inverse(item) if i.is_a("IfcBooleanResult")]
obj_matrix[obj.name] = obj.matrix_world.copy()
@@ -143,7 +145,8 @@ class ItemDecorator:
color = selected_elements_color
blf.color(font_id, *color)
- for item in context.scene.BIMGeometryProperties.item_objs:
+ props = tool.Geometry.get_geometry_props()
+ for item in props.item_objs:
if (obj := item.obj) and obj.hide_get() == False:
if obj.select_get():
centroid = obj.matrix_world @ Vector(obj.bound_box[0]).lerp(Vector(obj.bound_box[6]), 0.5)
diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py
index de59b7a61c..0763bd4963 100644
--- a/src/bonsai/bonsai/bim/module/geometry/operator.py
+++ b/src/bonsai/bonsai/bim/module/geometry/operator.py
@@ -23,12 +23,14 @@ import numpy as np
import numpy.typing as npt
import ifcopenshell
import ifcopenshell.api.layer
+import ifcopenshell.api.style
import ifcopenshell.util.element
import ifcopenshell.util.placement
import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
import ifcopenshell.util.unit
import ifcopenshell.api
+import ifcopenshell.api.boundary
import ifcopenshell.api.grid
import bonsai.core.geometry
import bonsai.core.geometry as core
@@ -77,7 +79,8 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator):
self.separate_element(element)
def separate_item(self, context, obj):
- item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ item = tool.Geometry.get_active_representation(obj)
+ assert item
if tool.Geometry.is_meshlike_item(item):
previous_selected_objects = context.selected_objects
bpy.ops.mesh.separate(type=self.type)
@@ -85,7 +88,7 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator):
if obj in previous_selected_objects:
continue
self.add_meshlike_item(obj)
- tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
+ tool.Geometry.reload_representation(tool.Geometry.get_geometry_props().representation_obj)
else:
self.report({"INFO"}, f"Separating an {item.is_a()} is not supported")
@@ -120,7 +123,7 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator):
representation.Items = list(representation.Items) + [item]
obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}"
- obj.data.BIMMeshProperties.ifc_definition_id = item.id()
+ tool.Ifc.link(item, obj)
props.add_item_object(obj, item)
def separate_element(self, element):
@@ -301,11 +304,12 @@ class AddRepresentation(bpy.types.Operator, tool.Ifc.Operator):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
+ props = tool.Geometry.get_geometry_props()
row = self.layout.row()
row.prop(self, "representation_conversion_method", text="")
if self.representation_conversion_method == "OBJECT":
row = self.layout.row()
- row.prop(context.scene.BIMGeometryProperties, "representation_from_object", text="")
+ row.prop(props, "representation_from_object", text="")
class SelectConnection(bpy.types.Operator, tool.Ifc.Operator):
@@ -456,13 +460,17 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
def update_obj_mesh_representation(self, context: bpy.types.Context, obj: bpy.types.Object) -> None:
+ data = obj.data
+ assert tool.Geometry.is_data_supported_for_adding_representation(data)
+ mprops = tool.Geometry.get_mesh_props(data)
+
product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
material = ifcopenshell.util.element.get_material(product, should_skip_usage=True)
# NOTE: Currently iterator doesn't detect whether opening is actually affected the representation
# or it's just present on the element. In theory, we can also allow editing representations
# if we know that representation wasn't affected by existing openings.
- has_openings = tool.Geometry.has_openings(product) and obj.data.BIMMeshProperties.has_openings_applied
+ has_openings = tool.Geometry.has_openings(product) and tool.Geometry.get_mesh_props(data).has_openings_applied
if has_openings and not self.apply_openings:
# Meshlike things with openings can only be updated without openings applied.
if self.from_ui:
@@ -479,13 +487,14 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
elif product.is_a("IfcRelSpaceBoundary"):
# TODO refactor
settings = tool.Boundary.get_assign_connection_geometry_settings(obj)
- ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
+ ifcopenshell.api.boundary.assign_connection_geometry(tool.Ifc.get(), **settings)
return
if tool.Ifc.is_moved(obj) or tool.Geometry.is_scaled(obj):
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
- old_representation = self.file.by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ old_representation = tool.Geometry.get_active_representation(obj)
+ assert old_representation
if material and material.is_a() in ["IfcMaterialProfileSet", "IfcMaterialLayerSet"]:
if self.ifc_representation_class == "IfcTessellatedFaceSet":
# We are explicitly casting to a tessellation, so remove all parametric materials.
@@ -544,12 +553,12 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
tool.Geometry.run_style_add_style(obj=mat)
for mat in tool.Geometry.get_object_materials_without_styles(obj)
]
- ifcopenshell.api.run(
- "style.assign_representation_styles",
+ props = tool.Geometry.get_geometry_props()
+ ifcopenshell.api.style.assign_representation_styles(
self.file,
shape_representation=new_representation,
styles=tool.Geometry.get_styles(obj, only_assigned_to_faces=True),
- should_use_presentation_style_assignment=context.scene.BIMGeometryProperties.should_use_presentation_style_assignment,
+ should_use_presentation_style_assignment=props.should_use_presentation_style_assignment,
)
tool.Geometry.record_object_materials(obj)
@@ -568,8 +577,8 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
continue
representation.RepresentationIdentifier = "Reference"
- obj.data.BIMMeshProperties.ifc_definition_id = int(new_representation.id())
- obj.data.name = f"{old_representation.ContextOfItems.id()}/{new_representation.id()}"
+ tool.Ifc.link(new_representation, data)
+ data.name = tool.Loader.get_mesh_name(new_representation)
# TODO: In simple scenarios, a type has a ShapeRepresentation of ID
# 123. This is then mapped through mapped representations by
@@ -585,7 +594,7 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
# transformations.
core.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_representation)
- if obj.data.BIMMeshProperties.ifc_parameters:
+ if mprops.ifc_parameters:
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
@@ -597,12 +606,13 @@ class UpdateParametricRepresentation(bpy.types.Operator):
@classmethod
def poll(cls, context):
- return context.active_object and context.active_object.mode == "OBJECT"
+ return (obj := context.active_object) and obj.mode == "OBJECT" and tool.Geometry.has_mesh_properties(obj.data)
def execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
obj = context.active_object
- props = obj.data.BIMMeshProperties
+ assert obj and tool.Geometry.has_mesh_properties(obj.data)
+ props = tool.Geometry.get_mesh_props(obj.data)
parameter = props.ifc_parameters[self.index]
self.file.by_id(parameter.step_id)[parameter.index] = parameter.value
show_representation_parameters = bool(props.ifc_parameters)
@@ -626,8 +636,10 @@ class GetRepresentationIfcParameters(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
- core.get_representation_ifc_parameters(tool.Geometry, obj=context.active_object)
- parameters = context.active_object.data.BIMMeshProperties.ifc_parameters
+ obj = context.active_object
+ assert obj and tool.Geometry.has_mesh_properties((data := obj.data))
+ core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
+ parameters = tool.Geometry.get_mesh_props(data).ifc_parameters
self.report({"INFO"}, f"{len(parameters)} parameters found.")
@@ -671,7 +683,7 @@ class CopyRepresentation(bpy.types.Operator, tool.Ifc.Operator):
)
-def lock_error_message(name):
+def lock_error_message(name: str) -> str:
return f"'{name}' is locked. Unlock it via the Spatial panel in the Project Overview tab."
@@ -719,8 +731,10 @@ class OverrideDelete(bpy.types.Operator):
if self.is_batch:
row = self.layout.row()
row.label(text="Warning: Faster deletion will use more memory.", icon="ERROR")
+ row = self.layout.row()
+ row.label(text="See system console for deletion progress.")
- def _execute(self, context):
+ def _execute(self, context: bpy.types.Context):
start_time = time()
if self.is_batch:
@@ -728,10 +742,19 @@ class OverrideDelete(bpy.types.Operator):
self.process_arrays(context)
clear_active_object = True
- for obj in context.selected_objects:
- try:
- obj.name
- except:
+ objects_to_remove = context.selected_objects
+ for i, obj in enumerate(objects_to_remove, 1):
+ # Log time.
+ time_since_start = time() - start_time
+ is_valid_data_block = tool.Blender.is_valid_data_block(obj)
+ if time_since_start > 10:
+ obj_name = f" ({obj.name})" if is_valid_data_block else ""
+ print(
+ f"Removing object {i}/{len(objects_to_remove)}{obj_name}. "
+ f"Time since start: {time_since_start:.2f} seconds."
+ )
+
+ if not is_valid_data_block:
continue
element = tool.Ifc.get_entity(obj)
if element:
@@ -781,7 +804,7 @@ class OverrideDelete(bpy.types.Operator):
data["old_file"].redo()
tool.Ifc.set(data["new_file"])
- def process_arrays(self, context):
+ def process_arrays(self, context: bpy.types.Context) -> None:
selected_objects = set(context.selected_objects)
array_parents = set()
for obj in context.selected_objects:
@@ -824,7 +847,7 @@ class OverrideOutlinerDelete(bpy.types.Operator):
# unintended IFC spatial modifications. To make life less confusing for
# the user, Delete means Delete. End of story.
# Deep magick from the dawn of time
- if IfcStore.get_file():
+ if tool.Ifc.get():
return IfcStore.execute_ifc_operator(self, context)
# https://blender.stackexchange.com/questions/203729/python-get-selected-objects-in-outliner
objects_to_delete = set()
@@ -948,7 +971,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
@staticmethod
def execute_duplicate_operator(self, context, linked=False):
# Deep magick from the dawn of time
- if IfcStore.get_file():
+ if tool.Ifc.get():
IfcStore.execute_ifc_operator(self, context)
if self.new_active_obj:
context.view_layer.objects.active = self.new_active_obj
@@ -1029,7 +1052,8 @@ class OverrideDuplicateMove(bpy.types.Operator):
# Unlink from previous boolean element
# and keep object tracked for decorations.
if is_tracked_opening:
- new_obj.data.BIMMeshProperties.ifc_boolean_id = 0
+ mprops = tool.Geometry.get_mesh_props(new_obj.data)
+ mprops.ifc_boolean_id = 0
tool.Root.add_tracked_opening(new_obj, tracked_opening_type)
if obj == context.active_object:
@@ -1053,7 +1077,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
if new.is_a("IfcRelSpaceBoundary"):
surface = new.ConnectionGeometry.SurfaceOnRelatingElement
temp_data.name = f"0/{surface.id()}"
- temp_data.BIMMeshProperties.ifc_definition_id = surface.id()
+ tool.Ifc.link(surface, temp_data)
else:
tool.Blender.remove_data_block(temp_data)
@@ -1089,12 +1113,14 @@ class OverrideDuplicateMove(bpy.types.Operator):
@staticmethod
def duplicate_item(obj: bpy.types.Object) -> None:
props = tool.Geometry.get_geometry_props()
- item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ item = tool.Geometry.get_active_representation(obj)
+ assert item
new_item = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), item)
new_obj = obj.copy()
+ assert tool.Geometry.has_mesh_properties(obj.data)
temp_data = obj.data.copy()
new_obj.data = temp_data
- new_obj.data.BIMMeshProperties.ifc_definition_id = new_item.id()
+ tool.Ifc.link(new_item, temp_data)
new_obj.name = obj.data.name = f"Item/{new_item.is_a()}/{new_item.id()}"
props.add_item_object(new_obj, new_item)
@@ -1670,7 +1696,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
def join_item(self) -> None:
props = tool.Geometry.get_geometry_props()
ifc_file = tool.Ifc.get()
- item = tool.Ifc.get().by_id(self.target.data.BIMMeshProperties.ifc_definition_id)
+ item = tool.Geometry.get_active_representation(self.target)
+ assert item
if tool.Geometry.is_meshlike_item(item):
tool.Geometry.dissolve_triangulated_edges(self.target)
item_objs = [i.obj for i in props.item_objs if i.obj]
@@ -1693,7 +1720,7 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
for item_data in items_data:
props.add_item_object(item_data["obj"], ifc_file.by_id(item_data["ifc_definition_id"]))
- tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
+ tool.Geometry.reload_representation(props.representation_obj)
bpy.context.view_layer.update()
tool.Root.reload_item_decorator()
@@ -1701,7 +1728,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
ifc_file = tool.Ifc.get()
builder = ShapeBuilder(ifc_file)
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
- representation = ifc_file.by_id(self.target.data.BIMMeshProperties.ifc_definition_id)
+ representation = tool.Geometry.get_active_representation(self.target)
+ assert representation
representation_type = representation.RepresentationType
if representation_type in ("Tessellation", "Brep"):
for obj in bpy.context.selected_objects:
@@ -1740,7 +1768,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
continue
# Only objects of the same representation type can be joined
- obj_rep = ifc_file.by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ obj_rep = tool.Geometry.get_active_representation(obj)
+ assert obj_rep
if obj_rep.RepresentationType != representation_type:
obj.select_set(False)
self.report(
@@ -1891,9 +1920,10 @@ class OverrideEscape(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
- if context.scene.BIMGeometryProperties.mode == "ITEM":
+ props = tool.Geometry.get_geometry_props()
+ if props.mode == "ITEM":
tool.Geometry.disable_item_mode()
- elif context.scene.BIMGeometryProperties.mode == "EDIT":
+ elif props.mode == "EDIT":
bpy.ops.bim.override_mode_set_object("INVOKE_DEFAULT", should_save=False)
tool.Geometry.disable_item_mode()
elif tool.Model.get_model_props().openings:
@@ -1949,6 +1979,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
def handle_single_object(self, context: bpy.types.Context, obj: bpy.types.Object) -> None:
element = tool.Ifc.get_entity(obj)
props = tool.Geometry.get_geometry_props()
+ pprops = tool.Project.get_project_props()
if obj == props.representation_obj:
self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly")
elif obj in [o.obj for o in context.scene.BIMAggregateProperties.not_editing_objects]:
@@ -1956,7 +1987,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
self.report(
{"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly"
)
- elif obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs:
+ elif obj in pprops.clipping_planes_objs:
self.report({"ERROR"}, "Clipping planes cannot be edited")
elif element:
if not obj.data:
@@ -2004,12 +2035,15 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
self.handle_single_object(context, obj)
def enable_editing_representation_item(self, context: bpy.types.Context, obj: bpy.types.Object) -> None:
- item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
- element = tool.Ifc.get_entity(context.scene.BIMGeometryProperties.representation_obj)
+ item = tool.Geometry.get_active_representation(obj)
+ assert item
+ element = tool.Ifc.get_entity(tool.Geometry.get_geometry_props().representation_obj)
if tool.Geometry.is_meshlike_item(item):
tool.Geometry.dissolve_triangulated_edges(obj)
tool.Blender.select_and_activate_single_object(context, obj)
- obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data)
+ assert isinstance(mesh := obj.data, bpy.types.Mesh)
+ props = tool.Geometry.get_mesh_props(mesh)
+ props.mesh_checksum = tool.Geometry.get_mesh_checksum(mesh)
self.enable_edit_mode(context)
elif (
item.is_a("IfcSweptAreaSolid")
@@ -2026,21 +2060,21 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
f"Couldn't import profile, editing it directly is not yet supported. Failing profile: {profile}.",
)
return
- obj.data.BIMMeshProperties.ifc_definition_id = item.id()
+ tool.Ifc.link(item, obj.data)
self.enable_edit_mode(context)
ProfileDecorator.install(context)
if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool")
elif item.is_a("IfcAnnotationFillArea"):
tool.Model.import_annotation_fill_area(item, obj=obj)
- obj.data.BIMMeshProperties.ifc_definition_id = item.id()
+ tool.Ifc.link(item, obj.data)
self.enable_edit_mode(context)
ProfileDecorator.install(context)
if not bpy.app.background:
tool.Blender.set_viewport_tool("bim.cad_tool")
elif tool.Geometry.is_curvelike_item(item):
tool.Model.import_curve(item, obj=obj)
- obj.data.BIMMeshProperties.ifc_definition_id = item.id()
+ tool.Ifc.link(item, obj.data)
self.enable_edit_mode(context)
ProfileDecorator.install(context)
if not bpy.app.background:
@@ -2051,10 +2085,11 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
def enable_edit_mode(self, context: bpy.types.Context) -> Union[None, set[str]]:
if tool.Blender.toggle_edit_mode(context) == {"CANCELLED"}:
return {"CANCELLED"}
- context.scene.BIMGeometryProperties.is_changing_mode = True
- if context.scene.BIMGeometryProperties.mode != "EDIT":
- context.scene.BIMGeometryProperties.mode = "EDIT"
- context.scene.BIMGeometryProperties.is_changing_mode = False
+ props = tool.Geometry.get_geometry_props()
+ props.is_changing_mode = True
+ if props.mode != "EDIT":
+ props.mode = "EDIT"
+ props.is_changing_mode = False
def has_aggregates(self, objs):
for obj in objs:
@@ -2101,14 +2136,15 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
tool.Blender.toggle_edit_mode(context)
- context.scene.BIMGeometryProperties.is_changing_mode = True
- if context.scene.BIMGeometryProperties.representation_obj:
- if context.scene.BIMGeometryProperties.mode != "ITEM":
- context.scene.BIMGeometryProperties.mode = "ITEM"
+ props = tool.Geometry.get_geometry_props()
+ props.is_changing_mode = True
+ if props.representation_obj:
+ if props.mode != "ITEM":
+ props.mode = "ITEM"
else:
- if context.scene.BIMGeometryProperties.mode != "OBJECT":
- context.scene.BIMGeometryProperties.mode = "OBJECT"
- context.scene.BIMGeometryProperties.is_changing_mode = False
+ if props.mode != "OBJECT":
+ props.mode = "OBJECT"
+ props.is_changing_mode = False
if context.active_object and self.should_save:
element = tool.Ifc.get_entity(context.active_object)
@@ -2154,24 +2190,27 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
else:
bpy.ops.bim.edit_extrusion_profile()
return self.execute(context)
- elif obj.data.BIMMeshProperties.ifc_definition_id:
- if not tool.Geometry.has_geometric_data(obj):
+ elif representation := tool.Geometry.get_active_representation(obj):
+ if not tool.Geometry.is_geometric_data(obj.data):
self.is_valid = False
self.should_save = False
- representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ assert tool.Geometry.has_mesh_properties(obj.data)
+ mesh_props = tool.Geometry.get_mesh_props(obj.data)
if tool.Geometry.is_meshlike(
representation
- ) and obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
+ ) and mesh_props.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
self.edited_objs.append(obj)
elif getattr(element, "HasOpenings", None):
self.unchanged_objs_with_openings.append(obj)
else:
tool.Ifc.finish_edit(obj)
elif element.is_a("IfcGridAxis"):
- if not tool.Geometry.has_geometric_data(obj):
+ if not tool.Geometry.is_geometric_data(obj.data):
self.is_valid = False
self.should_save = False
- if obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
+ assert tool.Geometry.has_mesh_properties(obj.data)
+ mesh_props = tool.Geometry.get_mesh_props(obj.data)
+ if mesh_props.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data):
self.edited_objs.append(obj)
else:
tool.Ifc.finish_edit(obj)
@@ -2196,9 +2235,11 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
def edit_representation_item(self, obj: bpy.types.Object) -> None:
- item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ props = tool.Geometry.get_geometry_props()
+ item = tool.Geometry.get_active_representation(obj)
+ assert item
if tool.Geometry.is_meshlike_item(item):
- if tool.Geometry.has_geometric_data(obj) and obj.data.polygons:
+ if tool.Geometry.is_geometric_data(obj.data) and obj.data.polygons:
tool.Geometry.edit_meshlike_item(obj)
else:
tool.Geometry.import_item(obj)
@@ -2219,11 +2260,11 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile)
- tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
+ tool.Geometry.reload_representation(props.representation_obj)
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
- element = tool.Ifc.get_entity(bpy.context.scene.BIMGeometryProperties.representation_obj)
+ element = tool.Ifc.get_entity(props.representation_obj)
# Only certain classes should have a footprint
if element.is_a() in ("IfcSlab", "IfcRamp"):
footprint_context = ifcopenshell.util.representation.get_context(
@@ -2276,9 +2317,9 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
for inverse in tool.Ifc.get().get_inverse(item):
ifcopenshell.util.element.replace_attribute(inverse, item, profile)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item)
- obj.data.BIMMeshProperties.ifc_definition_id = profile.id()
+ tool.Ifc.link(profile, obj.data)
- tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
+ tool.Geometry.reload_representation(props.representation_obj)
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
elif tool.Geometry.is_curvelike_item(item):
@@ -2304,10 +2345,9 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.util.element.replace_attribute(inverse, item, new)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item)
- obj.data.BIMMeshProperties.ifc_definition_id = new.id()
+ tool.Ifc.link(new, obj.data)
tool.Geometry.import_item(obj)
- props = tool.Geometry.get_geometry_props()
for item in additional_curves:
representation = tool.Geometry.get_active_representation(props.representation_obj)
representation = ifcopenshell.util.representation.resolve_representation(representation)
@@ -2316,7 +2356,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
name = f"Item/{item.is_a()}/{item.id()}"
mesh = bpy.data.meshes.new(name)
new_obj = bpy.data.objects.new(name, mesh)
- new_obj.data.BIMMeshProperties.ifc_definition_id = item.id()
+ tool.Ifc.link(item, new_obj.data)
bpy.context.collection.objects.link(new_obj)
props.add_item_object(new_obj, item)
new_obj.matrix_world = obj.matrix_world
@@ -2329,10 +2369,11 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
def enable_edit_mode(self, context):
if tool.Blender.toggle_edit_mode(context) == {"CANCELLED"}:
return {"CANCELLED"}
- context.scene.BIMGeometryProperties.is_changing_mode = True
- if context.scene.BIMGeometryProperties.mode != "EDIT":
- context.scene.BIMGeometryProperties.mode = "EDIT"
- context.scene.BIMGeometryProperties.is_changing_mode = False
+ props = tool.Geometry.get_geometry_props()
+ props.is_changing_mode = True
+ if props.mode != "EDIT":
+ props.mode = "EDIT"
+ props.is_changing_mode = False
class FlipObject(bpy.types.Operator):
@@ -2369,12 +2410,13 @@ class EnableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
item.tags += ","
item.tags += tag
- if obj.data and hasattr(obj.data, "BIMMeshProperties"):
- active_representation_id = obj.data.BIMMeshProperties.ifc_definition_id
- representation = tool.Ifc.get().by_id(active_representation_id)
+ if tool.Geometry.has_mesh_properties((data := obj.data)):
+ representation = tool.Geometry.get_data_representation(data)
+ assert representation
# Shape aspects must be considered from the PartOfProductDefinitionShape level
element = tool.Ifc.get_entity(obj)
+ assert element
product_reps = []
if element.is_a("IfcProduct"):
product_reps = [element.Representation]
@@ -2439,7 +2481,8 @@ class DisableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
- obj.BIMGeometryProperties.is_editing = False
+ assert obj
+ tool.Geometry.get_object_geometry_props(obj).is_editing = False
class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator):
@@ -2450,9 +2493,12 @@ class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
- if context.scene.BIMGeometryProperties.representation_obj:
+ if tool.Geometry.get_geometry_props().representation_obj:
return False # Artificial restriction for now to prevent removing when in item mode
- if not (obj := tool.Geometry.get_active_or_representation_obj()) or len(obj.BIMGeometryProperties.items) <= 1:
+ if (
+ not (obj := tool.Geometry.get_active_or_representation_obj())
+ or len(tool.Geometry.get_object_geometry_props(obj).items) <= 1
+ ):
cls.poll_message_set(
"Active object need to have more than 1 representation items to keep representation valid"
)
@@ -2487,13 +2533,17 @@ class SelectRepresentationItem(bpy.types.Operator):
def execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
- item = tool.Ifc.get().by_id(obj.BIMGeometryProperties.active_item.ifc_definition_id)
+ obj_props = tool.Geometry.get_object_geometry_props(obj)
+ assert obj_props.active_item
+ item = tool.Ifc.get().by_id(obj_props.active_item.ifc_definition_id)
item_ids = self.get_nested_item_ids(item)
props = tool.Geometry.get_geometry_props()
for item_obj in props.item_objs:
- if item_obj.obj.data.BIMMeshProperties.ifc_definition_id in item_ids:
- tool.Blender.select_object(item_obj.obj)
+ obj_ = item_obj.obj
+ props = tool.Geometry.get_mesh_props(obj_.data)
+ if props.ifc_definition_id in item_ids:
+ tool.Blender.select_object(obj_)
return {"FINISHED"}
def get_nested_item_ids(self, item):
@@ -2511,7 +2561,7 @@ class SelectRepresentationItem(bpy.types.Operator):
def poll_editing_representation_item_style(cls, context):
if not (obj := tool.Geometry.get_active_or_representation_obj()):
return False
- props = obj.BIMGeometryProperties
+ props = tool.Geometry.get_object_geometry_props(obj)
if not props.is_editing:
return False
if not (item := props.active_item):
@@ -2546,7 +2596,8 @@ class EnableEditingRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
- props = obj.BIMGeometryProperties
+ assert obj
+ props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_style = True
ifc_file = tool.Ifc.get()
@@ -2565,7 +2616,8 @@ class EditRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
- props = obj.BIMGeometryProperties
+ assert obj
+ props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_style = False
ifc_file = tool.Ifc.get()
@@ -2587,7 +2639,7 @@ class DisableEditingRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operato
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
- props = obj.BIMGeometryProperties
+ props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_style = False
@@ -2603,7 +2655,8 @@ class UnassignRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
active_obj = tool.Geometry.get_active_or_representation_obj()
- active_props = active_obj.BIMGeometryProperties
+ assert active_obj
+ active_props = tool.Geometry.get_object_geometry_props(active_obj)
active_props.is_editing_item_style = False
# Get active representation item
@@ -2670,7 +2723,8 @@ class EnableEditingRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Op
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
- props = obj.BIMGeometryProperties
+ assert obj
+ props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_shape_aspect = True
# set dropdown to currently active shape aspect
@@ -2686,11 +2740,13 @@ class EditRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
+ assert obj
element = tool.Ifc.get_entity(obj)
- props = obj.BIMGeometryProperties
+ props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_shape_aspect = False
ifc_file = tool.Ifc.get()
+ assert props.active_item
representation_item_id = props.active_item.ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
@@ -2743,7 +2799,8 @@ class DisableEditingRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.O
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
- props = obj.BIMGeometryProperties
+ assert obj
+ props = tool.Geometry.get_object_geometry_props(obj)
props.is_editing_item_shape_aspect = False
@@ -2754,10 +2811,12 @@ class RemoveRepresentationItemFromShapeAspect(bpy.types.Operator, tool.Ifc.Opera
def _execute(self, context):
obj = tool.Geometry.get_active_or_representation_obj()
+ assert obj
element = tool.Ifc.get_entity(obj)
- props = obj.BIMGeometryProperties
+ props = tool.Geometry.get_object_geometry_props(obj)
ifc_file = tool.Ifc.get()
+ assert props.active_item
representation_item_id = props.active_item.ifc_definition_id
representation_item = ifc_file.by_id(representation_item_id)
shape_aspect = ifc_file.by_id(props.active_item.shape_aspect_id)
@@ -2840,7 +2899,7 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
boolean_ids.add(item.SecondOperand.id())
continue
item_mesh = bpy.data.meshes.new(f"Item/{item.is_a()}/{item_id}")
- item_mesh.BIMMeshProperties.ifc_definition_id = item_id
+ tool.Ifc.link(item, item_mesh)
item_obj = bpy.data.objects.new(f"Item/{item.is_a()}/{item_id}", item_mesh)
item_obj.matrix_world = obj.matrix_world
@@ -2870,7 +2929,7 @@ class UpdateItemAttributes(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
tool.Geometry.sync_item_positions()
tool.Geometry.update_item_attributes(obj)
- tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
+ tool.Geometry.reload_representation(tool.Geometry.get_geometry_props().representation_obj)
tool.Geometry.import_item(obj)
tool.Root.reload_item_decorator()
@@ -2887,6 +2946,10 @@ class NameProfile(bpy.types.Operator, tool.Ifc.Operator):
options={"SKIP_SAVE"},
)
+ if TYPE_CHECKING:
+ extrusion_item_obj: str
+ profile_name: str
+
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -2901,7 +2964,7 @@ class NameProfile(bpy.types.Operator, tool.Ifc.Operator):
ifc_file = tool.Ifc.get()
extrusion_item_obj = bpy.data.objects[self.extrusion_item_obj]
- mesh_props = extrusion_item_obj.data.BIMMeshProperties
+ mesh_props = tool.Geometry.get_mesh_props(extrusion_item_obj.data)
extrusion = ifc_file.by_id(mesh_props.ifc_definition_id)
assert extrusion.is_a("IfcSweptAreaSolid")
profile = extrusion.SweptArea
@@ -2971,9 +3034,9 @@ class AddMeshlikeItem(bpy.types.Operator, tool.Ifc.Operator):
props.add_item_object(obj, item)
representation.Items = list(representation.Items) + [item]
- tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
+ tool.Geometry.reload_representation(props.representation_obj)
obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}"
- obj.data.BIMMeshProperties.ifc_definition_id = item.id()
+ tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id()
tool.Root.reload_item_decorator()
@@ -3019,10 +3082,10 @@ class AddSweptAreaSolidItem(bpy.types.Operator, tool.Ifc.Operator):
props.add_item_object(obj, item)
representation.Items = list(representation.Items) + [item]
- tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
+ tool.Geometry.reload_representation(props.representation_obj)
obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}"
- obj.data.BIMMeshProperties.ifc_definition_id = item.id()
+ tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id()
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
tool.Root.reload_item_decorator()
@@ -3090,10 +3153,10 @@ class AddCurvelikeItem(bpy.types.Operator, tool.Ifc.Operator):
props.add_item_object(obj, item)
representation.Items = list(representation.Items) + [item]
- tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
+ tool.Geometry.reload_representation(props.representation_obj)
obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}"
- obj.data.BIMMeshProperties.ifc_definition_id = item.id()
+ tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id()
tool.Geometry.import_item(obj)
tool.Geometry.import_item_attributes(obj)
@@ -3139,10 +3202,10 @@ class AddHalfSpaceSolidItem(bpy.types.Operator, tool.Ifc.Operator):
representation = ifcopenshell.util.representation.resolve_representation(representation)
representation.Items = list(representation.Items) + [item]
- tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
+ tool.Geometry.reload_representation(props.representation_obj)
obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}"
- obj.data.BIMMeshProperties.ifc_definition_id = item.id()
+ tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id()
tool.Geometry.import_item(obj)
# TODO refactor to core and not rely on selection
@@ -3168,7 +3231,7 @@ class OverrideMove(bpy.types.Operator):
def execute(self, context):
# Deep magick from the dawn of time
- if IfcStore.get_file():
+ if tool.Ifc.get():
IfcStore.execute_ifc_operator(self, context)
if self.new_active_obj:
context.view_layer.objects.active = self.new_active_obj
diff --git a/src/bonsai/bonsai/bim/module/geometry/prop.py b/src/bonsai/bonsai/bim/module/geometry/prop.py
index 2ab4ec5efe..7b80e9b040 100644
--- a/src/bonsai/bonsai/bim/module/geometry/prop.py
+++ b/src/bonsai/bonsai/bim/module/geometry/prop.py
@@ -32,7 +32,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
-from typing import Optional, TYPE_CHECKING, Union
+from typing import Optional, TYPE_CHECKING, Union, Literal
def get_contexts(self, context):
@@ -270,6 +270,9 @@ class BIMObjectGeometryProperties(PropertyGroup):
representation_item_layer: str
+GeometryMode = Literal["OBJECT", "ITEM", "EDIT"]
+
+
class BIMGeometryProperties(PropertyGroup):
# Revit workaround
should_use_presentation_style_assignment: BoolProperty(name="Force Presentation Style Assignment", default=False)
@@ -308,7 +311,7 @@ class BIMGeometryProperties(PropertyGroup):
should_force_faceted_brep: bool
should_force_triangulation: bool
is_changing_mode: bool
- mode: str
+ mode: GeometryMode
representation_obj: Union[bpy.types.Object, None]
item_objs: bpy.types.bpy_prop_collection_idprop[RepresentationItemObject]
representation_from_object: Union[bpy.types.Object, None]
diff --git a/src/bonsai/bonsai/bim/module/geometry/ui.py b/src/bonsai/bonsai/bim/module/geometry/ui.py
index 01d0a8ae0e..8ba3958222 100644
--- a/src/bonsai/bonsai/bim/module/geometry/ui.py
+++ b/src/bonsai/bonsai/bim/module/geometry/ui.py
@@ -20,7 +20,6 @@ import bpy
import bonsai.bim
import bonsai.tool as tool
from bpy.types import Panel, Menu, UIList
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import prop_with_search
from bonsai.bim.module.geometry.data import (
RepresentationsData,
@@ -53,9 +52,10 @@ def mode_menu(self, context):
UIData.load()
ifc_icon = f"{UIData.data['menu_icon_color_mode']}_ifc"
row = self.layout.row(align=True)
- if context.scene.BIMGeometryProperties.mode == "EDIT":
+ props = tool.Geometry.get_geometry_props()
+ if props.mode == "EDIT":
row.operator("bim.override_mode_set_object", icon="CANCEL", text="Discard Changes").should_save = False
- row.prop(context.scene.BIMGeometryProperties, "mode", text="", icon_value=bonsai.bim.icons[ifc_icon].icon_id)
+ row.prop(props, "mode", text="", icon_value=bonsai.bim.icons[ifc_icon].icon_id)
def object_menu(self, context):
@@ -336,9 +336,9 @@ class BIM_PT_connections(Panel):
def poll(cls, context):
if not context.active_object:
return False
- if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id):
+ if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id):
return False
- return IfcStore.get_file()
+ return tool.Ifc.get()
def draw(self, context):
if not ConnectionsData.is_loaded:
@@ -411,15 +411,18 @@ class BIM_PT_mesh(Panel):
@classmethod
def poll(cls, context):
return (
- context.active_object is not None
- and context.active_object.type == "MESH"
- and hasattr(context.active_object.data, "BIMMeshProperties")
- and context.active_object.data.BIMMeshProperties.ifc_definition_id
+ (obj := context.active_object) is not None
+ and (mesh := obj.data)
+ and isinstance(mesh, bpy.types.Mesh)
+ and tool.Geometry.get_mesh_props(mesh).ifc_definition_id
)
def draw(self, context):
- if not context.active_object.data:
- return
+ obj = context.active_object
+ assert obj
+ mesh = obj.data
+ assert isinstance(mesh, bpy.types.Mesh)
+
row = self.layout.row()
row.label(text="Advanced Users Only", icon="ERROR")
@@ -427,7 +430,7 @@ class BIM_PT_mesh(Panel):
row = layout.row()
text = "Manually Save Representation"
- if tool.Ifc.is_edited(context.active_object):
+ if tool.Ifc.is_edited(obj):
text += "*"
row.operator("bim.update_representation", text=text)
@@ -454,8 +457,8 @@ class BIM_PT_mesh(Panel):
op = row.operator("bim.update_representation", text="Convert To Arbitrary Extrusion With Voids")
op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids"
- if context.active_object and context.active_object.data:
- mprops = context.active_object.data.BIMMeshProperties
+ if True:
+ mprops = tool.Geometry.get_mesh_props(mesh)
row = layout.row()
row.operator("bim.get_representation_ifc_parameters")
for index, ifc_parameter in enumerate(mprops.ifc_parameters):
@@ -477,7 +480,7 @@ class BIM_PT_placement(Panel):
@classmethod
def poll(cls, context):
- return context.active_object and context.active_object.BIMObjectProperties.ifc_definition_id
+ return (obj := context.active_object) and obj.BIMObjectProperties.ifc_definition_id
def draw(self, context):
if not PlacementData.is_loaded:
@@ -571,10 +574,10 @@ class BIM_PT_workarounds(Panel):
@classmethod
def poll(cls, context):
return (
- context.active_object is not None
- and context.active_object.type == "MESH"
- and hasattr(context.active_object.data, "BIMMeshProperties")
- and context.active_object.data.BIMMeshProperties.ifc_definition_id
+ (obj := context.active_object) is not None
+ and (mesh := obj.data)
+ and isinstance(mesh, bpy.types.Mesh)
+ and tool.Geometry.get_mesh_props(mesh).ifc_definition_id
)
def draw(self, context):
@@ -588,7 +591,7 @@ class BIM_PT_workarounds(Panel):
class BIM_UL_representation_items(UIList):
- def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
+ def draw_item(self, context, layout: bpy.types.UILayout, data, item, icon, active_data, active_propname):
if item:
icon = "MATERIAL" if item.surface_style else "MESH_UVSPHERE"
row = layout.row(align=True)
diff --git a/src/bonsai/bonsai/bim/module/georeference/data.py b/src/bonsai/bonsai/bim/module/georeference/data.py
index 314de226ff..b64ef55c39 100644
--- a/src/bonsai/bonsai/bim/module/georeference/data.py
+++ b/src/bonsai/bonsai/bim/module/georeference/data.py
@@ -167,7 +167,7 @@ class GeoreferenceData:
result["rotation"] = str(round(ifcopenshell.util.geolocation.yaxis2angle(*wcs[:, 1][:2]), 3))
result["x"], result["y"], result["z"] = wcs[:, 3][:3]
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
blender_xyz = ifcopenshell.util.geolocation.enh2xyz(
result["x"],
@@ -193,7 +193,7 @@ class GeoreferenceData:
@classmethod
def local_origin(cls):
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if not props.has_blender_offset:
return
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
diff --git a/src/bonsai/bonsai/bim/module/georeference/decorator.py b/src/bonsai/bonsai/bim/module/georeference/decorator.py
index bd5a9b97e8..afd9dfba27 100644
--- a/src/bonsai/bonsai/bim/module/georeference/decorator.py
+++ b/src/bonsai/bonsai/bim/module/georeference/decorator.py
@@ -21,6 +21,7 @@ import blf
import gpu
import bmesh
import ifcopenshell
+import ifcopenshell.util.geolocation
import bonsai.tool as tool
from math import radians
from bpy.types import SpaceView3D
@@ -53,7 +54,8 @@ class GeoreferenceDecorator:
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
- self.scale = bpy.context.scene.BIMGeoreferenceProperties.visualization_scale
+ props = tool.Georeference.get_georeference_props()
+ self.scale = props.visualization_scale
content_pos = [v * self.scale for v in content_pos]
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
@@ -64,7 +66,7 @@ class GeoreferenceDecorator:
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
- props = context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if not props.model_origin: # If this is empty, no georeferencing data has been loaded.
return
@@ -177,7 +179,7 @@ class GeoreferenceDecorator:
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
- props = context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if not props.model_origin: # If this is empty, no georeferencing data has been loaded.
return
@@ -350,7 +352,7 @@ class GeoreferenceDecorator:
self.gn_angle = float(GeoreferenceData.data["map_derived_angle"] or 0)
self.tn_angle = float(GeoreferenceData.data["true_derived_angle"] or 0)
- props = context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
blender_angle = ifcopenshell.util.geolocation.xaxis2angle(
float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate)
diff --git a/src/bonsai/bonsai/bim/module/georeference/prop.py b/src/bonsai/bonsai/bim/module/georeference/prop.py
index f0458b2045..58a3088b86 100644
--- a/src/bonsai/bonsai/bim/module/georeference/prop.py
+++ b/src/bonsai/bonsai/bim/module/georeference/prop.py
@@ -33,15 +33,18 @@ from bpy.props import (
)
from bonsai.bim.module.georeference.data import GeoreferenceData
from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator
+from typing import TYPE_CHECKING
-def get_coordinate_operation_class(self, context):
+def get_coordinate_operation_class(
+ self: "BIMGeoreferenceProperties", context: bpy.types.Context
+) -> list[tuple[str, str, str]]:
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
return GeoreferenceData.data["coordinate_operation_class"]
-def update_true_north_angle(self, context):
+def update_true_north_angle(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
if self.is_changing_angle:
return
self.is_changing_angle = True
@@ -54,7 +57,7 @@ def update_true_north_angle(self, context):
self.is_changing_angle = False
-def update_true_north_vector(self, context):
+def update_true_north_vector(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
if self.is_changing_angle:
return
self.is_changing_angle = True
@@ -67,7 +70,7 @@ def update_true_north_vector(self, context):
self.is_changing_angle = False
-def update_grid_north_angle(self, context):
+def update_grid_north_angle(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
if self.is_changing_angle:
return
self.is_changing_angle = True
@@ -81,7 +84,7 @@ def update_grid_north_angle(self, context):
self.is_changing_angle = False
-def update_grid_north_vector(self, context):
+def update_grid_north_vector(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
if self.is_changing_angle:
return
self.is_changing_angle = True
@@ -95,15 +98,15 @@ def update_grid_north_vector(self, context):
self.is_changing_angle = False
-def update_should_visualise(self, context):
+def update_should_visualise(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
if self.should_visualise:
GeoreferenceDecorator.install(bpy.context)
else:
GeoreferenceDecorator.uninstall()
-def update_blender_coordinates(self, context):
- props = bpy.context.scene.BIMGeoreferenceProperties
+def update_blender_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
+ props = self
if props.is_updating_coordinates:
return
props.is_updating_coordinates = True
@@ -123,8 +126,8 @@ def update_blender_coordinates(self, context):
props.is_updating_coordinates = False
-def update_local_coordinates(self, context):
- props = bpy.context.scene.BIMGeoreferenceProperties
+def update_local_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
+ props = self
if props.is_updating_coordinates:
return
props.is_updating_coordinates = True
@@ -147,8 +150,8 @@ def update_local_coordinates(self, context):
props.is_updating_coordinates = False
-def update_map_coordinates(self, context):
- props = bpy.context.scene.BIMGeoreferenceProperties
+def update_map_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None:
+ props = self
if props.is_updating_coordinates:
return
props.is_updating_coordinates = True
@@ -248,3 +251,45 @@ class BIMGeoreferenceProperties(PropertyGroup):
wcs_y: StringProperty(name="WCS Y", default="0")
wcs_z: StringProperty(name="WCS Z", default="0")
wcs_rotation: StringProperty(name="WCS Rotation", default="0")
+
+ if TYPE_CHECKING:
+ coordinate_operation_class: str
+ is_changing_angle: bool
+ is_editing: bool
+ is_editing_wcs: bool
+ is_editing_true_north: bool
+ coordinate_operation: bpy.types.bpy_prop_collection_idprop[Attribute]
+ projected_crs: bpy.types.bpy_prop_collection_idprop[Attribute]
+ is_updating_coordinates: bool
+ blender_coordinates: str
+ local_coordinates: str
+ map_coordinates: str
+ should_visualise: bool
+ visualization_scale: float
+ grid_north_angle: str
+ x_axis_abscissa: str
+ x_axis_ordinate: str
+ x_axis_is_null: bool
+
+ host_model_origin: str
+ host_model_origin_si: str
+ host_model_project_north: str
+
+ model_origin: str
+ model_origin_si: str
+ model_project_north: str
+
+ has_blender_offset: bool
+ blender_offset_x: str
+ blender_offset_y: str
+ blender_offset_z: str
+ blender_x_axis_abscissa: str
+ blender_x_axis_ordinate: str
+
+ true_north_angle: str
+ true_north_abscissa: str
+ true_north_ordinate: str
+ wcs_x: str
+ wcs_y: str
+ wcs_z: str
+ wcs_rotation: str
diff --git a/src/bonsai/bonsai/bim/module/georeference/ui.py b/src/bonsai/bonsai/bim/module/georeference/ui.py
index 8b2dfd43e0..3dfec3a9f5 100644
--- a/src/bonsai/bonsai/bim/module/georeference/ui.py
+++ b/src/bonsai/bonsai/bim/module/georeference/ui.py
@@ -32,7 +32,7 @@ class BIM_PT_gis(Panel):
bl_parent_id = "BIM_PT_tab_geometry"
def draw_header(self, context):
- props = context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
row = self.layout.row(align=True)
icon = "HIDE_OFF" if props.should_visualise else "HIDE_ON"
row.label(text="") # empty text occupies the left of the row
@@ -43,7 +43,7 @@ class BIM_PT_gis(Panel):
def draw(self, context):
self.layout.use_property_split = True
self.layout.use_property_decorate = False
- props = context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
@@ -54,7 +54,7 @@ class BIM_PT_gis(Panel):
self.draw_ui(context)
def draw_editable_ui(self, context):
- props = context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
row = self.layout.row(align=True)
row.label(text="Projected CRS", icon="WORLD")
row.operator("bim.edit_georeferencing", icon="CHECKMARK", text="")
@@ -87,7 +87,7 @@ class BIM_PT_gis(Panel):
draw_attribute(attribute, self.layout.row())
def draw_ui(self, context):
- props = context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if tool.Ifc.get_schema() == "IFC2X3":
row = self.layout.row()
@@ -149,7 +149,7 @@ class BIM_PT_gis_true_north(Panel):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
- self.props = context.scene.BIMGeoreferenceProperties
+ self.props = tool.Georeference.get_georeference_props()
if self.props.is_editing_true_north:
self.draw_editable_ui(context)
@@ -200,7 +200,7 @@ class BIM_PT_gis_blender(Panel):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
- props = context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
row = self.layout.row()
@@ -233,7 +233,7 @@ class BIM_PT_gis_wcs(Panel):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
- props = context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if props.is_editing_wcs:
self.draw_editable_ui(context)
@@ -263,7 +263,7 @@ class BIM_PT_gis_wcs(Panel):
row.operator("bim.enable_editing_wcs", icon="GREASEPENCIL", text="")
def draw_editable_ui(self, context):
- props = context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
row = self.layout.row(align=True)
row.label(text="World Coordinate System", icon="EMPTY_ARROWS")
@@ -293,7 +293,7 @@ class BIM_PT_gis_calculator(Panel):
if not GeoreferenceData.is_loaded:
GeoreferenceData.load()
- props = context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
row = self.layout.row(align=True)
diff --git a/src/bonsai/bonsai/bim/module/group/operator.py b/src/bonsai/bonsai/bim/module/group/operator.py
index e991dcf4f1..9a9124ebfa 100644
--- a/src/bonsai/bonsai/bim/module/group/operator.py
+++ b/src/bonsai/bonsai/bim/module/group/operator.py
@@ -22,7 +22,6 @@ import ifcopenshell.api.group
import ifcopenshell.util.attribute
import bonsai.bim.helper
import bonsai.tool as tool
-from bonsai.bim.ifc import IfcStore
import json
@@ -129,8 +128,7 @@ class RemoveGroup(bpy.types.Operator, tool.Ifc.Operator):
group: bpy.props.IntProperty()
def _execute(self, context):
- props = context.scene.BIMGroupProperties
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run("group.remove_group", self.file, **{"group": self.file.by_id(self.group)})
bpy.ops.bim.load_groups()
return {"FINISHED"}
diff --git a/src/bonsai/bonsai/bim/module/group/ui.py b/src/bonsai/bonsai/bim/module/group/ui.py
index 88077f99eb..8cd7d1fe2a 100644
--- a/src/bonsai/bonsai/bim/module/group/ui.py
+++ b/src/bonsai/bonsai/bim/module/group/ui.py
@@ -17,8 +17,8 @@
# along with Bonsai. If not, see .
import bpy
+import bonsai.tool as tool
from bpy.types import Panel, UIList
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.group.data import GroupsData, ObjectGroupsData
@@ -34,7 +34,7 @@ class BIM_PT_groups(Panel):
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ return tool.Ifc.get()
def draw(self, context):
if not GroupsData.is_loaded:
@@ -93,7 +93,7 @@ class BIM_PT_object_groups(Panel):
def poll(cls, context):
if not context.active_object:
return False
- return IfcStore.get_file() and context.active_object.BIMObjectProperties.ifc_definition_id
+ return tool.Ifc.get() and context.active_object.BIMObjectProperties.ifc_definition_id
def draw(self, context):
if not ObjectGroupsData.is_loaded:
diff --git a/src/bonsai/bonsai/bim/module/layer/operator.py b/src/bonsai/bonsai/bim/module/layer/operator.py
index 9c2f4a2249..5480c86034 100644
--- a/src/bonsai/bonsai/bim/module/layer/operator.py
+++ b/src/bonsai/bonsai/bim/module/layer/operator.py
@@ -24,7 +24,6 @@ import ifcopenshell.util.element
import ifcopenshell.util.attribute
import bonsai.bim.helper
import bonsai.tool as tool
-from bonsai.bim.ifc import IfcStore
class LoadLayers(bpy.types.Operator):
@@ -33,7 +32,7 @@ class LoadLayers(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
props = context.scene.BIMLayerProperties
props.layers.clear()
for layer in tool.Ifc.get().by_type("IfcPresentationLayerAssignment"):
@@ -126,7 +125,7 @@ class RemovePresentationLayer(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMLayerProperties
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run("layer.remove_layer", self.file, **{"layer": self.file.by_id(self.layer)})
bpy.ops.bim.load_layers()
return {"FINISHED"}
@@ -142,12 +141,12 @@ class AssignPresentationLayer(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
item = bpy.data.meshes.get(self.item) if self.item else context.active_object.data
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"layer.assign_layer",
self.file,
**{
- "items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)],
+ "items": [self.file.by_id(tool.Geometry.get_mesh_props(item).ifc_definition_id)],
"layer": self.file.by_id(self.layer),
},
)
@@ -164,15 +163,10 @@ class UnassignPresentationLayer(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
item = bpy.data.meshes.get(self.item) if self.item else context.active_object.data
- self.file = IfcStore.get_file()
- ifcopenshell.api.run(
- "layer.unassign_layer",
- self.file,
- **{
- "items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)],
- "layer": self.file.by_id(self.layer),
- },
- )
+ ifc_file = tool.Ifc.get()
+ representation = tool.Geometry.get_data_representation(item)
+ assert representation
+ ifcopenshell.api.layer.unassign_layer(ifc_file, items=[representation], layer=ifc_file.by_id(self.layer))
return {"FINISHED"}
diff --git a/src/bonsai/bonsai/bim/module/layer/ui.py b/src/bonsai/bonsai/bim/module/layer/ui.py
index 8c3b77d30a..609cb62815 100644
--- a/src/bonsai/bonsai/bim/module/layer/ui.py
+++ b/src/bonsai/bonsai/bim/module/layer/ui.py
@@ -17,8 +17,8 @@
# along with Bonsai. If not, see .
import bpy
+import bonsai.tool as tool
from bpy.types import Panel, UIList, Mesh
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.layer.data import LayersData
@@ -34,7 +34,7 @@ class BIM_PT_layers(Panel):
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ return tool.Ifc.get()
def draw(self, context):
if not LayersData.is_loaded:
@@ -77,7 +77,6 @@ class BIM_UL_layers(UIList):
row.label(text=item.name)
if context.active_object and isinstance(context.active_object.data, Mesh):
- mprops = context.active_object.data.BIMMeshProperties
if item.ifc_definition_id in LayersData.data["active_layers"]:
op = row.operator("bim.unassign_presentation_layer", text="", icon="KEYFRAME_HLT", emboss=False)
op.layer = item.ifc_definition_id
diff --git a/src/bonsai/bonsai/bim/module/material/data.py b/src/bonsai/bonsai/bim/module/material/data.py
index 50a03603df..a332a89804 100644
--- a/src/bonsai/bonsai/bim/module/material/data.py
+++ b/src/bonsai/bonsai/bim/module/material/data.py
@@ -282,8 +282,9 @@ class ObjectMaterialData:
if item.is_a("IfcMaterialLayer"):
total_thickness = item.LayerThickness
unit_system = bpy.context.scene.unit_settings.system
+ props = tool.Drawing.get_document_props()
if unit_system == "IMPERIAL":
- precision = bpy.context.scene.DocProperties.imperial_precision
+ precision = props.imperial_precision
else:
precision = None
formatted_thickness = format_distance(
diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py
index 820ac5359e..3e9ff20924 100644
--- a/src/bonsai/bonsai/bim/module/material/operator.py
+++ b/src/bonsai/bonsai/bim/module/material/operator.py
@@ -29,7 +29,6 @@ import bonsai.tool as tool
import bonsai.core.style
import bonsai.core.material as core
import bonsai.bim.module.model.profile as model_profile
-from bonsai.bim.ifc import IfcStore
from typing import Any, Union, TYPE_CHECKING
from bonsai.bim.module.model import wall, slab
@@ -118,7 +117,7 @@ class AssignParameterizedProfile(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
profile = ifcopenshell.api.run(
"profile.add_parameterized_profile",
self.file,
@@ -292,7 +291,7 @@ class AddConstituent(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"material.add_constituent",
self.file,
@@ -328,7 +327,7 @@ class AddProfile(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
props = tool.Material.get_material_props()
ifcopenshell.api.run(
"material.add_profile",
@@ -387,7 +386,7 @@ class ReorderMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
material_set = self.file.by_id(self.material_set)
ifcopenshell.api.run(
"material.reorder_set_item",
@@ -443,7 +442,7 @@ class AddListItem(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"material.add_list_item",
self.file,
@@ -463,7 +462,7 @@ class RemoveListItem(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"material.remove_list_item",
self.file,
@@ -557,7 +556,7 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator):
material_set_usage: bpy.props.IntProperty()
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
active_obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
props = active_obj.BIMObjectMaterialProperties
element = tool.Ifc.get_entity(active_obj)
@@ -663,7 +662,7 @@ class EnableEditingMaterialSetItem(bpy.types.Operator):
material_set_item: bpy.props.IntProperty()
def execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
self.mprops = tool.Material.get_material_props()
self.props = obj.BIMObjectMaterialProperties
@@ -707,7 +706,7 @@ class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
props = obj.BIMObjectMaterialProperties
mprops = tool.Material.get_material_props()
element = tool.Ifc.get_entity(obj)
diff --git a/src/bonsai/bonsai/bim/module/material/prop.py b/src/bonsai/bonsai/bim/module/material/prop.py
index f21a34a702..04a596e73f 100644
--- a/src/bonsai/bonsai/bim/module/material/prop.py
+++ b/src/bonsai/bonsai/bim/module/material/prop.py
@@ -22,7 +22,6 @@ from ifcopenshell.util.doc import get_entity_doc
import bonsai.tool as tool
from bonsai.bim.module.material.data import MaterialsData, ObjectMaterialData
from bonsai.bim.module.profile.data import ProfileData
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
from bpy.props import (
diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py
index 19865be4e8..fe0fce2413 100644
--- a/src/bonsai/bonsai/bim/module/material/ui.py
+++ b/src/bonsai/bonsai/bim/module/material/ui.py
@@ -21,7 +21,6 @@ import bonsai.bim.helper
import bonsai.tool as tool
import bpy
from bpy.types import Panel, UIList
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import draw_attributes
from bonsai.bim.helper import prop_with_search
from bonsai.bim.module.material.data import MaterialsData, ObjectMaterialData
@@ -43,7 +42,7 @@ class BIM_PT_materials(Panel):
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ return tool.Ifc.get()
def draw(self, context):
if not MaterialsData.is_loaded:
@@ -143,9 +142,9 @@ class BIM_PT_object_material(Panel):
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
- if not IfcStore.get_element(props.ifc_definition_id):
+ if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
- if not hasattr(IfcStore.get_file().by_id(props.ifc_definition_id), "HasAssociations"):
+ if not hasattr(tool.Ifc.get().by_id(props.ifc_definition_id), "HasAssociations"):
return False
return True
@@ -153,7 +152,7 @@ class BIM_PT_object_material(Panel):
if not ObjectMaterialData.is_loaded:
ObjectMaterialData.load()
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
self.oprops = context.active_object.BIMObjectProperties
self.props = context.active_object.BIMObjectMaterialProperties
self.mprops = tool.Material.get_material_props()
@@ -351,9 +350,10 @@ class BIM_PT_object_material(Panel):
if ObjectMaterialData.data["total_thickness"]:
total_thickness = ObjectMaterialData.data["total_thickness"]
unit_system = bpy.context.scene.unit_settings.system
+ props = tool.Drawing.get_document_props()
if unit_system == "IMPERIAL":
- precision = bpy.context.scene.DocProperties.imperial_precision
+ precision = props.imperial_precision
else:
precision = None
formatted_thickness = format_distance(
diff --git a/src/bonsai/bonsai/bim/module/misc/operator.py b/src/bonsai/bonsai/bim/module/misc/operator.py
index 198408eeb5..20bd454d35 100644
--- a/src/bonsai/bonsai/bim/module/misc/operator.py
+++ b/src/bonsai/bonsai/bim/module/misc/operator.py
@@ -299,7 +299,7 @@ class DrawSystemArrows(bpy.types.Operator, tool.Ifc.Operator):
tool.Blender.select_and_activate_single_object(context, curve)
def get_absolute_matrix(self, matrix):
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
matrix = np.array(
ifcopenshell.util.geolocation.global2local(
diff --git a/src/bonsai/bonsai/bim/module/model/data.py b/src/bonsai/bonsai/bim/module/model/data.py
index cc713e5bd7..5cc0f64699 100644
--- a/src/bonsai/bonsai/bim/module/model/data.py
+++ b/src/bonsai/bonsai/bim/module/model/data.py
@@ -26,7 +26,7 @@ from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc
import bonsai.tool as tool
from math import degrees
from natsort import natsorted
-from typing import Union
+from typing import Union, Optional
def refresh():
@@ -45,17 +45,14 @@ class AuthoringData:
data = {}
type_thumbnails = {}
types_per_page = 9
- ifc_element_type = None
is_loaded = False
@classmethod
- def load(cls, ifc_element_type=None):
+ def load(cls, ifc_element_type: Optional[str] = None):
cls.is_loaded = True
cls.props = tool.Model.get_model_props()
- if ifc_element_type:
- cls.ifc_element_type = None if ifc_element_type == "all" else ifc_element_type
cls.data["default_container"] = cls.default_container()
- cls.data["ifc_element_type"] = cls.ifc_element_type
+ cls.data["ifc_element_type"] = ifc_element_type
cls.data["ifc_classes"] = cls.ifc_classes()
cls.data["ifc_class_current"] = cls.ifc_class_current()
# Make sure .ifc_classes() was run before next lines
@@ -97,7 +94,7 @@ class AuthoringData:
@classmethod
def default_container(cls) -> str | None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = tool.Spatial.get_spatial_props()
if props.default_container:
try:
return tool.Ifc.get().by_id(props.default_container).Name
diff --git a/src/bonsai/bonsai/bim/module/model/handler.py b/src/bonsai/bonsai/bim/module/model/handler.py
index 2b30a1238c..b69b595c34 100644
--- a/src/bonsai/bonsai/bim/module/model/handler.py
+++ b/src/bonsai/bonsai/bim/module/model/handler.py
@@ -20,7 +20,6 @@ import bpy
import ifcopenshell
import ifcopenshell.api
from bonsai.bim.module.model import product, wall, slab, profile, opening, task
-from bonsai.bim.ifc import IfcStore
from bpy.app.handlers import persistent
diff --git a/src/bonsai/bonsai/bim/module/model/opening.py b/src/bonsai/bonsai/bim/module/model/opening.py
index 0e73e71a91..319ffdd345 100644
--- a/src/bonsai/bonsai/bim/module/model/opening.py
+++ b/src/bonsai/bonsai/bim/module/model/opening.py
@@ -38,7 +38,6 @@ import bonsai.tool as tool
import bonsai.core.geometry
import bonsai.bim.import_ifc as import_ifc
from collections import defaultdict
-from bonsai.bim.ifc import IfcStore
from math import pi, radians
from mathutils import Vector, Matrix
from bpy.types import Operator
@@ -236,9 +235,9 @@ class FilledOpeningGenerator:
voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element]
for voided_element in voided_elements:
voided_obj = tool.Ifc.get_object(voided_element)
- if not voided_obj.data:
+ representation = tool.Geometry.get_active_representation(voided_obj)
+ if not representation:
continue
- representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
@@ -465,10 +464,13 @@ class AddBoolean(Operator, tool.Ifc.Operator):
self.report({"INFO"}, "At least two representation items must be selected to add a boolean.")
return {"CANCELLED"}
- props = context.scene.BIMBooleanProperties
+ props = tool.Feature.get_boolean_props()
- first_item = tool.Ifc.get().by_id(first_obj.data.BIMMeshProperties.ifc_definition_id)
- second_items = [tool.Ifc.get().by_id(o.data.BIMMeshProperties.ifc_definition_id) for o in second_objs]
+ first_item = tool.Geometry.get_active_representation(first_obj)
+ assert first_item
+ second_items = [
+ representation for o in second_objs if (representation := tool.Geometry.get_active_representation(o))
+ ]
booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator)
rep_obj = tool.Geometry.get_geometry_props().representation_obj
@@ -664,6 +666,7 @@ class EditOpenings(Operator, tool.Ifc.Operator):
def edit_openings(
self, building_objs: set[bpy.types.Object], opening_elements: set[ifcopenshell.entity_instance]
) -> None:
+ props = tool.Geometry.get_geometry_props()
objects_to_remove: set[bpy.types.Object] = set()
for opening_element in opening_elements:
opening_obj = tool.Ifc.get_object(opening_element)
@@ -690,8 +693,8 @@ class EditOpenings(Operator, tool.Ifc.Operator):
self.get_all_building_objects_of_similar_openings(opening_element)
) # NB this has nothing to do with clone similar_opening
tool.Ifc.unlink(element=opening_element)
- if bpy.context.scene.BIMGeometryProperties.representation_obj == opening_obj:
- bpy.context.scene.BIMGeometryProperties.representation_obj = None
+ if props.representation_obj == opening_obj:
+ props.representation_obj = None
objects_to_remove.add(opening_obj)
tool.Blender.remove_data_blocks(objects_to_remove, remove_unused_data=True)
@@ -817,11 +820,11 @@ class RemoveBoolean(Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
- props = context.scene.BIMBooleanProperties
+ props = tool.Feature.get_boolean_props()
return props.active_boolean
def _execute(self, context):
- props = context.scene.BIMBooleanProperties
+ props = tool.Feature.get_boolean_props()
ifcopenshell.api.geometry.remove_boolean(
tool.Ifc.get(), tool.Ifc.get().by_id(props.active_boolean.ifc_definition_id)
)
@@ -841,7 +844,7 @@ class SelectBoolean(Operator):
@classmethod
def poll(cls, context):
- props = context.scene.BIMBooleanProperties
+ props = tool.Feature.get_boolean_props()
return props.active_boolean
def invoke(self, context, event):
@@ -850,9 +853,9 @@ class SelectBoolean(Operator):
return self.execute(context)
def execute(self, context):
- props = context.scene.BIMBooleanProperties
+ props = tool.Feature.get_boolean_props()
queue = [tool.Ifc.get().by_id(props.active_boolean.ifc_definition_id)]
- items = {i.ifc_definition_id: i.obj for i in context.scene.BIMGeometryProperties.item_objs}
+ items = {i.ifc_definition_id: i.obj for i in tool.Geometry.get_geometry_props().item_objs}
while queue:
item = queue.pop()
if item.is_a("IfcBooleanResult"):
@@ -911,11 +914,12 @@ class DecorationsHandler:
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
+ gprops = tool.Geometry.get_geometry_props()
for opening in props.openings:
obj = opening.obj
- if context.scene.BIMGeometryProperties.representation_obj == obj:
+ if gprops.representation_obj == obj:
# We are editing the representation of the opening :
- for item in context.scene.BIMGeometryProperties.item_objs:
+ for item in gprops.item_objs:
if item.obj.mode == "EDIT":
obj = item.obj
break
diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py
index 2f780015a1..d879a5d474 100644
--- a/src/bonsai/bonsai/bim/module/model/polyline.py
+++ b/src/bonsai/bonsai/bim/module/model/polyline.py
@@ -35,7 +35,6 @@ import bonsai.core.root
import bonsai.core.geometry
import bonsai.core.model as core
import bonsai.tool as tool
-from bonsai.bim.ifc import IfcStore
from math import pi, sin, cos, degrees, tan, radians
from mathutils import Vector, Matrix, Quaternion
from bonsai.bim.module.model.opening import FilledOpeningGenerator
@@ -430,71 +429,90 @@ def get_horizontal_profile_preview_data(context, relating_type):
case "9":
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
- # Create profile curve
- scale_mat = Matrix.Scale(-1, 4, (1.0, 0.0, 0.0))
- grouped_verts = [scale_mat @ Vector(v) for v in grouped_verts]
- profile_curve = bpy.data.curves.new("Profile", type="CURVE")
- profile_curve.dimensions = "2D"
- profile_curve.splines.new("POLY")
- profile_curve.splines[0].points.add(len(grouped_verts))
-
- for i, point in enumerate(profile_curve.splines[0].points):
- if i == len(grouped_verts): # Close curve
- point.co = Vector((*grouped_verts[0], 0))
- continue
- point.co = Vector((*grouped_verts[i], 0))
- profile_obj = bpy.data.objects.new("Profile", profile_curve)
-
- # Create path curve with profile object as bevel
- path_curve = bpy.data.curves.new("Polyline", type="CURVE")
- path_curve.dimensions = "2D"
- path_curve.splines.new("POLY")
- path_curve.splines[0].points.add(len(polyline_verts) - 1)
- for i, point in enumerate(path_curve.splines[0].points):
- point.co = Vector((*polyline_verts[i], 0))
- path_curve.splines[0].use_smooth = False
- path_curve.bevel_mode = "OBJECT"
- path_curve.bevel_object = profile_obj
-
- # Convert path curve to mesh
- # This operation throws a warning when done during gpu drawing, so it was removed from the decorator file to be handled here
- path_obj = bpy.data.objects.new("Preview", path_curve)
- context.scene.collection.objects.link(path_obj)
- bpy.context.view_layer.objects.active = path_obj
- dg = context.evaluated_depsgraph_get()
- path_obj = path_obj.evaluated_get(dg)
- me = path_obj.to_mesh()
-
- # Create bmesh from path mesh
- bm = bmesh.new()
- new_verts = [bm.verts.new(v.co) for v in me.vertices]
- index = [[v for v in edge.vertices] for edge in me.edges]
- new_edges = [bm.edges.new((new_verts[i[0]], new_verts[i[1]])) for i in index]
- for face in me.polygons:
- verts = [new_verts[i] for i in face.vertices]
- bm.faces.new(verts)
- bm.verts.index_update()
- bm.edges.index_update()
- tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
-
- bpy.data.objects.remove(bpy.data.objects[path_obj.name], do_unlink=True)
- bpy.data.objects.remove(bpy.data.objects[profile_obj.name], do_unlink=True)
- try:
- bpy.data.curves.remove(profile_obj.data, do_unlink=True)
- except:
- pass
- try:
- bpy.data.curves.remove(path_obj.data, do_unlink=True)
- except:
- pass
-
data = {}
- data["verts"] = [tuple(v.co) for v in bm.verts]
- data["edges"] = [(edge.verts[0].index, edge.verts[1].index) for edge in bm.edges]
+ data["verts"] = []
+ data["edges"] = []
+ data["tris"] = []
+
+ grouped_verts = [(v) for v in grouped_verts]
+
+ all_bm = bmesh.new()
+ for i in range(len(polyline_verts) - 1):
+ mesh = bpy.data.meshes.new("TempMesh")
+ # Create the initial mesh from the profile verts
+ bm = create_bmesh_from_vertices(grouped_verts, is_closed=True)
+ bm.verts.ensure_lookup_table()
+ # Creates the clipping plane formed by two segments.
+ # The first one is for the profile start, based on the current and previous segment of the polyline.
+ # The second is for the profile end, based on the current and the next segment.
+ if i == 0:
+ d = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
+ clip_start = d
+ else:
+ d1 = (polyline_verts[i] - polyline_verts[i - 1]).normalized()
+ d2 = (polyline_verts[i] - polyline_verts[i + 1]).normalized()
+ clip_start = (d1 - d2).normalized()
+
+ if i == len(polyline_verts) - 2:
+ d = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
+ clip_end = d
+ else:
+ d1 = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
+ d2 = (polyline_verts[i + 1] - polyline_verts[i + 2]).normalized()
+ clip_end = (d1 - d2).normalized()
+
+ # Rotates the profile face to the right direction
+ direction = polyline_verts[i + 1] - polyline_verts[i]
+ position = polyline_verts[i]
+ rotation_matrix = direction.to_track_quat("Z", "Y").to_matrix().to_4x4()
+ bmesh.ops.transform(bm, verts=bm.verts, matrix=rotation_matrix)
+ bmesh.ops.translate(bm, verts=bm.verts, vec=position)
+ bmesh.ops.translate(bm, verts=bm.verts, vec=-direction)
+
+ # Extrude and move the new face
+ last_face = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:])
+ new_verts = [e for e in last_face["geom"] if isinstance(e, bmesh.types.BMVert)]
+ bmesh.ops.translate(bm, verts=new_verts, vec=direction * 3)
+ # Apply the cutting planes
+ cut = bmesh.ops.bisect_plane(
+ bm,
+ geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
+ plane_co=polyline_verts[i],
+ plane_no=clip_start,
+ clear_inner=True,
+ )
+ bm.verts.index_update()
+ bm.edges.index_update()
+ cut = bmesh.ops.bisect_plane(
+ bm,
+ geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
+ plane_co=polyline_verts[i + 1],
+ plane_no=clip_end,
+ clear_outer=True,
+ )
+
+ bm.to_mesh(mesh)
+ bm.free()
+ mesh.update()
+ all_bm.from_mesh(mesh)
+ bpy.data.meshes.remove(bpy.data.meshes["TempMesh"])
+
+ # It's necessary to add the mesh to an object to get the expected result.
+ mesh = bpy.data.meshes.new("TempMesh2")
+ all_bm.to_mesh(mesh)
+ all_bm.free()
+ obj = bpy.data.objects.new("TempObj", mesh)
+ bm = bmesh.new()
+ bm.from_mesh(obj.data)
+ bpy.data.meshes.remove(bpy.data.meshes["TempMesh2"])
+
+ verts = [tuple(v.co) for v in bm.verts]
+ edges = [[v.index for v in e.verts] for e in bm.edges]
+ tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
+ data["verts"] = verts
+ data["edges"] = edges
data["tris"] = tris
-
bm.free()
-
return data
@@ -598,7 +616,7 @@ class PolylineOperator:
self.report({"WARNING"}, "The number typed is not valid.")
return is_valid
else:
- if self.input_type in {"X", "Y"}:
+ if self.input_type in {"X", "Y", "Z"}:
tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state)
elif self.input_type in {"D", "A"}:
tool.Polyline.calculate_x_y_and_z(context, self.input_ui, self.tool_state)
@@ -633,21 +651,21 @@ class PolylineOperator:
if x:
if event.shift and event.value == "PRESS" and event.type == "X":
self.tool_state.use_default_container = False
- self.tool_state.plane_method = "YZ"
+ self.tool_state.plane_method = "YZ" if self.tool_state.plane_method != "YZ" else None
self.tool_state.axis_method = None
tool.Blender.update_viewport()
if y:
if event.shift and event.value == "PRESS" and event.type == "Y":
self.tool_state.use_default_container = False
- self.tool_state.plane_method = "XZ"
+ self.tool_state.plane_method = "XZ" if self.tool_state.plane_method != "XZ" else None
self.tool_state.axis_method = None
tool.Blender.update_viewport()
if z:
if event.shift and event.value == "PRESS" and event.type == "Z":
self.tool_state.use_default_container = False
- self.tool_state.plane_method = "XY"
+ self.tool_state.plane_method = "XY" if self.tool_state.plane_method != "XY" else None
self.tool_state.axis_method = None
tool.Blender.update_viewport()
diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py
index ddc9eb5fea..4e4ebd731d 100644
--- a/src/bonsai/bonsai/bim/module/model/product.py
+++ b/src/bonsai/bonsai/bim/module/model/product.py
@@ -58,7 +58,8 @@ class AddEmptyType(bpy.types.Operator, AddObjectHelper):
def execute(self, context):
obj = bpy.data.objects.new("TYPEX", None)
context.scene.collection.objects.link(obj)
- context.scene.BIMRootProperties.ifc_product = "IfcElementType"
+ rprops = tool.Root.get_root_props()
+ rprops.ifc_product = "IfcElementType"
tool.Blender.select_and_activate_single_object(context, obj)
return {"FINISHED"}
@@ -71,7 +72,7 @@ class AddDefaultType(bpy.types.Operator, tool.Ifc.Operator):
ifc_element_type: bpy.props.StringProperty()
def _execute(self, context):
- props = context.scene.BIMRootProperties
+ props = tool.Root.get_root_props()
props.ifc_product = "IfcElementType"
props.ifc_class = self.ifc_element_type
if self.ifc_element_type == "IfcWallType":
@@ -363,7 +364,7 @@ class AddConstrTypeInstance(bpy.types.Operator, tool.Ifc.Operator):
)
bonsai.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
- rprops = context.scene.BIMRootProperties
+ rprops = tool.Root.get_root_props()
ifc_context = None
if get_enum_items(rprops, "contexts", context):
ifc_context = int(rprops.contexts or "0") or None
@@ -757,7 +758,7 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings):
elements.append(element)
for element in elements:
- obj = IfcStore.get_element(element.id())
+ obj = tool.Ifc.get_object_by_identifier(element.id())
if not obj:
continue
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py
index 6286422f99..c094da200c 100644
--- a/src/bonsai/bonsai/bim/module/model/profile.py
+++ b/src/bonsai/bonsai/bim/module/model/profile.py
@@ -22,6 +22,7 @@ import bmesh
import mathutils.geometry
import ifcopenshell
import ifcopenshell.api
+import ifcopenshell.api.geometry
import ifcopenshell.util.type
import ifcopenshell.util.unit
import ifcopenshell.util.element
@@ -47,6 +48,7 @@ class DumbProfileGenerator:
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
def generate(self, insertion_type="CURSOR"):
+ self.insertion_type = insertion_type
self.file = tool.Ifc.get()
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
material = ifcopenshell.util.element.get_material(self.relating_type)
@@ -69,9 +71,9 @@ class DumbProfileGenerator:
self.rotation = 0
self.location = Vector((0, 0, 0))
self.cardinal_point = int(props.cardinal_point)
- if insertion_type == "POLYLINE":
+ if self.insertion_type == "POLYLINE":
return self.derive_from_polyline()
- elif insertion_type == "CURSOR":
+ elif self.insertion_type == "CURSOR":
return self.derive_from_cursor()
def derive_from_polyline(self) -> tuple[list[Union[dict[str, Any], None]], bool]:
@@ -106,13 +108,16 @@ class DumbProfileGenerator:
matrix_world = Matrix()
if self.relating_type.is_a() not in ("IfcColumnType", "IfcPileType"):
- matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world
- matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world
+ if self.insertion_type not in {"POLYLINE"}:
+ matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world
+ matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world
+ else:
+ rotation_matrix = self.direction.to_track_quat("Z", "Y")
+ matrix_world = rotation_matrix.to_matrix().to_4x4() @ matrix_world
matrix_world.translation = self.location
- if self.container_obj:
+ if self.insertion_type not in {"POLYLINE"} and self.container_obj:
matrix_world.translation.z = self.container_obj.location.z
-
element = bonsai.core.root.assign_class(
tool.Ifc,
tool.Collector,
@@ -170,19 +175,19 @@ class DumbProfileGenerator:
return obj
def create_profile_from_2_points(self, coords, should_round=False) -> Union[dict[str, Any], None]:
- direction = coords[1] - coords[0]
- length = direction.length
+ self.direction = coords[1] - coords[0]
+ length = self.direction.length
if round(length, 4) < 0.1:
return
data = {"coords": coords}
self.depth = length
- self.rotation = atan2(direction[1], direction[0])
+ self.rotation = atan2(self.direction[1], self.direction[0])
if should_round:
# Round to nearest 50mm (yes, metric for now)
self.length = 0.05 * round(length / 0.05)
# Round to nearest 5 degrees
- nearest_degree = (math.pi / 180) * 5
+ nearest_degree = (pi / 180) * 5
self.rotation = nearest_degree * round(self.rotation / nearest_degree)
self.location = coords[0]
data["obj"] = self.create_profile()
@@ -265,7 +270,7 @@ class DumbProfileRegenerator:
def _regenerate_from_type(self, related_object: ifcopenshell.entity_instance) -> None:
obj = tool.Ifc.get_object(related_object)
- if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
+ if not obj or not tool.Geometry.get_active_representation(obj):
return
DumbProfileRecalculator().recalculate([obj])
@@ -501,7 +506,9 @@ class DumbProfileJoiner:
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_axis
)
- def get_placement_axes(body_representation):
+ def get_placement_axes(
+ body_representation: Union[ifcopenshell.entity_instance, None],
+ ) -> Union[tuple[tuple[float, float, float], tuple[float, float, float]], tuple[None, None]]:
if not body_representation:
return None, None
extrusion = tool.Model.get_extrusion(body_representation)
@@ -513,8 +520,7 @@ class DumbProfileJoiner:
return ((0.0, 0.0, 1.0), (1.0, 0.0, 0.0))
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
- new_body = ifcopenshell.api.run(
- "geometry.add_profile_representation",
+ new_body = ifcopenshell.api.geometry.add_profile_representation(
tool.Ifc.get(),
context=self.body_context,
profile=self.profile,
@@ -527,8 +533,9 @@ class DumbProfileJoiner:
if old_body:
for inverse in tool.Ifc.get().get_inverse(old_body):
ifcopenshell.util.element.replace_attribute(inverse, old_body, new_body)
- obj.data.BIMMeshProperties.ifc_definition_id = int(new_body.id())
- obj.data.name = f"{self.body_context.id()}/{new_body.id()}"
+ assert isinstance(mesh := obj.data, bpy.types.Mesh)
+ tool.Ifc.link(new_body, mesh)
+ mesh.name = tool.Loader.get_mesh_name(new_body)
bonsai.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_body)
else:
ifcopenshell.api.run(
@@ -1119,6 +1126,8 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato
def __init__(self):
super().__init__()
+ self.input_ui = tool.Polyline.create_input_ui(init_z=True)
+ self.input_options = ["D", "A", "X", "Y", "Z"]
self.relating_type = None
props = tool.Model.get_model_props()
relating_type_id = props.relating_type_id
@@ -1163,7 +1172,9 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato
self.handle_mouse_move(context, event, should_round=True)
- self.choose_axis(event)
+ self.choose_axis(event, z=True)
+
+ self.choose_plane(event)
self.handle_snap_selection(context, event)
@@ -1199,6 +1210,6 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato
def _invoke(self, context, event):
super().invoke(context, event)
ProductDecorator.install(context)
- self.tool_state.use_default_container = True
- self.tool_state.plane_method = "XY"
+ self.tool_state.use_default_container = False
+ self.tool_state.plane_method = None
return {"RUNNING_MODAL"}
diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py
index 7466a4f70b..4864da1348 100644
--- a/src/bonsai/bonsai/bim/module/model/prop.py
+++ b/src/bonsai/bonsai/bim/module/model/prop.py
@@ -60,6 +60,8 @@ def get_materials(self, context):
def update_ifc_class(self, context):
bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class)
+ AuthoringData.data["ifc_class_current"] = self.ifc_class
+ AuthoringData.data["type_elements"] = AuthoringData.type_elements()
AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id()
AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail()
if tool.Blender.get_enum_safe(self, "relating_type_id") is None:
diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py
index de3078819d..1010217cae 100644
--- a/src/bonsai/bonsai/bim/module/model/slab.py
+++ b/src/bonsai/bonsai/bim/module/model/slab.py
@@ -259,7 +259,7 @@ class DumbSlabPlaner:
self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str], new_thickness: float
) -> None:
obj = tool.Ifc.get_object(related_object)
- if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
+ if not obj or not tool.Geometry.get_active_representation(obj):
return
material = ifcopenshell.util.element.get_material(related_object)
@@ -885,11 +885,13 @@ class DrawPolylineSlab(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
return {"FINISHED"}
slab = DumbSlabGenerator(self.relating_type).generate("POLYLINE")
+ if not slab:
+ return
model_props = tool.Model.get_model_props()
direction_sense = model_props.direction_sense
offset = model_props.offset
- model = IfcStore.get_file()
+ model = tool.Ifc.get()
element = tool.Ifc.get_entity(slab)
material = ifcopenshell.util.element.get_material(element)
material_set_usage = model.by_id(material.id())
diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py
index b0a59a3916..297ce5a2d7 100644
--- a/src/bonsai/bonsai/bim/module/model/ui.py
+++ b/src/bonsai/bonsai/bim/module/model/ui.py
@@ -117,9 +117,8 @@ class LaunchTypeManager(bpy.types.Operator):
op.ifc_product = "IfcElementType"
op.ifc_class = AuthoringData.data["ifc_element_type"] or props.ifc_class or ""
- if AuthoringData.data["total_types"]:
- row = self.layout.row(align=True)
- row.prop(props, "search_name", icon="FILTER", text="")
+ row = self.layout.row(align=True)
+ row.prop(props, "search_name", icon="FILTER", text="")
columns = self.layout.column_flow(columns=3)
if AuthoringData.data["total_pages"] > 0:
diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py
index 5a94be7528..daf9d9a43b 100644
--- a/src/bonsai/bonsai/bim/module/model/wall.py
+++ b/src/bonsai/bonsai/bim/module/model/wall.py
@@ -377,7 +377,7 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
walls, is_polyline_closed = DumbWallGenerator(self.relating_type).generate("POLYLINE")
for wall in walls:
- model = IfcStore.get_file()
+ model = tool.Ifc.get()
element = tool.Ifc.get_entity(wall["obj"])
material = ifcopenshell.util.element.get_material(element)
material_set_usage = model.by_id(material.id())
@@ -598,7 +598,7 @@ class DumbWallGenerator:
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
def generate(self, insertion_type="CURSOR"):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
self.layers = tool.Model.get_material_layer_parameters(self.relating_type)
if not self.layers["thickness"]:
return
@@ -908,7 +908,7 @@ class DumbWallPlaner:
self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str]
) -> None:
obj = tool.Ifc.get_object(related_object)
- if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
+ if not obj or not tool.Geometry.get_active_representation(obj):
return
material = ifcopenshell.util.element.get_material(related_object)
@@ -1331,7 +1331,9 @@ class DumbWallJoiner:
axis = body = tool.Model.get_wall_axis(obj)["reference"]
self.axis = copy.deepcopy(axis)
self.body = copy.deepcopy(body)
- extrusion_data = self.get_extrusion_data(tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id))
+ representation = tool.Geometry.get_active_representation(obj)
+ assert representation
+ extrusion_data = self.get_extrusion_data(representation)
height = extrusion_data["height"]
x_angle = extrusion_data["x_angle"]
self.clippings = []
@@ -1410,8 +1412,9 @@ class DumbWallJoiner:
if old_body:
for inverse in tool.Ifc.get().get_inverse(old_body):
ifcopenshell.util.element.replace_attribute(inverse, old_body, new_body)
- obj.data.BIMMeshProperties.ifc_definition_id = int(new_body.id())
- obj.data.name = f"{self.body_context.id()}/{new_body.id()}"
+ assert isinstance(mesh := obj.data, bpy.types.Mesh)
+ tool.Ifc.link(new_body, mesh)
+ mesh.name = tool.Loader.get_mesh_name(new_body)
bonsai.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_body)
else:
ifcopenshell.api.run(
@@ -1708,10 +1711,11 @@ class DumbWallJoiner:
return True
- def clip(self, wall1, slab2):
+ def clip(self, wall1: bpy.types.Object, slab2: bpy.types.Object) -> float:
"""returns height of the clipped wall, adds clipping plane to `clippings`"""
element1 = tool.Ifc.get_entity(wall1)
element2 = tool.Ifc.get_entity(slab2)
+ assert element1 and element2
layers1 = tool.Model.get_material_layer_parameters(element1)
axis1 = tool.Model.get_wall_axis(wall1, layers1)
@@ -1719,7 +1723,9 @@ class DumbWallJoiner:
bases = [axis1["base"][0].to_3d(), axis1["base"][1].to_3d(), axis1["side"][0].to_3d(), axis1["side"][1].to_3d()]
bases = [Vector((v[0], v[1], wall1.matrix_world.translation.z)) for v in bases] # add wall Z location
- extrusion = self.get_extrusion_data(tool.Ifc.get().by_id(wall1.data.BIMMeshProperties.ifc_definition_id))
+ representation = tool.Geometry.get_active_representation(wall1)
+ assert representation
+ extrusion = self.get_extrusion_data(representation)
wall_dir = wall1.matrix_world.to_quaternion() @ extrusion["direction"]
slab_pt = slab2.matrix_world @ Vector((0, 0, 0))
diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py
index ad4446b9b6..014ebd013b 100644
--- a/src/bonsai/bonsai/bim/module/model/workspace.py
+++ b/src/bonsai/bonsai/bim/module/model/workspace.py
@@ -96,14 +96,16 @@ class BimTool(WorkSpaceTool):
def draw_settings(
cls, context: bpy.types.Context, layout: bpy.types.UILayout, ws_tool: bpy.types.WorkSpaceTool
) -> None:
- if context.scene.BIMGeometryProperties.mode == "ITEM":
+ props = tool.Geometry.get_geometry_props()
+ ifc_element_type = None if cls.ifc_element_type == "all" else cls.ifc_element_type
+ if props.mode == "ITEM":
EditItemUI.draw(context, layout)
elif (
active_ifc_object := (context.active_object and tool.Ifc.get_entity(context.active_object))
) and context.selected_objects:
- EditObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
+ EditObjectUI.draw(context, layout, ifc_element_type=ifc_element_type)
else:
- CreateObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
+ CreateObjectUI.draw(context, layout, ifc_element_type=ifc_element_type)
class WallTool(BimTool):
@@ -430,7 +432,7 @@ class EditItemUI:
obj = context.active_object
assert obj
- mesh_props = obj.data.BIMMeshProperties
+ mesh_props = tool.Geometry.get_mesh_props(obj.data)
if AuthoringData.data["is_representation_item_swept_solid"]:
# TODO: support EndSweptArea for IfcRevolvedAreaSolidTapered,
# will need to add second attribute for this.
@@ -440,10 +442,10 @@ class EditItemUI:
op = row.operator("bim.name_profile", text="", icon="TAG")
op.extrusion_item_obj = obj.name
- for item_attribute in obj.data.BIMMeshProperties.item_attributes:
+ for item_attribute in mesh_props.item_attributes:
row = cls.layout.row()
draw_attribute(item_attribute, cls.layout)
- if len(obj.data.BIMMeshProperties.item_attributes) or AuthoringData.data["is_representation_item_swept_solid"]:
+ if len(mesh_props.item_attributes) or AuthoringData.data["is_representation_item_swept_solid"]:
row = cls.layout.row()
row.operator("bim.update_item_attributes", icon="FILE_REFRESH", text="")
@@ -499,9 +501,7 @@ class CreateObjectUI:
layout: bpy.types.UILayout
@classmethod
- def draw(
- cls, context: bpy.types.Context, layout: bpy.types.UILayout, ifc_element_type: Optional[str] = None
- ) -> None:
+ def draw(cls, context: bpy.types.Context, layout: bpy.types.UILayout, ifc_element_type: Union[str, None]) -> None:
cls.layout = layout
cls.props = tool.Model.get_model_props()
@@ -515,15 +515,13 @@ class CreateObjectUI:
if not AuthoringData.is_loaded:
AuthoringData.load(ifc_element_type)
- elif ifc_element_type == "all" and AuthoringData.data["ifc_element_type"] is not None:
- AuthoringData.load("all")
elif AuthoringData.data["ifc_element_type"] != ifc_element_type:
AuthoringData.load(ifc_element_type)
- if ifc_element_type and context.region.type == "TOOL_HEADER":
+ if context.region.type == "TOOL_HEADER":
tool_name = (
"Multi Object Tool"
- if ifc_element_type == "all"
+ if ifc_element_type is None
else format_ifc_camel_case(ifc_element_type.removesuffix("Type")) + " Tool"
)
cls.layout.label(text=tool_name, icon="TOOL_SETTINGS")
@@ -746,8 +744,6 @@ class EditObjectUI:
if not AuthoringData.is_loaded:
AuthoringData.load(ifc_element_type)
- elif ifc_element_type == "all" and AuthoringData.data["ifc_element_type"] is not None:
- AuthoringData.load("all")
elif AuthoringData.data["ifc_element_type"] != ifc_element_type:
AuthoringData.load(ifc_element_type)
@@ -1136,7 +1132,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
row.prop(self, "z")
def hotkey_S_A(self):
- if bpy.context.scene.BIMGeometryProperties.mode == "ITEM":
+ gprops = tool.Geometry.get_geometry_props()
+ if gprops.mode == "ITEM":
bpy.ops.wm.call_menu(name="BIM_MT_add_representation_item")
return
diff --git a/src/bonsai/bonsai/bim/module/nest/operator.py b/src/bonsai/bonsai/bim/module/nest/operator.py
index 00b1f15f33..f3247817ec 100644
--- a/src/bonsai/bonsai/bim/module/nest/operator.py
+++ b/src/bonsai/bonsai/bim/module/nest/operator.py
@@ -21,7 +21,6 @@ import ifcopenshell
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.nest as core
-from bonsai.bim.ifc import IfcStore
class BIM_OT_nest_assign_object(bpy.types.Operator, tool.Ifc.Operator):
@@ -113,7 +112,7 @@ class BIM_OT_select_components(bpy.types.Operator):
obj: bpy.props.StringProperty()
def execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
obj = bpy.data.objects.get(self.obj) or context.active_object
components = ifcopenshell.util.element.get_components(tool.Ifc.get_entity(obj))
component_objs = set(tool.Ifc.get_object(c) for c in components)
@@ -132,7 +131,7 @@ class BIM_OT_select_nest(bpy.types.Operator):
obj: bpy.props.StringProperty()
def execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
obj = bpy.data.objects.get(self.obj) or context.active_object
nest = ifcopenshell.util.element.get_nest(tool.Ifc.get_entity(obj))
nest_obj = tool.Ifc.get_object(nest)
diff --git a/src/bonsai/bonsai/bim/module/nest/ui.py b/src/bonsai/bonsai/bim/module/nest/ui.py
index b1983a2411..1fd8f5ca4a 100644
--- a/src/bonsai/bonsai/bim/module/nest/ui.py
+++ b/src/bonsai/bonsai/bim/module/nest/ui.py
@@ -16,9 +16,9 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+import bonsai.tool as tool
from bpy.types import Panel
from bonsai.bim.module.nest.data import NestData
-from bonsai.bim.ifc import IfcStore
class BIM_PT_nest(Panel):
@@ -37,9 +37,9 @@ class BIM_PT_nest(Panel):
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
- if not IfcStore.get_element(props.ifc_definition_id):
+ if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
- if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
+ if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"):
return False
return True
diff --git a/src/bonsai/bonsai/bim/module/profile/data.py b/src/bonsai/bonsai/bim/module/profile/data.py
index 88bd85197c..38a32d34f7 100644
--- a/src/bonsai/bonsai/bim/module/profile/data.py
+++ b/src/bonsai/bonsai/bim/module/profile/data.py
@@ -94,7 +94,7 @@ class ProfileData:
obj = bpy.context.active_object
return (
obj
- and obj.data
- and hasattr(obj.data, "BIMMeshProperties")
- and obj.data.BIMMeshProperties.subshape_type == "PROFILE"
+ and (data := obj.data)
+ and isinstance(data, bpy.types.Mesh)
+ and tool.Geometry.get_mesh_props(data).subshape_type == "PROFILE"
)
diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py
index b0d5073429..eab0db8ef2 100644
--- a/src/bonsai/bonsai/bim/module/profile/operator.py
+++ b/src/bonsai/bonsai/bim/module/profile/operator.py
@@ -18,6 +18,7 @@
import bpy
import ifcopenshell.api
+import ifcopenshell.api.profile
import ifcopenshell.util.element
import bonsai.bim.helper
import bonsai.tool as tool
@@ -174,12 +175,12 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator):
props.object_to_profile = None
if not indices:
points = [(0, 0), (0.1, 0), (0.1, 0.1), (0, 0.1), (0, 0)]
- profile = ifcopenshell.api.run("profile.add_arbitrary_profile", tool.Ifc.get(), profile=points)
+ profile = ifcopenshell.api.profile.add_arbitrary_profile(tool.Ifc.get(), profile=points)
else:
if "inner_curves" not in indices:
points = [(obj.data.vertices[i].co.x, obj.data.vertices[i].co.y) for i in indices["profile"]]
points.append(points[0])
- profile = ifcopenshell.api.run("profile.add_arbitrary_profile", tool.Ifc.get(), profile=points)
+ profile = ifcopenshell.api.profile.add_arbitrary_profile(tool.Ifc.get(), profile=points)
else:
outer_points = [(obj.data.vertices[i].co.x, obj.data.vertices[i].co.y) for i in indices["profile"]]
outer_points.append(outer_points[0])
@@ -189,8 +190,7 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator):
]
for curve in inner_points:
curve.append(curve[0])
- profile = ifcopenshell.api.run(
- "profile.add_arbitrary_profile_with_voids",
+ profile = ifcopenshell.api.profile.add_arbitrary_profile_with_voids(
tool.Ifc.get(),
outer_profile=outer_points,
inner_profiles=inner_points,
@@ -248,7 +248,12 @@ class EnableEditingArbitraryProfile(bpy.types.Operator):
def disable_editing_arbitrary_profile(context):
obj = context.active_object
- if obj and obj.type == "MESH" and obj.data and obj.data.BIMMeshProperties.subshape_type == "PROFILE":
+ if (
+ obj
+ and (mesh := obj.data)
+ and isinstance(mesh, bpy.types.Mesh)
+ and tool.Geometry.get_mesh_props(mesh).subshape_type == "PROFILE"
+ ):
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
profile_mesh = obj.data
diff --git a/src/bonsai/bonsai/bim/module/profile/prop.py b/src/bonsai/bonsai/bim/module/profile/prop.py
index 9a2da938e6..3f25c5f4e4 100644
--- a/src/bonsai/bonsai/bim/module/profile/prop.py
+++ b/src/bonsai/bonsai/bim/module/profile/prop.py
@@ -21,7 +21,6 @@ import ifcopenshell
import ifcopenshell.util.schema
import ifcopenshell.util.attribute
import bonsai.tool as tool
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty, Attribute
from bonsai.bim.module.profile.data import ProfileData
from bpy.types import PropertyGroup
diff --git a/src/bonsai/bonsai/bim/module/project/decorator.py b/src/bonsai/bonsai/bim/module/project/decorator.py
index cd00264224..1f738dbc90 100644
--- a/src/bonsai/bonsai/bim/module/project/decorator.py
+++ b/src/bonsai/bonsai/bim/module/project/decorator.py
@@ -31,7 +31,8 @@ from typing import Union
@persistent
def toggle_decorations_on_load(*args):
- if bpy.context.scene.BIMProjectProperties.clipping_planes:
+ props = tool.Project.get_project_props()
+ if props.clipping_planes:
ClippingPlaneDecorator.install(bpy.context)
else:
ClippingPlaneDecorator.uninstall()
@@ -99,7 +100,7 @@ class ProjectDecorator:
selected_edges = []
selected_tris = []
- props = context.scene.BIMProjectProperties
+ props = tool.Project.get_project_props()
try:
obj = props.queried_obj
selected_vertices = obj["selected_vertices"]
@@ -171,7 +172,8 @@ class ClippingPlaneDecorator:
unselected_edges = []
unselected_tris = []
- for clipping_plane in context.scene.BIMProjectProperties.clipping_planes:
+ props = tool.Project.get_project_props()
+ for clipping_plane in props.clipping_planes:
obj = clipping_plane.obj
if not obj or not obj.data:
continue
diff --git a/src/bonsai/bonsai/bim/module/project/gizmo.py b/src/bonsai/bonsai/bim/module/project/gizmo.py
index e5da7e3d52..bb372b9cb7 100644
--- a/src/bonsai/bonsai/bim/module/project/gizmo.py
+++ b/src/bonsai/bonsai/bim/module/project/gizmo.py
@@ -18,6 +18,7 @@
import bpy
+import bonsai.tool as tool
from bpy.types import GizmoGroup
from mathutils import Matrix
@@ -32,11 +33,12 @@ class ClippingPlane(GizmoGroup):
@classmethod
def poll(cls, context):
obj = context.object
+ props = tool.Project.get_project_props()
return (
context.selected_objects
and obj
and obj.name.startswith("ClippingPlane")
- and obj in [sp.obj for sp in context.scene.BIMProjectProperties.clipping_planes]
+ and obj in [sp.obj for sp in props.clipping_planes]
)
def setup(self, context):
diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py
index aa1c8b5403..b91e0d8158 100644
--- a/src/bonsai/bonsai/bim/module/project/operator.py
+++ b/src/bonsai/bonsai/bim/module/project/operator.py
@@ -75,35 +75,37 @@ class NewProject(bpy.types.Operator):
def execute(self, context):
bpy.ops.wm.read_homefile()
+ pprops = tool.Project.get_project_props()
+ bim_props = tool.Blender.get_bim_props()
if self.preset == "metric_m":
- bpy.context.scene.BIMProjectProperties.export_schema = "IFC4"
+ pprops.export_schema = "IFC4"
bpy.context.scene.unit_settings.system = "METRIC"
bpy.context.scene.unit_settings.length_unit = "METERS"
- bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
- bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
- bpy.context.scene.BIMProjectProperties.template_file = "0"
+ bim_props.area_unit = "SQUARE_METRE"
+ bim_props.volume_unit = "CUBIC_METRE"
+ pprops.template_file = "0"
elif self.preset == "metric_mm":
- bpy.context.scene.BIMProjectProperties.export_schema = "IFC4"
+ pprops.export_schema = "IFC4"
bpy.context.scene.unit_settings.system = "METRIC"
bpy.context.scene.unit_settings.length_unit = "MILLIMETERS"
- bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
- bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
- bpy.context.scene.BIMProjectProperties.template_file = "0"
+ bim_props.area_unit = "SQUARE_METRE"
+ bim_props.volume_unit = "CUBIC_METRE"
+ pprops.template_file = "0"
elif self.preset == "imperial_ft":
- bpy.context.scene.BIMProjectProperties.export_schema = "IFC4"
+ pprops.export_schema = "IFC4"
bpy.context.scene.unit_settings.system = "IMPERIAL"
bpy.context.scene.unit_settings.length_unit = "FEET"
- bpy.context.scene.BIMProperties.area_unit = "square foot"
- bpy.context.scene.BIMProperties.volume_unit = "cubic foot"
- bpy.context.scene.BIMProjectProperties.template_file = "0"
+ bim_props.area_unit = "square foot"
+ bim_props.volume_unit = "cubic foot"
+ pprops.template_file = "0"
elif self.preset == "demo":
- bpy.context.scene.BIMProjectProperties.export_schema = "IFC4"
+ pprops.export_schema = "IFC4"
bpy.context.scene.unit_settings.system = "METRIC"
bpy.context.scene.unit_settings.length_unit = "MILLIMETERS"
- bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
- bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
- bpy.context.scene.BIMProjectProperties.template_file = "IFC4 Demo Template.ifc"
+ bim_props.area_unit = "SQUARE_METRE"
+ bim_props.volume_unit = "CUBIC_METRE"
+ pprops.template_file = "IFC4 Demo Template.ifc"
if self.preset != "wizard":
bpy.ops.bim.create_project()
@@ -126,7 +128,7 @@ class CreateProject(bpy.types.Operator):
return {"FINISHED"}
def _execute(self, context):
- props = context.scene.BIMProjectProperties
+ props = tool.Project.get_project_props()
template = None if props.template_file == "0" else props.template_file
if tool.Blender.is_default_scene():
for obj in bpy.data.objects:
@@ -236,7 +238,7 @@ class RefreshLibrary(bpy.types.Operator):
elements.update(library_file.by_type(importable_type))
rels = tool.Project.get_project_library_rels(library_file)
elements = {e for e in elements if not tool.Project.is_element_assigned_to_project_library(e, rels)}
- self.props.add_library_project_library("Unassigned", len(elements), 0)
+ self.props.add_library_project_library("Unassigned", len(elements), 0, False)
ifc_project = library_file.by_type("IfcProject")[0]
hierarchy = tool.Project.get_project_hierarchy(library_file)
@@ -304,13 +306,14 @@ class ChangeLibraryElement(bpy.types.Operator):
if self.breadcrumb_type == "LIBRARY":
hierarchy = tool.Project.get_project_hierarchy(library_file)
assert active_project_library is not None
- if active_project_library == "NO_LIBRARY" or not hierarchy[active_project_library]:
- for appendable_type in sorted(tool.Project.get_appendable_asset_types()):
- elements = library_file.by_type(appendable_type)
- if elements := filter_elements(elements):
- self.props.add_library_asset_class(appendable_type, len(elements))
- else:
+ if active_project_library != "NO_LIBRARY" and hierarchy[active_project_library]:
tool.Project.load_project_libraries_to_ui(active_project_library, hierarchy)
+
+ for appendable_type in sorted(tool.Project.get_appendable_asset_types()):
+ elements = library_file.by_type(appendable_type)
+ if elements := filter_elements(elements):
+ self.props.add_library_asset_class(appendable_type, len(elements))
+
else: # breadcrumb_type CLASS.
elements = self.library_file.by_type(self.element_name)
elements = list(filter_elements(elements))
@@ -501,10 +504,10 @@ class AppendEntireLibrary(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ return tool.Ifc.get()
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
self.library = IfcStore.library_file
query = ", ".join(tool.Project.get_appendable_asset_types())
@@ -521,10 +524,10 @@ class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ return tool.Ifc.get()
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
self.library = IfcStore.library_file
for element in ifcopenshell.util.selector.filter_elements(self.library, self.query):
@@ -554,7 +557,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
- poll = bool(IfcStore.get_file())
+ poll = bool(tool.Ifc.get())
if not poll:
cls.poll_message_set("Please create or load a project first.")
return poll
@@ -591,7 +594,8 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
self.import_presentation_style_from_ifc(element, context)
try:
- context.scene.BIMProjectProperties.library_elements[self.prop_index].is_appended = True
+ props = tool.Project.get_project_props()
+ props.library_elements[self.prop_index].is_appended = True
except:
# TODO Remove this terrible code when I refactor this into the core
pass
@@ -599,7 +603,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
def import_material_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None:
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
logger = logging.getLogger("ImportIFC")
ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
@@ -617,7 +621,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
ifc_importer.create_style(style)
def import_product_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None:
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
logger = logging.getLogger("ImportIFC")
ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
ifc_importer = import_ifc.IfcImporter(ifc_import_settings)
@@ -630,7 +634,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
ifc_importer.place_objects_in_collections()
def import_type_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None:
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
logger = logging.getLogger("ImportIFC")
ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger)
@@ -645,7 +649,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
def import_materials(self, element: ifcopenshell.entity_instance, ifc_importer: import_ifc.IfcImporter) -> None:
for material in ifcopenshell.util.element.get_materials(element):
- if IfcStore.get_element(material.id()):
+ if tool.Ifc.get_object_by_identifier(material.id()):
continue
self.import_material_styles(material, ifc_importer)
@@ -659,7 +663,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
if not element.is_a("IfcRepresentationItem") or not element.StyledByItem:
continue
for element2 in self.file.traverse(element.StyledByItem[0]):
- if element2.is_a("IfcSurfaceStyle") and not IfcStore.get_element(element2.id()):
+ if element2.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element2.id()):
ifc_importer.create_style(element2)
def import_material_styles(
@@ -670,7 +674,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
if not material.HasRepresentation:
return
for element in self.file.traverse(material.HasRepresentation[0]):
- if element.is_a("IfcSurfaceStyle") and not IfcStore.get_element(element.id()):
+ if element.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element.id()):
ifc_importer.create_style(element)
@@ -767,14 +771,14 @@ class EnableEditingHeader(bpy.types.Operator):
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ return tool.Ifc.get()
def execute(self, context):
- self.file = IfcStore.get_file()
- props = context.scene.BIMProjectProperties
+ self.file = tool.Ifc.get()
+ props = tool.Project.get_project_props()
props.is_editing = True
- mvd = "".join(IfcStore.get_file().wrapped_data.header.file_description.description)
+ mvd = "".join(tool.Ifc.get().wrapped_data.header.file_description.description)
if "[" in mvd:
props.mvd = mvd.split("[")[1][0:-1]
else:
@@ -804,7 +808,7 @@ class EditHeader(bpy.types.Operator):
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ return tool.Ifc.get()
def execute(self, context):
IfcStore.begin_transaction(self)
@@ -817,8 +821,8 @@ class EditHeader(bpy.types.Operator):
return result
def _execute(self, context):
- self.file = IfcStore.get_file()
- props = context.scene.BIMProjectProperties
+ self.file = tool.Ifc.get()
+ props = tool.Project.get_project_props()
props.is_editing = True
self.file.wrapped_data.header.file_description.description = (f"ViewDefinition[{props.mvd}]",)
@@ -829,7 +833,7 @@ class EditHeader(bpy.types.Operator):
return {"FINISHED"}
def record_state(self):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
return {
"description": self.file.wrapped_data.header.file_description.description,
"author": self.file.wrapped_data.header.file_name.author,
@@ -838,14 +842,14 @@ class EditHeader(bpy.types.Operator):
}
def rollback(self, data):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
file.wrapped_data.header.file_description.description = data["old"]["description"]
file.wrapped_data.header.file_name.author = data["old"]["author"]
file.wrapped_data.header.file_name.organization = data["old"]["organisation"]
file.wrapped_data.header.file_name.authorization = data["old"]["authorisation"]
def commit(self, data):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
file.wrapped_data.header.file_description.description = data["new"]["description"]
file.wrapped_data.header.file_name.author = data["new"]["author"]
file.wrapped_data.header.file_name.organization = data["new"]["organisation"]
@@ -859,7 +863,8 @@ class DisableEditingHeader(bpy.types.Operator):
bl_description = "Cancel unsaved header information"
def execute(self, context):
- context.scene.BIMProjectProperties.is_editing = False
+ props = tool.Project.get_project_props()
+ props.is_editing = False
return {"FINISHED"}
@@ -975,16 +980,18 @@ class LoadProject(bpy.types.Operator, IFCFileSelector):
if not self.is_advanced and not self.should_start_fresh_session:
bpy.ops.bim.convert_to_blender()
- context.scene.BIMProperties.ifc_file = filepath
+ bim_props = tool.Blender.get_bim_props()
+ bim_props.ifc_file = filepath
if not tool.Ifc.get():
self.report(
{"ERROR"},
f"Error loading IFC file from filepath '{filepath}'. See logs above in the system console for the details.",
)
return {"CANCELLED"}
- context.scene.BIMProjectProperties.is_loading = True
- context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
- context.scene.BIMProjectProperties.use_relative_project_path = self.use_relative_path
+ props = tool.Project.get_project_props()
+ props.is_loading = True
+ props.total_elements = len(tool.Ifc.get().by_type("IfcElement"))
+ props.use_relative_project_path = self.use_relative_path
tool.Blender.register_toolbar()
tool.Project.add_recent_ifc_project(self.get_filepath_abs())
@@ -1034,13 +1041,15 @@ class RevertProject(bpy.types.Operator, IFCFileSelector):
@classmethod
def poll(cls, context):
- if not context.scene.BIMProperties.ifc_file:
+ props = tool.Blender.get_bim_props()
+ if not props.ifc_file:
cls.poll_message_set("IFC project need to be loaded and saved on the disk.")
return False
return True
def execute(self, context):
- bpy.ops.bim.load_project(should_start_fresh_session=True, filepath=context.scene.BIMProperties.ifc_file)
+ props = tool.Blender.get_bim_props()
+ bpy.ops.bim.load_project(should_start_fresh_session=True, filepath=props.ifc_file)
return {"FINISHED"}
@@ -1050,20 +1059,23 @@ class LoadProjectElements(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
- self.props = context.scene.BIMProjectProperties
- self.file = IfcStore.get_file()
+ self.props = tool.Project.get_project_props()
+ self.file = tool.Ifc.get()
bonsai.bim.schema.reload(self.file.schema_identifier)
start = time.time()
logger = logging.getLogger("ImportIFC")
path_log = tool.Blender.get_data_dir_path("process.log")
if not os.access(path_log.parent, os.W_OK):
- path_log = os.path.join(tempfile.mkdtemp(), "process.log")
+ path_log = os.path.join(
+ tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None), "process.log"
+ )
logging.basicConfig(
filename=path_log,
filemode="a",
level=logging.DEBUG,
)
- settings = import_ifc.IfcImportSettings.factory(context, context.scene.BIMProperties.ifc_file, logger)
+ props = tool.Blender.get_bim_props()
+ settings = import_ifc.IfcImportSettings.factory(context, props.ifc_file, logger)
settings.has_filter = self.props.filter_mode != "NONE"
settings.should_filter_spatial_elements = self.props.should_filter_spatial_elements
if self.props.filter_mode == "DECOMPOSITION":
@@ -1081,7 +1093,8 @@ class LoadProjectElements(bpy.types.Operator):
ifc_importer.execute()
settings.logger.info("Import finished in {:.2f} seconds".format(time.time() - start))
print("Import finished in {:.2f} seconds".format(time.time() - start))
- context.scene.BIMProjectProperties.is_loading = False
+ props = tool.Project.get_project_props()
+ props.is_loading = False
tool.Project.load_pset_templates()
tool.Project.load_default_thumbnails()
@@ -1153,7 +1166,8 @@ class ToggleFilterCategories(bpy.types.Operator):
should_select: bpy.props.BoolProperty(name="Should Select", default=True)
def execute(self, context):
- for filter_category in context.scene.BIMProjectProperties.filter_categories:
+ props = tool.Project.get_project_props()
+ for filter_category in props.filter_categories:
filter_category.is_selected = self.should_select
return {"FINISHED"}
@@ -1180,7 +1194,7 @@ class LinkIfc(bpy.types.Operator):
directory: str
def draw(self, context):
- pprops = context.scene.BIMProjectProperties
+ pprops = tool.Project.get_project_props()
row = self.layout.row()
row.prop(self, "use_relative_path")
row = self.layout.row()
@@ -1201,7 +1215,8 @@ class LinkIfc(bpy.types.Operator):
if bpy.data.filepath and filepath.samefile(bpy.data.filepath):
self.report({"INFO"}, "Can't link the current .blend file")
continue
- new = context.scene.BIMProjectProperties.links.add()
+ props = tool.Project.get_project_props()
+ new = props.links.add()
filepath = tool.Ifc.get_uri(filepath, use_relative_path=self.use_relative_path)
new.name = filepath
status = bpy.ops.bim.load_link(filepath=filepath, use_cache=self.use_cache)
@@ -1232,9 +1247,10 @@ class UnlinkIfc(bpy.types.Operator):
def execute(self, context):
filepath = Path(self.filepath).as_posix()
bpy.ops.bim.unload_link(filepath=filepath)
- index = context.scene.BIMProjectProperties.links.find(filepath)
+ props = tool.Project.get_project_props()
+ index = props.links.find(filepath)
if index != -1:
- context.scene.BIMProjectProperties.links.remove(index)
+ props.links.remove(index)
return {"FINISHED"}
@@ -1254,8 +1270,9 @@ class UnloadLink(bpy.types.Operator):
if tool.Blender.ensure_blender_path_is_abs(Path(library.filepath)) == filepath:
bpy.data.libraries.remove(library)
- links = context.scene.BIMProjectProperties.links
- link = links.get(self.filepath)
+ props = tool.Project.get_project_props()
+ links = props.links
+ link = links[self.filepath]
# Let's assume that user might delete it.
if empty_handle := link.empty_handle:
bpy.data.objects.remove(empty_handle)
@@ -1264,7 +1281,7 @@ class UnloadLink(bpy.types.Operator):
if not any([l.is_loaded for l in links]):
ProjectDecorator.uninstall()
# we make sure we don't draw queried object from the file that was just unlinked
- elif queried_obj := context.scene.BIMProjectProperties.queried_obj:
+ elif queried_obj := props.queried_obj:
queried_filepath = Path(queried_obj["ifc_filepath"])
if queried_filepath == filepath:
ProjectDecorator.uninstall()
@@ -1296,7 +1313,7 @@ class LoadLink(bpy.types.Operator):
def link_blend(self, filepath: Path) -> None:
with bpy.data.libraries.load(str(filepath), link=True) as (data_from, data_to):
data_to.scenes = data_from.scenes
- link = bpy.context.scene.BIMProjectProperties.links[self.filepath]
+ link = tool.Project.get_project_props().links[self.filepath]
for scene in bpy.data.scenes:
if not scene.library or Path(scene.library.filepath) != filepath:
continue
@@ -1322,13 +1339,14 @@ class LoadLink(bpy.types.Operator):
if not blend_filepath.exists():
pprops = tool.Project.get_project_props()
- gprops = bpy.context.scene.BIMGeoreferenceProperties
+ gprops = tool.Georeference.get_georeference_props()
code = f"""
import bpy
def run():
- gprops = bpy.context.scene.BIMGeoreferenceProperties
+ import bonsai.tool as tool
+ gprops = tool.Georeference.get_georeference_props()
# Our model origin becomes their host model origin
gprops.host_model_origin = "{gprops.model_origin}"
gprops.host_model_origin_si = "{gprops.model_origin_si}"
@@ -1339,7 +1357,7 @@ def run():
gprops.blender_offset_z = "{gprops.blender_offset_z}"
gprops.blender_x_axis_abscissa = "{gprops.blender_x_axis_abscissa}"
gprops.blender_x_axis_ordinate = "{gprops.blender_x_axis_ordinate}"
- pprops = bpy.context.scene.BIMProjectProperties
+ pprops = tool.Project.get_project_props()
pprops.distance_limit = {pprops.distance_limit}
pprops.false_origin_mode = "{pprops.false_origin_mode}"
pprops.false_origin = "{pprops.false_origin}"
@@ -1394,7 +1412,7 @@ except Exception as e:
with open(json_filepath, "r") as f:
data = json.load(f)
- gprops = bpy.context.scene.BIMGeoreferenceProperties
+ gprops = tool.Georeference.get_georeference_props()
for prop in ("model_origin", "model_origin_si", "model_project_north"):
if (value := data.get(prop, None)) is not None:
setattr(gprops, prop, value)
@@ -1430,8 +1448,8 @@ class ToggleLinkSelectability(bpy.types.Operator):
link: bpy.props.StringProperty(name="Linked IFC Filepath")
def execute(self, context):
- props = context.scene.BIMProjectProperties
- link = props.links.get(self.link)
+ props = tool.Project.get_project_props()
+ link = props.links[self.link]
self.library_filepath = tool.Blender.ensure_blender_path_is_abs(Path(self.link).with_suffix(".ifc.cache.blend"))
link.is_selectable = (is_selectable := not link.is_selectable)
for collection in self.get_linked_collections():
@@ -1457,8 +1475,8 @@ class ToggleLinkVisibility(bpy.types.Operator):
mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")))
def execute(self, context):
- props = context.scene.BIMProjectProperties
- link = props.links.get(self.link)
+ props = tool.Project.get_project_props()
+ link = props.links[self.link]
self.library_filepath = tool.Blender.ensure_blender_path_is_abs(Path(self.link).with_suffix(".ifc.cache.blend"))
if self.mode == "WIREFRAME":
self.toggle_wireframe(link)
@@ -1504,7 +1522,7 @@ class SelectLinkHandle(bpy.types.Operator):
index: bpy.props.IntProperty(name="Link Index")
def execute(self, context):
- props = context.scene.BIMProjectProperties
+ props = tool.Project.get_project_props()
link = props.links[self.index]
handle = link.empty_handle
if not handle:
@@ -1546,8 +1564,9 @@ class ExportIFC(bpy.types.Operator):
bpy.ops.wm.save_mainfile("INVOKE_DEFAULT")
return {"FINISHED"}
- self.use_relative_path = context.scene.BIMProjectProperties.use_relative_project_path
- if (filepath := context.scene.BIMProperties.ifc_file) and not self.should_save_as:
+ self.use_relative_path = tool.Project.get_project_props().use_relative_project_path
+ props = tool.Blender.get_bim_props()
+ if (filepath := props.ifc_file) and not self.should_save_as:
self.filepath = str(tool.Blender.ensure_blender_path_is_abs(Path(filepath)))
return self.execute(context)
if not self.filepath:
@@ -1561,7 +1580,7 @@ class ExportIFC(bpy.types.Operator):
return {"RUNNING_MODAL"}
def execute(self, context):
- project_props = context.scene.BIMProjectProperties
+ project_props = tool.Project.get_project_props()
project_props.use_relative_project_path = self.use_relative_path
if project_props.should_disable_undo_on_save:
old_history_size = tool.Ifc.get().history_size
@@ -1579,7 +1598,9 @@ class ExportIFC(bpy.types.Operator):
logger = logging.getLogger("ExportIFC")
path_log = tool.Blender.get_data_dir_path("process.log")
if not os.access(path_log.parent, os.W_OK):
- path_log = os.path.join(tempfile.mkdtemp(), "process.log")
+ path_log = os.path.join(
+ tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None), "process.log"
+ )
logging.basicConfig(
filename=path_log,
filemode="a",
@@ -1606,18 +1627,20 @@ class ExportIFC(bpy.types.Operator):
print("Export finished in {:.2f} seconds".format(time.time() - start))
# New project created in Bonsai should be in recent projects too.
tool.Project.add_recent_ifc_project(Path(output_file))
- scene = context.scene
- if not scene.DocProperties.ifc_files:
- new = scene.DocProperties.ifc_files.add()
+ props = tool.Drawing.get_document_props()
+ if not props.ifc_files:
+ new = props.ifc_files.add()
new.name = output_file
- if context.scene.BIMProjectProperties.use_relative_project_path and bpy.data.is_saved:
+ props = tool.Project.get_project_props()
+ if props.use_relative_project_path and bpy.data.is_saved:
output_file = os.path.relpath(output_file, bpy.path.abspath("//"))
- if scene.BIMProperties.ifc_file != output_file and extension not in ("ifczip", "ifcjson"):
- scene.BIMProperties.ifc_file = output_file
+ bim_props = tool.Blender.get_bim_props()
+ if bim_props.ifc_file != output_file and extension not in ("ifczip", "ifcjson"):
+ bim_props.ifc_file = output_file
save_blend_file = bool(bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath)
if save_blend_file:
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath)
- bpy.context.scene.BIMProperties.is_dirty = False
+ bim_props.is_dirty = False
bonsai.bim.handler.refresh_ui_data()
self.report(
{"INFO"},
@@ -1655,8 +1678,8 @@ class LoadLinkedProject(bpy.types.Operator):
start = time.time()
- pprops = bpy.context.scene.BIMProjectProperties
- gprops = bpy.context.scene.BIMGeoreferenceProperties
+ pprops = tool.Project.get_project_props()
+ gprops = tool.Georeference.get_georeference_props()
self.filepath = Path(self.filepath).as_posix()
print("Processing", self.filepath)
@@ -1870,7 +1893,7 @@ class LoadLinkedProject(bpy.types.Operator):
mesh = bpy.data.meshes.new("Mesh")
geometry = shape.geometry
- gprops = bpy.context.scene.BIMGeoreferenceProperties
+ gprops = tool.Georeference.get_georeference_props()
if (
gprops.has_blender_offset
and geometry.verts
@@ -1976,9 +1999,8 @@ class QueryLinkedElement(bpy.types.Operator):
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
LinksData.linked_data = {}
- props = context.scene.BIMProjectProperties
+ props = tool.Project.get_project_props()
props.queried_obj = None
- props.quried_obj_root = None
for area in bpy.context.screen.areas:
if area.type == "PROPERTIES":
@@ -2114,6 +2136,7 @@ class AppendInspectedLinkedElement(AppendLibraryElement):
def _execute(self, context):
from bonsai.bim.module.project.data import LinksData
+ props = tool.Project.get_project_props()
if not LinksData.linked_data:
self.report({"INFO"}, "No linked element found.")
return {"CANCELLED"}
@@ -2123,7 +2146,7 @@ class AppendInspectedLinkedElement(AppendLibraryElement):
self.report({"INFO"}, "Cannot find Global Id for element.")
return {"CANCELLED"}
- queried_obj = context.scene.BIMProjectProperties.queried_obj
+ queried_obj = props.queried_obj
ifc_file = tool.Ifc.get()
linked_ifc_file: ifcopenshell.file
@@ -2270,34 +2293,36 @@ class RefreshClippingPlanes(bpy.types.Operator):
def modal(self, context, event):
should_refresh = False
+ props = tool.Project.get_project_props()
self.clean_deleted_planes(context)
- for clipping_plane in context.scene.BIMProjectProperties.clipping_planes:
+ for clipping_plane in props.clipping_planes:
if clipping_plane.obj and self.is_moved(clipping_plane.obj):
should_refresh = True
break
- total_planes = len(context.scene.BIMProjectProperties.clipping_planes)
+ total_planes = len(props.clipping_planes)
if should_refresh or total_planes != self.total_planes:
self.refresh_clipping_planes(context)
- for clipping_plane in context.scene.BIMProjectProperties.clipping_planes:
+ for clipping_plane in props.clipping_planes:
if clipping_plane.obj:
tool.Geometry.record_object_position(clipping_plane.obj)
self.total_planes = total_planes
return {"PASS_THROUGH"}
- def clean_deleted_planes(self, context):
+ def clean_deleted_planes(self, context: bpy.types.Context) -> None:
+ props = tool.Project.get_project_props()
while True:
- for i, clipping_plane in enumerate(context.scene.BIMProjectProperties.clipping_planes):
+ for i, clipping_plane in enumerate(props.clipping_planes):
if clipping_plane.obj:
try:
clipping_plane.obj.name
except:
- context.scene.BIMProjectProperties.clipping_planes.remove(i)
+ props.clipping_planes.remove(i)
break
else:
- context.scene.BIMProjectProperties.clipping_planes.remove(i)
+ props.clipping_planes.remove(i)
break
else:
break
@@ -2321,14 +2346,15 @@ class RefreshClippingPlanes(bpy.types.Operator):
region = next(r for r in area.regions if r.type == "WINDOW")
data = region.data
- if not len(context.scene.BIMProjectProperties.clipping_planes):
+ props = tool.Project.get_project_props()
+ if not len(props.clipping_planes):
data.use_clip_planes = False
else:
with bpy.context.temp_override(area=area, region=region):
bpy.ops.view3d.clip_border()
clip_planes = []
- for clipping_plane in bpy.context.scene.BIMProjectProperties.clipping_planes:
+ for clipping_plane in tool.Project.get_project_props().clipping_planes:
obj = clipping_plane.obj
if not obj:
continue
@@ -2367,8 +2393,8 @@ class CreateClippingPlane(bpy.types.Operator):
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
# Clean up deleted planes
-
- if len(context.scene.BIMProjectProperties.clipping_planes) > 5:
+ props = tool.Project.get_project_props()
+ if len(props.clipping_planes) > 5:
self.report({"INFO"}, "Maximum of six clipping planes allowed.")
return {"FINISHED"}
@@ -2408,7 +2434,7 @@ class CreateClippingPlane(bpy.types.Operator):
context.scene.cursor.location = location
- new = context.scene.BIMProjectProperties.clipping_planes.add()
+ new = tool.Project.get_project_props().clipping_planes.add()
new.obj = plane_obj
tool.Blender.set_active_object(plane_obj)
@@ -2439,7 +2465,7 @@ class FlipClippingPlane(bpy.types.Operator):
def execute(self, context):
obj = context.active_object
- if obj in context.scene.BIMProjectProperties.clipping_planes_objs:
+ if obj in tool.Project.get_project_props().clipping_planes_objs:
obj.rotation_euler[0] += radians(180)
context.view_layer.update()
return {"FINISHED"}
@@ -2457,14 +2483,15 @@ class BIM_OT_save_clipping_planes(bpy.types.Operator):
@classmethod
def poll(cls, context):
if IfcStore.path:
- return context.scene.BIMProjectProperties.clipping_planes
+ return tool.Project.get_project_props().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
+ clipping_planes = tool.Project.get_project_props().clipping_planes
for clipping_plane in clipping_planes:
obj = clipping_plane.obj
+ assert 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]
@@ -2490,13 +2517,14 @@ class BIM_OT_load_clipping_planes(bpy.types.Operator):
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()
+ props = tool.Project.get_project_props()
+ bpy.data.batch_remove(props.clipping_planes_objs)
+ props.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 = props.clipping_planes_objs[-1]
obj.name = name
obj.location = values["location"]
obj.rotation_euler = values["rotation"]
diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py
index e56826be57..a570724fee 100644
--- a/src/bonsai/bonsai/bim/module/project/prop.py
+++ b/src/bonsai/bonsai/bim/module/project/prop.py
@@ -121,7 +121,7 @@ def update_filter_mode(self: "BIMProjectProperties", context: bpy.types.Context)
self.filter_categories.clear()
if self.filter_mode == "NONE":
return
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
if self.filter_mode == "DECOMPOSITION":
if file.schema == "IFC2X3":
elements = file.by_type("IfcSpatialStructureElement")
@@ -180,6 +180,8 @@ class LibraryElement(PropertyGroup):
element_type: EnumProperty(items=[(i, i, "") for i in get_args(LibraryElementType)], name="Element Type")
# Asset group.
asset_count: IntProperty(name="Asset Count")
+ # Asset library.
+ has_sublibraries: BoolProperty(name="Has Sublibraries", default=False)
# Asset.
ifc_definition_id: IntProperty(name="IFC Definition ID")
is_declared: BoolProperty(name="Is Declared", default=False)
@@ -194,6 +196,7 @@ class LibraryElement(PropertyGroup):
name: str
element_type: LibraryElementType
asset_count: int
+ has_sublibraries: bool
ifc_definition_id: int
is_declared: bool
is_appended: bool
@@ -299,7 +302,14 @@ class BIMProjectProperties(PropertyGroup):
should_merge_materials_by_colour: BoolProperty(name="Merge Materials by Colour", default=False)
should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False)
should_load_geometry: BoolProperty(name="Load Geometry", default=True)
- should_clean_mesh: BoolProperty(name="Clean Meshes", default=False)
+ should_clean_mesh: BoolProperty(
+ name="Clean Meshes",
+ description=(
+ "Convert all triangles to quads for meshes. "
+ "By default Bonsai is importing meshes triangulated (even if they are not stored as triangulated in IFC)."
+ ),
+ default=False,
+ )
should_cache: BoolProperty(name="Cache", default=False)
deflection_tolerance: FloatProperty(name="Deflection Tolerance", default=0.001)
angular_tolerance: FloatProperty(name="Angular Tolerance", default=0.5)
@@ -395,12 +405,15 @@ class BIMProjectProperties(PropertyGroup):
def clipping_planes_objs(self) -> list[bpy.types.Object]:
return list({cp.obj for cp in self.clipping_planes if cp.obj})
- def add_library_project_library(self, name: str, asset_count: int, ifc_definition_id: int) -> LibraryElement:
+ def add_library_project_library(
+ self, name: str, asset_count: int, ifc_definition_id: int, has_sublibraries: bool
+ ) -> LibraryElement:
new = self.library_elements.add()
new["name"] = name
new.asset_count = asset_count
new.element_type = "LIBRARY"
new.ifc_definition_id = ifc_definition_id
+ new.has_sublibraries = has_sublibraries
return new
def add_library_asset_class(self, name: str, asset_count: int) -> LibraryElement:
diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py
index cd0c69512f..c5d390c780 100644
--- a/src/bonsai/bonsai/bim/module/project/ui.py
+++ b/src/bonsai/bonsai/bim/module/project/ui.py
@@ -28,7 +28,7 @@ from bonsai.bim.module.project.data import ProjectData, LinksData
from typing import TYPE_CHECKING
if TYPE_CHECKING:
- from bonsai.bim.module.project.prop import LibraryElement, BIMProjectProperties
+ from bonsai.bim.module.project.prop import LibraryElement, BIMProjectProperties, FilterCategory, Link
def file_import_menu(self, context):
@@ -151,9 +151,9 @@ class BIM_PT_project(Panel):
self.layout.use_property_decorate = False
self.layout.use_property_split = True
- props = context.scene.BIMProperties
+ props = tool.Blender.get_bim_props()
pprops = self.props = tool.Project.get_project_props()
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
if pprops.is_loading:
self.draw_advanced_loading_ui(context)
elif self.file or props.ifc_file:
@@ -247,7 +247,7 @@ class BIM_PT_project(Panel):
def draw_editing_buttons(self, context, row):
pprops = self.props
- if IfcStore.get_file():
+ if tool.Ifc.get():
if pprops.is_editing:
row.operator("bim.edit_header", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_header", icon="CANCEL", text="")
@@ -257,10 +257,10 @@ class BIM_PT_project(Panel):
def draw_editable_file_info(self, context):
pprops = self.props
- if IfcStore.get_file():
+ if tool.Ifc.get():
row = self.layout.row(align=True)
row.label(text="IFC Schema", icon="FILE_CACHE")
- row.label(text=IfcStore.get_file().schema)
+ row.label(text=tool.Ifc.get().schema)
if pprops.is_editing:
row = self.layout.row(align=True)
@@ -281,7 +281,7 @@ class BIM_PT_project(Panel):
else:
row = self.layout.row(align=True)
row.label(text="IFC MVD", icon="FILE_HIDDEN")
- mvd = "".join(IfcStore.get_file().wrapped_data.header.file_description.description)
+ mvd = "".join(tool.Ifc.get().wrapped_data.header.file_description.description)
if "[" in mvd:
mvd = mvd.split("[")[1][0:-1]
row.label(text=mvd)
@@ -305,7 +305,7 @@ class BIM_PT_project(Panel):
def draw_loaded_project_ui(self, context):
# file name row
- props = context.scene.BIMProperties
+ props = tool.Blender.get_bim_props()
file_name_row = self.layout.row(align=True)
file_name_row.label(text=os.path.basename(props.ifc_file), icon="FILE")
self.draw_editing_buttons(context, file_name_row)
@@ -315,7 +315,7 @@ class BIM_PT_project(Panel):
# file path row and actions section
row = self.layout.row(align=True)
- if context.scene.BIMProperties.is_dirty:
+ if props.is_dirty:
row.label(text="Saved*", icon="EXPORT")
else:
row.label(text="Saved", icon="EXPORT")
@@ -339,7 +339,7 @@ class BIM_PT_new_project_wizard(Panel):
self.layout.use_property_decorate = False
self.layout.use_property_split = True
- props = context.scene.BIMProperties
+ props = tool.Blender.get_bim_props()
pprops = tool.Project.get_project_props()
prop_with_search(self.layout, pprops, "export_schema")
row = self.layout.row()
@@ -512,7 +512,7 @@ class BIM_UL_library(UIList):
):
if item:
row = layout.row(align=True)
- if item.element_type != "ASSET" and item.asset_count > 0:
+ if item.element_type != "ASSET" and (item.asset_count > 0 or item.has_sublibraries):
op = row.operator("bim.change_library_element", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False)
op.element_name = item.name
op.breadcrumb_type = item.element_type
@@ -542,7 +542,16 @@ class BIM_UL_library(UIList):
class BIM_UL_filter_categories(UIList):
- def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
+ def draw_item(
+ self,
+ context,
+ layout: bpy.types.UILayout,
+ data: BIMProjectProperties,
+ item: FilterCategory,
+ icon,
+ active_data,
+ active_propname,
+ ):
if item:
row = layout.row(align=True)
row.label(text=f"{item.name} ({item.total_elements})")
@@ -556,7 +565,17 @@ class BIM_UL_filter_categories(UIList):
class BIM_UL_links(UIList):
- def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
+ def draw_item(
+ self,
+ context,
+ layout: bpy.types.UILayout,
+ data: BIMProjectProperties,
+ item: Link,
+ icon,
+ active_data,
+ active_propname,
+ index,
+ ):
if item:
row = layout.row(align=True)
if item.is_loaded:
diff --git a/src/bonsai/bonsai/bim/module/pset/operator.py b/src/bonsai/bonsai/bim/module/pset/operator.py
index 4165951a53..691d2e6930 100644
--- a/src/bonsai/bonsai/bim/module/pset/operator.py
+++ b/src/bonsai/bonsai/bim/module/pset/operator.py
@@ -94,7 +94,7 @@ class EditPset(bpy.types.Operator, tool.Ifc.Operator):
properties: bpy.props.StringProperty()
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
props = tool.Pset.get_pset_props(self.obj, self.obj_type)
ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context)
element = tool.Ifc.get().by_id(ifc_definition_id)
@@ -226,7 +226,7 @@ class AddQto(bpy.types.Operator, tool.Ifc.Operator):
obj_type: bpy.props.StringProperty()
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
qto_name = tool.Pset.get_pset_name(self.obj, self.obj_type, pset_type="QTO")
bpy.ops.bim.enable_pset_editing(
pset_id=0, pset_name=qto_name, pset_type="QTO", obj=self.obj, obj_type=self.obj_type
@@ -314,7 +314,7 @@ class BIM_OT_rename_parameters(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props_to_map = context.scene.RenameProperties
- ifc_file = IfcStore.get_file()
+ ifc_file = tool.Ifc.get()
all_ifc_elements = ifc_file.by_type("IfcElement")
for ifc_element in all_ifc_elements:
@@ -347,7 +347,7 @@ class BIM_OT_add_edit_custom_property(bpy.types.Operator, tool.Ifc.Operator):
index: bpy.props.IntProperty()
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
props = context.scene.AddEditProperties
for obj in tool.Blender.get_selected_objects():
@@ -402,7 +402,7 @@ class BIM_OT_bulk_remove_psets(bpy.types.Operator, tool.Ifc.Operator):
index: bpy.props.IntProperty()
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
props = context.scene.DeletePsets
for obj in tool.Blender.get_selected_objects():
diff --git a/src/bonsai/bonsai/bim/module/pset/prop.py b/src/bonsai/bonsai/bim/module/pset/prop.py
index 86a3363557..6a1b68841f 100644
--- a/src/bonsai/bonsai/bim/module/pset/prop.py
+++ b/src/bonsai/bonsai/bim/module/pset/prop.py
@@ -26,7 +26,6 @@ import bonsai.tool as tool
from bonsai.bim.prop import Attribute, StrProperty
from bonsai.bim.module.pset.data import AddEditCustomPropertiesData, ObjectPsetsData, MaterialPsetsData
from bonsai.bim.module.material.data import ObjectMaterialData
-from bonsai.bim.ifc import IfcStore
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
@@ -135,7 +134,7 @@ def get_resource_pset_names(self, context):
global psetnames
rprops = context.scene.BIMResourceProperties
rtprops = context.scene.BIMResourceTreeProperties
- ifc_class = IfcStore.get_file().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a()
+ ifc_class = tool.Ifc.get().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a()
if ifc_class not in psetnames:
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True, schema=tool.Ifc.get_schema())
psetnames[ifc_class] = blender_formatted_enum_from_psets(psets)
@@ -146,7 +145,7 @@ def get_resource_qto_names(self, context):
global qtonames
rprops = context.scene.BIMResourceProperties
rtprops = context.scene.BIMResourceTreeProperties
- ifc_class = IfcStore.get_file().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a()
+ ifc_class = tool.Ifc.get().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a()
if ifc_class not in qtonames:
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, qto_only=True, schema=tool.Ifc.get_schema())
qtonames[ifc_class] = blender_formatted_enum_from_psets(psets)
@@ -174,7 +173,7 @@ def get_group_qto_names(self, context):
def get_profile_pset_names(self, context):
global psetnames
pprops = tool.Profile.get_profile_props()
- ifc_class = IfcStore.get_file().by_id(pprops.profiles[pprops.active_profile_index].ifc_definition_id).is_a()
+ ifc_class = tool.Ifc.get().by_id(pprops.profiles[pprops.active_profile_index].ifc_definition_id).is_a()
if ifc_class not in psetnames:
psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True, schema=tool.Ifc.get_schema())
psetnames[ifc_class] = blender_formatted_enum_from_psets(psets)
@@ -290,7 +289,8 @@ class AddEditProperties(PropertyGroup):
enum_values: CollectionProperty(name="Enum Values", type=Attribute)
def get_value_name(self) -> Union[Literal["string_value", "bool_value", "int_value", "float_value"], None]:
- ifc_data_type = IfcStore.get_schema().declaration_by_name(self.primary_measure_type)
+ schema = tool.Ifc.schema()
+ ifc_data_type = schema.declaration_by_name(self.primary_measure_type)
data_type = ifcopenshell.util.attribute.get_primitive_type(ifc_data_type)
if data_type == "string":
return "string_value"
diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py
index 21f3c1b083..4adda725dc 100644
--- a/src/bonsai/bonsai/bim/module/pset/ui.py
+++ b/src/bonsai/bonsai/bim/module/pset/ui.py
@@ -20,7 +20,6 @@ from __future__ import annotations
import bpy
import bonsai.tool as tool
from bpy.types import Panel
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import prop_with_search, get_display_value
from bonsai.bim.module.pset.data import (
ObjectPsetsData,
@@ -247,7 +246,7 @@ class BIM_PT_object_psets(Panel):
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
- if not IfcStore.get_element(props.ifc_definition_id):
+ if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
return True
@@ -325,7 +324,7 @@ class BIM_PT_object_qtos(Panel):
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
- if not IfcStore.get_element(props.ifc_definition_id):
+ if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
return True
diff --git a/src/bonsai/bonsai/bim/module/pset_template/prop.py b/src/bonsai/bonsai/bim/module/pset_template/prop.py
index 66cd7e3fea..fdd6188a76 100644
--- a/src/bonsai/bonsai/bim/module/pset_template/prop.py
+++ b/src/bonsai/bonsai/bim/module/pset_template/prop.py
@@ -21,6 +21,7 @@ import bpy
import ifcopenshell
import ifcopenshell.util.attribute
from ifcopenshell.util.doc import get_attribute_doc
+import bonsai.tool as tool
from bonsai.bim.module.pset_template.data import PsetTemplatesData
from bonsai.bim.prop import StrProperty, Attribute
from bonsai.bim.ifc import IfcStore
@@ -183,7 +184,8 @@ class PropTemplate(PropertyGroup):
def get_value_name(self) -> str:
if self.primary_measure_type == "-":
return "string_value"
- ifc_data_type = IfcStore.get_schema().declaration_by_name(self.primary_measure_type)
+ schema = tool.Ifc.schema()
+ ifc_data_type = schema.declaration_by_name(self.primary_measure_type)
data_type = ifcopenshell.util.attribute.get_primitive_type(ifc_data_type)
if data_type == "string":
return "string_value"
diff --git a/src/bonsai/bonsai/bim/module/qto/calculator.py b/src/bonsai/bonsai/bim/module/qto/calculator.py
index e4ba0580a7..871b128db6 100644
--- a/src/bonsai/bonsai/bim/module/qto/calculator.py
+++ b/src/bonsai/bonsai/bim/module/qto/calculator.py
@@ -451,7 +451,8 @@ def get_side_area(o: bpy.types.Object) -> float:
def get_cross_section_area(obj: bpy.types.Object) -> float:
- representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ representation = tool.Geometry.get_active_representation(obj)
+ assert representation
item = representation.Items[0]
while True:
if item.is_a("IfcExtrudedAreaSolid"):
diff --git a/src/bonsai/bonsai/bim/module/qto/operator.py b/src/bonsai/bonsai/bim/module/qto/operator.py
index fe1574d1fa..63d16335a1 100644
--- a/src/bonsai/bonsai/bim/module/qto/operator.py
+++ b/src/bonsai/bonsai/bim/module/qto/operator.py
@@ -21,7 +21,6 @@ import ifcopenshell
import ifcopenshell.api
import bonsai.tool as tool
import bonsai.core.qto as core
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.qto import helper
diff --git a/src/bonsai/bonsai/bim/module/resource/prop.py b/src/bonsai/bonsai/bim/module/resource/prop.py
index 8613f44d1a..b5f1360c4e 100644
--- a/src/bonsai/bonsai/bim/module/resource/prop.py
+++ b/src/bonsai/bonsai/bim/module/resource/prop.py
@@ -19,7 +19,6 @@
import bpy
import ifcopenshell.api
import ifcopenshell.util.resource
-from bonsai.bim.ifc import IfcStore
import bonsai.tool as tool
import bonsai.bim.module.pset.data
import bonsai.bim.module.resource.data
diff --git a/src/bonsai/bonsai/bim/module/resource/ui.py b/src/bonsai/bonsai/bim/module/resource/ui.py
index 5ffdac8881..898749ff67 100644
--- a/src/bonsai/bonsai/bim/module/resource/ui.py
+++ b/src/bonsai/bonsai/bim/module/resource/ui.py
@@ -17,9 +17,9 @@
# along with Bonsai. If not, see .
import bpy
+import bonsai.tool as tool
import bonsai.bim.helper
from bpy.types import Panel, UIList
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.resource.data import ResourceData
from typing import Any
@@ -35,7 +35,7 @@ class BIM_PT_resources(Panel):
@classmethod
def poll(cls, context):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
diff --git a/src/bonsai/bonsai/bim/module/root/data.py b/src/bonsai/bonsai/bim/module/root/data.py
index 10c50839ec..64edaa2a9d 100644
--- a/src/bonsai/bonsai/bim/module/root/data.py
+++ b/src/bonsai/bonsai/bim/module/root/data.py
@@ -54,32 +54,14 @@ class IfcClassData:
@classmethod
def ifc_products(cls):
- products = [
- "IfcElementType",
- "IfcElement",
- "IfcFeatureElement",
- "IfcSpatialElement",
- "IfcSpatialElementType",
- "IfcStructuralItem",
- "IfcAnnotation",
- "IfcRelSpaceBoundary",
- ]
+ products = tool.Root.get_ifc_products()
version = tool.Ifc.get_schema()
- if version == "IFC2X3":
- products = [
- "IfcElementType",
- "IfcElement",
- "IfcFeatureElement",
- "IfcSpatialStructureElement",
- "IfcStructuralItem",
- "IfcAnnotation",
- "IfcRelSpaceBoundary",
- ]
return [(e, e, (get_entity_doc(version, e) or {}).get("description", "")) for e in products]
@classmethod
def ifc_classes(cls):
- ifc_product = bpy.context.scene.BIMRootProperties.ifc_product
+ rprops = tool.Root.get_root_props()
+ ifc_product = rprops.ifc_product
declaration = tool.Ifc.schema().declaration_by_name(ifc_product)
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
names = [d.name() for d in declarations]
@@ -99,7 +81,8 @@ class IfcClassData:
@classmethod
def ifc_predefined_types(cls):
types_enum = []
- ifc_class = bpy.context.scene.BIMRootProperties.ifc_class
+ rprops = tool.Root.get_root_props()
+ ifc_class = rprops.ifc_class
declaration = tool.Ifc.schema().declaration_by_name(ifc_class)
version = tool.Ifc.get_schema()
for attribute in declaration.attributes():
@@ -133,7 +116,8 @@ class IfcClassData:
@classmethod
def representation_template(cls):
- ifc_class = bpy.context.scene.BIMRootProperties.ifc_class
+ rprops = tool.Root.get_root_props()
+ ifc_class = rprops.ifc_class
templates = [
("EMPTY", "No Geometry", "Start with an empty object"),
None,
diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py
index 4801d3f999..a699287365 100644
--- a/src/bonsai/bonsai/bim/module/root/operator.py
+++ b/src/bonsai/bonsai/bim/module/root/operator.py
@@ -42,32 +42,33 @@ class EnableReassignClass(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
+ rprops = tool.Root.get_root_props()
obj = context.active_object
self.file = tool.Ifc.get()
- ifc_class = obj.name.split("/")[0]
+ element = tool.Ifc.get_entity(obj)
+ assert element
+ ifc_class = element.is_a()
context.active_object.BIMObjectProperties.is_reassigning_class = True
- ifc_products = [
- "IfcElement",
- "IfcElementType",
- "IfcSpatialElement",
- "IfcGroup",
- "IfcStructural",
- "IfcPositioningElement",
- "IfcContext",
- "IfcAnnotation",
- "IfcRelSpaceBoundary",
- ]
+ ifc_products = tool.Root.get_ifc_products()
+ schema = tool.Ifc.schema()
+ declaration = schema.declaration_by_name(ifc_class)
for ifc_product in ifc_products:
- if ifcopenshell.util.schema.is_a(IfcStore.get_schema().declaration_by_name(ifc_class), ifc_product):
- context.scene.BIMRootProperties.ifc_product = ifc_product
+ if ifcopenshell.util.schema.is_a(declaration, ifc_product):
+ rprops.ifc_product = ifc_product
+ break
+ else:
+ self.report({"ERROR"}, f"Couldn't find matching IFC product for the selected object: '{element}'.")
+ obj.BIMObjectProperties.is_reassigning_class = False
+ return {"CANCELLED"}
+
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
- context.scene.BIMRootProperties.ifc_class = element.is_a()
- context.scene.BIMRootProperties.relating_class_object = None
+ rprops.ifc_class = element.is_a()
+ rprops.relating_class_object = None
if hasattr(element, "PredefinedType"):
if element.PredefinedType:
- context.scene.BIMRootProperties.ifc_predefined_type = element.PredefinedType
+ rprops.ifc_predefined_type = element.PredefinedType
userdefined_type = ifcopenshell.util.element.get_predefined_type(element)
- context.scene.BIMRootProperties.ifc_userdefined_type = userdefined_type or ""
+ rprops.ifc_userdefined_type = userdefined_type or ""
return {"FINISHED"}
@@ -94,9 +95,9 @@ class ReassignClass(bpy.types.Operator, tool.Ifc.Operator):
else:
objects = set(context.selected_objects + [context.active_object])
self.file = tool.Ifc.get()
- root_props = context.scene.BIMRootProperties
- ifc_product: str = root_props.ifc_product
- ifc_class: str = root_props.ifc_class
+ root_props = tool.Root.get_root_props()
+ ifc_product = root_props.ifc_product
+ ifc_class = root_props.ifc_class
type_ifc_class = next(iter(ifcopenshell.util.type.get_applicable_types(ifc_class, self.file.schema)), None)
predefined_type = root_props.ifc_predefined_type
@@ -176,7 +177,7 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
ifc_representation_class: bpy.props.StringProperty()
def _execute(self, context):
- props = context.scene.BIMRootProperties
+ props = tool.Root.get_root_props()
objects: list[bpy.types.Object] = []
if self.obj:
objects = [bpy.data.objects[self.obj]]
@@ -358,7 +359,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE")
def _invoke(self, context, event):
- props = context.scene.BIMRootProperties
+ props = tool.Root.get_root_props()
# For convenience, preselect OBJs if applicable
if props.ifc_product == "IfcFeatureElement":
if (obj := tool.Blender.get_active_object(is_selected=True)) and obj.type == "MESH":
@@ -376,7 +377,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
return context.window_manager.invoke_props_dialog(self)
def _execute(self, context):
- props = context.scene.BIMRootProperties
+ props = tool.Root.get_root_props()
predefined_type = (
props.ifc_userdefined_type if props.ifc_predefined_type == "USERDEFINED" else props.ifc_predefined_type
)
@@ -500,7 +501,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
else:
material = ifcopenshell.api.run("material.add_material", tool.Ifc.get(), name="Unknown")
if representation_template == "PROFILESET":
- profile_id = tool.Blender.get_enum_safe(context.scene.BIMRootProperties, "profile")
+ profile_id = tool.Blender.get_enum_safe(props, "profile")
if profile_id in ("-", None):
profile = next((p for p in ifc_file.by_type("IfcProfileDef") if p.ProfileName), None)
if profile is None:
@@ -589,7 +590,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
tool.Blender.set_active_object(obj)
def draw(self, context):
- props = context.scene.BIMRootProperties
+ props = tool.Root.get_root_props()
self.layout.use_property_split = True
self.layout.use_property_decorate = False
row = self.layout.row()
@@ -600,7 +601,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
if not self.ifc_product:
prop_with_search(self.layout, props, "ifc_product", text="Definition", should_click_ok=True)
prop_with_search(self.layout, props, "ifc_class", should_click_ok=True)
- ifc_predefined_types = root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context)
+ ifc_predefined_types = root_prop.get_ifc_predefined_types(props, context)
if ifc_predefined_types:
prop_with_search(self.layout, props, "ifc_predefined_type", should_click_ok=True)
if props.ifc_predefined_type == "USERDEFINED":
diff --git a/src/bonsai/bonsai/bim/module/root/prop.py b/src/bonsai/bonsai/bim/module/root/prop.py
index 5243d95c03..4c358dfde1 100644
--- a/src/bonsai/bonsai/bim/module/root/prop.py
+++ b/src/bonsai/bonsai/bim/module/root/prop.py
@@ -34,28 +34,27 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
+from typing import TYPE_CHECKING, Union
-def get_ifc_predefined_types(self, context):
+def get_ifc_predefined_types(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]:
if not IfcClassData.is_loaded:
IfcClassData.load()
return IfcClassData.data["ifc_predefined_types"]
-def get_representation_template(self, context):
+def get_representation_template(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]:
if not IfcClassData.is_loaded:
IfcClassData.load()
return IfcClassData.data["representation_template"]
-def refresh_classes(self, context):
- old_class = context.scene.BIMRootProperties.ifc_class
- old_predefined_type = (
- context.scene.BIMRootProperties.ifc_predefined_type if get_ifc_predefined_types(self, context) else ""
- )
+def refresh_classes(self: "BIMRootProperties", context: bpy.types.Context) -> None:
+ old_class = self.ifc_class
+ old_predefined_type = self.ifc_predefined_type if get_ifc_predefined_types(self, context) else ""
enum = get_ifc_classes(self, context)
- context.scene.BIMRootProperties.ifc_class = enum[0][0]
+ self.ifc_class = enum[0][0]
IfcClassData.load()
if self.ifc_product == "IfcFeatureElement":
@@ -81,48 +80,44 @@ def refresh_classes(self, context):
self.ifc_predefined_type = old_predefined_type
-def refresh_predefined_types(self, context):
+def refresh_predefined_types(self: "BIMRootProperties", context: bpy.types.Context) -> None:
IfcClassData.load()
enum = get_ifc_predefined_types(self, context)
if enum:
- context.scene.BIMRootProperties.ifc_predefined_type = enum[0][0]
+ self.ifc_predefined_type = enum[0][0]
-def update_class_enum(self, context):
- self.ifc_class = self.ifc_class_filter_enum
-
-
-def get_ifc_products(self, context):
+def get_ifc_products(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]:
if not IfcClassData.is_loaded:
IfcClassData.load()
return IfcClassData.data["ifc_products"]
-def get_ifc_classes(self, context):
+def get_ifc_classes(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]:
if not IfcClassData.is_loaded:
IfcClassData.load()
return IfcClassData.data["ifc_classes"]
-def get_ifc_classes_suggestions():
+def get_ifc_classes_suggestions(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]:
if not IfcClassData.is_loaded:
IfcClassData.load()
return IfcClassData.data["ifc_classes_suggestions"]
-def get_contexts(self, context):
+def get_contexts(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]:
if not IfcClassData.is_loaded:
IfcClassData.load()
return IfcClassData.data["contexts"]
-def get_profile(self, context):
+def get_profile(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]:
if not IfcClassData.is_loaded:
IfcClassData.load()
return IfcClassData.data["profile"]
-def update_relating_class_from_object(self, context):
+def update_relating_class_from_object(self: "BIMRootProperties", context: bpy.types.Context) -> None:
if self.relating_class_object is None:
return
element = tool.Ifc.get_entity(self.relating_class_object)
@@ -139,7 +134,7 @@ def update_relating_class_from_object(self, context):
bpy.ops.bim.reassign_class()
-def is_object_class_applicable(self, obj):
+def is_object_class_applicable(self: "BIMRootProperties", obj: bpy.types.Object) -> bool:
element = tool.Ifc.get_entity(obj)
if not element:
return False
@@ -149,11 +144,11 @@ def is_object_class_applicable(self, obj):
return element.is_a("IfcTypeObject") == active_element.is_a("IfcTypeObject")
-def poll_representation_obj(self, obj):
+def poll_representation_obj(self: "BIMRootProperties", obj: bpy.types.Object) -> bool:
return obj.type == "MESH" and obj.data.polygons
-def poll_featured_obj(self, obj):
+def poll_featured_obj(self: "BIMRootProperties", obj: bpy.types.Object) -> bool:
return tool.Ifc.get_entity(obj)
@@ -192,3 +187,16 @@ class BIMRootProperties(PropertyGroup):
getter_enum_suggestions = {
"ifc_class": get_ifc_classes_suggestions,
}
+
+ if TYPE_CHECKING:
+ contexts: str
+ description: str
+ ifc_product: str
+ ifc_class: str
+ ifc_predefined_type: str
+ ifc_userdefined_type: str
+ featured_obj: Union[bpy.types.Object, None]
+ representation_template: str
+ representation_obj: Union[bpy.types.Object, None]
+ profile: str
+ relating_class_object: Union[bpy.types.Object, None]
diff --git a/src/bonsai/bonsai/bim/module/root/ui.py b/src/bonsai/bonsai/bim/module/root/ui.py
index 809230656f..6985bf832b 100644
--- a/src/bonsai/bonsai/bim/module/root/ui.py
+++ b/src/bonsai/bonsai/bim/module/root/ui.py
@@ -17,9 +17,9 @@
# along with Bonsai. If not, see .
import bpy
+import bonsai.tool as tool
import bonsai.bim.module.root.prop as root_prop
from bpy.types import Panel
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import prop_with_search
from bonsai.bim.module.root.data import IfcClassData
from bonsai.bim.module.model.data import AuthoringData
@@ -38,12 +38,13 @@ class BIM_PT_class(Panel):
def poll(cls, context):
if not context.active_object:
return False
- return IfcStore.get_file()
+ return tool.Ifc.get()
def draw(self, context):
if not IfcClassData.is_loaded:
IfcClassData.load()
props = context.active_object.BIMObjectProperties
+ rprops = tool.Root.get_root_props()
if props.ifc_definition_id:
if not IfcClassData.data["has_entity"]:
row = self.layout.row(align=True)
@@ -58,10 +59,10 @@ class BIM_PT_class(Panel):
row.operator("bim.disable_reassign_class", icon="CANCEL", text="")
self.draw_class_dropdowns(
context,
- root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context),
+ root_prop.get_ifc_predefined_types(rprops, context),
is_reassigning_class=True,
)
- self.layout.prop(context.scene.BIMRootProperties, "relating_class_object", icon="COPYDOWN")
+ self.layout.prop(rprops, "relating_class_object", icon="COPYDOWN")
else:
row = self.layout.row(align=True)
row.label(
@@ -78,16 +79,16 @@ class BIM_PT_class(Panel):
if AuthoringData.data["is_representation_item_active"]:
return
- ifc_predefined_types = root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context)
+ ifc_predefined_types = root_prop.get_ifc_predefined_types(rprops, context)
self.draw_class_dropdowns(context, ifc_predefined_types)
row = self.layout.row(align=True)
op = row.operator("bim.assign_class")
- op.ifc_class = context.scene.BIMRootProperties.ifc_class
- op.predefined_type = context.scene.BIMRootProperties.ifc_predefined_type if ifc_predefined_types else ""
- op.userdefined_type = context.scene.BIMRootProperties.ifc_userdefined_type
+ op.ifc_class = rprops.ifc_class
+ op.predefined_type = rprops.ifc_predefined_type if ifc_predefined_types else ""
+ op.userdefined_type = rprops.ifc_userdefined_type
def draw_class_dropdowns(self, context, ifc_predefined_types, is_reassigning_class=False):
- props = context.scene.BIMRootProperties
+ props = tool.Root.get_root_props()
layout = self.layout
prop_with_search(layout, props, "ifc_product")
prop_with_search(layout, props, "ifc_class")
diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py
index 5083fb2445..32d3729d58 100644
--- a/src/bonsai/bonsai/bim/module/search/operator.py
+++ b/src/bonsai/bonsai/bim/module/search/operator.py
@@ -39,6 +39,7 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
+from typing import TYPE_CHECKING, Literal, get_args
class AddFilterGroup(Operator):
@@ -144,13 +145,19 @@ class Search(Operator):
bl_idname = "bim.search"
bl_label = "Search"
- property_group: bpy.props.StringProperty(name="Property Group", default="")
+ PropertyGroupType = Literal["CsvProperties", "BIMSearchProperties"]
+ property_group: bpy.props.EnumProperty(
+ name="Property Group", items=[(i, i, "") for i in get_args(PropertyGroupType)]
+ )
+
+ if TYPE_CHECKING:
+ property_group: PropertyGroupType
def execute(self, context):
if self.property_group == "CsvProperties":
props = context.scene.CsvProperties
elif self.property_group == "BIMSearchProperties":
- props = context.scene.BIMSearchProperties
+ props = tool.Search.get_search_props()
else:
raise Exception(f"bim.search - unexpected property group name '{self.property_group}'.")
@@ -213,11 +220,12 @@ 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))
+ props = tool.Search.get_search_props()
+ group = tool.Ifc.get().by_id(int(props.saved_searches))
tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups)
def draw(self, context):
- props = context.scene.BIMSearchProperties
+ props = tool.Search.get_search_props()
row = self.layout.row()
row.prop(props, "saved_searches", text="")
@@ -239,7 +247,7 @@ class ColourByProperty(Operator):
return result
def _execute(self, context):
- props = context.scene.BIMSearchProperties
+ props = tool.Search.get_search_props()
query = props.colourscheme_query if props.colourscheme_key == "QUERY" else props.colourscheme_key
if not query:
@@ -358,11 +366,11 @@ class SelectByProperty(Operator):
@classmethod
def poll(cls, context):
- props = context.scene.BIMSearchProperties
+ props = tool.Search.get_search_props()
return props.active_colourscheme_index < len(props.colourscheme)
def execute(self, context):
- props = context.scene.BIMSearchProperties
+ props = tool.Search.get_search_props()
query = props.colourscheme_query if props.colourscheme_key == "QUERY" else props.colourscheme_key
if not query:
@@ -420,7 +428,7 @@ class SaveColourscheme(Operator, tool.Ifc.Operator):
if not self.name:
return
- props = context.scene.BIMSearchProperties
+ props = tool.Search.get_search_props()
query = props.colourscheme_query
group = [g for g in tool.Ifc.get().by_type("IfcGroup") if g.Name == self.name]
@@ -446,7 +454,7 @@ class LoadColourscheme(Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
- props = context.scene.BIMSearchProperties
+ props = tool.Search.get_search_props()
group = tool.Ifc.get().by_id(int(props.saved_colourschemes))
description = json.loads(group.Description)
props.colourscheme_query = description.get("colourscheme_query")
@@ -458,7 +466,7 @@ class LoadColourscheme(Operator, tool.Ifc.Operator):
new.colour = data["colour"]
def draw(self, context):
- props = context.scene.BIMSearchProperties
+ props = tool.Search.get_search_props()
row = self.layout.row()
row.prop(props, "saved_colourschemes", text="")
@@ -549,7 +557,7 @@ class ResetObjectColours(Operator):
def execute(self, context):
for obj in context.visible_objects:
obj.color = (1, 1, 1, 1)
- props = context.scene.BIMSearchProperties
+ props = tool.Search.get_search_props()
props.colourscheme.clear()
return {"FINISHED"}
@@ -562,7 +570,7 @@ class ToggleFilterSelection(Operator):
action: EnumProperty(items=(("SELECT", "Select", ""), ("DESELECT", "Deselect", "")))
def execute(self, context):
- props = bpy.context.scene.BIMSearchProperties
+ props = tool.Search.get_search_props()
self.selecting_actionbool = self.action == "SELECT"
if props.filter_type == "CLASSES":
for ifc_class in props.filter_classes:
@@ -587,7 +595,7 @@ class ActivateIfcClassFilter(Operator):
return True
def invoke(self, context, event):
- props = bpy.context.scene.BIMSearchProperties
+ props = tool.Search.get_search_props()
props.filter_classes.clear()
ifc_types = {}
for obj in context.selected_objects:
@@ -606,18 +614,20 @@ class ActivateIfcClassFilter(Operator):
return context.window_manager.invoke_props_dialog(self, width=250)
def execute(self, context):
- bpy.context.scene.BIMSearchProperties.filter_classes.clear()
+ props = tool.Search.get_search_props()
+ props.filter_classes.clear()
return {"FINISHED"}
def draw(self, context):
+ props = tool.Search.get_search_props()
self.layout.template_list(
"BIM_UL_ifc_class_filter",
"",
- context.scene.BIMSearchProperties,
+ props,
"filter_classes",
- context.scene.BIMSearchProperties,
+ props,
"filter_classes_index",
- rows=min(len(bpy.context.scene.BIMSearchProperties.filter_classes), 20),
+ rows=min(len(props.filter_classes), 20),
)
row = self.layout.row(align=True)
row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT"
@@ -638,7 +648,7 @@ class ActivateContainerFilter(Operator):
return True
def invoke(self, context, event):
- props = bpy.context.scene.BIMSearchProperties
+ props = tool.Search.get_search_props()
props.filter_container.clear()
containers = {}
@@ -661,18 +671,20 @@ class ActivateContainerFilter(Operator):
return context.window_manager.invoke_props_dialog(self, width=250)
def execute(self, context):
- bpy.context.scene.BIMSearchProperties.filter_container.clear()
+ props = tool.Search.get_search_props()
+ props.filter_container.clear()
return {"FINISHED"}
def draw(self, context):
+ props = tool.Search.get_search_props()
self.layout.template_list(
"BIM_UL_ifc_building_storey_filter",
"",
- context.scene.BIMSearchProperties,
+ props,
"filter_container",
- context.scene.BIMSearchProperties,
+ props,
"filter_container_index",
- rows=min(len(bpy.context.scene.BIMSearchProperties.filter_container), 20),
+ rows=min(len(props.filter_container), 20),
)
row = self.layout.row(align=True)
row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT"
@@ -711,14 +723,14 @@ class SelectSimilar(Operator, tool.Ifc.Operator):
return self.execute(context)
def _execute(self, context):
- props = context.scene.BIMSearchProperties
obj = context.active_object
element = tool.Ifc.get_entity(obj)
key = self.key
if key == "PredefinedType":
key = "predefined_type"
value = ifcopenshell.util.selector.get_element_value(element, key)
- tolerance = bpy.context.scene.DocProperties.tolerance
+ dprops = tool.Drawing.get_document_props()
+ tolerance = dprops.tolerance
# Determine the number of decimal places based on the magnitude of the rounding value
if tolerance < 1:
diff --git a/src/bonsai/bonsai/bim/module/search/prop.py b/src/bonsai/bonsai/bim/module/search/prop.py
index 08cfb46d8f..61cff222d3 100644
--- a/src/bonsai/bonsai/bim/module/search/prop.py
+++ b/src/bonsai/bonsai/bim/module/search/prop.py
@@ -33,33 +33,34 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
+from typing import TYPE_CHECKING
-def get_element_key(self, context):
+def get_element_key(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not SelectSimilarData.is_loaded:
SelectSimilarData.load()
return SelectSimilarData.data["element_key"]
-def get_colourscheme_key(self, context):
+def get_colourscheme_key(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not ColourByPropertyData.is_loaded:
ColourByPropertyData.load()
return ColourByPropertyData.data["colourscheme_key"]
-def get_saved_searches(self, context):
+def get_saved_searches(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not SearchData.is_loaded:
SearchData.load()
return SearchData.data["saved_searches"]
-def get_saved_colourschemes(self, context):
+def get_saved_colourschemes(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not ColourByPropertyData.is_loaded:
ColourByPropertyData.load()
return ColourByPropertyData.data["saved_colourschemes"]
-def update_is_class_selected(self, context):
+def update_is_class_selected(self: "BIMFilterClasses", context: bpy.types.Context) -> None:
if self.is_selected:
for obj in self.unselected_objects:
obj.obj.select_set(True)
@@ -73,7 +74,7 @@ def update_is_class_selected(self, context):
new.obj = obj
-def update_is_container_selected(self, context):
+def update_is_container_selected(self: "BIMFilterBuildingStoreys", context: bpy.types.Context) -> None:
if self.is_selected:
for obj in self.unselected_objects:
obj.obj.select_set(True)
@@ -87,15 +88,15 @@ def update_is_container_selected(self, context):
new.obj = obj
-def update_show_flat_colours(self, context):
+def update_show_flat_colours(self: "BIMSearchProperties", context: bpy.types.Context) -> None:
+ space = tool.Blender.get_view3d_space()
+ assert space
if self.show_flat_colours:
- space = tool.Blender.get_view3d_space()
space.shading.light = "FLAT"
space.shading.color_type = "OBJECT"
space.shading.show_object_outline = True
space.shading.show_cavity = True
else:
- space = tool.Blender.get_view3d_space()
space.shading.type = "SOLID"
space.shading.light = "STUDIO"
space.shading.show_object_outline = True
@@ -108,6 +109,11 @@ class BIMFilterClasses(PropertyGroup):
total: IntProperty(name="Total")
unselected_objects: CollectionProperty(type=ObjProperty, name="Unfiltered Objects")
+ if TYPE_CHECKING:
+ is_selected: bool
+ total: int
+ unselected_objects: bpy.types.bpy_prop_collection_idprop[ObjProperty]
+
class BIMFilterBuildingStoreys(PropertyGroup):
name: StringProperty(name="Name")
@@ -115,12 +121,21 @@ class BIMFilterBuildingStoreys(PropertyGroup):
total: IntProperty(name="Total")
unselected_objects: CollectionProperty(type=ObjProperty, name="Unfiltered Objects")
+ if TYPE_CHECKING:
+ is_selected: bool
+ total: int
+ unselected_objects: bpy.types.bpy_prop_collection_idprop[ObjProperty]
+
class BIMColour(PropertyGroup):
name: StringProperty(name="Name")
total: IntProperty(name="Total")
colour: FloatVectorProperty(name="Colour", subtype="COLOR", default=(1, 0, 0), min=0.0, max=1.0)
+ if TYPE_CHECKING:
+ total: int
+ colour: tuple[float, float, float]
+
class BIMSearchProperties(PropertyGroup):
element_key: EnumProperty(items=get_element_key, name="Element Key")
@@ -189,6 +204,29 @@ class BIMSearchProperties(PropertyGroup):
filter_container_index: IntProperty(name="Filter Level Index")
show_flat_colours: BoolProperty(name="Flat Colours", default=False, update=update_show_flat_colours)
+ if TYPE_CHECKING:
+ element_key: str
+ filter_query: str
+ filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
+ facet: str
+ saved_searches: str
+ saved_colourschemes: str
+ colourscheme_key: str
+ colourscheme_query: str
+ palette: str
+ min_mode: Literal["AUTO", "MANUAL"]
+ max_mode: Literal["AUTO", "MANUAL"]
+ min_value: float
+ max_value: float
+ colourscheme: bpy.types.bpy_prop_collection_idprop[BIMColour]
+ active_colourscheme_index: int
+ filter_type: str
+ filter_classes: bpy.types.bpy_prop_collection_idprop[BIMFilterClasses]
+ filter_classes_index: int
+ filter_container: bpy.types.bpy_prop_collection_idprop[BIMFilterBuildingStoreys]
+ filter_container_index: int
+ show_flat_colours: bool
+
def get_classes(self, ifc_product):
declaration = tool.Ifc.schema().declaration_by_name(ifc_product)
diff --git a/src/bonsai/bonsai/bim/module/search/ui.py b/src/bonsai/bonsai/bim/module/search/ui.py
index ce0b346027..a9254415e7 100644
--- a/src/bonsai/bonsai/bim/module/search/ui.py
+++ b/src/bonsai/bonsai/bim/module/search/ui.py
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see .
import bpy
+import bonsai.tool as tool
import bonsai.bim.helper
from bpy.types import Panel
from bonsai.bim.module.search.data import SearchData, ColourByPropertyData, SelectSimilarData
@@ -34,7 +35,7 @@ class BIM_PT_search(Panel):
if not SearchData.is_loaded:
SearchData.load()
- props = context.scene.BIMSearchProperties
+ props = tool.Search.get_search_props()
bonsai.bim.helper.draw_filter(self.layout, props.filter_groups, SearchData, "search")
@@ -73,7 +74,7 @@ class BIM_PT_colour_by_property(Panel):
if not ColourByPropertyData.is_loaded:
ColourByPropertyData.load()
- props = context.scene.BIMSearchProperties
+ props = tool.Search.get_search_props()
row = self.layout.row(align=True)
row.label(text=f"{len(ColourByPropertyData.data['saved_colourschemes'])} Saved Colourschemes")
@@ -127,7 +128,7 @@ class BIM_PT_select_similar(Panel):
if not SelectSimilarData.is_loaded:
SelectSimilarData.load()
- props = context.scene.BIMSearchProperties
+ props = tool.Search.get_search_props()
if SelectSimilarData.data["element_key"]:
row = self.layout.row(align=True)
diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py
index 6730353b00..5f5854edd2 100644
--- a/src/bonsai/bonsai/bim/module/sequence/operator.py
+++ b/src/bonsai/bonsai/bim/module/sequence/operator.py
@@ -31,7 +31,6 @@ import ifcopenshell.util.sequence
import ifcopenshell.util.selector
from datetime import datetime
from dateutil import parser, relativedelta
-from bonsai.bim.ifc import IfcStore
from bpy_extras.io_utils import ImportHelper
from typing import get_args, TYPE_CHECKING
from typing_extensions import assert_never
@@ -700,7 +699,7 @@ class ImportP6(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
def _execute(self, context):
from ifc4d.p62ifc import P62Ifc
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
start = time.time()
p62ifc = P62Ifc()
p62ifc.xml = self.filepath
@@ -728,7 +727,7 @@ class ImportP6XER(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
def _execute(self, context):
from ifc4d.p6xer2ifc import P6XER2Ifc
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
start = time.time()
p6xer2ifc = P6XER2Ifc()
p6xer2ifc.xer = self.filepath
@@ -756,7 +755,7 @@ class ImportPP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
def _execute(self, context):
from ifc4d.pp2ifc import PP2Ifc
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
start = time.time()
pp2ifc = PP2Ifc()
pp2ifc.pp = self.filepath
@@ -784,7 +783,7 @@ class ImportMSP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
def _execute(self, context):
from ifc4d.msp2ifc import MSP2Ifc
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
start = time.time()
msp2ifc = MSP2Ifc()
msp2ifc.xml = self.filepath
@@ -814,7 +813,7 @@ class ExportMSP(bpy.types.Operator, ImportHelper):
def execute(self, context):
from ifc4d.ifc2msp import Ifc2Msp
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
start = time.time()
ifc2msp = Ifc2Msp()
ifc2msp.work_schedule = self.file.by_type("IfcWorkSchedule")[0]
@@ -847,7 +846,7 @@ class ExportP6(bpy.types.Operator, ImportHelper):
def execute(self, context):
from ifc4d.ifc2p6 import Ifc2P6
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
start = time.time()
ifc2p6 = Ifc2P6()
ifc2p6.xml = bpy.path.ensure_ext(self.filepath, ".xml")
diff --git a/src/bonsai/bonsai/bim/module/sequence/prop.py b/src/bonsai/bonsai/bim/module/sequence/prop.py
index d2e550138d..ce0e307076 100644
--- a/src/bonsai/bonsai/bim/module/sequence/prop.py
+++ b/src/bonsai/bonsai/bim/module/sequence/prop.py
@@ -23,7 +23,6 @@ import ifcopenshell.util.attribute
import ifcopenshell.util.date
import bonsai.tool as tool
import bonsai.core.sequence as core
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.sequence.data import SequenceData, AnimationColorSchemeData, refresh as refresh_sequence_data
import bonsai.bim.module.resource.data
import bonsai.bim.module.pset.data
diff --git a/src/bonsai/bonsai/bim/module/sequence/ui.py b/src/bonsai/bonsai/bim/module/sequence/ui.py
index 2320bb80b4..21ed962852 100644
--- a/src/bonsai/bonsai/bim/module/sequence/ui.py
+++ b/src/bonsai/bonsai/bim/module/sequence/ui.py
@@ -22,7 +22,6 @@ import isodate
import bonsai.tool as tool
import bonsai.bim.helper
from bpy.types import Panel, UIList
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import draw_attributes
from bonsai.bim.module.sequence.data import (
WorkPlansData,
@@ -45,7 +44,7 @@ class BIM_PT_status(Panel):
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ return tool.Ifc.get()
def draw(self, context):
self.props = context.scene.BIMStatusProperties
@@ -77,7 +76,7 @@ class BIM_PT_work_plans(Panel):
@classmethod
def poll(cls, context):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
return file and file.schema != "IFC2X3"
def draw(self, context):
@@ -147,7 +146,7 @@ class BIM_PT_work_schedules(Panel):
@classmethod
def poll(cls, context):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
@@ -592,7 +591,7 @@ class BIM_PT_animation_Color_Scheme(Panel):
@classmethod
def poll(cls, context):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
@@ -980,7 +979,7 @@ class BIM_PT_work_calendars(Panel):
@classmethod
def poll(cls, context):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
diff --git a/src/bonsai/bonsai/bim/module/spatial/__init__.py b/src/bonsai/bonsai/bim/module/spatial/__init__.py
index 3194d18f3a..ee40768f08 100644
--- a/src/bonsai/bonsai/bim/module/spatial/__init__.py
+++ b/src/bonsai/bonsai/bim/module/spatial/__init__.py
@@ -26,7 +26,6 @@ classes = (
operator.DeleteContainer,
operator.DereferenceStructure,
operator.DisableEditingContainer,
- operator.EditContainerAttributes,
operator.EnableEditingContainer,
operator.ExpandContainer,
operator.ImportSpatialDecomposition,
diff --git a/src/bonsai/bonsai/bim/module/spatial/data.py b/src/bonsai/bonsai/bim/module/spatial/data.py
index eb8e6585b4..b3e863a329 100644
--- a/src/bonsai/bonsai/bim/module/spatial/data.py
+++ b/src/bonsai/bonsai/bim/module/spatial/data.py
@@ -58,7 +58,7 @@ class SpatialData:
@classmethod
def default_container(cls) -> str | None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = tool.Spatial.get_spatial_props()
if props.default_container:
try:
return tool.Ifc.get().by_id(props.default_container).Name
@@ -102,7 +102,7 @@ class SpatialDecompositionData:
@classmethod
def default_container(cls) -> str | None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = tool.Spatial.get_spatial_props()
if props.default_container:
try:
return tool.Ifc.get().by_id(props.default_container).Name
@@ -112,7 +112,7 @@ class SpatialDecompositionData:
@classmethod
def subelement_class(cls) -> list[tuple[str, str, str]]:
results = []
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = tool.Spatial.get_spatial_props()
if not (container := props.active_container):
return results
container_class = tool.Ifc.get().by_id(container.ifc_definition_id).is_a()
diff --git a/src/bonsai/bonsai/bim/module/spatial/decorator.py b/src/bonsai/bonsai/bim/module/spatial/decorator.py
index b977210805..a7c1059fc7 100644
--- a/src/bonsai/bonsai/bim/module/spatial/decorator.py
+++ b/src/bonsai/bonsai/bim/module/spatial/decorator.py
@@ -66,7 +66,8 @@ class GridDecorator:
blf.size(font_id, 12)
blf.enable(font_id, blf.SHADOW)
- for axis in context.scene.BIMGridProperties.grid_axes:
+ grid_props = tool.Spatial.get_grid_props()
+ for axis in grid_props.grid_axes:
if not (obj := axis.obj) or obj.hide_get() == True:
continue
if obj.select_get() and context.mode != "OBJECT":
@@ -120,7 +121,8 @@ class GridDecorator:
selected_edges = []
unselected_verts = []
unselected_edges = []
- for axis in context.scene.BIMGridProperties.grid_axes:
+ grid_props = tool.Spatial.get_grid_props()
+ for axis in grid_props.grid_axes:
if (obj := axis.obj) and obj.hide_get() == False:
if obj.select_get():
if context.mode != "OBJECT":
diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py
index 009e1739ed..1e6ed86ee6 100644
--- a/src/bonsai/bonsai/bim/module/spatial/operator.py
+++ b/src/bonsai/bonsai/bim/module/spatial/operator.py
@@ -96,7 +96,7 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
container = tool.Ifc.get().by_id(self.container)
elif (
(obj := tool.Blender.get_active_object())
- and (props := obj.BIMObjectSpatialProperties)
+ and (props := tool.Spatial.get_object_spatial_props(obj))
and (container_obj := props.container_obj)
and (container := tool.Ifc.get_entity(container_obj))
):
@@ -243,17 +243,6 @@ class ImportSpatialDecomposition(bpy.types.Operator):
return {"FINISHED"}
-class EditContainerAttributes(bpy.types.Operator):
- bl_idname = "bim.edit_container_attributes"
- bl_label = "Edit container attributes"
- bl_options = {"REGISTER", "UNDO"}
- container: bpy.props.IntProperty()
-
- def execute(self, context):
- core.edit_container_attributes(tool.Spatial, entity=tool.Ifc.get().by_id(self.container))
- return {"FINISHED"}
-
-
class ContractContainer(bpy.types.Operator):
bl_idname = "bim.contract_container"
bl_label = "Contract Container"
diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py
index 1017d8f069..910bb2e28c 100644
--- a/src/bonsai/bonsai/bim/module/spatial/prop.py
+++ b/src/bonsai/bonsai/bim/module/spatial/prop.py
@@ -36,15 +36,18 @@ import bonsai.core.geometry
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.unit
+from typing import TYPE_CHECKING, Union, Literal
-def get_subelement_class(self, context):
+def get_subelement_class(
+ self: "BIMSpatialDecompositionProperties", context: bpy.types.Context
+) -> list[tuple[str, str, str]]:
if not SpatialDecompositionData.is_loaded:
SpatialDecompositionData.load()
return SpatialDecompositionData.data["subelement_class"]
-def update_elevation(self, context):
+def update_elevation(self: "BIMContainer", context: bpy.types.Context) -> None:
try:
elevation = float(self.elevation)
if self.elevation != str(elevation):
@@ -91,7 +94,7 @@ def update_element_mode(self: "BIMSpatialDecompositionProperties", context: bpy.
tool.Spatial.load_contained_elements()
-def update_grid_is_locked(self, context):
+def update_grid_is_locked(self: "BIMGridProperties", context: bpy.types.Context) -> None:
if not tool.Ifc.get():
return
if tool.Ifc.get().schema in ("IFC2X3", "IFC4"):
@@ -108,7 +111,7 @@ def update_grid_is_locked(self, context):
bonsai.bim.handler.refresh_ui_data()
-def update_spatial_is_locked(self, context):
+def update_spatial_is_locked(self: "BIMSpatialDecompositionProperties", context: bpy.types.Context) -> None:
if not tool.Ifc.get():
return
if tool.Ifc.get().schema == "IFC2X3":
@@ -150,6 +153,10 @@ class BIMObjectSpatialProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
container_obj: PointerProperty(type=bpy.types.Object, name="Container", poll=poll_container_obj)
+ if TYPE_CHECKING:
+ is_editing: bool
+ container_obj: Union[bpy.types.Object, None]
+
class BIMContainer(PropertyGroup):
name: StringProperty(name="Name", update=update_name)
@@ -162,6 +169,16 @@ class BIMContainer(PropertyGroup):
is_expanded: BoolProperty(name="Is Expanded")
ifc_definition_id: IntProperty(name="IFC Definition ID")
+ if TYPE_CHECKING:
+ ifc_class: str
+ description: str
+ long_name: str
+ elevation: str
+ level_index: int
+ has_children: bool
+ is_expanded: bool
+ ifc_definition_id: int
+
class Element(PropertyGroup):
name: StringProperty(name="Name")
@@ -185,6 +202,16 @@ class Element(PropertyGroup):
),
)
+ if TYPE_CHECKING:
+ ifc_class: str
+ identification: str
+ ifc_definition_id: int
+ level: int
+ has_children: bool
+ total: int
+ is_expanded: bool
+ type: Literal["CLASS", "TYPE", "CLASSIFICATION", "OCCURRENCE"]
+
class BIMSpatialDecompositionProperties(PropertyGroup):
is_locked: BoolProperty(
@@ -223,6 +250,23 @@ class BIMSpatialDecompositionProperties(PropertyGroup):
name="Should Include Children", default=True, update=update_should_include_children
)
+ if TYPE_CHECKING:
+ is_locked: bool
+ is_visible: bool
+ container_filter: str
+ containers: bpy.types.bpy_prop_collection_idprop[BIMContainer]
+ contracted_containers: str
+ active_container_index: int
+ element_filter: str
+ elements: bpy.types.bpy_prop_collection_idprop[Element]
+ expanded_elements: str
+ active_element_index: int
+ total_elements: int
+ element_mode: Literal["TYPE", "DECOMPOSITION", "CLASSIFICATION"]
+ subelement_class: str
+ default_container: int
+ should_include_children: bool
+
@property
def active_container(self):
if self.containers and self.active_container_index < len(self.containers):
@@ -248,3 +292,8 @@ class BIMGridProperties(PropertyGroup):
update=update_grid_is_visible,
)
grid_axes: CollectionProperty(name="Grid Axes", type=ObjProperty)
+
+ if TYPE_CHECKING:
+ is_locked: bool
+ is_visible: bool
+ grid_axes: bpy.types.bpy_prop_collection_idprop[ObjProperty]
diff --git a/src/bonsai/bonsai/bim/module/spatial/ui.py b/src/bonsai/bonsai/bim/module/spatial/ui.py
index 0569504e25..da20a63d7c 100644
--- a/src/bonsai/bonsai/bim/module/spatial/ui.py
+++ b/src/bonsai/bonsai/bim/module/spatial/ui.py
@@ -16,10 +16,15 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+from __future__ import annotations
import bpy
from bpy.types import Panel, UIList
from bonsai.bim.module.spatial.data import SpatialData, SpatialDecompositionData
import bonsai.tool as tool
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from bonsai.bim.module.spatial.prop import BIMSpatialDecompositionProperties, BIMContainer, Element
class BIM_PT_spatial(Panel):
@@ -40,7 +45,9 @@ class BIM_PT_spatial(Panel):
if not SpatialData.is_loaded:
SpatialData.load()
- osprops = context.active_object.BIMObjectSpatialProperties
+ obj = context.active_object
+ assert obj
+ osprops = tool.Spatial.get_object_spatial_props(obj)
if osprops.is_editing:
if SpatialData.data["default_container"]:
@@ -103,7 +110,7 @@ class BIM_PT_spatial_decomposition(Panel):
return tool.Ifc.get()
def draw_header(self, context):
- props = context.scene.BIMSpatialDecompositionProperties
+ props = tool.Spatial.get_spatial_props()
row = self.layout.row(align=True)
row.label(text="") # empty text occupies the left of the row
icon = "HIDE_OFF" if props.is_visible else "HIDE_ON"
@@ -114,7 +121,7 @@ class BIM_PT_spatial_decomposition(Panel):
def draw(self, context):
if not SpatialDecompositionData.is_loaded:
SpatialDecompositionData.load()
- self.props = context.scene.BIMSpatialDecompositionProperties
+ self.props = tool.Spatial.get_spatial_props()
if SpatialDecompositionData.data["default_container"]:
row = self.layout.row(align=True)
@@ -233,7 +240,7 @@ class BIM_PT_grids(Panel):
self.layout.row().operator("mesh.add_grid", icon="ADD", text="Add Grids")
def draw_header(self, context):
- props = context.scene.BIMGridProperties
+ props = tool.Spatial.get_grid_props()
row = self.layout.row(align=True)
row.label(text="") # empty text occupies the left of the row
icon = "HIDE_OFF" if props.is_visible else "HIDE_ON"
@@ -243,25 +250,36 @@ class BIM_PT_grids(Panel):
class BIM_UL_containers_manager(UIList):
+ icon_by_class = {
+ "IfcProject": "FILE",
+ "IfcSite": "WORLD",
+ "IfcBuilding": "HOME",
+ "IfcBuildingStorey": "LINENUMBERS_OFF",
+ "IfcSpace": "ANTIALIASED",
+ "IfcFacilityPart": "MOD_FLUID",
+ "IfcBridgePart": "MOD_FLUID",
+ "IfcFacilityPartCommon": "MOD_FLUID",
+ "IfcMarinePart": "MOD_FLUID",
+ "IfcRailwayPart": "MOD_FLUID",
+ "IfcRoadPart": "MOD_FLUID",
+ }
+
def __init__(self):
self.use_filter_show = True
- def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
+ def draw_item(
+ self,
+ context,
+ layout: bpy.types.UILayout,
+ data: BIMSpatialDecompositionProperties,
+ item: BIMContainer,
+ icon,
+ active_data,
+ active_propname,
+ ):
if item:
row = layout.row(align=True)
- icon = {
- "IfcProject": "FILE",
- "IfcSite": "WORLD",
- "IfcBuilding": "HOME",
- "IfcBuildingStorey": "LINENUMBERS_OFF",
- "IfcSpace": "ANTIALIASED",
- "IfcFacilityPart": "MOD_FLUID",
- "IfcBridgePart": "MOD_FLUID",
- "IfcFacilityPartCommon": "MOD_FLUID",
- "IfcMarinePart": "MOD_FLUID",
- "IfcRailwayPart": "MOD_FLUID",
- "IfcRoadPart": "MOD_FLUID",
- }.get(item.ifc_class, "META_PLANE")
+ icon = self.icon_by_class.get(item.ifc_class, "META_PLANE")
split = row.split(factor=0.85)
if item.long_name:
split2 = split.split(factor=0.7)
@@ -275,7 +293,7 @@ class BIM_UL_containers_manager(UIList):
row.prop(item, "name", emboss=False, text="", icon=icon)
split.prop(item, "elevation", emboss=False, text="")
- def draw_hierarchy(self, row, item):
+ def draw_hierarchy(self, row: bpy.types.UILayout, item: BIMContainer) -> None:
if item.level_index:
for i in range(0, item.level_index - 1):
row.label(text="", icon="BLANK1")
@@ -293,11 +311,12 @@ class BIM_UL_containers_manager(UIList):
def draw_filter(self, context, layout):
row = layout.row()
- row.prop(context.scene.BIMSpatialDecompositionProperties, "container_filter", text="", icon="VIEWZOOM")
+ props = tool.Spatial.get_spatial_props()
+ row.prop(props, "container_filter", text="", icon="VIEWZOOM")
- def filter_items(self, context, data, propname):
+ def filter_items(self, context: bpy.types.Context, data: BIMSpatialDecompositionProperties, propname: str):
items = getattr(data, propname)
- filter_name = context.scene.BIMSpatialDecompositionProperties.container_filter.lower()
+ filter_name = data.container_filter.lower()
filter_flags = [self.bitflag_filter_item] * len(items)
for idx, item in enumerate(items):
@@ -313,7 +332,7 @@ class BIM_UL_containers_manager(UIList):
return filter_flags, []
items = getattr(data, propname)
- filter_name = context.scene.BIMSpatialDecompositionProperties.container_filter
+ filter_name = data.container_filter
filtered = bpy.types.UI_UL_list.filter_items_by_name(filter_name, self.bitflag_filter_item, items, "name")
return filtered, []
@@ -326,7 +345,18 @@ class BIM_UL_elements(UIList):
icon_id = "DISCLOSURE_TRI_DOWN" if is_expanded else "DISCLOSURE_TRI_RIGHT"
row.operator("bim.toggle_container_element", text="", emboss=False, icon=icon_id).element_index = index
- def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, fit_flag):
+ def draw_item(
+ self,
+ context,
+ layout: bpy.types.UILayout,
+ data: BIMSpatialDecompositionProperties,
+ item: Element,
+ icon,
+ active_data,
+ active_propname,
+ index,
+ fit_flag,
+ ):
if item:
row = layout.row(align=True)
for _ in range(item.level):
@@ -341,11 +371,12 @@ class BIM_UL_elements(UIList):
def draw_filter(self, context, layout):
row = layout.row()
- row.prop(context.scene.BIMSpatialDecompositionProperties, "element_filter", text="", icon="VIEWZOOM")
+ props = tool.Spatial.get_spatial_props()
+ row.prop(props, "element_filter", text="", icon="VIEWZOOM")
- def filter_items(self, context, data, propname):
+ def filter_items(self, context: bpy.types.Context, data: BIMSpatialDecompositionProperties, propname: str):
items = getattr(data, propname)
- filter_name = context.scene.BIMSpatialDecompositionProperties.element_filter
+ filter_name = data.element_filter
filtered = bpy.types.UI_UL_list.filter_items_by_name(filter_name, self.bitflag_filter_item, items, "name")
return filtered, []
diff --git a/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py b/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py
index 8230c55c66..a91a7947c5 100644
--- a/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py
+++ b/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py
@@ -26,7 +26,6 @@ import ifcopenshell.api
import ifcopenshell.util.attribute
import ifcopenshell.util.unit as ifcunit
import bonsai.tool as tool
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.structural.shader import DecorationShader
from typing import Literal, TypedDict, Iterable
@@ -307,7 +306,7 @@ class ShaderInfo:
props = bpy.context.scene.BIMStructuralProperties
group_definition_id = int(props.load_group_to_show)
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
groups = [file.by_id(group_definition_id)]
recursive_subgroups(groups, 10, props.activity_type)
@@ -333,7 +332,7 @@ class ShaderInfo:
orientation = np.eye(3)
if reference_frame == "LOCAL_COORDS":
orientation = rotation
- blender_object: bpy.types.Object = IfcStore.get_element(getattr(surf, "GlobalId", None))
+ blender_object: bpy.types.Object = tool.Ifc.get_object_by_identifier(getattr(surf, "GlobalId", None))
mat = blender_object.matrix_world
mesh: bpy.types.Mesh = blender_object.data
@@ -454,7 +453,7 @@ class ShaderInfo:
activity_list = value["activities"]
if len(activity_list) == 0:
continue
- blender_object = IfcStore.get_element(getattr(conn, "GlobalId", None))
+ blender_object = tool.Ifc.get_object_by_identifier(getattr(conn, "GlobalId", None))
if blender_object.type == "MESH":
conn_location = blender_object.matrix_world @ blender_object.data.vertices[0].co
rotation = self.get_point_connection_rotation(conn)
@@ -580,7 +579,7 @@ class ShaderInfo:
if len(activity_list) == 0:
continue
- blender_object = IfcStore.get_element(getattr(member, "GlobalId", None))
+ blender_object = tool.Ifc.get_object_by_identifier(getattr(member, "GlobalId", None))
start_co = blender_object.matrix_world @ blender_object.data.vertices[0].co
end_co = blender_object.matrix_world @ blender_object.data.vertices[1].co
diff --git a/src/bonsai/bonsai/bim/module/structural/operator.py b/src/bonsai/bonsai/bim/module/structural/operator.py
index 646e5ad294..ba08227179 100644
--- a/src/bonsai/bonsai/bim/module/structural/operator.py
+++ b/src/bonsai/bonsai/bim/module/structural/operator.py
@@ -20,13 +20,13 @@ import bpy
import json
import ifcopenshell
import ifcopenshell.api
+import ifcopenshell.api.structural
import ifcopenshell.util.attribute
import bonsai.bim.helper
import bonsai.core.structural as core
import bonsai.tool as tool
from math import degrees
from mathutils import Vector, Matrix
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.structural.decorator import LoadsDecorator
@@ -77,13 +77,12 @@ class AddStructuralMemberConnection(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
oprops = obj.BIMObjectProperties
props = obj.BIMStructuralProperties
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
related_structural_connection = file.by_id(oprops.ifc_definition_id)
relating_structural_member = file.by_id(props.relating_structural_member.BIMObjectProperties.ifc_definition_id)
if not relating_structural_member.is_a("IfcStructuralMember"):
return {"FINISHED"}
- ifcopenshell.api.run(
- "structural.add_structural_member_connection",
+ ifcopenshell.api.structural.add_structural_member_connection(
file,
relating_structural_member=relating_structural_member,
related_structural_connection=related_structural_connection,
@@ -125,7 +124,7 @@ class RemoveStructuralConnectionCondition(bpy.types.Operator, tool.Ifc.Operator)
connects_structural_member: bpy.props.IntProperty()
def _execute(self, context):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
relation = file.by_id(self.connects_structural_member)
connection = relation.RelatedStructuralConnection
ifcopenshell.api.run("structural.remove_structural_connection_condition", file, **{"relation": relation})
@@ -139,7 +138,7 @@ class AddStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
connection: bpy.props.IntProperty()
def _execute(self, context):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
connection = file.by_id(self.connection)
ifcopenshell.api.run("structural.add_structural_boundary_condition", file, **{"connection": connection})
return {"FINISHED"}
@@ -152,7 +151,7 @@ class RemoveStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
connection: bpy.props.IntProperty()
def _execute(self, context):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
connection = file.by_id(self.connection)
ifcopenshell.api.run("structural.remove_structural_boundary_condition", file, **{"connection": connection})
return {"FINISHED"}
@@ -170,8 +169,9 @@ class EnableEditingStructuralBoundaryCondition(bpy.types.Operator):
props.boundary_condition_attributes.clear()
condition = tool.Ifc.get().by_id(self.boundary_condition)
+ schema = tool.Ifc.schema()
- for attribute in IfcStore.get_schema().declaration_by_name(condition.is_a()).all_attributes():
+ for attribute in schema.declaration_by_name(condition.is_a()).all_attributes():
value = getattr(condition, attribute.name(), None)
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
new = props.boundary_condition_attributes.add()
@@ -207,7 +207,7 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
props = obj.BIMStructuralProperties
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
connection = file.by_id(self.connection)
condition = connection.AppliedCondition
@@ -353,7 +353,7 @@ class EnableEditingStructuralItemAxis(bpy.types.Operator):
oprops = obj.BIMObjectProperties
props = obj.BIMStructuralProperties
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
item = self.file.by_id(oprops.ifc_definition_id)
z_axis = Vector(item.Axis.DirectionRatios).normalized() @ obj.matrix_world if item.Axis else None
x_axis = (obj.data.vertices[1].co - obj.data.vertices[0].co).normalized()
@@ -407,7 +407,7 @@ class EditStructuralItemAxis(bpy.types.Operator, tool.Ifc.Operator):
props = obj.BIMStructuralProperties
relative_matrix = props.axis_empty.matrix_world @ obj.matrix_world.inverted()
z_axis = relative_matrix.col[2][0:3]
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"structural.edit_structural_item_axis",
self.file,
@@ -428,7 +428,7 @@ class EnableEditingStructuralConnectionCS(bpy.types.Operator):
oprops = obj.BIMObjectProperties
props = obj.BIMStructuralProperties
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
item = self.file.by_id(oprops.ifc_definition_id)
location = obj.data.vertices[0].co
@@ -496,7 +496,7 @@ class EditStructuralConnectionCS(bpy.types.Operator, tool.Ifc.Operator):
relative_matrix = props.ccs_empty.matrix_world @ obj.matrix_world.inverted()
x_axis = relative_matrix.col[0][0:3]
z_axis = relative_matrix.col[2][0:3]
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"structural.edit_structural_connection_cs",
self.file,
@@ -515,7 +515,7 @@ class AssignStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator):
load_case: bpy.props.IntProperty()
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"aggregate.assign_object",
self.file,
@@ -534,7 +534,7 @@ class UnassignStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator):
load_case: bpy.props.IntProperty()
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"aggregate.unassign_object",
self.file,
@@ -552,7 +552,7 @@ class AddStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
- ifcopenshell.api.run("structural.add_structural_load_case", IfcStore.get_file())
+ ifcopenshell.api.run("structural.add_structural_load_case", tool.Ifc.get())
return {"FINISHED"}
@@ -564,7 +564,7 @@ class EditStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMStructuralProperties
attributes = bonsai.bim.helper.export_attributes(props.load_case_attributes)
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"structural.edit_structural_load_case",
self.file,
@@ -581,7 +581,7 @@ class RemoveStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator):
load_case: bpy.props.IntProperty()
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"structural.remove_structural_load_case", self.file, load_case=self.file.by_id(self.load_case)
)
@@ -639,7 +639,7 @@ class AddStructuralLoadGroup(bpy.types.Operator, tool.Ifc.Operator):
load_case: bpy.props.IntProperty()
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
load_group = ifcopenshell.api.run("structural.add_structural_load_group", self.file)
ifcopenshell.api.run(
"group.assign_group", self.file, products=[load_group], group=self.file.by_id(self.load_case)
@@ -654,7 +654,7 @@ class RemoveStructuralLoadGroup(bpy.types.Operator, tool.Ifc.Operator):
load_group: bpy.props.IntProperty()
def _execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"structural.remove_structural_load_group", self.file, load_group=self.file.by_id(self.load_group)
)
@@ -668,7 +668,7 @@ class EnableEditingStructuralLoadGroupActivities(bpy.types.Operator):
load_group: bpy.props.IntProperty()
def execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
self.props = context.scene.BIMStructuralProperties
self.props.active_load_group_id = self.load_group
self.props.load_group_editing_type = "ACTIVITY"
@@ -693,7 +693,7 @@ class AddStructuralActivity(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
self.props = context.scene.BIMStructuralProperties
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
for obj in context.selected_objects:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
@@ -741,7 +741,7 @@ class LoadStructuralLoads(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
props = context.scene.BIMStructuralProperties
props.structural_loads.clear()
loads = tool.Ifc.get().by_type("IfcStructuralLoad")
@@ -750,7 +750,7 @@ class LoadStructuralLoads(bpy.types.Operator):
for structural_load in loads:
if (
names.count(structural_load.Name or "Unnamed") > 1
- and len(self.file.get_inverse(structural_load)) < 2
+ and self.file.get_total_inverses(structural_load) < 2
):
continue
new = props.structural_loads.add()
@@ -787,7 +787,7 @@ class AddStructuralLoad(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
result = ifcopenshell.api.run(
- "structural.add_structural_load", IfcStore.get_file(), name="New Load", ifc_class=self.ifc_class
+ "structural.add_structural_load", tool.Ifc.get(), name="New Load", ifc_class=self.ifc_class
)
bpy.ops.bim.load_structural_loads()
bpy.ops.bim.enable_editing_structural_load(structural_load=result.id())
@@ -828,7 +828,7 @@ class RemoveStructuralLoad(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMStructuralProperties
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"structural.remove_structural_load",
self.file,
@@ -846,7 +846,7 @@ class EditStructuralLoad(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMStructuralProperties
attributes = bonsai.bim.helper.export_attributes(props.structural_load_attributes)
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"structural.edit_structural_load",
self.file,
@@ -877,7 +877,7 @@ class LoadBoundaryConditions(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
props = context.scene.BIMStructuralProperties
props.boundary_conditions.clear()
conditions = tool.Ifc.get().by_type("IfcBoundaryCondition")
@@ -936,7 +936,7 @@ class AddBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
result = ifcopenshell.api.run(
"structural.add_structural_boundary_condition",
- IfcStore.get_file(),
+ tool.Ifc.get(),
name="New Load",
ifc_class=self.ifc_class,
)
@@ -957,7 +957,8 @@ class EnableEditingBoundaryCondition(bpy.types.Operator):
boundary_condition = tool.Ifc.get().by_id(self.boundary_condition)
# bonsai.bim.helper.import_attributes(data["type"], props.boundary_condition_attributes, data)
- for attribute in IfcStore.get_schema().declaration_by_name(boundary_condition.is_a()).all_attributes():
+ schema = tool.Ifc.schema()
+ for attribute in schema.declaration_by_name(boundary_condition.is_a()).all_attributes():
value = getattr(boundary_condition, attribute.name(), None)
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute)
new = props.boundary_condition_attributes.add()
@@ -1000,7 +1001,7 @@ class RemoveBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMStructuralProperties
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
ifcopenshell.api.run(
"structural.remove_structural_boundary_condition",
self.file,
@@ -1017,7 +1018,7 @@ class EditBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
props = context.scene.BIMStructuralProperties
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
# attributes = bonsai.bim.helper.export_attributes(props.boundary_condition_attributes)
attributes = {}
for attribute in props.boundary_condition_attributes:
diff --git a/src/bonsai/bonsai/bim/module/structural/prop.py b/src/bonsai/bonsai/bim/module/structural/prop.py
index 740c97f1e6..2ecc29834a 100644
--- a/src/bonsai/bonsai/bim/module/structural/prop.py
+++ b/src/bonsai/bonsai/bim/module/structural/prop.py
@@ -19,7 +19,6 @@
from math import radians
import bpy
import bonsai.tool as tool
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty, Attribute
from bonsai.bim.module.structural.data import (
StructuralLoadCasesData,
diff --git a/src/bonsai/bonsai/bim/module/structural/ui.py b/src/bonsai/bonsai/bim/module/structural/ui.py
index 767724bdd5..d8f69e232e 100644
--- a/src/bonsai/bonsai/bim/module/structural/ui.py
+++ b/src/bonsai/bonsai/bim/module/structural/ui.py
@@ -17,9 +17,9 @@
# along with Bonsai. If not, see .
import bpy
+import bonsai.tool as tool
import bonsai.bim.helper
from bpy.types import Panel, UIList
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import draw_attributes, prop_with_search
from bonsai.bim.module.structural.data import (
StructuralBoundaryConditionsData,
@@ -90,9 +90,9 @@ class BIM_PT_structural_boundary_conditions(Panel):
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
- if not IfcStore.get_element(props.ifc_definition_id):
+ if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
- if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"):
+ if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"):
return False
return True
@@ -125,9 +125,9 @@ class BIM_PT_connected_structural_members(Panel):
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
- if not IfcStore.get_element(props.ifc_definition_id):
+ if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
- if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"):
+ if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"):
return False
return True
@@ -178,9 +178,9 @@ class BIM_PT_structural_member(Panel):
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
- if not IfcStore.get_element(props.ifc_definition_id):
+ if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
- if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcStructuralMember"):
+ if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralMember"):
return False
return True
@@ -221,9 +221,9 @@ class BIM_PT_structural_connection(Panel):
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
- if not IfcStore.get_element(props.ifc_definition_id):
+ if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id):
return False
- if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"):
+ if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"):
return False
return True
@@ -274,7 +274,7 @@ class BIM_PT_structural_analysis_models(Panel):
@classmethod
def poll(cls, context):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
@@ -348,7 +348,7 @@ class BIM_PT_structural_load_cases(Panel):
@classmethod
def poll(cls, context):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
@@ -443,7 +443,7 @@ class BIM_PT_show_structural_activities(Panel):
@classmethod
def poll(cls, context):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
@@ -474,7 +474,7 @@ class BIM_PT_structural_loads(Panel):
@classmethod
def poll(cls, context):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
@@ -543,7 +543,7 @@ class BIM_PT_boundary_conditions(Panel):
@classmethod
def poll(cls, context):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
return file and hasattr(file, "schema") and file.schema != "IFC2X3"
def draw(self, context):
diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py
index ef7914c69e..5f2d6d2a52 100644
--- a/src/bonsai/bonsai/bim/module/style/operator.py
+++ b/src/bonsai/bonsai/bim/module/style/operator.py
@@ -155,10 +155,10 @@ class UnlinkStyle(bpy.types.Operator, tool.Ifc.Operator):
# for unlinked blender material.
updated_meshes = set()
for obj in bpy.data.objects:
- mesh = obj.data
- if not isinstance(mesh, bpy.types.Mesh):
+ if not (mesh := obj.data) or not isinstance(mesh, bpy.types.Mesh):
continue
- if not mesh.BIMMeshProperties.ifc_definition_id:
+ representation = tool.Geometry.get_data_representation(mesh)
+ if not representation:
continue
if mesh in updated_meshes:
continue
@@ -1137,13 +1137,16 @@ class AssignStyleToSelected(bpy.types.Operator, tool.Ifc.Operator):
ifc_file = tool.Ifc.get()
style = ifc_file.by_id(self.style_id)
material = tool.Ifc.get_object(style)
+ assert isinstance(material, bpy.types.Material)
has_items = False
representations: dict[ifcopenshell.entity_instance, bpy.types.Object] = {}
for obj in context.selected_objects:
if tool.Geometry.is_representation_item(obj):
has_items = True
- item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ assert isinstance(obj.data, bpy.types.Mesh)
+ item = tool.Geometry.get_active_representation(obj)
+ assert item
tool.Style.assign_style_to_representation_item(item, style)
obj.data.materials.clear()
obj.data.materials.append(material)
@@ -1156,7 +1159,8 @@ class AssignStyleToSelected(bpy.types.Operator, tool.Ifc.Operator):
return {"FINISHED"}
if has_items:
- tool.Geometry.reload_representation(context.scene.BIMGeometryProperties.representation_obj)
+ gprops = tool.Geometry.get_geometry_props()
+ tool.Geometry.reload_representation(gprops.representation_obj)
bpy.ops.bim.disable_editing_representation_items()
bpy.ops.bim.enable_editing_representation_items()
diff --git a/src/bonsai/bonsai/bim/module/style/ui.py b/src/bonsai/bonsai/bim/module/style/ui.py
index 2486ee91e8..2b7eaad760 100644
--- a/src/bonsai/bonsai/bim/module/style/ui.py
+++ b/src/bonsai/bonsai/bim/module/style/ui.py
@@ -20,9 +20,7 @@ import bpy
import bonsai.bim.helper
import bonsai.tool as tool
from bpy.types import Panel, UIList
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.style.data import StylesData, BlenderMaterialStyleData
-from typing import Union
class BIM_PT_styles(Panel):
@@ -36,7 +34,7 @@ class BIM_PT_styles(Panel):
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ return tool.Ifc.get()
def draw(self, context):
if not StylesData.is_loaded:
diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py
index 4ef3c255f7..5c9750dc23 100644
--- a/src/bonsai/bonsai/bim/module/system/operator.py
+++ b/src/bonsai/bonsai/bim/module/system/operator.py
@@ -21,9 +21,7 @@ import ifcopenshell.api
import bonsai.tool as tool
import bonsai.core.system as core
import bonsai.bim.helper
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.system.data import PortData
-from mathutils import Matrix
class LoadSystems(bpy.types.Operator):
diff --git a/src/bonsai/bonsai/bim/module/tester/operator.py b/src/bonsai/bonsai/bim/module/tester/operator.py
index 652d0ff61a..b16abd3fcd 100644
--- a/src/bonsai/bonsai/bim/module/tester/operator.py
+++ b/src/bonsai/bonsai/bim/module/tester/operator.py
@@ -72,7 +72,7 @@ class ExecuteIfcTester(bpy.types.Operator):
# No need for if-statement, just postponing lots of diffs.
if True:
- dirpath = tempfile.mkdtemp()
+ dirpath = tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None)
start = time.time()
output = Path(os.path.join(dirpath, "{}_{}.html".format(Path(ifc_path).name, Path(specs_path).name)))
diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py
index 866407be3e..ee370e5ae5 100644
--- a/src/bonsai/bonsai/bim/module/type/operator.py
+++ b/src/bonsai/bonsai/bim/module/type/operator.py
@@ -29,7 +29,6 @@ import bonsai.tool as tool
import bonsai.core.geometry
import bonsai.core.type as core
import bonsai.core.root
-from bonsai.bim.ifc import IfcStore
class AssignType(bpy.types.Operator, tool.Ifc.Operator):
@@ -66,7 +65,7 @@ class UnassignType(bpy.types.Operator, tool.Ifc.Operator):
def exclude_callback(attribute):
return attribute.is_a("IfcProfileDef") and attribute.ProfileName
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
objs = [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects
for obj in objs:
element = tool.Ifc.get_entity(obj)
@@ -187,7 +186,7 @@ class SelectSimilarType(bpy.types.Operator):
related_object: bpy.props.StringProperty()
def execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
objects = bpy.context.selected_objects
# store relating types to avoid selecting same elements multiple times
@@ -229,7 +228,7 @@ class SelectTypeObjects(bpy.types.Operator):
relating_type: bpy.props.StringProperty()
def execute(self, context):
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
relating_type = bpy.data.objects.get(self.relating_type) if self.relating_type else context.active_object
at_least_one_selectable_typed_object = False
for element in ifcopenshell.util.element.get_types(tool.Ifc.get_entity(relating_type)):
diff --git a/src/bonsai/bonsai/bim/module/type/prop.py b/src/bonsai/bonsai/bim/module/type/prop.py
index ac7986fe6b..aedb5c1993 100644
--- a/src/bonsai/bonsai/bim/module/type/prop.py
+++ b/src/bonsai/bonsai/bim/module/type/prop.py
@@ -20,8 +20,6 @@ import bpy
import ifcopenshell.util.element
import ifcopenshell.util.type
from bonsai.bim.module.type.data import TypeData
-from bonsai.bim.prop import StrProperty, Attribute
-from bonsai.bim.ifc import IfcStore
import bonsai.tool as tool
from bpy.types import PropertyGroup
from bpy.props import (
diff --git a/src/bonsai/bonsai/bim/module/type/ui.py b/src/bonsai/bonsai/bim/module/type/ui.py
index 4bfca516e3..405507f168 100644
--- a/src/bonsai/bonsai/bim/module/type/ui.py
+++ b/src/bonsai/bonsai/bim/module/type/ui.py
@@ -19,7 +19,6 @@
import bonsai.tool as tool
import bonsai.bim.module.type.prop as type_prop
from bpy.types import Panel
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import prop_with_search
from bonsai.bim.module.type.data import TypeData
diff --git a/src/bonsai/bonsai/bim/module/unit/prop.py b/src/bonsai/bonsai/bim/module/unit/prop.py
index edd23f2ce4..f0f04e16ae 100644
--- a/src/bonsai/bonsai/bim/module/unit/prop.py
+++ b/src/bonsai/bonsai/bim/module/unit/prop.py
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see .
import bpy
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.prop import StrProperty, Attribute
from bonsai.bim.module.unit.data import UnitsData
from bpy.types import PropertyGroup
@@ -31,21 +30,22 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
+from typing import TYPE_CHECKING
-def get_unit_classes(self, context):
+def get_unit_classes(self: "BIMUnitProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not UnitsData.is_loaded:
UnitsData.load()
return UnitsData.data["unit_classes"]
-def get_conversion_unit_types(self, context):
+def get_conversion_unit_types(self: "BIMUnitProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not UnitsData.is_loaded:
UnitsData.load()
return UnitsData.data["conversion_unit_types"]
-def get_named_unit_types(self, context):
+def get_named_unit_types(self: "BIMUnitProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
if not UnitsData.is_loaded:
UnitsData.load()
return UnitsData.data["named_unit_types"]
@@ -58,6 +58,12 @@ class Unit(PropertyGroup):
ifc_class: StringProperty(name="IFC Class")
ifc_definition_id: IntProperty(name="IFC Definition ID")
+ if TYPE_CHECKING:
+ unit_type: str
+ is_assigned: bool
+ ifc_class: str
+ ifc_definition_id: int
+
class BIMUnitProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
@@ -68,3 +74,13 @@ class BIMUnitProperties(PropertyGroup):
conversion_unit_types: EnumProperty(items=get_conversion_unit_types, name="Conversion Unit Types")
named_unit_types: EnumProperty(items=get_named_unit_types, name="Named Unit Types")
unit_attributes: CollectionProperty(name="Unit Attributes", type=Attribute)
+
+ if TYPE_CHECKING:
+ is_editing: bool
+ units: bpy.types.bpy_prop_collection_idprop[Unit]
+ active_unit_index: int
+ active_unit_id: int
+ unit_classes: str
+ conversion_unit_types: str
+ named_unit_types: str
+ unit_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
diff --git a/src/bonsai/bonsai/bim/module/unit/ui.py b/src/bonsai/bonsai/bim/module/unit/ui.py
index 77e0117699..f7b55a96e3 100644
--- a/src/bonsai/bonsai/bim/module/unit/ui.py
+++ b/src/bonsai/bonsai/bim/module/unit/ui.py
@@ -17,8 +17,8 @@
# along with Bonsai. If not, see .
import bonsai.bim.helper
+import bonsai.tool as tool
from bpy.types import Panel, UIList
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import prop_with_search
from bonsai.bim.module.unit.data import UnitsData
@@ -34,14 +34,14 @@ class BIM_PT_units(Panel):
@classmethod
def poll(cls, context):
- file = IfcStore.get_file()
+ file = tool.Ifc.get()
return file
def draw(self, context):
if not UnitsData.is_loaded:
UnitsData.load()
- self.props = context.scene.BIMUnitProperties
+ self.props = tool.Unit.get_unit_props()
row = self.layout.row(align=True)
row.label(text="{} Units Found".format(UnitsData.data["total_units"]), icon="SNAP_GRID")
@@ -103,7 +103,7 @@ class BIM_PT_units(Panel):
class BIM_UL_units(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
- props = context.scene.BIMUnitProperties
+ props = tool.Unit.get_unit_props()
if item:
icon = "MOD_MESHDEFORM"
if item.ifc_class == "IfcSIUnit":
diff --git a/src/bonsai/bonsai/bim/module/void/data.py b/src/bonsai/bonsai/bim/module/void/data.py
index fe387b6f81..133d3e15f5 100644
--- a/src/bonsai/bonsai/bim/module/void/data.py
+++ b/src/bonsai/bonsai/bim/module/void/data.py
@@ -127,26 +127,14 @@ class BooleansData:
def booleans(cls):
props = tool.Geometry.get_geometry_props()
obj = props.representation_obj or bpy.context.active_object
- if (
- not obj.data
- or not hasattr(obj.data, "BIMMeshProperties")
- or not obj.data.BIMMeshProperties.ifc_definition_id
- ):
+ if not (representation := tool.Geometry.get_active_representation(obj)):
return []
-
- representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
return tool.Model.get_booleans(representation=representation)
@classmethod
def manual_booleans(cls):
props = tool.Geometry.get_geometry_props()
obj = props.representation_obj or bpy.context.active_object
- if (
- not obj.data
- or not hasattr(obj.data, "BIMMeshProperties")
- or not obj.data.BIMMeshProperties.ifc_definition_id
- ):
+ if not (representation := tool.Geometry.get_active_representation(obj)):
return []
-
- representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
return tool.Model.get_manual_booleans(tool.Ifc.get_entity(obj), representation=representation)
diff --git a/src/bonsai/bonsai/bim/module/void/operator.py b/src/bonsai/bonsai/bim/module/void/operator.py
index 5867ed9950..91537ed528 100644
--- a/src/bonsai/bonsai/bim/module/void/operator.py
+++ b/src/bonsai/bonsai/bim/module/void/operator.py
@@ -24,7 +24,6 @@ import bonsai.tool as tool
import bonsai.core.geometry
import bonsai.core.root
import bonsai.bim.handler
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.model.opening import FilledOpeningGenerator
@@ -98,7 +97,7 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
break
element_had_openings = tool.Geometry.has_openings(voided_element)
- body_context = ifcopenshell.util.representation.get_context(IfcStore.get_file(), "Model", "Body")
+ body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body")
if not element2:
element2 = bonsai.core.root.assign_class(
tool.Ifc,
@@ -139,7 +138,8 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc, tool.Geometry, tool.Surveyor, obj=voided_obj
)
- representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id)
+ representation = tool.Geometry.get_active_representation(voided_obj)
+ assert representation
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
@@ -214,7 +214,7 @@ class AddFilling(bpy.types.Operator, tool.Ifc.Operator):
opening = context.scene.objects.get(self.opening, context.scene.VoidProperties.desired_opening)
if opening is None:
return {"FINISHED"}
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
element_id = obj.BIMObjectProperties.ifc_definition_id
opening_id = opening.BIMObjectProperties.ifc_definition_id
if not element_id or not opening_id or element_id == opening_id:
@@ -269,20 +269,18 @@ class BooleansMarkAsManual(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
obj = context.active_object
- if (
- obj
- and tool.Ifc.get_entity(obj)
- and hasattr(obj.data, "BIMMeshProperties")
- and obj.data.BIMMeshProperties.ifc_definition_id
- ):
+ if obj and tool.Ifc.get_entity(obj) and tool.Geometry.get_active_representation(obj):
return True
cls.poll_message_set("Need to select IFC element with representation")
return False
def _execute(self, context):
obj = context.active_object
+ assert obj
element = tool.Ifc.get_entity(obj)
- representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ assert element
+ representation = tool.Geometry.get_active_representation(obj)
+ assert representation
booleans = tool.Model.get_booleans(representation=representation)
if self.mark_as_manual:
@@ -304,13 +302,13 @@ class EnableEditingBooleans(bpy.types.Operator):
@classmethod
def poll(cls, context):
- if not bpy.context.scene.BIMGeometryProperties.representation_obj:
+ if not tool.Geometry.get_geometry_props().representation_obj:
cls.poll_message_set("To enable editing booleans object should be in item mode.")
return False
return True
def execute(self, context):
- props = context.scene.BIMBooleanProperties
+ props = tool.Feature.get_boolean_props()
gprops = tool.Geometry.get_geometry_props()
rep_obj = gprops.representation_obj
assert rep_obj
@@ -344,6 +342,6 @@ class DisableEditingBooleans(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
- props = context.scene.BIMBooleanProperties
+ props = tool.Feature.get_boolean_props()
props.is_editing = False
return {"FINISHED"}
diff --git a/src/bonsai/bonsai/bim/module/void/prop.py b/src/bonsai/bonsai/bim/module/void/prop.py
index 8c6bbaf4e6..0170dbd606 100644
--- a/src/bonsai/bonsai/bim/module/void/prop.py
+++ b/src/bonsai/bonsai/bim/module/void/prop.py
@@ -19,34 +19,51 @@
import bpy
from bpy.types import PropertyGroup
from bpy.props import PointerProperty, StringProperty, IntProperty, BoolProperty, CollectionProperty, EnumProperty
-from typing import Union
+from typing import Union, TYPE_CHECKING, Literal, get_args
+
+OperatorType = Literal["DIFFERENCE", "INTERSECTION", "UNION"]
class Boolean(PropertyGroup):
name: StringProperty(name="Name")
- operator: StringProperty(name="Operator")
+ operator: EnumProperty(
+ items=[(i, i, "") for i in get_args(OperatorType)],
+ name="Operator",
+ default="DIFFERENCE",
+ )
ifc_definition_id: IntProperty(name="IFC Definition ID")
level: IntProperty(name="Level")
+ if TYPE_CHECKING:
+ operator: OperatorType
+ name: str
+ ifc_definition_id: int
+ level: int
+
class VoidProperties(PropertyGroup):
desired_opening: PointerProperty(name="Desired Opening To Fill", type=bpy.types.Object)
+ if TYPE_CHECKING:
+ desired_opening: Union[bpy.types.Object, None]
+
class BIMBooleanProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing", default=False)
booleans: CollectionProperty(name="Booleans", type=Boolean)
active_boolean_index: IntProperty(name="Active Boolean Index")
operator: EnumProperty(
- items=[
- ("DIFFERENCE", "DIFFERENCE", ""),
- ("INTERSECTION", "INTERSECTION", ""),
- ("UNION", "UNION", ""),
- ],
+ items=[(i, i, "") for i in get_args(OperatorType)],
name="Operator",
default="DIFFERENCE",
)
+ if TYPE_CHECKING:
+ is_editing: bool
+ booleans: bpy.types.bpy_prop_collection_idprop[Boolean]
+ active_boolean_index: int
+ operator: OperatorType
+
@property
def active_boolean(self) -> Union[Boolean, None]:
if self.booleans and 0 <= self.active_boolean_index < len(self.booleans):
diff --git a/src/bonsai/bonsai/bim/module/void/ui.py b/src/bonsai/bonsai/bim/module/void/ui.py
index d07266a56f..69b34a40e1 100644
--- a/src/bonsai/bonsai/bim/module/void/ui.py
+++ b/src/bonsai/bonsai/bim/module/void/ui.py
@@ -126,13 +126,10 @@ class BIM_PT_booleans(Panel):
@classmethod
def poll(cls, context):
return (
- context.active_object is not None
- and context.active_object.type == "MESH"
- and hasattr(context.active_object.data, "BIMMeshProperties")
- and (
- context.active_object.data.BIMMeshProperties.ifc_definition_id
- or context.active_object.data.BIMMeshProperties.ifc_boolean_id
- )
+ (obj := context.active_object) is not None
+ and isinstance(data := obj.data, bpy.types.Mesh)
+ and (mesh_props := tool.Geometry.get_mesh_props(data))
+ and (mesh_props.ifc_definition_id or mesh_props.ifc_boolean_id)
)
def draw(self, context):
@@ -141,13 +138,13 @@ class BIM_PT_booleans(Panel):
obj = context.active_object
assert obj
+ mesh = obj.data
+ assert isinstance(mesh, bpy.types.Mesh)
- if not context.active_object.data:
- return
layout = self.layout
- props = context.scene.BIMBooleanProperties
+ props = tool.Feature.get_boolean_props()
- if context.active_object.data.BIMMeshProperties.ifc_definition_id:
+ if tool.Geometry.get_mesh_props(mesh).ifc_definition_id:
row = layout.row(align=True)
total_booleans = BooleansData.data["total_booleans"]
manual_booleans = BooleansData.data["manual_booleans"]
diff --git a/src/bonsai/bonsai/bim/module/web/data.py b/src/bonsai/bonsai/bim/module/web/data.py
index fa94ad26d8..db939cadfa 100644
--- a/src/bonsai/bonsai/bim/module/web/data.py
+++ b/src/bonsai/bonsai/bim/module/web/data.py
@@ -39,9 +39,11 @@ class WebData:
@classmethod
def get_ifc_file_name(cls):
- filename = os.path.basename(bpy.context.scene.BIMProperties.ifc_file)
+ props = tool.Blender.get_bim_props()
+ filename = os.path.basename(props.ifc_file)
return filename
@classmethod
def get_is_dirty(cls):
- return bpy.context.scene.BIMProperties.is_dirty
+ props = tool.Blender.get_bim_props()
+ return props.is_dirty
diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py
index 5ba1596203..f32cd2ef65 100644
--- a/src/bonsai/bonsai/bim/operator.py
+++ b/src/bonsai/bonsai/bim/operator.py
@@ -130,7 +130,8 @@ class CloseBlendWarning(bpy.types.Operator):
bl_label = "Close Blend Warning"
def execute(self, context):
- bpy.context.scene.BIMProperties.has_blend_warning = False
+ props = tool.Blender.get_bim_props()
+ props.has_blend_warning = False
return {"FINISHED"}
def draw(self, context):
@@ -213,11 +214,13 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector):
def execute(self, context):
if self.is_existing_ifc_file():
- context.scene.BIMProperties.ifc_file = self.get_filepath()
+ props = tool.Blender.get_bim_props()
+ props.ifc_file = self.get_filepath()
return {"FINISHED"}
def invoke(self, context, event):
- filepath = Path(context.scene.BIMProperties.ifc_file)
+ props = tool.Blender.get_bim_props()
+ filepath = Path(props.ifc_file)
res = tool.Blender.operator_invoke_filepath_hotkeys(self, context, event, filepath)
if res is not None:
return res
@@ -226,47 +229,27 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector):
return {"RUNNING_MODAL"}
-class SelectDataDir(bpy.types.Operator):
- bl_idname = "bim.select_data_dir"
- bl_label = "Select Data Directory"
+class SelectDir(bpy.types.Operator):
+ bl_idname = "bim.select_dir"
+ bl_label = "Select Directory"
bl_options = {"REGISTER", "UNDO"}
- bl_description = "Select the directory that contains all IFC data es. PSet, styles, etc..."
+ bl_description = "Open a file browser to choose the directory"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
+ data_path: bpy.props.StringProperty(name="Data Path")
def execute(self, context):
- context.scene.BIMProperties.data_dir = os.path.dirname(self.filepath)
- return {"FINISHED"}
-
- def invoke(self, context, event):
- context.window_manager.fileselect_add(self)
- return {"RUNNING_MODAL"}
-
-
-class SelectCacheDir(bpy.types.Operator):
- bl_idname = "bim.select_cache_dir"
- bl_label = "Select Cache Directory"
- bl_options = {"REGISTER", "UNDO"}
- bl_description = "Select the directory that contains HDF5 cache files"
- filepath: bpy.props.StringProperty(subtype="FILE_PATH")
-
- def execute(self, context):
- context.scene.BIMProperties.cache_dir = os.path.dirname(self.filepath)
- return {"FINISHED"}
-
- def invoke(self, context, event):
- context.window_manager.fileselect_add(self)
- return {"RUNNING_MODAL"}
-
-
-class SelectSchemaDir(bpy.types.Operator):
- bl_idname = "bim.select_schema_dir"
- bl_label = "Select Schema Directory"
- bl_options = {"REGISTER", "UNDO"}
- bl_description = "Select the directory containing the IFC schema specification"
- filepath: bpy.props.StringProperty(subtype="FILE_PATH")
-
- def execute(self, context):
- context.scene.BIMProperties.schema_dir = os.path.dirname(self.filepath)
+ crumbs = self.data_path.split(".")
+ if crumbs[0] == "preferences":
+ crumbs.pop(0)
+ data = tool.Blender.get_addon_preferences()
+ else:
+ data = context
+ while crumbs:
+ crumb = crumbs.pop(0)
+ if crumbs:
+ data = getattr(data, crumb)
+ else:
+ setattr(data, crumb, os.path.dirname(self.filepath))
return {"FINISHED"}
def invoke(self, context, event):
@@ -595,7 +578,8 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
backfacing.location = mix_backfacing.location + Vector((-200, 200))
emission = nodes.new(type="ShaderNodeEmission")
- emission.inputs[0].default_value = list(context.scene.BIMProperties.section_plane_colour) + [1]
+ props = tool.Blender.get_bim_props()
+ emission.inputs[0].default_value = list(props.section_plane_colour) + [1]
emission.location = mix_backfacing.location - Vector((200, 150))
cut_obj = nodes.new(type="ShaderNodeTexCoord")
@@ -659,7 +643,8 @@ class BIM_OT_add_section_plane(bpy.types.Operator):
material = bpy.data.materials.new("Section Override")
material.use_nodes = True
- if context.scene.BIMProperties.should_section_selected_objects:
+ props = tool.Blender.get_bim_props()
+ if props.should_section_selected_objects:
objects = list(context.selected_objects)
else:
objects = list(context.visible_objects)
@@ -816,7 +801,9 @@ class ReloadIfcFile(bpy.types.Operator, tool.Ifc.Operator):
logger = logging.getLogger("ImportIFC")
path_log = tool.Blender.get_data_dir_path("process.log")
if not os.access(path_log.parent, os.W_OK):
- path_log = os.path.join(tempfile.mkdtemp(), "process.log")
+ path_log = os.path.join(
+ tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None), "process.log"
+ )
logging.basicConfig(
filename=path_log,
filemode="a",
@@ -832,7 +819,8 @@ class ReloadIfcFile(bpy.types.Operator, tool.Ifc.Operator):
settings.logger.info("Import finished in {:.2f} seconds".format(time.time() - start))
print("Import finished in {:.2f} seconds".format(time.time() - start))
- context.scene.BIMProperties.ifc_file = self.filepath
+ bim_props = tool.Blender.get_bim_props()
+ bim_props.ifc_file = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
@@ -846,7 +834,8 @@ class AddIfcFile(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
- context.scene.DocProperties.ifc_files.add()
+ props = tool.Drawing.get_document_props()
+ props.ifc_files.add()
return {"FINISHED"}
@@ -857,7 +846,8 @@ class RemoveIfcFile(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
- context.scene.DocProperties.ifc_files.remove(self.index)
+ props = tool.Drawing.get_document_props()
+ props.ifc_files.remove(self.index)
return {"FINISHED"}
@@ -868,7 +858,8 @@ class FetchObjectPassport(bpy.types.Operator):
def execute(self, context):
# TODO: this is dead code, awaiting reimplementation. See #1222.
for reference in context.active_object.BIMObjectProperties.document_references:
- reference = context.scene.BIMProperties.document_references[reference.name]
+ bim_props = tool.Blender.get_bim_props()
+ reference = bim_props.document_references[reference.name]
if reference.location[-6:] == ".blend":
self.fetch_blender(reference, context)
return {"FINISHED"}
@@ -1078,7 +1069,8 @@ class ClippingPlaneCutWithCappings(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
- cutting_planes = [p.obj for p in context.scene.BIMProjectProperties.clipping_planes]
+ props = tool.Project.get_project_props()
+ cutting_planes = [obj for p in props.clipping_planes if (obj := p.obj)]
if not cutting_planes:
self.report({"INFO"}, "No cutting planes found.")
return {"FINISHED"}
@@ -1088,7 +1080,7 @@ class ClippingPlaneCutWithCappings(bpy.types.Operator):
objects_processed, t0 = 0, time.time()
wm.progress_begin(0, len(context.selected_objects))
for obj_i, obj in enumerate(context.selected_objects):
- if obj.type != "MESH":
+ if not isinstance((mesh := obj.data), bpy.types.Mesh):
continue
if obj in cutting_planes:
@@ -1100,7 +1092,6 @@ class ClippingPlaneCutWithCappings(bpy.types.Operator):
ws_to_ls = obj.matrix_world.inverted()
rotation = ws_to_ls.to_quaternion()
- mesh = obj.data
bm = tool.Blender.get_bmesh_for_mesh(mesh)
object_changed = False
@@ -1124,7 +1115,7 @@ class ClippingPlaneCutWithCappings(bpy.types.Operator):
# don't swap mesh if it wasn't affected by any of the cutting planes
if object_changed:
temp_mesh = bpy.data.meshes.new("temp_cut")
- temp_mesh.BIMMeshProperties.replaced_mesh = mesh
+ tool.Geometry.get_mesh_props(temp_mesh).replaced_mesh = mesh
for material in mesh.materials:
temp_mesh.materials.append(material)
obj.data = temp_mesh
@@ -1181,9 +1172,10 @@ class RevertClippingPlaneCut(bpy.types.Operator):
self.report({"INFO"}, f"{objects_processed} processed - {time.time()-t0:.3f} sec")
return {"FINISHED"}
- def revert_object_mesh(self, obj):
+ def revert_object_mesh(self, obj: bpy.types.Object) -> None:
mesh = obj.data
- replaced_mesh = mesh.BIMMeshProperties.replaced_mesh
+ assert isinstance(mesh, bpy.types.Mesh)
+ replaced_mesh = tool.Geometry.get_mesh_props(mesh).replaced_mesh
if replaced_mesh:
obj.data = replaced_mesh
tool.Blender.remove_data_block(mesh, do_unlink=False)
diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py
index 8c790444f1..9a69807c23 100644
--- a/src/bonsai/bonsai/bim/prop.py
+++ b/src/bonsai/bonsai/bim/prop.py
@@ -119,23 +119,27 @@ def get_attribute_enum_values(prop: "Attribute", context: bpy.types.Context) ->
def update_schema_dir(self: "BIMProperties", context: bpy.types.Context) -> None:
import bonsai.bim.schema
- bonsai.bim.schema.ifc.schema_dir = context.scene.BIMProperties.schema_dir
+ bim_props = tool.Blender.get_bim_props()
+ bonsai.bim.schema.ifc.schema_dir = bim_props.schema_dir
def update_data_dir(self: "BIMProperties", context: bpy.types.Context) -> None:
import bonsai.bim.schema
- bonsai.bim.schema.ifc.data_dir = context.scene.BIMProperties.data_dir
+ bim_props = tool.Blender.get_bim_props()
+ bonsai.bim.schema.ifc.data_dir = bim_props.data_dir
def update_cache_dir(self: "BIMProperties", context: bpy.types.Context) -> None:
import bonsai.bim.schema
- bonsai.bim.schema.ifc.cache_dir = context.scene.BIMProperties.cache_dir
+ bim_props = tool.Blender.get_bim_props()
+ bonsai.bim.schema.ifc.cache_dir = bim_props.cache_dir
def update_ifc_file(self: "BIMProperties", context: bpy.types.Context) -> None:
- if context.scene.BIMProperties.ifc_file:
+ bim_props = tool.Blender.get_bim_props()
+ if bim_props.ifc_file:
bonsai.bim.handler.loadIfcStore(context.scene)
@@ -230,15 +234,16 @@ def update_attribute_value(self: "Attribute", context: bpy.types.Context) -> Non
def update_is_null(self: "Attribute", context: bpy.types.Context) -> None:
- if not self.is_null:
- return
- self.string_value = ""
- self.int_value = 0
- self.float_value = 0
- self.length_value = 0
- self.bool_value = False
- if self.is_null is not True:
- self.is_null = True
+ if self.is_null:
+ if self.data_type != "enum" and self.get_value() != (default := self.get_value_default()):
+ self.set_value(default)
+ if self.is_null is not True:
+ self.is_null = True
+ if self.update:
+ update = globals()
+ for name in self.update.split("."):
+ update = update[name] if isinstance(update, dict) else getattr(update, name)
+ update(self, context)
def set_int_value(self: "Attribute", new_value: int) -> None:
@@ -268,12 +273,16 @@ def set_length_value(self: "Attribute", value: float) -> None:
def get_display_name(self: "Attribute") -> str:
+ DISPLAY_UNIT_TYPES = ("AREA", "VOLUME", "FORCE")
name = self.name
- if not self.special_type or self.special_type == "LENGTH":
+ if not self.special_type or self.special_type not in DISPLAY_UNIT_TYPES:
return name
unit_type = f"{self.special_type}UNIT"
project_unit = ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), unit_type)
+ if not project_unit:
+ return name
+
unit_symbol = ifcopenshell.util.unit.get_unit_symbol(project_unit)
return f"{name}, {unit_symbol}"
@@ -326,13 +335,35 @@ class Attribute(PropertyGroup):
value_max_constraint: BoolProperty(default=False, description="True if the numerical value has an upper bound")
special_type: StringProperty(name="Special Value Type", default="")
metadata: StringProperty(name="Metadata", description="For storing some additional information about the attribute")
+ update: StringProperty(name="Update", description="Custom update function to be executed")
if TYPE_CHECKING:
name: str
+ display_name: str
description: str
ifc_class: str
data_type: AttributeDataType
special_type: AttributeSpecialType
+ string_value: str
+ bool_value: bool
+ int_value: int
+ float_value: float
+ length_value: float
+ enum_items: str
+ enum_descriptions: bpy.types.bpy_prop_collection_idprop[StrProperty]
+ enum_value: str
+ filepath_value: MultipleFileSelect
+ filter_glob: str
+ is_null: bool
+ is_optional: bool
+ is_uri: bool
+ is_selected: bool
+ value_min: float
+ value_min_constraint: bool
+ value_max: float
+ value_max_constraint: bool
+ metadata: str
+ update: str
def get_value(self) -> Union[str, float, int, bool, None]:
if self.is_optional and self.is_null:
@@ -437,6 +468,13 @@ class BIMAreaProperties(PropertyGroup):
active_tab: BoolProperty(default=True, name="Active Tab")
inactive_tab: BoolProperty(default=False, name="Inactive Tab")
+ if TYPE_CHECKING:
+ tab: str
+ previous_tab: str
+ alt_tab: str
+ active_tab: bool
+ inactive_tab: bool
+
# BIMAreaProperties exists per area and is setup on load post. However, for new
# or temporary screens, they may not be setup yet, so this global tab
@@ -447,6 +485,11 @@ class BIMTabProperties(PropertyGroup):
active_tab: BoolProperty(default=True, name="Active Tab")
inactive_tab: BoolProperty(default=False, name="Inactive Tab")
+ if TYPE_CHECKING:
+ tab: str
+ active_tab: bool
+ inactive_tab: bool
+
class BIMProperties(PropertyGroup):
is_dirty: BoolProperty(name="Is Dirty", default=False)
@@ -515,6 +558,21 @@ class BIMProperties(PropertyGroup):
name="IFC Volume Unit",
)
+ if TYPE_CHECKING:
+ is_dirty: bool
+ schema_dir: str
+ data_dir: str
+ cache_dir: str
+ has_blend_warning: bool
+ pset_dir: str
+ ifc_file: str
+ last_transaction: str
+ should_section_selected_objects: bool
+ section_plane_colour: tuple[float, float, float]
+ section_line_decorator_width: float
+ area_unit: str
+ volume_unit: str
+
class IfcParameter(PropertyGroup):
name: StringProperty(name="Name")
@@ -523,6 +581,13 @@ class IfcParameter(PropertyGroup):
value: FloatProperty(name="Value") # For now, only floats
type: StringProperty(name="Type")
+ if TYPE_CHECKING:
+ name: str
+ step_id: int
+ index: int
+ value: float
+ type: str
+
class PsetQto(PropertyGroup):
name: StringProperty(name="Name")
@@ -530,20 +595,32 @@ class PsetQto(PropertyGroup):
is_expanded: BoolProperty(name="Is Expanded", default=True)
is_editable: BoolProperty(name="Is Editable")
+ if TYPE_CHECKING:
+ properties: bpy.types.bpy_prop_collection_idprop[Attribute]
+ is_expanded: bool
+ is_editable: bool
+
class GlobalId(PropertyGroup):
name: StringProperty(name="Name")
+ ifc_definition_id: IntProperty(name="IFC Definition ID")
class BIMCollectionProperties(PropertyGroup):
obj: PointerProperty(type=bpy.types.Object)
+ if TYPE_CHECKING:
+ obj: Union[bpy.types.Object, None]
+
+
+BlenderOffsetType = Literal["NONE", "OBJECT_PLACEMENT", "CARTESIAN_POINT", "NOT_APPLICABLE"]
+
class BIMObjectProperties(PropertyGroup):
collection: PointerProperty(type=bpy.types.Collection)
ifc_definition_id: IntProperty(name="IFC Definition ID")
blender_offset_type: EnumProperty(
- items=[(o, o, "") for o in ["NONE", "OBJECT_PLACEMENT", "CARTESIAN_POINT", "NOT_APPLICABLE"]],
+ items=[(o, o, "") for o in get_args(BlenderOffsetType)],
name="Blender Offset",
default="NONE",
)
@@ -553,6 +630,16 @@ class BIMObjectProperties(PropertyGroup):
location_checksum: StringProperty(name="Location Checksum")
rotation_checksum: StringProperty(name="Rotation Checksum")
+ if TYPE_CHECKING:
+ collection: Union[bpy.types.Collection, None]
+ ifc_definition_id: int
+ blender_offset_type: BlenderOffsetType
+ cartesian_point_offset: str
+ is_reassigning_class: bool
+ is_renaming: bool
+ location_checksum: str
+ rotation_checksum: str
+
def get_profiles(self: "BIMMeshProperties", context: bpy.types.Context):
from bonsai.bim.module.model.data import ItemData
@@ -562,6 +649,9 @@ def get_profiles(self: "BIMMeshProperties", context: bpy.types.Context):
return ItemData.data["profiles_enum"]
+SubshapeType = Literal["-", "PROFILE", "AXIS"]
+
+
class BIMMeshProperties(PropertyGroup):
ifc_definition_id: IntProperty(name="IFC Definition ID")
ifc_boolean_id: IntProperty(name="IFC Boolean ID")
@@ -570,7 +660,7 @@ class BIMMeshProperties(PropertyGroup):
is_native: BoolProperty(name="Is Native", default=False)
is_swept_solid: BoolProperty(name="Is Swept Solid")
is_parametric: BoolProperty(name="Is Parametric", default=False)
- subshape_type: EnumProperty(name="Subshape Type", items=[(i, i, "") for i in ("-", "PROFILE", "AXIS")])
+ subshape_type: EnumProperty(name="Subshape Type", items=[(i, i, "") for i in get_args(SubshapeType)])
ifc_parameters: CollectionProperty(name="IFC Parameters", type=IfcParameter)
item_attributes: CollectionProperty(name="Item Attributes", type=Attribute)
item_profile: EnumProperty(name="Item Profile", items=get_profiles)
@@ -578,6 +668,22 @@ class BIMMeshProperties(PropertyGroup):
mesh_checksum: StringProperty(name="Mesh Checksum", default="")
replaced_mesh: PointerProperty(type=bpy.types.Mesh, description="Original mesh to revert section cutaway")
+ if TYPE_CHECKING:
+ ifc_definition_id: int
+ ifc_boolean_id: int
+ obj: Union[bpy.types.Object, None]
+ has_openings_applied: bool
+ is_native: bool
+ is_swept_solid: bool
+ is_parametric: bool
+ subshape_type: SubshapeType
+ ifc_parameters: bpy.types.bpy_prop_collection_idprop[IfcParameter]
+ item_attributes: bpy.types.bpy_prop_collection_idprop[Attribute]
+ item_profile: str
+ material_checksum: str
+ mesh_checksum: str
+ replaced_mesh: Union[bpy.types.Mesh, None]
+
class BIMFacet(PropertyGroup):
name: StringProperty(name="Name")
@@ -597,16 +703,30 @@ class BIMFacet(PropertyGroup):
],
)
+ if TYPE_CHECKING:
+ pset: str
+ value: str
+ type: str
+ comparison: Literal["=", "!=", ">=", "<=", ">", "<", "*=", "!*="]
+
class BIMFilterGroup(PropertyGroup):
filters: CollectionProperty(type=BIMFacet, name="filters")
+ if TYPE_CHECKING:
+ filters: bpy.types.bpy_prop_collection_idprop[BIMFacet]
+
class BIMSnapGroups(PropertyGroup):
object: BoolProperty(name="Object", default=True)
polyline: BoolProperty(name="Polyline", default=True)
measure: BoolProperty(name="Measure", default=True)
+ if TYPE_CHECKING:
+ object: bool
+ polyline: bool
+ measure: bool
+
class BIMSnapProperties(PropertyGroup):
vertex: BoolProperty(name="Vertex", default=True)
@@ -614,3 +734,10 @@ class BIMSnapProperties(PropertyGroup):
edge_center: BoolProperty(name="Edge Center", default=True)
edge_intersection: BoolProperty(name="Edge Intersection", default=True)
face: BoolProperty(name="Face", default=True)
+
+ if TYPE_CHECKING:
+ vertex: bool
+ edge: bool
+ edge_center: bool
+ edge_intersection: bool
+ face: bool
diff --git a/src/bonsai/bonsai/bim/schema.py b/src/bonsai/bonsai/bim/schema.py
index 6d67d2e20a..cae1e32146 100644
--- a/src/bonsai/bonsai/bim/schema.py
+++ b/src/bonsai/bonsai/bim/schema.py
@@ -19,6 +19,8 @@
import ifcopenshell
import ifcopenshell.util.pset
import bonsai.tool as tool
+import bpy
+import bpy_restrict_state
class IfcSchema:
@@ -47,10 +49,16 @@ class IfcSchema:
self.psetqto.get_applicable.cache_clear()
self.psetqto.get_applicable_names.cache_clear()
self.psetqto.get_by_name.cache_clear()
+
+ # During register we cannot access the context either way.
+ if isinstance(bpy.context, bpy_restrict_state._RestrictContext):
+ return
for path in tool.Blender.get_data_dir_paths("pset", "*.ifc"):
self.psetqto.templates.append(ifcopenshell.open(path))
+# TODO: do we really need to load it on module import?
+# Loading it on IFC load should be enough.
ifc = IfcSchema()
diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py
index a666d3fd94..4223706b8a 100644
--- a/src/bonsai/bonsai/bim/ui.py
+++ b/src/bonsai/bonsai/bim/ui.py
@@ -19,6 +19,7 @@
import os
import bpy
import platform
+import bonsai.bim.helper
from pathlib import Path
from bpy.types import Panel
from bpy.props import StringProperty, IntProperty, BoolProperty
@@ -34,7 +35,7 @@ import bonsai.bim
import bonsai.tool as tool
from ifcopenshell.util.file import IfcHeaderExtractor
from bonsai.bim.prop import Attribute
-from typing import Optional
+from typing import Optional, TYPE_CHECKING
class IFCFileSelector:
@@ -120,7 +121,7 @@ class BIM_PT_section_plane(Panel):
def draw(self, context):
layout = self.layout
layout.use_property_split = True
- props = context.scene.BIMProperties
+ props = tool.Blender.get_bim_props()
layout.prop(props, "should_section_selected_objects")
layout.prop(props, "section_plane_colour")
@@ -147,7 +148,7 @@ class BIM_PT_section_with_cappings(Panel):
row.operator("bim.clipping_plane_cut_with_cappings", icon="XRAY", text="Cut")
row.operator("bim.revert_clipping_plane_cut", icon="FILE_REFRESH", text="Revert Cut")
- props = context.scene.BIMProjectProperties
+ props = tool.Project.get_project_props()
box = layout.box()
header = box.row(align=True)
header.label(text="Clipping Planes")
@@ -245,6 +246,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
description="If disabled, the toolbar will only load when an IFC model is active",
)
should_play_chaching_sound: BoolProperty(name="Play A Cha-Ching Sound When Project Costs Updates", default=False)
+ tmp_dir: StringProperty(
+ name="Temporary Directory",
+ description="Path to create and store temporary files. If left blank, a system default will be used.",
+ )
spatial_elements_unselectable: BoolProperty(
name="Make Spatial Elements Unselectable By Default",
default=True,
@@ -298,7 +303,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
size=4,
description="Color of background overlays",
)
-
opening_focus_opacity: bpy.props.IntProperty(
default=100,
min=0,
@@ -308,7 +312,29 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
description="When modifying openings, other elements of the model will display with some transparency.\n0 is fully transparent and 100 is fully opaque",
)
- def draw(self, context):
+ if TYPE_CHECKING:
+ svg2pdf_command: str
+ svg2dxf_command: str
+ svg_command: str
+ layout_svg_command: str
+ pdf_command: str
+ spreadsheet_command: str
+ should_hide_empty_props: bool
+ should_setup_workspace: bool
+ activate_workspace: bool
+ should_setup_toolbar: bool
+ should_play_chaching_sound: bool
+ spatial_elements_unselectable: bool
+ tmp_dir: str
+ decorations_colour: tuple[float, float, float, float]
+ decorator_color_selected: tuple[float, float, float, float]
+ decorator_color_unselected: tuple[float, float, float, float]
+ decorator_color_special: tuple[float, float, float, float]
+ decorator_color_error: tuple[float, float, float, float]
+ decorator_color_background: tuple[float, float, float, float]
+ opening_focus_opacity: int
+
+ def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
row = layout.row()
@@ -329,7 +355,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Drawing", self.draw_drawing_settings)
bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Openings", self.draw_openings_settings)
- def draw_commands(self, layout, context):
+ def draw_commands(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "svg2pdf_command")
layout.prop(self, "svg2dxf_command")
layout.prop(self, "svg_command")
@@ -337,15 +363,16 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
layout.prop(self, "pdf_command")
layout.prop(self, "spreadsheet_command")
- def draw_misc_settings(self, layout, context):
+ def draw_misc_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "should_hide_empty_props")
layout.prop(self, "should_setup_workspace")
layout.prop(self, "activate_workspace")
layout.prop(self, "should_setup_toolbar")
layout.prop(self, "should_play_chaching_sound")
layout.prop(self, "spatial_elements_unselectable")
- layout.prop(context.scene.BIMProjectProperties, "should_disable_undo_on_save")
- layout.prop(context.scene.BIMProjectProperties, "should_stream")
+ props = tool.Project.get_project_props()
+ layout.prop(props, "should_disable_undo_on_save")
+ layout.prop(props, "should_stream")
def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
props = tool.Model.get_model_props()
@@ -353,34 +380,41 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
if props.occurrence_name_style == "CUSTOM":
layout.prop(props, "occurrence_name_function")
- def draw_directories(self, layout, context):
+ def draw_directories(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
+ props = tool.Blender.get_bim_props()
row = layout.row(align=True)
- row.prop(context.scene.BIMProperties, "data_dir")
- row.operator("bim.select_data_dir", icon="FILE_FOLDER", text="")
+ row.prop(props, "data_dir")
+ row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.data_dir"
row = layout.row(align=True)
- row.prop(context.scene.BIMProperties, "cache_dir")
- row.operator("bim.select_cache_dir", icon="FILE_FOLDER", text="")
+ row.prop(props, "cache_dir")
+ row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.cache_dir"
- def draw_drawing_settings(self, layout, context):
- layout.prop(context.scene.BIMProperties, "pset_dir")
- layout.prop(context.scene.DocProperties, "sheets_dir")
- layout.prop(context.scene.DocProperties, "layouts_dir")
- layout.prop(context.scene.DocProperties, "titleblocks_dir")
- layout.prop(context.scene.DocProperties, "drawings_dir")
- layout.prop(context.scene.DocProperties, "stylesheet_path")
- layout.prop(context.scene.DocProperties, "schedules_stylesheet_path")
- layout.prop(context.scene.DocProperties, "markers_path")
- layout.prop(context.scene.DocProperties, "symbols_path")
- layout.prop(context.scene.DocProperties, "patterns_path")
- layout.prop(context.scene.DocProperties, "shadingstyles_path")
- layout.prop(context.scene.DocProperties, "shadingstyle_default")
+ row = layout.row(align=True)
+ row.prop(self, "tmp_dir")
+ row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "preferences.tmp_dir"
+
+ def draw_drawing_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
+ props = tool.Blender.get_bim_props()
+ layout.prop(props, "pset_dir")
+ dprops = tool.Drawing.get_document_props()
+ layout.prop(dprops, "sheets_dir")
+ layout.prop(dprops, "layouts_dir")
+ layout.prop(dprops, "titleblocks_dir")
+ layout.prop(dprops, "drawings_dir")
+ layout.prop(dprops, "stylesheet_path")
+ layout.prop(dprops, "schedules_stylesheet_path")
+ layout.prop(dprops, "markers_path")
+ layout.prop(dprops, "symbols_path")
+ layout.prop(dprops, "patterns_path")
+ layout.prop(dprops, "shadingstyles_path")
+ layout.prop(dprops, "shadingstyle_default")
row = layout.row()
- row.prop(context.scene.DocProperties, "drawing_font")
- row.prop(context.scene.DocProperties, "magic_font_scale")
- layout.prop(context.scene.DocProperties, "imperial_precision")
- layout.prop(context.scene.DocProperties, "tolerance")
- layout.prop(context.scene.DocProperties, "classes_to_wireframe")
+ row.prop(dprops, "drawing_font")
+ row.prop(dprops, "magic_font_scale")
+ layout.prop(dprops, "imperial_precision")
+ layout.prop(dprops, "tolerance")
+ layout.prop(dprops, "classes_to_wireframe")
def draw_decorator_colors(self, layout, context):
layout.row().prop(self, "decorations_colour")
@@ -478,8 +512,8 @@ class BIM_PT_tabs(Panel):
if not tool.Ifc.get():
return
- props = context.scene.BIMProperties
- if props.has_blend_warning:
+ bim_props = tool.Blender.get_bim_props()
+ if bim_props.has_blend_warning:
box = self.layout.box()
box.alert = True
row = box.row(align=True)
@@ -488,9 +522,10 @@ class BIM_PT_tabs(Panel):
op.uri = "https://docs.bonsaibim.org/guides/troubleshooting.html#saving-and-loading-blend-files"
row.operator("bim.close_blend_warning", text="", icon="CANCEL")
- if context.mode == "OBJECT" and context.scene.BIMGeometryProperties.mode in ("OBJECT", "ITEM"):
+ gprops = tool.Geometry.get_geometry_props()
+ if context.mode == "OBJECT" and gprops.mode in ("OBJECT", "ITEM"):
pass
- elif context.mode.startswith("EDIT") and context.scene.BIMGeometryProperties.mode == "EDIT":
+ elif context.mode.startswith("EDIT") and gprops.mode == "EDIT":
pass
else:
box = self.layout.box()
@@ -524,11 +559,11 @@ class BIM_PT_tab_new_project_wizard(Panel):
def poll(cls, context):
if not tool.Blender.is_tab(context, "PROJECT"):
return False
- props = context.scene.BIMProperties
- pprops = context.scene.BIMProjectProperties
+ bim_props = tool.Blender.get_bim_props()
+ pprops = tool.Project.get_project_props()
if pprops.is_loading:
return False
- elif tool.Ifc.get() or props.ifc_file:
+ elif tool.Ifc.get() or bim_props.ifc_file:
return False
return True
@@ -546,11 +581,11 @@ class BIM_PT_tab_project_info(Panel):
def poll(cls, context):
if not tool.Blender.is_tab(context, "PROJECT"):
return False
- props = context.scene.BIMProperties
- pprops = context.scene.BIMProjectProperties
+ bim_props = tool.Blender.get_bim_props()
+ pprops = tool.Project.get_project_props()
if pprops.is_loading:
return True
- elif tool.Ifc.get() or props.ifc_file:
+ elif tool.Ifc.get() or bim_props.ifc_file:
return True
return False
@@ -846,6 +881,7 @@ class BIM_PT_tab_object_metadata(Panel):
@classmethod
def poll(cls, context):
+ props = tool.Project.get_project_props()
return (
tool.Blender.is_tab(context, "OBJECT")
and tool.Ifc.get()
@@ -854,7 +890,7 @@ class BIM_PT_tab_object_metadata(Panel):
and (
obj.type != "EMPTY"
or not obj.instance_collection
- or not any(l.empty_handle == obj for l in context.scene.BIMProjectProperties.links)
+ or not any(l.empty_handle == obj for l in props.links)
)
)
@@ -1223,7 +1259,7 @@ class BIM_PT_decorators_overlay(Panel):
view = context.space_data
overlay = view.overlay
- georeference_props = bpy.context.scene.BIMGeoreferenceProperties
+ georeference_props = tool.Georeference.get_georeference_props()
aggregate_props = bpy.context.scene.BIMAggregateProperties
nest_props = bpy.context.scene.BIMNestProperties
model_props = tool.Model.get_model_props()
diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py
index e5ae3cd493..c01b70bd55 100644
--- a/src/bonsai/bonsai/core/drawing.py
+++ b/src/bonsai/bonsai/core/drawing.py
@@ -417,6 +417,7 @@ def add_annotation(
drawing_tool.show_decorations()
obj = drawing_tool.create_annotation_object(drawing, object_type)
element = ifc.get_entity(obj)
+ # TODO: element is never None?
if not element:
relating_type_rep = drawing_tool.get_annotation_representation(relating_type) if relating_type else None
element = drawing_tool.run_root_assign_class(
diff --git a/src/bonsai/bonsai/core/spatial.py b/src/bonsai/bonsai/core/spatial.py
index bdfb410a43..50e80bbed8 100644
--- a/src/bonsai/bonsai/core/spatial.py
+++ b/src/bonsai/bonsai/core/spatial.py
@@ -130,11 +130,6 @@ def set_orientation_slot(spatial: tool.Spatial, product: ifcopenshell.entity_ins
spatial.create_orientation_slots([product], use=True)
-def edit_container_attributes(spatial: tool.Spatial, entity: ifcopenshell.entity_instance) -> None:
- spatial.edit_container_attributes(entity)
- spatial.import_spatial_decomposition()
-
-
def contract_container(spatial: tool.Spatial, container: ifcopenshell.entity_instance) -> None:
spatial.contract_container(container)
spatial.import_spatial_decomposition()
diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py
index b84e931871..d96b019df2 100644
--- a/src/bonsai/bonsai/core/tool.py
+++ b/src/bonsai/bonsai/core/tool.py
@@ -901,7 +901,6 @@ class Spatial:
def deselect_objects(cls): pass
def disable_editing(cls, obj): pass
def duplicate_object_and_data(cls, obj): pass
- def edit_container_attributes(cls, entity): pass
def edit_container_name(cls, container, name): pass
def enable_editing(cls, obj): pass
def expand_container(cls, container): pass
diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py
index ad62b1fabc..a4a2ed4558 100644
--- a/src/bonsai/bonsai/tool/blender.py
+++ b/src/bonsai/bonsai/tool/blender.py
@@ -37,9 +37,12 @@ from mathutils import Vector
from pathlib import Path
from functools import lru_cache
from bonsai.bim.ifc import IFC_CONNECTED_TYPE
-from typing import Any, Optional, Union, Literal, Iterable, Callable, TypeVar, Generator
+from typing import Any, Optional, Union, Literal, Iterable, Callable, TypeVar, Generator, TYPE_CHECKING
from typing_extensions import assert_never
+if TYPE_CHECKING:
+ from bonsai.bim.prop import BIMProperties
+
VIEWPORT_ATTRIBUTES = [
"view_matrix",
@@ -913,7 +916,7 @@ class Blender(bonsai.core.tool.Blender):
return False
if not (element := tool.Ifc.get_entity(obj)):
return True
- if obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs:
+ if obj in tool.Project.get_project_props().clipping_planes_objs:
return False
usage_type = tool.Model.get_usage_type(element)
if usage_type in ("LAYER1", "LAYER2"):
@@ -1477,10 +1480,8 @@ class Blender(bonsai.core.tool.Blender):
@classmethod
def get_user_data_dir(cls) -> Path:
- try:
- return Path(bpy.context.scene.BIMProperties.data_dir)
- except AttributeError:
- return Path()
+ props = tool.Blender.get_bim_props()
+ return Path(props.data_dir)
@classmethod
def get_data_dir_path(cls, relative_path: Union[str, Path]) -> Path:
@@ -1538,3 +1539,9 @@ class Blender(bonsai.core.tool.Blender):
dct = {cls.bl_idname: cls.ifc_element_type for cls in (BimTool.__subclasses__())}
return types.MappingProxyType(dct)
+
+ @classmethod
+ def get_bim_props(cls, scene: Optional[bpy.types.Scene] = None) -> BIMProperties:
+ if scene is None:
+ scene = bpy.context.scene
+ return scene.BIMProperties
diff --git a/src/bonsai/bonsai/tool/brick.py b/src/bonsai/bonsai/tool/brick.py
index 4b034f2160..acd8d50b01 100644
--- a/src/bonsai/bonsai/tool/brick.py
+++ b/src/bonsai/bonsai/tool/brick.py
@@ -86,10 +86,11 @@ class Brick(bonsai.core.tool.Brick):
project = tool.Ifc.get().by_type("IfcProject")[0]
ns = Namespace(namespace)
brick_project = ns[project.GlobalId]
+ props = tool.Blender.get_bim_props()
with BrickStore.new_changeset() as cs:
cs.add((brick_project, A, REF.ifcProject))
cs.add((brick_project, REF.ifcProjectID, Literal(project.GlobalId)))
- cs.add((brick_project, REF.ifcFileLocation, Literal(bpy.context.scene.BIMProperties.ifc_file)))
+ cs.add((brick_project, REF.ifcFileLocation, Literal(props.ifc_file)))
if project.Name:
cs.add((brick_project, URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal(project.Name)))
return str(brick_project)
diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py
index 0dd9b6c855..7752423ce4 100644
--- a/src/bonsai/bonsai/tool/debug.py
+++ b/src/bonsai/bonsai/tool/debug.py
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+from __future__ import annotations
import os
import json
import bpy
@@ -30,10 +31,17 @@ import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from mathutils import Vector
from collections import defaultdict
-from typing import Iterable, Literal
+from typing import Iterable, Literal, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from bonsai.bim.module.debug.prop import BIMDebugProperties
class Debug(bonsai.core.tool.Debug):
+ @classmethod
+ def get_debug_props(cls) -> BIMDebugProperties:
+ return bpy.context.scene.BIMDebugProperties
+
@classmethod
def add_schema_identifier(cls, schema: W.schema_definition) -> None:
IfcStore.schema_identifiers.append(schema.name())
@@ -46,7 +54,8 @@ class Debug(bonsai.core.tool.Debug):
@classmethod
def purge_hdf5_cache(cls) -> None:
- cache_dir = bpy.context.scene.BIMProperties.cache_dir
+ props = tool.Blender.get_bim_props()
+ cache_dir = props.cache_dir
filelist = [f for f in os.listdir(cache_dir) if f.endswith(".h5")]
for f in filelist:
try:
diff --git a/src/bonsai/bonsai/tool/document.py b/src/bonsai/bonsai/tool/document.py
index 25799948fc..5d1e5eee54 100644
--- a/src/bonsai/bonsai/tool/document.py
+++ b/src/bonsai/bonsai/tool/document.py
@@ -16,56 +16,68 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+from __future__ import annotations
import bpy
import ifcopenshell.util.system
import bonsai.bim.helper
import bonsai.core.tool
import bonsai.tool as tool
-from typing import Any, Union, Sequence
+from typing import Any, Union, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from bonsai.bim.module.document.prop import BIMDocumentProperties
class Document(bonsai.core.tool.Document):
+ @classmethod
+ def get_document_props(cls) -> BIMDocumentProperties:
+ return bpy.context.scene.BIMDocumentProperties
+
@classmethod
def add_breadcrumb(cls, document: ifcopenshell.entity_instance) -> None:
- props = bpy.context.scene.BIMDocumentProperties
+ props = cls.get_document_props()
new = props.breadcrumbs.add()
new.name = str(document.id())
@classmethod
def clear_breadcrumbs(cls) -> None:
- props = bpy.context.scene.BIMDocumentProperties
+ props = cls.get_document_props()
props.breadcrumbs.clear()
@classmethod
def clear_document_tree(cls) -> None:
- props = bpy.context.scene.BIMDocumentProperties
+ props = cls.get_document_props()
props.documents.clear()
@classmethod
def disable_editing_document(cls) -> None:
- bpy.context.scene.BIMDocumentProperties.active_document_id = 0
+ props = cls.get_document_props()
+ props.active_document_id = 0
@classmethod
def disable_editing_ui(cls) -> None:
- bpy.context.scene.BIMDocumentProperties.is_editing = False
+ props = cls.get_document_props()
+ props.is_editing = False
@classmethod
def enable_editing_ui(cls) -> None:
- bpy.context.scene.BIMDocumentProperties.is_editing = True
+ props = cls.get_document_props()
+ props.is_editing = True
@classmethod
def export_document_attributes(cls) -> dict[str, Any]:
- return bonsai.bim.helper.export_attributes(bpy.context.scene.BIMDocumentProperties.document_attributes)
+ props = cls.get_document_props()
+ return bonsai.bim.helper.export_attributes(props.document_attributes)
@classmethod
def get_active_breadcrumb(cls) -> Union[ifcopenshell.entity_instance, None]:
- props = bpy.context.scene.BIMDocumentProperties
+ props = cls.get_document_props()
if len(props.breadcrumbs):
return tool.Ifc.get().by_id(int(props.breadcrumbs[-1].name))
@classmethod
def import_document_attributes(cls, document: ifcopenshell.entity_instance) -> None:
- props = bpy.context.scene.BIMDocumentProperties
+ props = cls.get_document_props()
props.document_attributes.clear()
def callback(attr_name: str, _, data: dict[str, Any]) -> Union[bool, None]:
@@ -86,7 +98,7 @@ class Document(bonsai.core.tool.Document):
@classmethod
def import_project_documents(cls) -> None:
- props = bpy.context.scene.BIMDocumentProperties
+ props = cls.get_document_props()
props.documents.clear()
project = tool.Ifc.get().by_type("IfcProject")[0]
for rel in project.HasAssociations or []:
@@ -100,7 +112,7 @@ class Document(bonsai.core.tool.Document):
@classmethod
def import_references(cls, document: ifcopenshell.entity_instance) -> None:
- props = bpy.context.scene.BIMDocumentProperties
+ props = cls.get_document_props()
is_ifc2x3 = tool.Ifc.get_schema() == "IFC2X3"
references = cls.get_document_references(document)
for element in references:
@@ -115,7 +127,7 @@ class Document(bonsai.core.tool.Document):
@classmethod
def import_subdocuments(cls, document: ifcopenshell.entity_instance) -> None:
- props = bpy.context.scene.BIMDocumentProperties
+ props = cls.get_document_props()
if document.IsPointer:
for element in document.IsPointer[0].RelatedDocuments or []:
new = props.documents.add()
@@ -130,13 +142,14 @@ class Document(bonsai.core.tool.Document):
@classmethod
def remove_latest_breadcrumb(cls) -> None:
- props = bpy.context.scene.BIMDocumentProperties
+ props = cls.get_document_props()
if len(props.breadcrumbs):
props.breadcrumbs.remove(len(props.breadcrumbs) - 1)
@classmethod
def set_active_document(cls, document: ifcopenshell.entity_instance) -> None:
- bpy.context.scene.BIMDocumentProperties.active_document_id = document.id()
+ props = cls.get_document_props()
+ props.active_document_id = document.id()
@classmethod
def get_document_information_id(cls, document: ifcopenshell.entity_instance) -> Union[str, None]:
diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py
index 134f3bcf5f..9f72fd51ee 100644
--- a/src/bonsai/bonsai/tool/drawing.py
+++ b/src/bonsai/bonsai/tool/drawing.py
@@ -51,7 +51,7 @@ from shapely.ops import unary_union
from lxml import etree
from mathutils import Vector, Matrix
from fractions import Fraction
-from typing import Optional, Union, Iterable, Any, Literal, Sequence, TYPE_CHECKING
+from typing import Optional, Union, Iterable, Any, Literal, Sequence, TYPE_CHECKING, NamedTuple
from pathlib import Path
if TYPE_CHECKING:
@@ -65,25 +65,32 @@ class Drawing(bonsai.core.tool.Drawing):
# ObjectType: annotation_name, description, icon, data_type
# fmt: off
- ANNOTATION_TYPES_DATA = {
- "DIMENSION": ("Dimension", "Add dimensions annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "FIXED_SIZE", "curve"),
- "ANGLE": ("Angle", "", "DRIVER_ROTATIONAL_DIFFERENCE", "curve"),
- "RADIUS": ("Radius", "", "FORWARD", "curve"),
- "DIAMETER": ("Diameter", "Add diameter annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "ARROW_LEFTRIGHT", "curve"),
- "TEXT": ("Text", "", "SMALL_CAPS", "empty"),
- "TEXT_LEADER": ("Leader", "", "TRACKING_BACKWARDS", "curve"),
- "STAIR_ARROW": ("Stair Arrow", "Add stair arrow annotation.\nIf you have IfcStairFlight object selected, it will be used as a reference for the annotation", "SCREEN_BACK", "curve"),
- "PLAN_LEVEL": ("Level (Plan)", "", "SORTBYEXT", "curve"),
- "SECTION_LEVEL": ("Level (Section)", "", "TRIA_DOWN", "curve"),
- "BREAKLINE": ("Breakline", "", "FCURVE", "mesh"),
- "SYMBOL": ("Symbol", "", "KEYFRAME", "empty"),
- "MULTI_SYMBOL": ("Multi-Symbol", "", "OUTLINER_DATA_POINTCLOUD", "mesh"),
- "LINEWORK": ("Line", "", "SNAP_MIDPOINT", "mesh"),
- "BATTING": ("Batting", "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set", "FORCE_FORCE", "mesh"),
- "REVISION_CLOUD":("Revision Cloud", "Add revision cloud", "VOLUME_DATA", "mesh"),
- "FILL_AREA": ("Fill Area", "", "NODE_TEXTURE", "mesh"),
- "FALL": ("Fall", "", "SORT_ASC", "curve"),
- "IMAGE": ("Image", "Add reference image attached to the drawing", "TEXTURE", "mesh"),
+
+ class AnnotationObjectType(NamedTuple):
+ annotation_name: str
+ description: str
+ icon: str
+ data_type: Drawing.ANNOTATION_DATA_TYPE
+
+ ANNOTATION_TYPES_DATA: dict[str, AnnotationObjectType] = {
+ "DIMENSION": AnnotationObjectType("Dimension", "Add dimensions annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "FIXED_SIZE", "curve"),
+ "ANGLE": AnnotationObjectType("Angle", "", "DRIVER_ROTATIONAL_DIFFERENCE", "curve"),
+ "RADIUS": AnnotationObjectType("Radius", "", "FORWARD", "curve"),
+ "DIAMETER": AnnotationObjectType("Diameter", "Add diameter annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "ARROW_LEFTRIGHT", "curve"),
+ "TEXT": AnnotationObjectType("Text", "", "SMALL_CAPS", "empty"),
+ "TEXT_LEADER": AnnotationObjectType("Leader", "", "TRACKING_BACKWARDS", "curve"),
+ "STAIR_ARROW": AnnotationObjectType("Stair Arrow", "Add stair arrow annotation.\nIf you have IfcStairFlight object selected, it will be used as a reference for the annotation", "SCREEN_BACK", "curve"),
+ "PLAN_LEVEL": AnnotationObjectType("Level (Plan)", "", "SORTBYEXT", "curve"),
+ "SECTION_LEVEL": AnnotationObjectType("Level (Section)", "", "TRIA_DOWN", "curve"),
+ "BREAKLINE": AnnotationObjectType("Breakline", "", "FCURVE", "mesh"),
+ "SYMBOL": AnnotationObjectType("Symbol", "", "KEYFRAME", "empty"),
+ "MULTI_SYMBOL": AnnotationObjectType("Multi-Symbol", "", "OUTLINER_DATA_POINTCLOUD", "mesh"),
+ "LINEWORK": AnnotationObjectType("Line", "", "SNAP_MIDPOINT", "mesh"),
+ "BATTING": AnnotationObjectType("Batting", "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set", "FORCE_FORCE", "mesh"),
+ "REVISION_CLOUD":AnnotationObjectType("Revision Cloud", "Add revision cloud", "VOLUME_DATA", "mesh"),
+ "FILL_AREA": AnnotationObjectType("Fill Area", "", "NODE_TEXTURE", "mesh"),
+ "FALL": AnnotationObjectType("Fall", "", "SORT_ASC", "curve"),
+ "IMAGE": AnnotationObjectType("Image", "Add reference image attached to the drawing", "TEXTURE", "mesh"),
}
# fmt: on
@@ -112,7 +119,7 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def get_annotation_data_type(cls, object_type: str) -> ANNOTATION_DATA_TYPE:
- return cls.ANNOTATION_TYPES_DATA[object_type][3]
+ return cls.ANNOTATION_TYPES_DATA[object_type].data_type
@classmethod
def create_annotation_object(cls, drawing: ifcopenshell.entity_instance, object_type: str) -> bpy.types.Object:
@@ -238,7 +245,7 @@ class Drawing(bonsai.core.tool.Drawing):
element_type = element.is_a()
- if element_type == "IfcAnnotation" and element.ObjectType in object_types:
+ if element_type == "IfcAnnotation" and ifcopenshell.util.element.get_predefined_type(element) in object_types:
return True
if element_type == "IfcTypeProduct" and (
@@ -346,19 +353,23 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def disable_editing_drawings(cls) -> None:
- bpy.context.scene.DocProperties.is_editing_drawings = False
+ props = tool.Drawing.get_document_props()
+ props.is_editing_drawings = False
@classmethod
def disable_editing_schedules(cls) -> None:
- bpy.context.scene.DocProperties.is_editing_schedules = False
+ props = tool.Drawing.get_document_props()
+ props.is_editing_schedules = False
@classmethod
def disable_editing_references(cls) -> None:
- bpy.context.scene.DocProperties.is_editing_references = False
+ props = tool.Drawing.get_document_props()
+ props.is_editing_references = False
@classmethod
def disable_editing_sheets(cls) -> None:
- bpy.context.scene.DocProperties.is_editing_sheets = False
+ props = tool.Drawing.get_document_props()
+ props.is_editing_sheets = False
@classmethod
def disable_editing_text(cls, obj: bpy.types.Object) -> None:
@@ -383,19 +394,23 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def enable_editing_drawings(cls) -> None:
- bpy.context.scene.DocProperties.is_editing_drawings = True
+ props = tool.Drawing.get_document_props()
+ props.is_editing_drawings = True
@classmethod
def enable_editing_schedules(cls) -> None:
- bpy.context.scene.DocProperties.is_editing_schedules = True
+ props = tool.Drawing.get_document_props()
+ props.is_editing_schedules = True
@classmethod
def enable_editing_references(cls) -> None:
- bpy.context.scene.DocProperties.is_editing_references = True
+ props = tool.Drawing.get_document_props()
+ props.is_editing_references = True
@classmethod
def enable_editing_sheets(cls) -> None:
- bpy.context.scene.DocProperties.is_editing_sheets = True
+ props = tool.Drawing.get_document_props()
+ props.is_editing_sheets = True
@classmethod
def enable_editing_text(cls, obj: bpy.types.Object) -> None:
@@ -616,7 +631,8 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def is_editing_sheets(cls) -> bool:
- return bpy.context.scene.DocProperties.is_editing_sheets
+ props = tool.Drawing.get_document_props()
+ return props.is_editing_sheets
@classmethod
def remove_literal_from_annotation(cls, obj: bpy.types.Object, literal: ifcopenshell.entity_instance) -> None:
@@ -810,7 +826,7 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def import_drawings(cls) -> None:
- props = bpy.context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
expanded_target_views = {d.target_view for d in props.drawings if d.is_expanded}
if not hasattr(cls, "drawing_selected_states"):
cls.drawing_selected_states = {}
@@ -1048,7 +1064,8 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def show_decorations(cls) -> None:
- bpy.context.scene.DocProperties.should_draw_decorations = True
+ props = tool.Drawing.get_document_props()
+ props.should_draw_decorations = True
@classmethod
def update_text_value(cls, obj: bpy.types.Object) -> None:
@@ -1147,36 +1164,34 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def get_default_layout_path(cls, identification: str, name: str) -> str:
project = tool.Ifc.get().by_type("IfcProject")[0]
+ props = tool.Drawing.get_document_props()
layouts_dir = (
- ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "LayoutsDir")
- or bpy.context.scene.DocProperties.layouts_dir
+ ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "LayoutsDir") or props.layouts_dir
)
return os.path.join(layouts_dir, cls.sanitise_filename(f"{identification} - {name}.svg")).replace("\\", "/")
@classmethod
def get_default_sheet_path(cls, identification: str, name: str) -> str:
project = tool.Ifc.get().by_type("IfcProject")[0]
- sheets_dir = (
- ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "SheetsDir")
- or bpy.context.scene.DocProperties.sheets_dir
- )
+ props = tool.Drawing.get_document_props()
+ sheets_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "SheetsDir") or props.sheets_dir
return os.path.join(sheets_dir, cls.sanitise_filename(f"{identification} - {name}.svg")).replace("\\", "/")
@classmethod
def get_default_titleblock_path(cls, name: str) -> str:
project = tool.Ifc.get().by_type("IfcProject")[0]
+ props = tool.Drawing.get_document_props()
titleblocks_dir = (
- ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir")
- or bpy.context.scene.DocProperties.titleblocks_dir
+ ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir") or props.titleblocks_dir
)
return os.path.join(titleblocks_dir, cls.sanitise_filename(f"{name}.svg")).replace("\\", "/")
@classmethod
def get_default_drawing_path(cls, name: str) -> str:
project = tool.Ifc.get().by_type("IfcProject")[0]
+ props = tool.Drawing.get_document_props()
drawings_dir = (
- ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "DrawingsDir")
- or bpy.context.scene.DocProperties.drawings_dir
+ ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "DrawingsDir") or props.drawings_dir
)
return os.path.join(drawings_dir, cls.sanitise_filename(f"{name}.svg")).replace("\\", "/")
@@ -1187,15 +1202,16 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def get_default_drawing_resource_path(cls, resource: str) -> Union[str, None]:
project = tool.Ifc.get().by_type("IfcProject")[0]
+ props = tool.Drawing.get_document_props()
resource_path = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", f"{resource}Path") or getattr(
- bpy.context.scene.DocProperties, f"{resource.lower()}_path"
+ props, f"{resource.lower()}_path"
)
if resource_path:
return resource_path.replace("\\", "/")
@classmethod
def get_default_shading_style(cls) -> str:
- dprops = bpy.context.scene.DocProperties
+ dprops = tool.Drawing.get_document_props()
return dprops.shadingstyle_default
@classmethod
@@ -1501,7 +1517,7 @@ class Drawing(bonsai.core.tool.Drawing):
dst.data = dst.data.copy()
dst.name = dst.name.replace("IfcGridAxis/", "")
dst.BIMObjectProperties.ifc_definition_id = 0
- dst.data.BIMMeshProperties.ifc_definition_id = 0
+ tool.Geometry.get_geometry_props(dst).ifc_definition_id = 0
return dst
def disassemble(obj: bpy.types.Object) -> tuple[bpy.types.Object, bmesh.types.BMesh]:
@@ -1883,7 +1899,8 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def is_active_drawing(cls, drawing: ifcopenshell.entity_instance) -> bool:
- return drawing.id() == bpy.context.scene.DocProperties.active_drawing_id
+ props = tool.Drawing.get_document_props()
+ return drawing.id() == props.active_drawing_id
@classmethod
def run_drawing_activate_model(cls) -> None:
diff --git a/src/bonsai/bonsai/tool/feature.py b/src/bonsai/bonsai/tool/feature.py
index d8fd8921eb..0990aef008 100644
--- a/src/bonsai/bonsai/tool/feature.py
+++ b/src/bonsai/bonsai/tool/feature.py
@@ -16,17 +16,25 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+from __future__ import annotations
import bpy
import bonsai.core.tool
import bonsai.tool as tool
import bonsai.bim.helper
import ifcopenshell
-from typing import Iterable
+from typing import Iterable, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from bonsai.bim.module.void.prop import BIMBooleanProperties
class Feature(bonsai.core.tool.Feature):
# TODO: consolidate module/model/opening and module/void into new module/feature
+ @classmethod
+ def get_boolean_props(cls) -> BIMBooleanProperties:
+ return bpy.context.scene.BIMBooleanProperties
+
@classmethod
def add_feature(cls, featured_obj: bpy.types.Object, feature_objs: Iterable[bpy.types.Object]) -> None:
featured_element = tool.Ifc.get_entity(featured_obj)
diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py
index fca6541088..4947666bd6 100644
--- a/src/bonsai/bonsai/tool/geometry.py
+++ b/src/bonsai/bonsai/tool/geometry.py
@@ -27,6 +27,7 @@ import numpy.typing as npt
import multiprocessing
import ifcopenshell
import ifcopenshell.api
+import ifcopenshell.api.geometry
import ifcopenshell.api.grid
import ifcopenshell.api.profile
import ifcopenshell.api.style
@@ -53,11 +54,23 @@ from math import radians, pi
from mathutils import Vector, Matrix
from mathutils.bvhtree import BVHTree
from bonsai.bim.ifc import IfcStore
-from typing import Union, Iterable, Optional, Literal, Iterator, List, TYPE_CHECKING, get_args, Generator, cast
+from typing import (
+ Union,
+ Iterable,
+ Optional,
+ Literal,
+ Iterator,
+ List,
+ TYPE_CHECKING,
+ get_args,
+ Generator,
+ cast,
+ TypeGuard,
+)
from typing_extensions import TypeIs
if TYPE_CHECKING:
- from bonsai.bim.prop import Attribute
+ from bonsai.bim.prop import Attribute, BIMMeshProperties
from bonsai.bim.module.geometry.prop import BIMObjectGeometryProperties, BIMGeometryProperties
@@ -70,6 +83,10 @@ class Geometry(bonsai.core.tool.Geometry):
def get_object_geometry_props(cls, object: bpy.types.Object) -> BIMObjectGeometryProperties:
return object.BIMGeometryProperties
+ @classmethod
+ def get_mesh_props(cls, mesh: TYPES_WITH_MESH_PROPERTIES) -> BIMMeshProperties:
+ return mesh.BIMMeshProperties
+
@classmethod
def change_object_data(cls, obj: bpy.types.Object, data: bpy.types.ID, is_global: bool = False) -> None:
if is_global:
@@ -130,11 +147,11 @@ class Geometry(bonsai.core.tool.Geometry):
def is_locked(cls, element: ifcopenshell.entity_instance) -> bool:
if element.is_a("IfcProject"):
return True
- elif tool.Root.is_spatial_element(element) and bpy.context.scene.BIMSpatialDecompositionProperties.is_locked:
+ elif tool.Root.is_spatial_element(element) and tool.Spatial.get_spatial_props().is_locked:
return True
elif (
element.is_a("IfcPositioningElement") or element.is_a("IfcGrid") or element.is_a("IfcGridAxis")
- ) and bpy.context.scene.BIMGridProperties.is_locked:
+ ) and tool.Spatial.get_grid_props().is_locked:
return True
return False
@@ -182,7 +199,9 @@ class Geometry(bonsai.core.tool.Geometry):
if item_obj.obj == obj:
props.item_objs.remove(i)
break
- item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ mesh = obj.data
+ assert isinstance(mesh, bpy.types.Mesh)
+ item = tool.Ifc.get().by_id(tool.Geometry.get_mesh_props(mesh).ifc_definition_id)
cls.remove_representation_item(item)
cls.reload_representation(props.representation_obj)
bpy.data.objects.remove(obj)
@@ -249,17 +268,20 @@ class Geometry(bonsai.core.tool.Geometry):
bonsai.core.system.remove_port(tool.Ifc, tool.System, port=port)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
- if isinstance(obj.data, bpy.types.Mesh) and not tool.Ifc.get_entity_by_id(
- obj.data.BIMMeshProperties.ifc_definition_id
+ data = obj.data
+ if (
+ tool.Geometry.has_mesh_properties(data)
+ and tool.Ifc.get_entity_by_id(tool.Geometry.get_mesh_props(data).ifc_definition_id) is None
):
- tool.Blender.remove_data_block(obj.data)
+ tool.Blender.remove_data_block(data)
if is_spatial:
bonsai.core.spatial.import_spatial_decomposition(tool.Spatial)
try:
obj.name
- if bpy.context.scene.BIMGeometryProperties.representation_obj == obj:
- bpy.context.scene.BIMGeometryProperties.representation_obj = None
+ props = tool.Geometry.get_geometry_props()
+ if props.representation_obj == obj:
+ props.representation_obj = None
bpy.data.objects.remove(obj)
except:
pass
@@ -268,7 +290,9 @@ class Geometry(bonsai.core.tool.Geometry):
def dissolve_triangulated_edges(cls, obj: bpy.types.Object) -> None:
# AdvancedBreps may contain non-faceted, curved faces (e.g. as part of
# a cylinder) so dissolving edges should not be allowed.
- mesh_element = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ mesh = obj.data
+ assert isinstance(mesh, Geometry.TYPES_WITH_MESH_PROPERTIES)
+ mesh_element = tool.Ifc.get().by_id(tool.Geometry.get_mesh_props(mesh).ifc_definition_id)
if (
(
mesh_element.is_a("IfcShapeRepresentation")
@@ -279,26 +303,30 @@ class Geometry(bonsai.core.tool.Geometry):
or not obj.data
):
return
- if hasattr(obj.data, "attributes") and (ios_edges_attribute := obj.data.attributes.get("ios_edges")):
+
+ if not isinstance(mesh, bpy.types.Mesh):
+ return
+
+ if hasattr(mesh, "attributes") and (ios_edges_attribute := mesh.attributes.get("ios_edges")):
# Edges from a forced triangulation are stored as True in a boolean attribute on the mesh
bm = bmesh.new()
- bm.from_mesh(obj.data)
+ bm.from_mesh(mesh)
edges_to_dissolve = [e for i, e in enumerate(bm.edges) if not ios_edges_attribute.data[i].value]
bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve)
- bm.to_mesh(obj.data)
+ bm.to_mesh(mesh)
bm.free()
- elif "ios_edges" in obj.data:
+ elif "ios_edges" in mesh:
bm = bmesh.new()
- bm.from_mesh(obj.data)
- edges_to_keep = set(map(frozenset, obj.data["ios_edges"]))
+ bm.from_mesh(mesh)
+ edges_to_keep = set(map(frozenset, mesh["ios_edges"]))
edges_to_dissolve = []
for edge in bm.edges:
if frozenset([vert.index for vert in edge.verts]) not in edges_to_keep:
edges_to_dissolve.append(edge)
bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve)
- bm.to_mesh(obj.data)
+ bm.to_mesh(mesh)
bm.free()
- del obj.data["ios_edges"]
+ del mesh["ios_edges"]
@classmethod
def apply_item_ids_as_vertex_groups(cls, obj: bpy.types.Object) -> None:
@@ -486,13 +514,19 @@ class Geometry(bonsai.core.tool.Geometry):
@classmethod
def get_active_representation(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
""":return: IfcRepresentation/IfcRepresentationItem or None"""
- if obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.ifc_definition_id:
- return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ if (
+ (data := obj.data)
+ and isinstance(data, Geometry.TYPES_WITH_MESH_PROPERTIES)
+ and (ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id)
+ ):
+ return tool.Ifc.get().by_id(ifc_id)
@classmethod
- def get_data_representation(cls, data: bpy.types.Mesh) -> ifcopenshell.entity_instance | None:
- if hasattr(data, "BIMMeshProperties") and data.BIMMeshProperties.ifc_definition_id:
- return tool.Ifc.get().by_id(data.BIMMeshProperties.ifc_definition_id)
+ def get_data_representation(cls, data: bpy.types.ID) -> ifcopenshell.entity_instance | None:
+ if isinstance(data, Geometry.TYPES_WITH_MESH_PROPERTIES) and (
+ ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id
+ ):
+ return tool.Ifc.get().by_id(ifc_id)
@classmethod
def get_active_representation_context(cls, obj: bpy.types.Object) -> ifcopenshell.entity_instance:
@@ -670,13 +704,13 @@ class Geometry(bonsai.core.tool.Geometry):
return data.users != 0
@classmethod
- def has_geometric_data(cls, obj: bpy.types.Object) -> bool:
- if not obj.data:
+ def is_geometric_data(cls, data: Union[bpy.types.ID, None]) -> TypeGuard[Union[bpy.types.Mesh, bpy.types.Curve]]:
+ if not data:
return False
- if isinstance(obj.data, bpy.types.Mesh):
- return bool(obj.data.vertices)
- elif isinstance(obj.data, bpy.types.Curve):
- return bool(obj.data.splines)
+ if isinstance(data, bpy.types.Mesh):
+ return bool(data.vertices)
+ elif isinstance(data, bpy.types.Curve):
+ return bool(data.splines)
return False
@classmethod
@@ -826,7 +860,8 @@ class Geometry(bonsai.core.tool.Geometry):
ifc_importer.material_creator.load_existing_materials()
shape_has_openings = cls.does_shape_has_openings(shape)
ifc_importer.material_creator.create(element, obj, mesh, shape_has_openings)
- mesh.BIMMeshProperties.has_openings_applied = apply_openings
+ mprops = tool.Geometry.get_mesh_props(mesh)
+ mprops.has_openings_applied = apply_openings
if not shape_has_openings:
tool.Loader.load_indexed_colour_map(representation, mesh)
tool.Loader.link_mesh(shape, mesh)
@@ -852,7 +887,8 @@ class Geometry(bonsai.core.tool.Geometry):
ifc_importer.material_creator.load_existing_materials()
shape_has_openings = False
ifc_importer.material_creator.create(element, obj, mesh, shape_has_openings)
- mesh.BIMMeshProperties.has_openings_applied = apply_openings
+ mprops = tool.Geometry.get_mesh_props(mesh)
+ mprops.has_openings_applied = apply_openings
if not shape_has_openings:
tool.Loader.load_indexed_colour_map(representation, mesh)
meshes[mesh_name] = mesh
@@ -867,7 +903,7 @@ class Geometry(bonsai.core.tool.Geometry):
@classmethod
def import_representation_parameters(cls, data: bpy.types.Mesh) -> None:
- props = data.BIMMeshProperties
+ props = tool.Geometry.get_mesh_props(data)
elements = tool.Ifc.get().traverse(tool.Ifc.get().by_id(props.ifc_definition_id))
props.ifc_parameters.clear()
for element in elements:
@@ -974,7 +1010,8 @@ class Geometry(bonsai.core.tool.Geometry):
@classmethod
def is_profile_based(cls, data: bpy.types.Mesh) -> bool:
- return data.BIMMeshProperties.subshape_type == "PROFILE"
+ props = tool.Geometry.get_mesh_props(data)
+ return props.subshape_type == "PROFILE"
@classmethod
def is_profile_object_active(cls) -> bool:
@@ -992,7 +1029,7 @@ class Geometry(bonsai.core.tool.Geometry):
data = obj.data
if (
isinstance(data, Geometry.TYPES_WITH_MESH_PROPERTIES)
- and (ifc_id := data.BIMMeshProperties.ifc_definition_id)
+ and (ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id)
and ((item := tool.Ifc.get().by_id(ifc_id)).is_a("IfcRepresentationItem"))
):
return item
@@ -1008,14 +1045,14 @@ class Geometry(bonsai.core.tool.Geometry):
if tool.Ifc.get_entity(obj):
return obj
elif tool.Geometry.is_representation_item(obj):
- return bpy.context.scene.BIMGeometryProperties.representation_obj
+ return tool.Geometry.get_geometry_props().representation_obj
@classmethod
def is_boolean_operand(cls, obj: bpy.types.Object) -> bool:
return bool(
(data := obj.data)
and isinstance(data, Geometry.TYPES_WITH_MESH_PROPERTIES)
- and (ifc_id := data.BIMMeshProperties.ifc_definition_id)
+ and (ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id)
and (item := tool.Ifc.get().by_id(ifc_id))
and (
item.is_a("IfcBooleanResult")
@@ -1041,7 +1078,8 @@ class Geometry(bonsai.core.tool.Geometry):
@classmethod
def record_object_materials(cls, obj: bpy.types.Object) -> None:
- obj.data.BIMMeshProperties.material_checksum = cls.get_material_checksum(obj)
+ props = tool.Geometry.get_mesh_props(obj.data)
+ props.material_checksum = cls.get_material_checksum(obj)
@classmethod
def record_object_position(cls, obj: bpy.types.Object) -> None:
@@ -1146,11 +1184,13 @@ class Geometry(bonsai.core.tool.Geometry):
@classmethod
def should_force_faceted_brep(cls) -> bool:
- return bpy.context.scene.BIMGeometryProperties.should_force_faceted_brep
+ props = tool.Geometry.get_geometry_props()
+ return props.should_force_faceted_brep
@classmethod
def should_force_triangulation(cls) -> bool:
- return bpy.context.scene.BIMGeometryProperties.should_force_triangulation
+ props = tool.Geometry.get_geometry_props()
+ return props.should_force_triangulation
@classmethod
def should_generate_uvs(cls, obj: bpy.types.Object) -> bool:
@@ -1167,7 +1207,8 @@ class Geometry(bonsai.core.tool.Geometry):
@classmethod
def should_use_presentation_style_assignment(cls) -> bool:
- return bpy.context.scene.BIMGeometryProperties.should_use_presentation_style_assignment
+ props = tool.Geometry.get_geometry_props()
+ return props.should_use_presentation_style_assignment
@classmethod
def get_model_representations(cls) -> list[ifcopenshell.entity_instance]:
@@ -1238,7 +1279,8 @@ class Geometry(bonsai.core.tool.Geometry):
In the most cases just use reload_representation
as it will handle those complications by itself.
"""
- representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ representation = cls.get_active_representation(obj)
+ assert representation
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
@@ -1526,7 +1568,7 @@ class Geometry(bonsai.core.tool.Geometry):
@classmethod
def get_blender_offset_type(cls, obj: bpy.types.Object) -> Optional[str]:
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
if (result := obj.BIMObjectProperties.blender_offset_type) == "NONE":
result = obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT"
@@ -1641,7 +1683,8 @@ class Geometry(bonsai.core.tool.Geometry):
for item_obj in props.item_objs:
if not (obj := item_obj.obj) or not tool.Ifc.is_moved(obj):
continue
- item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ item = cls.get_active_representation(obj)
+ assert item
if item.is_a("IfcSweptAreaSolid"):
has_changed = True
old_position = item.Position
@@ -1683,7 +1726,7 @@ class Geometry(bonsai.core.tool.Geometry):
@classmethod
def import_item_attributes(cls, obj: bpy.types.Object) -> None:
- props = obj.data.BIMMeshProperties
+ props = tool.Geometry.get_mesh_props(obj.data)
props.item_attributes.clear()
item = tool.Ifc.get().by_id(props.ifc_definition_id)
allowed_attributes = [
@@ -1710,10 +1753,10 @@ class Geometry(bonsai.core.tool.Geometry):
@classmethod
def update_item_attributes(cls, obj: bpy.types.Object) -> None:
- props = obj.data.BIMMeshProperties
+ props = tool.Geometry.get_mesh_props(obj.data)
ifc_file = tool.Ifc.get()
- item = tool.Ifc.get().by_id(props.ifc_definition_id)
+ item = ifc_file.by_id(props.ifc_definition_id)
for attribute in props.item_attributes:
setattr(item, attribute.name, attribute.get_value())
@@ -1738,7 +1781,9 @@ class Geometry(bonsai.core.tool.Geometry):
tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(tool.Ifc.get())
tool.Loader.settings.context_settings = tool.Loader.create_settings()
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
- item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ assert isinstance(obj.data, bpy.types.Mesh)
+ item = tool.Geometry.get_active_representation(obj)
+ assert item
obj.data.clear_geometry()
if item.is_a("IfcHalfSpaceSolid"):
@@ -1802,12 +1847,14 @@ class Geometry(bonsai.core.tool.Geometry):
props.mode = "OBJECT"
props.is_changing_mode = False
props.representation_obj = None
- bpy.context.scene.BIMBooleanProperties.is_editing = False
+ tool.Feature.get_boolean_props().is_editing = False
@classmethod
def edit_meshlike_item(cls, obj: bpy.types.Object) -> None:
- item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
- if obj.data.BIMMeshProperties.mesh_checksum == cls.get_mesh_checksum(obj.data):
+ item = tool.Geometry.get_active_representation(obj)
+ assert item
+ mprops = tool.Geometry.get_mesh_props(obj.data)
+ if mprops.mesh_checksum == cls.get_mesh_checksum(obj.data):
return
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
@@ -1830,8 +1877,8 @@ class Geometry(bonsai.core.tool.Geometry):
for inverse in tool.Ifc.get().get_inverse(item):
ifcopenshell.util.element.replace_attribute(inverse, item, new_item)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item)
- obj.data.BIMMeshProperties.ifc_definition_id = new_item.id()
- cls.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
+ tool.Ifc.link(new_item, obj.data)
+ cls.reload_representation(props.representation_obj)
@classmethod
def split_by_loose_parts(cls, obj: bpy.types.Object) -> List[bpy.types.Mesh]:
diff --git a/src/bonsai/bonsai/tool/georeference.py b/src/bonsai/bonsai/tool/georeference.py
index 9cd80ce993..7b8ee46d24 100644
--- a/src/bonsai/bonsai/tool/georeference.py
+++ b/src/bonsai/bonsai/tool/georeference.py
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+from __future__ import annotations
import bpy
import json
import numpy as np
@@ -27,24 +28,34 @@ import ifcopenshell.util.unit
import bonsai.core.tool
import bonsai.tool as tool
import bonsai.bim.helper
-from typing import Any, Union, Literal
+from typing import Any, Union, Literal, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from bonsai.bim.module.georeference.prop import BIMGeoreferenceProperties
class Georeference(bonsai.core.tool.Georeference):
COORDINATE_TYPE = Literal["blender", "local", "map"]
+ @classmethod
+ def get_georeference_props(cls) -> BIMGeoreferenceProperties:
+ return bpy.context.scene.BIMGeoreferenceProperties
+
@classmethod
def add_georeferencing(cls) -> None:
+ props = cls.get_georeference_props()
tool.Ifc.run(
"georeference.add_georeferencing",
- ifc_class=bpy.context.scene.BIMGeoreferenceProperties.coordinate_operation_class,
+ ifc_class=props.coordinate_operation_class,
)
@classmethod
def import_projected_crs(cls) -> None:
+ props = tool.Georeference.get_georeference_props()
+
def callback(name, prop, data):
if name == "MapUnit":
- new = bpy.context.scene.BIMGeoreferenceProperties.projected_crs.add()
+ new = props.projected_crs.add()
new.name = name
new.data_type = "enum"
new.is_null = data[name] is None
@@ -58,9 +69,9 @@ class Georeference(bonsai.core.tool.Georeference):
)
if data["MapUnit"]:
new.enum_value = str(data["MapUnit"].id())
+ new.update = "tool.Georeference.update_map_unit"
return True
- props = bpy.context.scene.BIMGeoreferenceProperties
props.projected_crs.clear()
if tool.Ifc.get_schema() == "IFC2X3":
@@ -72,11 +83,27 @@ class Georeference(bonsai.core.tool.Georeference):
bonsai.bim.helper.import_attributes2(projected_crs, props.projected_crs, callback=callback)
return
+ @classmethod
+ def update_map_unit(cls, self, context) -> None:
+ if unit_id := self.get_value():
+ map_unit = tool.Ifc.get().by_id(int(unit_id))
+ project_unit = ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT")
+ if map_unit and project_unit:
+ result = ifcopenshell.util.unit.convert_unit(1, project_unit, map_unit)
+ else:
+ result = 1.0
+ else:
+ result = 1.0
+ props = cls.get_georeference_props()
+ for attribute in props.coordinate_operation:
+ if attribute.name == "Scale":
+ attribute.set_value(str(result))
+
@classmethod
def import_coordinate_operation(cls) -> None:
def callback(name, prop, data):
if name in ("FirstCoordinate", "SecondCoordinate"):
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = cls.get_georeference_props()
if name == "FirstCoordinate":
new = props.coordinate_operation.add()
new.name = "Measure Type"
@@ -94,7 +121,7 @@ class Georeference(bonsai.core.tool.Georeference):
prop.string_value = "" if prop.is_null else str(data[name].wrappedValue)
return True
elif name == "XAxisAbscissa":
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = cls.get_georeference_props()
props.is_changing_angle = True
if data["XAxisAbscissa"] is None or data["XAxisOrdinate"] is None:
props.x_axis_is_null = True
@@ -116,7 +143,7 @@ class Georeference(bonsai.core.tool.Georeference):
prop.string_value = "" if prop.is_null else str(data[name])
return True
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = cls.get_georeference_props()
props.coordinate_operation.clear()
if tool.Ifc.get_schema() == "IFC2X3":
@@ -135,7 +162,7 @@ class Georeference(bonsai.core.tool.Georeference):
if tool.Ifc.get_schema() == "IFC2X3":
return
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = cls.get_georeference_props()
props.is_changing_angle = True
props.true_north_abscissa = "0"
props.true_north_ordinate = "1"
@@ -159,7 +186,7 @@ class Georeference(bonsai.core.tool.Georeference):
attributes[prop.name] = tool.Ifc.get().by_id(int(prop.enum_value))
return True
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = cls.get_georeference_props()
return bonsai.bim.helper.export_attributes(props.projected_crs, callback=callback)
@classmethod
@@ -175,7 +202,7 @@ class Georeference(bonsai.core.tool.Georeference):
attributes[prop.name] = tool.Ifc.get().create_entity(measure_type, float(prop.string_value))
return True
elif prop.name == "XAxisAbscissa":
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = cls.get_georeference_props()
if props.x_axis_is_null:
attributes["XAxisAbscissa"] = None
attributes["XAxisOrdinate"] = None
@@ -190,12 +217,12 @@ class Georeference(bonsai.core.tool.Georeference):
attributes[prop.name] = float(prop.string_value)
return True
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = cls.get_georeference_props()
return bonsai.bim.helper.export_attributes(props.coordinate_operation, callback=callback)
@classmethod
def get_true_north_attributes(cls) -> Union[list[float], None]:
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = cls.get_georeference_props()
try:
return [float(props.true_north_abscissa), float(props.true_north_ordinate)]
except ValueError:
@@ -203,36 +230,42 @@ class Georeference(bonsai.core.tool.Georeference):
@classmethod
def enable_editing(cls) -> None:
- bpy.context.scene.BIMGeoreferenceProperties.is_editing = True
+ props = cls.get_georeference_props()
+ props.is_editing = True
@classmethod
def disable_editing(cls) -> None:
- bpy.context.scene.BIMGeoreferenceProperties.is_editing = False
+ props = cls.get_georeference_props()
+ props.is_editing = False
@classmethod
def enable_editing_wcs(cls) -> None:
- bpy.context.scene.BIMGeoreferenceProperties.is_editing_wcs = True
+ props = cls.get_georeference_props()
+ props.is_editing_wcs = True
@classmethod
def disable_editing_wcs(cls) -> None:
- bpy.context.scene.BIMGeoreferenceProperties.is_editing_wcs = False
+ props = cls.get_georeference_props()
+ props.is_editing_wcs = False
@classmethod
def enable_editing_true_north(cls) -> None:
- bpy.context.scene.BIMGeoreferenceProperties.is_editing_true_north = True
+ props = cls.get_georeference_props()
+ props.is_editing_true_north = True
@classmethod
def disable_editing_true_north(cls) -> None:
- bpy.context.scene.BIMGeoreferenceProperties.is_editing_true_north = False
+ props = cls.get_georeference_props()
+ props.is_editing_true_north = False
@classmethod
def set_coordinates(cls, io: COORDINATE_TYPE, coordinates: list[float]) -> None:
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = cls.get_georeference_props()
setattr(props, f"{io}_coordinates", ",".join([str(o) for o in coordinates]))
@classmethod
def get_coordinates(cls, io: COORDINATE_TYPE) -> list[float]:
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = cls.get_georeference_props()
return [float(co) for co in getattr(props, f"{io}_coordinates").split(",")]
@classmethod
@@ -244,7 +277,7 @@ class Georeference(bonsai.core.tool.Georeference):
def xyz2enh(
cls, coordinates: tuple[float, float, float], should_return_in_map_units: bool = True
) -> tuple[float, float, float]:
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = cls.get_georeference_props()
if props.has_blender_offset:
coordinates = ifcopenshell.util.geolocation.xyz2enh(
coordinates[0],
@@ -263,7 +296,7 @@ class Georeference(bonsai.core.tool.Georeference):
@classmethod
def enh2xyz(cls, coordinates: tuple[float, float, float]) -> tuple[float, float, float]:
coordinates = ifcopenshell.util.geolocation.auto_enh2xyz(tool.Ifc.get(), *coordinates)
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = cls.get_georeference_props()
if props.has_blender_offset:
coordinates = ifcopenshell.util.geolocation.enh2xyz(
coordinates[0],
@@ -313,7 +346,7 @@ class Georeference(bonsai.core.tool.Georeference):
@classmethod
def import_wcs(cls) -> None:
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = cls.get_georeference_props()
wcs = None
for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
wcs = context.WorldCoordinateSystem
@@ -330,7 +363,7 @@ class Georeference(bonsai.core.tool.Georeference):
@classmethod
def export_wcs(cls) -> dict[str, float]:
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = cls.get_georeference_props()
return {
"x": float(props.wcs_x),
"y": float(props.wcs_y),
@@ -345,7 +378,7 @@ class Georeference(bonsai.core.tool.Georeference):
@classmethod
def set_model_origin(cls) -> None:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
- gprops = bpy.context.scene.BIMGeoreferenceProperties
+ gprops = tool.Georeference.get_georeference_props()
e, n, h = cls.xyz2enh((0, 0, 0), should_return_in_map_units=False)
gprops.model_origin = f"{e},{n},{h}"
gprops.model_origin_si = f"{e * unit_scale},{n * unit_scale},{h * unit_scale}"
@@ -359,4 +392,4 @@ class Georeference(bonsai.core.tool.Georeference):
@classmethod
def has_blender_offset(cls) -> bool:
- return bpy.context.scene.BIMGeoreferenceProperties.has_blender_offset
+ return tool.Georeference.get_georeference_props().has_blender_offset
diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py
index 5b3f8bb48a..ce9f856f80 100644
--- a/src/bonsai/bonsai/tool/ifc.py
+++ b/src/bonsai/bonsai/tool/ifc.py
@@ -111,7 +111,7 @@ class Ifc(bonsai.core.tool.Ifc):
elif isinstance(obj, bpy.types.Material):
props = obj.BIMStyleProperties
else:
- props = obj.BIMMeshProperties
+ props = tool.Geometry.get_mesh_props(obj)
if props and (ifc_definition_id := props.ifc_definition_id):
try:
@@ -129,9 +129,13 @@ class Ifc(bonsai.core.tool.Ifc):
return None
@classmethod
- def get_object(cls, element: ifcopenshell.entity_instance) -> IFC_CONNECTED_TYPE:
+ def get_object(cls, element: ifcopenshell.entity_instance) -> Union[IFC_CONNECTED_TYPE, None]:
return IfcStore.get_element(element.id())
+ @classmethod
+ def get_object_by_identifier(cls, id_or_guid: Union[int, str]) -> Union[IFC_CONNECTED_TYPE, None]:
+ return IfcStore.get_element(id_or_guid)
+
@classmethod
def rebuild_element_maps(cls) -> None:
"""Rebuilds the id_map and guid_map
@@ -180,7 +184,7 @@ class Ifc(bonsai.core.tool.Ifc):
cls.setup_listeners(obj)
IfcStore.edited_objs = set()
- edited_objs = bpy.context.scene.BIMProjectProperties.edited_objs
+ edited_objs = tool.Project.get_project_props().edited_objs
for i in range(len(edited_objs))[::-1]:
obj = edited_objs[i].obj
if obj:
@@ -220,7 +224,7 @@ class Ifc(bonsai.core.tool.Ifc):
"""
if obj in IfcStore.edited_objs:
return
- edited_objs = bpy.context.scene.BIMProjectProperties.edited_objs
+ edited_objs = tool.Project.get_project_props().edited_objs
edited_objs.add().obj = obj
IfcStore.edited_objs.add(obj)
IfcStore.history_edit_object(obj, finish_editing=False)
@@ -233,7 +237,7 @@ class Ifc(bonsai.core.tool.Ifc):
"""
if obj not in IfcStore.edited_objs:
return
- edited_objs = bpy.context.scene.BIMProjectProperties.edited_objs
+ edited_objs = tool.Project.get_project_props().edited_objs
edited_objs.remove(next(i for i, o in enumerate(edited_objs) if o.obj == obj))
IfcStore.edited_objs.discard(obj)
IfcStore.history_edit_object(obj, finish_editing=True)
diff --git a/src/bonsai/bonsai/tool/ifcgit.py b/src/bonsai/bonsai/tool/ifcgit.py
index 5b0f9b88e5..93475c090c 100644
--- a/src/bonsai/bonsai/tool/ifcgit.py
+++ b/src/bonsai/bonsai/tool/ifcgit.py
@@ -340,8 +340,8 @@ class IfcGit:
@classmethod
def get_revisions_step_ids(cls) -> Union[STEP_IDS, None]:
-
- path_ifc = bpy.data.scenes["Scene"].BIMProperties.ifc_file
+ props = tool.Blender.get_bim_props()
+ path_ifc = tool.Blender.get_bim_props().ifc_file
props = bpy.context.scene.IfcGitProperties
repo = IfcGitRepo.repo
item = props.ifcgit_commits[props.commit_index]
diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py
index f79f4b4509..7fb5fc4788 100644
--- a/src/bonsai/bonsai/tool/loader.py
+++ b/src/bonsai/bonsai/tool/loader.py
@@ -89,7 +89,7 @@ class Loader(bonsai.core.tool.Loader):
@classmethod
def get_mesh_name(cls, representation: ifcopenshell.entity_instance) -> str:
- context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0
+ context_id = context.id() if (context := getattr(representation, "ContextOfItems", None)) else 0
return "{}/{}".format(context_id, representation.id())
@classmethod
@@ -105,7 +105,7 @@ class Loader(bonsai.core.tool.Loader):
mesh: tool.Geometry.TYPES_WITH_MESH_PROPERTIES,
) -> None:
geometry = shape.geometry if hasattr(shape, "geometry") else shape
- mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id.split("-")[0])
+ tool.Geometry.get_mesh_props(mesh).ifc_definition_id = int(geometry.id.split("-")[0])
@classmethod
def create_surface_style_shading(
@@ -698,7 +698,7 @@ class Loader(bonsai.core.tool.Loader):
project_north = 0
if has_offset or has_rotation:
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
props.blender_offset_x = str(model_offset[0])
props.blender_offset_y = str(model_offset[1])
props.blender_offset_z = str(model_offset[2])
@@ -747,7 +747,7 @@ class Loader(bonsai.core.tool.Loader):
cls, element: ifcopenshell.entity_instance, is_gross: bool = False
) -> Union[ifcopenshell.geom.ShapeElementType, None]:
context_settings = cls.settings.gross_context_settings if is_gross else cls.settings.context_settings
- geometry_library = bpy.context.scene.BIMProjectProperties.geometry_library
+ geometry_library = tool.Project.get_project_props().geometry_library
for settings in context_settings:
try:
result = ifcopenshell.geom.create_shape(settings, element, geometry_library=geometry_library)
@@ -952,7 +952,7 @@ class Loader(bonsai.core.tool.Loader):
matrix[1][3] = offset_xyz[1]
matrix[2][3] = offset_xyz[2]
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if props.has_blender_offset:
if obj.BIMObjectProperties.blender_offset_type == "NONE":
obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT"
@@ -1018,6 +1018,74 @@ class Loader(bonsai.core.tool.Loader):
mesh["ios_material_ids"] = ifcopenshell.util.shape.get_faces_material_style_ids(geometry).tolist()
return mesh
+ @classmethod
+ def slice_layerset_mesh(cls, element: ifcopenshell.entity_instance, mesh: bpy.types.Mesh) -> bpy.types.Mesh:
+ if True: # This feature is still experimental
+ return mesh
+ if not (material := ifcopenshell.util.element.get_material(element)):
+ return mesh
+ elif material.is_a("IfcMaterialLayerSetUsage"):
+ usage = material
+ layer_set = material.ForLayerSet
+ offset = usage.OffsetFromReferenceLine * cls.unit_scale
+ sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
+ elif material.is_a("IfcMaterialLayerSet"):
+ usage = None
+ layer_set = material
+ offset = 0
+ sense_factor = 1
+ else:
+ return mesh
+ if len(layer_set.MaterialLayers) == 1:
+ return mesh
+ bm = bmesh.new()
+ bm.from_mesh(mesh)
+ prev_co = None
+ co = Vector((0.0, offset, 0.0))
+ no = Vector((0.0, 1.0, 0.0))
+ # Cache this
+ body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
+ styles = {}
+ has_layer_styles = False
+ for i, material in mesh.materials:
+ if style := tool.Ifc.get_entity(material):
+ styles[style] = i
+ for layer in layer_set.MaterialLayers[:-1]:
+ prev_co = co.copy()
+ co.y = layer.LayerThickness * cls.unit_scale * sense_factor
+ bisect_geom = bmesh.ops.bisect_plane(
+ bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no
+ )
+ bmesh.ops.duplicate(bm, geom=bisect_geom["geom_cut"])
+ if style := ifcopenshell.util.representation.get_material_style(layer.Material, body):
+ if (material_index := styles.get(style, None)) is None:
+ material_index = len(mesh.materials)
+ mesh.materials.append(tool.Ifc.get_object(style))
+ for face in bisect_geom["geom"]:
+ if isinstance(face, bmesh.types.BMFace):
+ center = face.calc_center_bounds() * sense_factor
+ if center.y < co.y and center.y > prev_co.y:
+ face.material_index = material_index
+ has_layer_styles = True
+
+ # Last layer
+ layer = layer_set.MaterialLayers[-1]
+ if style := ifcopenshell.util.representation.get_material_style(layer.Material, body):
+ if (material_index := styles.get(style, None)) is None:
+ material_index = len(mesh.materials)
+ mesh.materials.append(tool.Ifc.get_object(style))
+ for face in bisect_geom["geom"]:
+ if isinstance(face, bmesh.types.BMFace):
+ center = face.calc_center_bounds() * sense_factor
+ if center.y > co.y:
+ face.material_index = material_index
+ has_layer_styles = True
+
+ bm.to_mesh(mesh)
+ bm.free()
+ mesh["has_layer_styles"] = has_layer_styles
+ return mesh
+
@classmethod
def create_mesh_from_shape(
cls,
diff --git a/src/bonsai/bonsai/tool/misc.py b/src/bonsai/bonsai/tool/misc.py
index 87d367895b..db43a14c5d 100644
--- a/src/bonsai/bonsai/tool/misc.py
+++ b/src/bonsai/bonsai/tool/misc.py
@@ -113,10 +113,11 @@ class Misc(bonsai.core.tool.Misc):
new_objs = []
for obj in objs:
- if obj.type != "MESH" or obj == cutter:
+ mesh = obj.data
+ if not isinstance(mesh, bpy.types.Mesh) or obj == cutter:
continue
new_obj = obj.copy()
- new_obj.data = obj.data.copy()
+ new_obj.data = mesh.copy()
for collection in obj.users_collection:
collection.objects.link(new_obj)
diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py
index 6db9718d4d..e55798602f 100644
--- a/src/bonsai/bonsai/tool/model.py
+++ b/src/bonsai/bonsai/tool/model.py
@@ -277,7 +277,7 @@ class Model(bonsai.core.tool.Model):
mesh = bpy.data.meshes.new("Axis")
mesh.from_pydata(cls.vertices, cls.edges, [])
- mesh.BIMMeshProperties.subshape_type = "AXIS"
+ tool.Geometry.get_mesh_props(mesh).subshape_type = "AXIS"
if obj is None:
obj = bpy.data.objects.new("Axis", mesh)
@@ -334,7 +334,7 @@ class Model(bonsai.core.tool.Model):
mesh = bpy.data.meshes.new("Profile")
mesh.from_pydata(cls.vertices, cls.edges, [])
- mesh.BIMMeshProperties.subshape_type = "PROFILE"
+ tool.Geometry.get_mesh_props(mesh).subshape_type = "PROFILE"
if obj is None:
obj = bpy.data.objects.new("Profile", mesh)
@@ -376,7 +376,7 @@ class Model(bonsai.core.tool.Model):
mesh = bpy.data.meshes.new("Curve")
mesh.from_pydata(cls.vertices, cls.edges, [])
- mesh.BIMMeshProperties.subshape_type = "PROFILE"
+ tool.Geometry.get_mesh_props(mesh).subshape_type = "PROFILE"
if obj is None:
obj = bpy.data.objects.new("Curve", mesh)
@@ -417,7 +417,7 @@ class Model(bonsai.core.tool.Model):
mesh = bpy.data.meshes.new("Surface")
mesh.from_pydata(cls.vertices, cls.edges, [])
- mesh.BIMMeshProperties.subshape_type = "PROFILE"
+ tool.Geometry.get_mesh_props(mesh).subshape_type = "PROFILE"
if obj is None:
obj = bpy.data.objects.new("Surface", mesh)
@@ -569,7 +569,10 @@ class Model(bonsai.core.tool.Model):
element: Optional[ifcopenshell.entity_instance] = None,
representation: Optional[ifcopenshell.entity_instance] = None,
) -> list[ifcopenshell.entity_instance]:
+ """Either element or representation must be provided."""
+ assert element or representation, "Either element or representation must be provided."
if representation is None:
+ assert element
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
return []
@@ -1461,9 +1464,9 @@ class Model(bonsai.core.tool.Model):
after material assignment or material unassignment.
"""
for element in elements:
- if not (obj := tool.Ifc.get_object(element)) or not obj.data:
+ if not (obj := tool.Ifc.get_object(element)) or not (data := obj.data):
continue
- representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ representation = tool.Ifc.get().by_id(tool.Geometry.get_mesh_props(data).ifc_definition_id)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
@@ -1931,7 +1934,9 @@ class Model(bonsai.core.tool.Model):
or it's not referring to an object (e.g. potential boolean object)."""
if obj.type != "MESH":
return
- return obj.data.BIMMeshProperties.obj
+ mesh = obj.data
+ assert isinstance(mesh, bpy.types.Mesh)
+ return tool.Geometry.get_mesh_props(mesh).obj
@classmethod
def get_tracked_opening_type(cls, obj: bpy.types.Object) -> Union[Literal["OPENING", "BOOLEAN"], None]:
diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py
index 00fbad7483..b5c9ad4232 100644
--- a/src/bonsai/bonsai/tool/polyline.py
+++ b/src/bonsai/bonsai/tool/polyline.py
@@ -20,6 +20,7 @@ import bpy
import bmesh
import math
import ifcopenshell
+import ifcopenshell.util.unit
import bonsai.core.tool
import bonsai.tool as tool
from bonsai.bim.module.drawing.helper import format_distance
@@ -266,10 +267,20 @@ class Polyline(bonsai.core.tool.Polyline):
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
- if tool_state.use_default_container:
- snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
+ if tool_state.is_input_on:
+ if tool_state.use_default_container:
+ mouse_vector = Vector(
+ (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), default_container_elevation)
+ )
+ else:
+ mouse_vector = Vector(
+ (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), input_ui.get_number_value("Z"))
+ )
else:
- snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
+ if tool_state.use_default_container:
+ snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
+ else:
+ snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
if len(polyline_points) > 1:
second_to_last_point_data = polyline_points[len(polyline_points) - 2]
@@ -452,13 +463,17 @@ class Polyline(bonsai.core.tool.Polyline):
def format_input_ui_units(cls, value: float, is_area: bool = False) -> str:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
if bpy.context.scene.unit_settings.system == "IMPERIAL":
- precision = bpy.context.scene.DocProperties.imperial_precision
+ dprops = tool.Drawing.get_document_props()
+ precision = dprops.imperial_precision
+ if is_area:
+ props = tool.Blender.get_bim_props()
+ area_unit = props.area_unit
+ unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get(), unit_type=area_unit)
else:
precision = None
- value = value if is_area else value / unit_scale
return format_distance(
- value,
+ value / unit_scale,
precision=precision,
hide_units=False,
isArea=is_area,
@@ -506,9 +521,13 @@ class Polyline(bonsai.core.tool.Polyline):
for point in polyline_points[1:]: # The first can be repeated to form a wall loop
if (x, y, z) == (point.x, point.y, point.z):
return "Cannot create two points at the same location"
- # Avoids duplicating an edge
+ # Avoids creating overlapping edges
if len(polyline_points) > 1:
- if Vector((x, y, z)) == Vector((polyline_points[-2].x, polyline_points[-2].y, polyline_points[-2].z)):
+ v1 = Vector((x, y, z))
+ v2 = Vector((polyline_points[-1].x, polyline_points[-1].y, polyline_points[-1].z))
+ v3 = Vector((polyline_points[-2].x, polyline_points[-2].y, polyline_points[-2].z))
+ angle = tool.Cad.angle_3_vectors(v1, v2, v3, new_angle=None, degrees=True)
+ if tool.Cad.is_x(angle, 0):
return
# TODO move this limitation to be Wall tool specific. Right now it also affects Measure tool
# Avoids creating segments smaller then 0.1. This is a limitation from create_wall_from_2_points
diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py
index 7b529cdea9..4741e16a59 100644
--- a/src/bonsai/bonsai/tool/project.py
+++ b/src/bonsai/bonsai/tool/project.py
@@ -70,7 +70,8 @@ class Project(bonsai.core.tool.Project):
@classmethod
def load_pset_templates(cls):
- pset_dir = tool.Ifc.resolve_uri(bpy.context.scene.BIMProperties.pset_dir)
+ props = tool.Blender.get_bim_props()
+ pset_dir = tool.Ifc.resolve_uri(props.pset_dir)
if os.path.isdir(pset_dir):
for path in Path(pset_dir).glob("*.ifc"):
bonsai.bim.schema.ifc.psetqto.templates.append(ifcopenshell.open(path))
@@ -136,13 +137,15 @@ class Project(bonsai.core.tool.Project):
@classmethod
def set_context(cls, context):
bonsai.bim.handler.refresh_ui_data()
- bpy.context.scene.BIMRootProperties.contexts = str(context.id())
+ rprops = tool.Root.get_root_props()
+ rprops.contexts = str(context.id())
@classmethod
def set_default_context(cls):
context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
if context:
- bpy.context.scene.BIMRootProperties.contexts = str(context.id())
+ rprops = tool.Root.get_root_props()
+ rprops.contexts = str(context.id())
@classmethod
def set_default_modeling_dimensions(cls):
@@ -235,7 +238,7 @@ class Project(bonsai.core.tool.Project):
@classmethod
def load_linked_models_from_ifc(cls) -> None:
- links = bpy.context.scene.BIMProjectProperties.links
+ links = tool.Project.get_project_props().links
links.clear()
links_document = cls.get_linked_models_document()
if not links_document:
@@ -252,7 +255,7 @@ class Project(bonsai.core.tool.Project):
@classmethod
def save_linked_models_to_ifc(cls) -> None:
ifc_file = tool.Ifc.get()
- links = bpy.context.scene.BIMProjectProperties.links
+ links = tool.Project.get_project_props().links
filepaths: set[Path] = set()
for link in links:
filepaths.add(Path(link.name))
@@ -372,8 +375,12 @@ class Project(bonsai.core.tool.Project):
props = cls.get_project_props()
for project_library in libraries:
library_elements = tool.Project.get_project_library_elements(project_library)
+ subhierarchy = libraries[project_library]
+ for sublibrary in subhierarchy:
+ sublibrary_elements = tool.Project.get_project_library_elements(sublibrary)
+ library_elements.update(sublibrary_elements)
props.add_library_project_library(
- project_library.Name or "Unnamed", len(library_elements), project_library.id()
+ project_library.Name or "Unnamed", len(library_elements), project_library.id(), bool(subhierarchy)
)
@classmethod
diff --git a/src/bonsai/bonsai/tool/pset_template.py b/src/bonsai/bonsai/tool/pset_template.py
index e2b69705b6..8977d1e32c 100644
--- a/src/bonsai/bonsai/tool/pset_template.py
+++ b/src/bonsai/bonsai/tool/pset_template.py
@@ -133,7 +133,8 @@ class PsetTemplate(bonsai.core.tool.PsetTemplate):
for f in tool.Blender.get_data_dir_paths("pset", "*.ifc"):
paths.append((f, "Global Pset Template"))
- pset_dir = Path(tool.Ifc.resolve_uri(bpy.context.scene.BIMProperties.pset_dir))
+ props = tool.Blender.get_bim_props()
+ pset_dir = Path(tool.Ifc.resolve_uri(props.pset_dir))
if pset_dir.is_dir():
for path in Path(pset_dir).glob("*.ifc"):
paths.append((path, "Project Pset Template"))
diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py
index b18e649338..6e92d8cf5a 100644
--- a/src/bonsai/bonsai/tool/root.py
+++ b/src/bonsai/bonsai/tool/root.py
@@ -16,9 +16,11 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+from __future__ import annotations
import bpy
import ifcopenshell
import ifcopenshell.api
+import ifcopenshell.api.style
import ifcopenshell.util.representation
import ifcopenshell.util.element
import ifcopenshell.util.placement
@@ -26,12 +28,19 @@ import bonsai.core.tool
import bonsai.core.aggregate
import bonsai.core.geometry
import bonsai.tool as tool
-from typing import Union, Optional, Any, Literal
+from typing import Union, Optional, Any, Literal, TYPE_CHECKING
from bonsai.bim.module.spatial.decorator import GridDecorator
from bonsai.bim.module.geometry.decorator import ItemDecorator
+if TYPE_CHECKING:
+ from bonsai.bim.module.root.prop import BIMRootProperties
+
class Root(bonsai.core.tool.Root):
+ @classmethod
+ def get_root_props(cls) -> BIMRootProperties:
+ return bpy.context.scene.BIMRootProperties
+
@classmethod
def add_tracked_opening(cls, obj: bpy.types.Object, opening_type: Literal["OPENING", "BOOLEAN"]) -> None:
"""Add tracked opening or boolean object."""
@@ -49,12 +58,12 @@ class Root(bonsai.core.tool.Root):
tool.Geometry.run_style_add_style(obj=mat)
for mat in tool.Geometry.get_object_materials_without_styles(obj)
]
- ifcopenshell.api.run(
- "style.assign_representation_styles",
+ props = tool.Geometry.get_geometry_props()
+ ifcopenshell.api.style.assign_representation_styles(
tool.Ifc.get(),
shape_representation=body,
styles=tool.Geometry.get_styles(obj),
- should_use_presentation_style_assignment=bpy.context.scene.BIMGeometryProperties.should_use_presentation_style_assignment,
+ should_use_presentation_style_assignment=props.should_use_presentation_style_assignment,
)
@classmethod
@@ -111,7 +120,7 @@ class Root(bonsai.core.tool.Root):
@classmethod
def get_default_container(cls) -> Optional[ifcopenshell.entity_instance]:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = tool.Spatial.get_spatial_props()
if container := props.default_container:
try:
return tool.Ifc.get().by_id(container)
@@ -169,8 +178,8 @@ class Root(bonsai.core.tool.Root):
@classmethod
def get_object_representation(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
- if obj.data and obj.data.BIMMeshProperties.ifc_definition_id:
- return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
+ if obj.data and (mesh_props := tool.Geometry.get_mesh_props(obj.data)).ifc_definition_id:
+ return tool.Ifc.get().by_id(mesh_props.ifc_definition_id)
element = tool.Ifc.get_entity(obj)
if element.is_a("IfcTypeProduct"):
if element.RepresentationMaps:
@@ -232,7 +241,7 @@ class Root(bonsai.core.tool.Root):
@classmethod
def reload_grid_decorator(cls) -> None:
- axes = bpy.context.scene.BIMGridProperties.grid_axes
+ axes = tool.Spatial.get_grid_props().grid_axes
axes.clear()
for axis in tool.Ifc.get().by_type("IfcGridAxis"):
if obj := tool.Ifc.get_object(axis):
@@ -302,8 +311,8 @@ class Root(bonsai.core.tool.Root):
voided_objs.append(subobj)
for voided_obj in voided_objs:
- if voided_obj.data:
- representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id)
+ if data := voided_obj.data:
+ representation = tool.Ifc.get().by_id(tool.Geometry.get_mesh_props(data).ifc_definition_id)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
@@ -421,10 +430,36 @@ class Root(bonsai.core.tool.Root):
to unlink them.
"""
tool.Ifc.unlink(obj=obj)
- if hasattr(obj.data, "BIMMeshProperties"):
- obj.data.BIMMeshProperties.ifc_definition_id = 0
+ if tool.Geometry.has_mesh_properties((data := obj.data)):
+ tool.Geometry.get_mesh_props(data).ifc_definition_id = 0
for material_slot in obj.material_slots:
if material := material_slot.material:
tool.Ifc.unlink(obj=material)
if "Ifc" in obj.name and "/" in obj.name:
obj.name = obj.name.split("/", 1)[1]
+
+ @classmethod
+ def get_ifc_products(cls) -> tuple[str, ...]:
+ version = tool.Ifc.get_schema()
+ if version == "IFC2X3":
+ products = (
+ "IfcElementType",
+ "IfcElement",
+ "IfcFeatureElement",
+ "IfcSpatialStructureElement",
+ "IfcStructuralItem",
+ "IfcAnnotation",
+ "IfcRelSpaceBoundary",
+ )
+ else:
+ products = (
+ "IfcElementType",
+ "IfcElement",
+ "IfcFeatureElement",
+ "IfcSpatialElement",
+ "IfcSpatialElementType",
+ "IfcStructuralItem",
+ "IfcAnnotation",
+ "IfcRelSpaceBoundary",
+ )
+ return products
diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py
index 3b90961559..a48e1404b6 100644
--- a/src/bonsai/bonsai/tool/search.py
+++ b/src/bonsai/bonsai/tool/search.py
@@ -30,9 +30,14 @@ from typing import Union, Literal, TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.prop import BIMFilterGroup
+ from bonsai.bim.module.search.prop import BIMSearchProperties
class Search(bonsai.core.tool.Search):
+ @classmethod
+ def get_search_props(cls) -> BIMSearchProperties:
+ return bpy.context.scene.BIMSearchProperties
+
@classmethod
def get_group_query(cls, group: ifcopenshell.entity_instance) -> str:
return json.loads(group.Description)["query"]
@@ -40,7 +45,7 @@ class Search(bonsai.core.tool.Search):
@classmethod
def get_filter_groups(cls, module: str) -> bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]:
if module == "search":
- return bpy.context.scene.BIMSearchProperties.filter_groups
+ return cls.get_search_props().filter_groups
elif module == "csv":
return bpy.context.scene.CsvProperties.filter_groups
elif module == "diff":
diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py
index b06d9a65bb..8f89088e63 100644
--- a/src/bonsai/bonsai/tool/snap.py
+++ b/src/bonsai/bonsai/tool/snap.py
@@ -25,6 +25,7 @@ import math
import mathutils
from mathutils import Matrix, Vector
from lark import Lark, Transformer
+from typing import Union
class Snap(bonsai.core.tool.Snap):
@@ -192,7 +193,7 @@ class Snap(bonsai.core.tool.Snap):
rot_intersection = rot_mat @ translated_intersection
proximity = rot_intersection.y
if tool_state.plane_method == "XZ":
- proximity = rot_intersection.z
+ proximity = rot_intersection.x
is_on_rot_axis = abs(proximity) <= stick_factor
if is_on_rot_axis:
@@ -207,10 +208,14 @@ class Snap(bonsai.core.tool.Snap):
# If lock axis is on it will use the snap angle so there is no need to search for eligible axis
if elegible_axis or tool_state.lock_axis:
# Adapt axis to make snap angle work with other plane method
- if tool_state.plane_method == "XZ":
- axis = -axis
- if tool_state.plane_method == "YZ":
- axis = 90 - (axis * -1)
+ if elegible_axis:
+ if tool_state.plane_method == "XZ":
+ axis = 90 - (axis * -1)
+ else:
+ if tool_state.plane_method == "XZ":
+ axis = -axis
+ if tool_state.plane_method == "YZ":
+ axis = 90 - (axis * -1)
rot_mat = Matrix.Rotation(math.radians(360 - axis), 3, pivot_axis)
rot_mat = tool.Polyline.use_transform_orientations(rot_mat)
rot_intersection = rot_mat @ translated_intersection
@@ -313,7 +318,9 @@ class Snap(bonsai.core.tool.Snap):
plane_normal = tool.Polyline.use_transform_orientations(plane_normal)
return plane_origin, plane_normal
- def cast_rays_to_single_object(obj, mouse_pos):
+ def cast_rays_to_single_object(
+ obj: bpy.types.Object, mouse_pos: tuple[int, int]
+ ) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]:
if obj.type != "MESH":
return None, None, None
hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj)
@@ -332,7 +339,9 @@ class Snap(bonsai.core.tool.Snap):
else:
return None, None, None
- def cast_rays_and_get_best_object(objs_to_raycast, mouse_pos):
+ def cast_rays_and_get_best_object(
+ objs_to_raycast: list[bpy.types.Object], mouse_pos: tuple[int, int]
+ ) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]:
best_length_squared = 1.0
best_obj = None
best_hit = None
@@ -568,7 +577,7 @@ class Snap(bonsai.core.tool.Snap):
"object": obj,
}
snaps_by_type.insert(0, snap_point)
- cls.update_snapping_point(snap_point["point"], snap_point["type"])
+ cls.update_snapping_point(snap_point["point"], snap_point["type"])
return snaps_by_type
cls.update_snapping_point(point["point"], point["type"])
return snaps_by_type
diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py
index 5976462dc3..54d4f45f7b 100644
--- a/src/bonsai/bonsai/tool/spatial.py
+++ b/src/bonsai/bonsai/tool/spatial.py
@@ -42,12 +42,31 @@ import numpy as np
from math import pi
from mathutils import Vector, Matrix
from shapely import Polygon
-from typing import Generator, Optional, Union, Literal, List, Any, Iterable
+from typing import Generator, Optional, Union, Literal, List, Any, Iterable, TYPE_CHECKING
from collections import defaultdict
from natsort import natsorted
+if TYPE_CHECKING:
+ from bonsai.bim.module.spatial.prop import (
+ BIMGridProperties,
+ BIMSpatialDecompositionProperties,
+ BIMObjectSpatialProperties,
+ )
+
class Spatial(bonsai.core.tool.Spatial):
+ @classmethod
+ def get_spatial_props(cls) -> BIMSpatialDecompositionProperties:
+ return bpy.context.scene.BIMSpatialDecompositionProperties
+
+ @classmethod
+ def get_object_spatial_props(cls, obj: bpy.types.Object) -> BIMObjectSpatialProperties:
+ return obj.BIMObjectSpatialProperties
+
+ @classmethod
+ def get_grid_props(cls) -> BIMGridProperties:
+ return bpy.context.scene.BIMGridProperties
+
@classmethod
def can_contain(cls, container: ifcopenshell.entity_instance, element_obj: Union[bpy.types.Object, None]) -> bool:
if not (element := tool.Ifc.get_entity(element_obj)):
@@ -82,7 +101,8 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def disable_editing(cls, obj: bpy.types.Object) -> None:
- obj.BIMObjectSpatialProperties.is_editing = False
+ props = cls.get_object_spatial_props(obj)
+ props.is_editing = False
@classmethod
def duplicate_object_and_data(cls, obj: bpy.types.Object) -> bpy.types.Object:
@@ -93,8 +113,9 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def enable_editing(cls, obj: bpy.types.Object) -> None:
- obj.BIMObjectSpatialProperties.is_editing = True
- obj.BIMObjectSpatialProperties.relating_container_object = None
+ props = cls.get_object_spatial_props(obj)
+ props.is_editing = True
+ props.relating_container_object = None
@classmethod
def get_container(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
@@ -208,7 +229,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def get_container_elements_grouped_by_classification(cls, container: ifcopenshell.entity_instance) -> dict:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
results = {}
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
@@ -255,7 +276,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def get_container_elements_grouped_by_type(cls, container: ifcopenshell.entity_instance) -> dict:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
results: defaultdict[str, dict[int, Any]] = defaultdict(dict)
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
@@ -281,7 +302,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def load_contained_elements(cls) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
props.elements.clear()
if not (container := props.active_container):
return
@@ -296,7 +317,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def load_contained_elements_by_type(cls, container: ifcopenshell.entity_instance) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
results = cls.get_container_elements_grouped_by_type(container)
expanded_elements = json.loads(props.expanded_elements)
@@ -351,7 +372,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def load_contained_elements_by_decomposition(cls, container: ifcopenshell.entity_instance) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
expanded_elements = json.loads(props.expanded_elements)
expanded_ifc_ids = expanded_elements.get("IFC_ID", [])
@@ -382,7 +403,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def load_contained_elements_by_classification(cls, container: ifcopenshell.entity_instance) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
expanded_elements = json.loads(props.expanded_elements)
expanded_classifications = expanded_elements.get("CLASSIFICATION", [])
expanded_classifications_r = expanded_elements.get("CLASSIFICATION_R", [])
@@ -446,7 +467,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def import_spatial_decomposition(cls) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
previous_container_index = props.active_container_index
props.containers.clear()
cls.contracted_containers = json.loads(props.contracted_containers)
@@ -457,7 +478,7 @@ class Spatial(bonsai.core.tool.Spatial):
def import_spatial_element(cls, element: ifcopenshell.entity_instance, level_index: int) -> None:
if not element.is_a("IfcProject") and not tool.Root.is_spatial_element(element):
return
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
new = props.containers.add()
new.ifc_class = element.is_a()
new["name"] = element.Name or "Unnamed"
@@ -556,28 +577,28 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def get_active_container(cls) -> Union[ifcopenshell.entity_instance, None]:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
if props.active_container_index < len(props.containers):
container = tool.Ifc.get().by_id(props.containers[props.active_container_index].ifc_definition_id)
return container
@classmethod
def contract_container(cls, container: ifcopenshell.entity_instance) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
contracted_containers = json.loads(props.contracted_containers)
contracted_containers.append(container.id())
props.contracted_containers = json.dumps(contracted_containers)
@classmethod
def expand_container(cls, container: ifcopenshell.entity_instance) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
contracted_containers = json.loads(props.contracted_containers)
contracted_containers.remove(container.id())
props.contracted_containers = json.dumps(contracted_containers)
@classmethod
def toggle_container_element(cls, element_index: int, is_recursive: bool) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
if props.element_mode == "TYPE":
cls.toggle_container_element_by_type(element_index, is_recursive)
elif props.element_mode == "DECOMPOSITION":
@@ -587,7 +608,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def toggle_container_element_by_type(cls, element_index: int, is_recursive: bool) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
expanded_elements: dict[str, list[Union[str, int]]] = json.loads(props.expanded_elements)
element = props.elements[element_index]
@@ -636,7 +657,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def toggle_container_element_by_decomposition(cls, element_index: int, is_recursive: bool) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
element = props.elements[element_index]
expanded_elements: dict[str, list[Union[str, int]]] = json.loads(props.expanded_elements)
expanded_elements_list: list[Union[str, int]] = expanded_elements.setdefault("IFC_ID", [])
@@ -669,7 +690,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def toggle_container_element_by_classification(cls, element_index: int, is_recursive: bool) -> None:
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
expanded_elements: dict[str, list[str]] = json.loads(props.expanded_elements)
expanded_elements_list: list[str] = expanded_elements.setdefault("CLASSIFICATION", [])
@@ -750,7 +771,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def get_boundary_lines_from_context_visible_objects(cls) -> list[shapely.LineString]:
- props = props = tool.Model.get_model_props()
+ props = tool.Model.get_model_props()
calculation_rl = props.rl3
container = tool.Root.get_default_container()
container_obj = tool.Ifc.get_object(container)
@@ -761,9 +782,10 @@ class Spatial(bonsai.core.tool.Spatial):
for obj in bpy.context.visible_objects:
visible_element = tool.Ifc.get_entity(obj)
+ old_mesh = obj.data
if (
not visible_element
- or obj.type != "MESH"
+ or not isinstance(old_mesh, bpy.types.Mesh)
or not cls.is_bounding_class(visible_element)
or not tool.Drawing.is_intersecting_plane(obj, cut_point, cut_normal)
):
@@ -1021,7 +1043,7 @@ class Spatial(bonsai.core.tool.Spatial):
old_mesh = active_obj.data
old_mesh_name = old_mesh.name
assert active_obj and isinstance(old_mesh, bpy.types.Mesh)
- mesh.BIMMeshProperties.ifc_definition_id = old_mesh.BIMMeshProperties.ifc_definition_id
+ tool.Geometry.get_mesh_props(mesh).ifc_definition_id = tool.Geometry.get_mesh_props(old_mesh).ifc_definition_id
tool.Geometry.change_object_data(active_obj, mesh, is_global=True)
tool.Ifc.edit(active_obj)
tool.Blender.remove_data_block(old_mesh)
@@ -1224,8 +1246,8 @@ class Spatial(bonsai.core.tool.Spatial):
def set_default_container(cls, container: ifcopenshell.entity_instance) -> None:
from bonsai.bim.module.spatial.data import SpatialDecompositionData
- assert bpy.context
- bpy.context.scene.BIMSpatialDecompositionProperties.default_container = container.id()
+ props = cls.get_spatial_props()
+ props.default_container = container.id()
SpatialDecompositionData.data["default_container"] = SpatialDecompositionData.default_container()
project = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
@@ -1282,16 +1304,16 @@ class Spatial(bonsai.core.tool.Spatial):
def set_target_container_as_default(cls) -> None:
if (
(container := tool.Root.get_default_container())
- and (obj := tool.Ifc.get_object(container))
- and bpy.context.active_object
+ and (container_obj := tool.Ifc.get_object(container))
+ and (obj := bpy.context.active_object)
):
- props = bpy.context.active_object.BIMObjectSpatialProperties
- props.container_obj = obj
+ props = cls.get_object_spatial_props(obj)
+ props.container_obj = container_obj
@classmethod
def get_filtered_elements(cls, should_filter: bool = True) -> Iterable[ifcopenshell.entity_instance]:
ifc_file = tool.Ifc.get()
- props = bpy.context.scene.BIMSpatialDecompositionProperties
+ props = cls.get_spatial_props()
container = ifc_file.by_id(props.active_container.ifc_definition_id)
element_filter = props.element_filter
active_element = props.active_element
diff --git a/src/bonsai/bonsai/tool/structural.py b/src/bonsai/bonsai/tool/structural.py
index 7dfa3dbdd0..30cbb00191 100644
--- a/src/bonsai/bonsai/tool/structural.py
+++ b/src/bonsai/bonsai/tool/structural.py
@@ -23,8 +23,6 @@ import json
import bonsai.bim.helper
import bonsai.core.tool
import bonsai.tool as tool
-from bonsai.bim.ifc import IfcStore
-from pprint import pprint
from typing import Union, Any
@@ -129,7 +127,8 @@ class Structural(bonsai.core.tool.Structural):
def load_structural_analysis_model_attributes(cls, data: dict[str, Any]) -> None:
props = bpy.context.scene.BIMStructuralProperties
props.structural_analysis_model_attributes.clear()
- for attribute in IfcStore.get_schema().declaration_by_name("IfcStructuralAnalysisModel").all_attributes():
+ schema = tool.Ifc.schema()
+ for attribute in schema.declaration_by_name("IfcStructuralAnalysisModel").all_attributes():
data_type = str(attribute.type_of_attribute)
if " npt.NDArray[np.float64]:
M_TRANSLATION = (slice(0, 3), 3)
matrix = np.array(obj.matrix_world)
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
if props.has_blender_offset and obj.BIMObjectProperties.blender_offset_type != "NOT_APPLICABLE":
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
coordinate_offset = tool.Geometry.get_cartesian_point_offset(obj)
diff --git a/src/bonsai/bonsai/tool/unit.py b/src/bonsai/bonsai/tool/unit.py
index 37597cf95c..808ea5ead0 100644
--- a/src/bonsai/bonsai/tool/unit.py
+++ b/src/bonsai/bonsai/tool/unit.py
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+from __future__ import annotations
import bpy
import json
import math
@@ -23,24 +24,34 @@ import ifcopenshell
import bonsai.bim.helper
import bonsai.core.tool
import bonsai.tool as tool
-from typing import Union, Literal, Any
+from typing import Union, Literal, Any, TYPE_CHECKING
from typing_extensions import assert_never
+if TYPE_CHECKING:
+ from bonsai.bim.module.unit.prop import BIMUnitProperties
+
class Unit(bonsai.core.tool.Unit):
UNIT_TYPE = Literal["LENGTHUNIT", "AREAUNIT", "VOLUMEUNIT"]
+ @classmethod
+ def get_unit_props(cls) -> BIMUnitProperties:
+ return bpy.context.scene.BIMUnitProperties
+
@classmethod
def clear_active_unit(cls) -> None:
- bpy.context.scene.BIMUnitProperties.active_unit_id = 0
+ props = cls.get_unit_props()
+ props.active_unit_id = 0
@classmethod
def disable_editing_units(cls) -> None:
- bpy.context.scene.BIMUnitProperties.is_editing = False
+ props = cls.get_unit_props()
+ props.is_editing = False
@classmethod
def enable_editing_units(cls) -> None:
- bpy.context.scene.BIMUnitProperties.is_editing = True
+ props = cls.get_unit_props()
+ props.is_editing = True
@classmethod
def export_unit_attributes(cls) -> dict[str, Any]:
@@ -52,11 +63,12 @@ class Unit(bonsai.core.tool.Unit):
attributes[prop.name] = (0, 0, 0, 0, 0, 0, 0)
return True
- props = bpy.context.scene.BIMUnitProperties
+ props = cls.get_unit_props()
return bonsai.bim.helper.export_attributes(props.unit_attributes, callback=callback)
@classmethod
def get_scene_unit_name(cls, unit_type: UNIT_TYPE) -> str:
+ bim_props = tool.Blender.get_bim_props()
if unit_type == "LENGTHUNIT":
props = bpy.context.scene.unit_settings
if props.length_unit == "MILES":
@@ -69,23 +81,24 @@ class Unit(bonsai.core.tool.Unit):
return "thou"
return "foot"
elif unit_type == "AREAUNIT":
- return bpy.context.scene.BIMProperties.area_unit
+ return bim_props.area_unit
elif unit_type == "VOLUMEUNIT":
- return bpy.context.scene.BIMProperties.volume_unit
+ return bim_props.volume_unit
else:
assert_never()
@classmethod
def get_scene_unit_si_prefix(cls, unit_type: UNIT_TYPE) -> Union[str, None]:
+ bim_props = tool.Blender.get_bim_props()
if unit_type == "LENGTHUNIT":
props = bpy.context.scene.unit_settings
if props.length_unit == "ADAPTIVE" or props.length_unit == "METERS":
return
return props.length_unit.replace("METERS", "")
elif unit_type == "AREAUNIT":
- unit = bpy.context.scene.BIMProperties.area_unit
+ unit = bim_props.area_unit
elif unit_type == "VOLUMEUNIT":
- unit = bpy.context.scene.BIMProperties.volume_unit
+ unit = bim_props.volume_unit
else:
assert_never(unit_type)
if "/" in unit:
@@ -93,9 +106,11 @@ class Unit(bonsai.core.tool.Unit):
@classmethod
def import_unit_attributes(cls, unit: ifcopenshell.entity_instance) -> None:
+ props = cls.get_unit_props()
+
def callback(name, prop, data):
if name == "Dimensions" and data["type"] != "IfcSIUnit":
- new = bpy.context.scene.BIMUnitProperties.unit_attributes.add()
+ new = props.unit_attributes.add()
new.name = name
new.is_null = data[name] is None
new.is_optional = False
@@ -103,13 +118,12 @@ class Unit(bonsai.core.tool.Unit):
new.string_value = json.dumps([e for e in tool.Ifc.get().by_id(data["id"]).Dimensions])
return True
- props = bpy.context.scene.BIMUnitProperties
props.unit_attributes.clear()
bonsai.bim.helper.import_attributes2(unit, props.unit_attributes, callback=callback)
@classmethod
def import_units(cls) -> None:
- props = bpy.context.scene.BIMUnitProperties
+ props = tool.Unit.get_unit_props()
props.units.clear()
units = []
@@ -158,7 +172,8 @@ class Unit(bonsai.core.tool.Unit):
@classmethod
def set_active_unit(cls, unit: ifcopenshell.entity_instance) -> None:
- bpy.context.scene.BIMUnitProperties.active_unit_id = unit.id()
+ props = cls.get_unit_props()
+ props.active_unit_id = unit.id()
@classmethod
def get_project_currency_unit(cls) -> Union[ifcopenshell.entity_instance, None]:
diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py
index 13f5ff7897..e37911b75e 100644
--- a/src/bonsai/bonsai/tool/web.py
+++ b/src/bonsai/bonsai/tool/web.py
@@ -730,7 +730,8 @@ class Web(bonsai.core.tool.Web):
if operator_data["type"] == "getDrawings":
drawings_data = []
sheets_data = []
- ifc_file_dir = os.path.dirname(bpy.context.scene.BIMProperties.ifc_file)
+ props = tool.Blender.get_bim_props()
+ ifc_file_dir = os.path.dirname(props.ifc_file)
sheets = [d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SHEET"]
for sheet in sorted(sheets, key=lambda s: getattr(s, "Identification", getattr(s, "DocumentId", None))):
diff --git a/src/bonsai/docs/guides/development/undo_system.rst b/src/bonsai/docs/guides/development/undo_system.rst
index c4e95bf28a..a123e8f96e 100644
--- a/src/bonsai/docs/guides/development/undo_system.rst
+++ b/src/bonsai/docs/guides/development/undo_system.rst
@@ -62,7 +62,7 @@ Instead, stuff happens in Blender operators.
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
- ifcopenshell.api.run("foo.bar", IfcStore.get_file())
+ ifcopenshell.api.run("foo.bar", tool.Ifc.get())
return {"FINISHED"}
When your operator manipulates (creates, removes, or edits) IFC data directly or
diff --git a/src/bonsai/scripts/headless_import.py b/src/bonsai/scripts/headless_import.py
index cda13e3878..111b07d78b 100644
--- a/src/bonsai/scripts/headless_import.py
+++ b/src/bonsai/scripts/headless_import.py
@@ -1,12 +1,12 @@
# This can be run using `blender -b -P headless_import.py`
import bpy
-from bonsai.bim.ifc import IfcStore
+import bonsai.tool as tool
# When federating, you may wish to manually specify the origin to ensure models
# with different or arbitrary origin conventions will turn up in the right spot.
-props = bpy.context.scene.BIMGeoreferenceProperties
+props = tool.Georeference.get_georeference_props()
# A good idea it to test import a portion of the model (or grids only) and check
# georeferencing coordinates in the IFC Georeferencing panel before filling out
@@ -19,7 +19,7 @@ props = bpy.context.scene.BIMGeoreferenceProperties
# props.blender_x_axis_ordinate = '0.989063862448262'
# props.has_blender_offset = True
-props = bpy.context.scene.BIMProjectProperties
+props = tool.Project.get_project_props()
# Generally recommended to disable caching for stability right now
props.should_cache = False
diff --git a/src/bonsai/scripts/waldo.py b/src/bonsai/scripts/waldo.py
new file mode 100644
index 0000000000..18c6267d39
--- /dev/null
+++ b/src/bonsai/scripts/waldo.py
@@ -0,0 +1,392 @@
+import numpy as np
+import ifcopenshell
+import ifcopenshell.api.root
+import ifcopenshell.api.type
+import ifcopenshell.api.unit
+import ifcopenshell.api.project
+import ifcopenshell.api.context
+import ifcopenshell.api.spatial
+import ifcopenshell.api.material
+import ifcopenshell.api.geometry
+import ifcopenshell.util.shape_builder
+import ifcopenshell.util.element
+
+# from ifcopenshell.util.shape_builder import VectorType, SequenceOfVectors
+from collections import namedtuple
+
+f = ifcopenshell.api.project.create_file()
+
+ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject")
+meters = ifcopenshell.api.unit.add_si_unit(f)
+ifcopenshell.api.unit.assign_unit(f, units=[meters])
+
+model = ifcopenshell.api.context.add_context(f, context_type="Model")
+plan = ifcopenshell.api.context.add_context(f, context_type="Plan")
+axis = ifcopenshell.api.context.add_context(
+ f, context_type="Plan", context_identifier="Axis", target_view="GRAPH_VIEW", parent=plan
+)
+body = ifcopenshell.api.context.add_context(
+ f, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model
+)
+material1 = ifcopenshell.api.material.add_material(f, name="material1", category="material1")
+material2 = ifcopenshell.api.material.add_material(f, name="material2", category="material2")
+site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite")
+builder = ifcopenshell.util.shape_builder.ShapeBuilder(f)
+
+style = ifcopenshell.api.style.add_style(f)
+attributes = {"SurfaceColour": {"Name": None, "Red": 1.0, "Green": 0.5, "Blue": 0.5}, "Transparency": 0.0}
+ifcopenshell.api.style.add_surface_style(f, style=style, ifc_class="IfcSurfaceStyleShading", attributes=attributes)
+ifcopenshell.api.style.assign_material_style(f, material=material1, style=style, context=body)
+
+style = ifcopenshell.api.style.add_style(f)
+attributes = {"SurfaceColour": {"Name": None, "Red": 0.5, "Green": 0.5, "Blue": 1.0}, "Transparency": 0.0}
+ifcopenshell.api.style.add_surface_style(f, style=style, ifc_class="IfcSurfaceStyleShading", attributes=attributes)
+ifcopenshell.api.style.assign_material_style(f, material=material2, style=style, context=body)
+
+
+def test_wall(offset, p1, p2, p3, p4):
+ offset *= 1.5
+ wall_type_a = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWallType", name="A")
+ wall_type_b = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWallType", name="B")
+
+ set_a = ifcopenshell.api.material.add_material_set(f, set_type="IfcMaterialLayerSet")
+ structure = ifcopenshell.api.material.add_layer(f, layer_set=set_a, material=material1, name="structure")
+ structure.Priority = p1
+ structure.LayerThickness = 0.1
+ cladding = ifcopenshell.api.material.add_layer(f, layer_set=set_a, material=material2, name="cladding")
+ cladding.Priority = p2
+ cladding.LayerThickness = 0.05
+
+ set_b = ifcopenshell.api.material.add_material_set(f, set_type="IfcMaterialLayerSet")
+ structure = ifcopenshell.api.material.add_layer(f, layer_set=set_b, material=material1, name="structure")
+ structure.Priority = p3
+ structure.LayerThickness = 0.1
+ cladding = ifcopenshell.api.material.add_layer(f, layer_set=set_b, material=material2, name="cladding")
+ cladding.Priority = p4
+ cladding.LayerThickness = 0.05
+
+ ifcopenshell.api.material.assign_material(f, products=[wall_type_a], material=set_a)
+ ifcopenshell.api.material.assign_material(f, products=[wall_type_b], material=set_b)
+
+ for i, rotation in enumerate((-90, -60, -120, 90, 60, 120)):
+ for i2, connection in enumerate(("ATEND", "ATSTART", "MIX")):
+ wall_a = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"A{p1}{p2}")
+ wall_b = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"B{p3}{p4}")
+
+ ifcopenshell.api.spatial.assign_container(f, products=[wall_a, wall_b], relating_structure=site)
+
+ ifcopenshell.api.type.assign_type(f, related_objects=[wall_a], relating_type=wall_type_a)
+ ifcopenshell.api.type.assign_type(f, related_objects=[wall_b], relating_type=wall_type_b)
+
+ axis_a = builder.polyline(((0.0, 0.0), (1.0, 0.0)))
+ axis_b = builder.polyline(((0.0, 0.0), (1.0, 0.0)))
+ rep_a = builder.get_representation(axis, [axis_a])
+ rep_b = builder.get_representation(axis, [axis_b])
+
+ ifcopenshell.api.geometry.assign_representation(f, product=wall_a, representation=rep_a)
+ ifcopenshell.api.geometry.assign_representation(f, product=wall_b, representation=rep_b)
+
+ x_offset = i * 2
+ x_offset += i2 * (2 * 6)
+ if connection == "ATEND":
+ sign_offset = 0 if rotation < 0 else 1
+ matrix_a = np.eye(4)
+ matrix_a[:, 3][0:3] = (0 + x_offset, 0 + offset + sign_offset, 0)
+ matrix_b = np.eye(4)
+ matrix_b = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_b
+ matrix_b[:, 3][0:3] = (1 + x_offset, 1 + offset - sign_offset, 0)
+ ifcopenshell.api.geometry.edit_object_placement(f, product=wall_a, matrix=matrix_a)
+ ifcopenshell.api.geometry.edit_object_placement(f, product=wall_b, matrix=matrix_b)
+
+ ifcopenshell.api.geometry.connect_path(
+ f,
+ relating_element=wall_a,
+ related_element=wall_b,
+ relating_connection="ATEND",
+ related_connection="ATEND",
+ )
+ elif connection == "ATSTART":
+ sign_offset = 0 if rotation < 0 else 1
+ matrix_a = np.eye(4)
+ matrix_a[:, 3][0:3] = (0 + x_offset, 1 + offset - sign_offset, 0)
+ matrix_b = np.eye(4)
+ matrix_b = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_b
+ matrix_b[:, 3][0:3] = (0 + x_offset, 1 + offset - sign_offset, 0)
+ ifcopenshell.api.geometry.edit_object_placement(f, product=wall_a, matrix=matrix_a)
+ ifcopenshell.api.geometry.edit_object_placement(f, product=wall_b, matrix=matrix_b)
+
+ ifcopenshell.api.geometry.connect_path(
+ f,
+ relating_element=wall_a,
+ related_element=wall_b,
+ relating_connection="ATSTART",
+ related_connection="ATSTART",
+ )
+ elif connection == "MIX":
+ sign_offset = 0 if rotation < 0 else 1
+ matrix_a = np.eye(4)
+ matrix_a[:, 3][0:3] = (0 + x_offset, 1 + offset - sign_offset, 0)
+ matrix_b = np.eye(4)
+ matrix_b = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_b
+ matrix_b[:, 3][0:3] = (1 + x_offset, 1 + offset - sign_offset, 0)
+ ifcopenshell.api.geometry.edit_object_placement(f, product=wall_a, matrix=matrix_a)
+ ifcopenshell.api.geometry.edit_object_placement(f, product=wall_b, matrix=matrix_b)
+
+ ifcopenshell.api.geometry.connect_path(
+ f,
+ relating_element=wall_a,
+ related_element=wall_b,
+ relating_connection="ATEND",
+ related_connection="ATSTART",
+ )
+
+ Foo(f, body, axis).regenerate(wall_a)
+ Foo(f, body, axis).regenerate(wall_b)
+
+
+PrioritisedLayer = namedtuple("PrioritisedLayer", "priority thickness")
+
+
+class Foo:
+ def __init__(self, file, body, axis):
+ self.file = file
+ self.body = body
+ self.axis = axis
+
+ def regenerate(self, wall):
+ print("-" * 100)
+ print(wall)
+ layers = self.get_layers(wall)
+ if not layers:
+ return
+ reference = self.get_reference_line(wall)
+ self.reference_p1, self.reference_p2 = reference
+ axes = self.get_axes(wall, reference, layers)
+ self.end_point = None
+ self.start_points = []
+ self.end_points = []
+ for rel in wall.ConnectedTo:
+ if rel.is_a("IfcRelConnectsPathElements"):
+ wall2 = rel.RelatedElement
+ layers1 = self.combine_layers(layers.copy(), rel.RelatingPriorities)
+ layers2 = self.combine_layers(self.get_layers(wall2), rel.RelatedPriorities)
+ if not layers1 or not layers2:
+ continue
+ self.join(wall, wall2, layers1, layers2, rel.RelatingConnectionType, rel.RelatedConnectionType)
+
+ for rel in wall.ConnectedFrom:
+ if rel.is_a("IfcRelConnectsPathElements"):
+ wall2 = rel.RelatingElement
+ layers1 = self.combine_layers(layers.copy(), rel.RelatedPriorities)
+ layers2 = self.combine_layers(self.get_layers(wall2), rel.RelatingPriorities)
+ if not layers1 or not layers2:
+ continue
+ self.join(wall, wall2, layers1, layers2, rel.RelatedConnectionType, rel.RelatingConnectionType)
+
+ # for rel in wall.ConnectedFrom:
+ # if rel.is_a("IfcRelConnectsPathElements"):
+ # connection = rel.RelatedConnectionType
+ if not self.start_points:
+ minx = axes[0][0][0]
+ self.start_points = [
+ np.array((minx, axes[0][0][1])),
+ np.array((minx, axes[-1][0][1])),
+ ]
+ if not self.end_points:
+ maxx = axes[0][1][0]
+ self.end_points = [
+ np.array((maxx, axes[0][0][1])),
+ np.array((maxx, axes[-1][0][1])),
+ ]
+ print("FINISHED")
+ print(self.start_points)
+ print(self.end_points)
+
+ points = []
+ if self.start_points[0][1] < self.start_points[-1][1]:
+ points.extend((self.start_points))
+ else:
+ points.extend(reversed(self.start_points))
+ if self.end_points[0][1] > self.end_points[-1][1]:
+ points.extend((self.end_points))
+ else:
+ points.extend(reversed(self.end_points))
+
+ builder = ifcopenshell.util.shape_builder.ShapeBuilder(wall.file)
+ item = builder.extrude(builder.polyline(points, closed=True), magnitude=1.0)
+ rep = builder.get_representation(self.body, items=[item])
+ if old_rep := ifcopenshell.util.representation.get_representation(wall, self.body):
+ ifcopenshell.util.element.replace_element(old_rep, rep)
+ else:
+ ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep)
+
+ item = builder.polyline([self.reference_p1, self.reference_p2])
+ rep = builder.get_representation(self.axis, items=[item])
+ if old_rep := ifcopenshell.util.representation.get_representation(wall, self.axis):
+ ifcopenshell.util.element.replace_element(old_rep, rep)
+ else:
+ ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep)
+
+ def join(self, wall1, wall2, layers1, layers2, connection1, connection2):
+ if connection1 == "NOTDEFINED" or connection2 == "NOTDEFINED":
+ return
+ print("joining", wall1, layers1, connection1)
+ print("to", wall2, layers2, connection2)
+
+ # axes = self.get_axes(wall2, layers2)
+ reference1 = self.get_reference_line(wall1)
+ reference2 = self.get_reference_line(wall2)
+ axes1 = self.get_axes(wall1, reference1, layers1)
+ axes2 = self.get_axes(wall2, reference2, layers2)
+ matrix1i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement))
+ matrix2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement)
+ print(axes1)
+ print(axes2)
+
+ # Convert wall2 data to wall1 local coordinates
+ for axis in axes2:
+ axis[0] = (matrix1i @ matrix2 @ np.concatenate((axis[0], (0, 1))))[:2]
+ axis[1] = (matrix1i @ matrix2 @ np.concatenate((axis[1], (0, 1))))[:2]
+ reference2[0] = (matrix1i @ matrix2 @ np.concatenate((reference2[0], (0, 1))))[:2]
+ reference2[1] = (matrix1i @ matrix2 @ np.concatenate((reference2[1], (0, 1))))[:2]
+
+ # Sort axes from interior to exterior
+ if connection1 == "ATEND":
+ if axes2[0][0][0] > axes2[-1][0][0]: # We process layers in a +X direction
+ axes2 = list(reversed(axes2))
+ layers2 = list(reversed(layers2))
+ elif connection1 == "ATSTART":
+ if axes2[-1][0][0] > axes2[0][0][0]: # We process layers in a -X direction
+ axes2 = list(reversed(axes2))
+ layers2 = list(reversed(layers2))
+
+ # wall2_x = matrix2[:,0][:2]
+ axis2 = axes2[0] # Take an arbitrary axis
+ if connection2 == "ATSTART":
+ axis2 = [axis2[1], axis2[0]] # Flip direction so the axis "points" in the direction of join
+ if axis2[0][1] < axis2[1][1]: # Pointing +Y
+ if axes1[-1][0][1] < axes1[0][0][1]: # We process layers1 in a +Y direction
+ axes1 = list(reversed(axes1))
+ layers1 = list(reversed(layers1))
+ else: # Pointing -Y
+ if axes1[0][0][1] < axes1[-1][0][1]: # We process layers1 in a -Y direction
+ axes1 = list(reversed(axes1))
+ layers1 = list(reversed(layers1))
+
+ print("modified")
+ print(axes1)
+ print(axes2)
+ # Checked
+
+ last_y = axes1[-1][0][1]
+ ys = iter([a[0][1] for a in axes1])
+ print("ys are", [a[0][1] for a in axes1])
+
+ last_axis2 = axes2[-1]
+ axes2 = iter(axes2)
+ axis2 = next(axes2)
+ y = next(ys)
+ x = self.intersect_axis(*axis2, y=y)
+ points = [np.array((x, y))]
+ print("first point", points)
+
+ layers1 = iter(layers1)
+ layers2 = iter(layers2)
+ layer1 = next(layers1, None)
+ layer2 = next(layers2, None)
+
+ while layer1 and layer2:
+ print("considering", layer1, layer2)
+ if layer1.priority > layer2.priority:
+ axis2 = next(axes2)
+ x = self.intersect_axis(*axis2, y=y)
+ layer2 = next(layers2, None)
+ elif layer2.priority > layer1.priority:
+ y = next(ys)
+ x = self.intersect_axis(*axis2, y=y)
+ layer1 = next(layers1, None)
+ else:
+ y = next(ys)
+ x = self.intersect_axis(*next(axes2), y=y)
+ layer1 = next(layers1, None)
+ layer2 = next(layers2, None)
+ points.append(np.array((x, y)))
+
+ print("points", points)
+ if points[-1][1] != last_y:
+ points.append(np.array((self.intersect_axis(*last_axis2, y=last_y), last_y)))
+ print("fpoints", points)
+ if connection1 == "ATSTART":
+ self.start_points = points
+ self.reference_p1[0] = self.intersect_axis(*reference2, y=reference1[0][1])
+ elif connection1 == "ATEND":
+ self.end_points = points
+ self.reference_p2[0] = self.intersect_axis(*reference2, y=reference1[0][1])
+
+ def get_layers(self, wall) -> list:
+ material = ifcopenshell.util.element.get_material(wall, should_skip_usage=True)
+ if not material or not material.is_a("IfcMaterialLayerSet"):
+ return []
+ return [PrioritisedLayer(l.Priority or 0, l.LayerThickness) for l in material.MaterialLayers]
+
+ def combine_layers(self, layers, override_priorities):
+ results = []
+ if override_priorities:
+ for i, priority in enumerate(override_priorities[: len(layers)]):
+ layers[i][0] = priority
+ if not layers:
+ return []
+ results = [layers.pop(0)]
+ for layer in layers:
+ if not layer.thickness:
+ continue
+ if layer.priority == results[-1].priority:
+ results[-1] = PrioritisedLayer(layer.priority, results[-1].thickness + layer.thickness)
+ else:
+ results.append(layer)
+ return results
+
+ def intersect_axis(self, p1, p2, y=0):
+ # Assumes lines are horizontal
+ x1, y1 = p1
+ x2, y2 = p2
+ t = (y - y1) / (y2 - y1)
+ return x1 + t * (x2 - x1)
+
+ def get_reference_line(self, wall):
+ if axis := ifcopenshell.util.representation.get_representation(wall, "Plan", "Axis", "GRAPH_VIEW"):
+ for item in ifcopenshell.util.representation.resolve_representation(axis).Items:
+ if item.is_a("IfcPolyline"):
+ points = item.Points
+ elif item.is_a("IfcIndexedPolyCurve"):
+ points = item.Points.CoordList
+ else:
+ continue
+ if points[0][0] < points[1][0]: # An axis always goes in the +X direction
+ return [np.array(points[0]), np.array(points[1])]
+ return [np.array(points[1]), np.array(points[0])]
+ return [np.array((0.0, 0.0)), np.array((1.0, 0.0))]
+
+ def get_axes(self, wall, reference, layers: list[PrioritisedLayer]):
+ axes = [[p.copy() for p in reference]]
+ # Apply usage to convert the Reference line into MlsBase
+ sense_factor = 1
+ if (usage := ifcopenshell.util.element.get_material(wall)) and usage.is_a("IfcMaterialLayerSetUage"):
+ for point in axes[0]:
+ point[1] += usage.OffsetFromReferenceLine
+ sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1
+
+ for layer in layers:
+ axes.append([p.copy() + np.array((0.0, layer.thickness * sense_factor)) for p in axes[-1]])
+ return axes
+
+
+test_wall(0, 1, 1, 1, 1)
+test_wall(1, 2, 1, 1, 2)
+test_wall(2, 2, 1, 1, 1)
+test_wall(3, 1, 2, 1, 1)
+test_wall(4, 1, 2, 1, 2)
+test_wall(5, 3, 1, 2, 4)
+
+
+f.write("/home/dion/wall.ifc")
diff --git a/src/bonsai/test/bim/bootstrap.py b/src/bonsai/test/bim/bootstrap.py
index bea49df413..308d140298 100644
--- a/src/bonsai/test/bim/bootstrap.py
+++ b/src/bonsai/test/bim/bootstrap.py
@@ -23,6 +23,7 @@ import bpy
import pytest
import webbrowser
import bonsai.bim.handler
+import bonsai.tool as tool
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.representation
@@ -66,7 +67,8 @@ class NewIfc4X3:
bpy.data.batch_remove(bpy.data.objects)
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
bonsai.bim.handler.load_post(None)
- bpy.context.scene.BIMProjectProperties.export_schema = "IFC4X3_ADD2"
+ props = tool.Project.get_project_props()
+ props.export_schema = "IFC4X3_ADD2"
bpy.ops.bim.create_project()
@@ -172,14 +174,14 @@ def the_object_name_exists(name):
def an_ifc_file_exists():
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
if not ifc:
assert False, "No IFC file is available"
return ifc
def an_ifc_file_does_not_exist():
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
if ifc:
assert False, "An IFC is available"
@@ -281,7 +283,7 @@ def the_object_name1_has_no_boolean_difference_by_name2(name1, name2):
def the_object_name_is_voided_by_void(name, void):
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id)
for rel in element.HasOpenings:
if rel.RelatedOpeningElement.Name == void:
@@ -290,7 +292,7 @@ def the_object_name_is_voided_by_void(name, void):
def the_object_name_is_not_voided_by_void(name, void):
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id)
for rel in element.HasOpenings:
if rel.RelatedOpeningElement.Name == void:
@@ -298,21 +300,21 @@ def the_object_name_is_not_voided_by_void(name, void):
def the_object_name_is_not_voided(name):
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id)
if any(element.HasOpenings):
assert False, "An opening was found"
def the_object_name_is_not_a_void(name):
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id)
if any(element.VoidsElements):
assert False, "A void was found"
def the_void_name_is_filled_by_filling(name, filling):
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id)
if any(rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings):
return True
@@ -320,14 +322,14 @@ def the_void_name_is_filled_by_filling(name, filling):
def the_void_name_is_not_filled_by_filling(name, filling):
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id)
if any(rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings):
assert False, "A filling was found"
def the_object_name_is_not_a_filling(name):
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id)
if any(element.FillsVoids):
assert False, "A filling was found"
diff --git a/src/bonsai/test/bim/feature/type.feature b/src/bonsai/test/bim/feature/type.feature
index 6b68fe7566..c32f7a8cfb 100644
--- a/src/bonsai/test/bim/feature/type.feature
+++ b/src/bonsai/test/bim/feature/type.feature
@@ -202,11 +202,11 @@ Scenario: Select similar type
Scenario: Purge unused types
Given an empty IFC project
And I press "bim.launch_type_manager"
- And I set "scene.BIMModelProperties.type_class" to "IfcWallType"
- And I set "scene.BIMModelProperties.type_predefined_type" to "SOLIDWALL"
- And I set "scene.BIMModelProperties.type_template" to "EMPTY"
- When I press "bim.add_type"
- Then the object "IfcWallType/TYPEX" is an "IfcWallType"
- And the object "IfcWallType/TYPEX" has no data
- When I press "bim.purge_unused_types"
- Then the object "IfcWallType/TYPEX" does not exist
+ And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType"
+ And I set "scene.BIMRootProperties.ifc_predefined_type" to "SOLIDWALL"
+ And I set "scene.BIMRootProperties.representation_template" to "EMPTY"
+ When I press "bim.add_element"
+ Then the object "IfcWallType/Unnamed" is an "IfcWallType"
+ And the object "IfcWallType/Unnamed" has no data
+ When I press "bim.purge_unused_objects(object_type='TYPE')"
+ Then the object "IfcWallType/Unnamed" does not exist
diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py
index 95945ced81..1332c63e8e 100644
--- a/src/bonsai/test/bim/test_feature.py
+++ b/src/bonsai/test/bim/test_feature.py
@@ -39,7 +39,7 @@ scenarios("feature")
variables = {
"cwd": Path.cwd().as_posix(),
- "ifc": "IfcStore.get_file()",
+ "ifc": "tool.Ifc.get()",
"pset_ifc": "IfcStore.pset_template_file",
"classification_ifc": "IfcStore.classification_file",
}
@@ -190,7 +190,8 @@ def an_empty_blender_session():
# default project settings
bpy.context.scene.unit_settings.system = "METRIC"
bpy.context.scene.unit_settings.length_unit = "MILLIMETERS"
- bpy.context.scene.BIMProjectProperties.template_file = "0"
+ props = tool.Project.get_project_props()
+ props.template_file = "0"
tool.Blender.get_addon_preferences().should_play_chaching_sound = False
@@ -203,7 +204,8 @@ def an_empty_ifc_project():
@given("an empty IFC2X3 project")
def an_empty_ifc_2x3_project():
an_empty_blender_session()
- bpy.context.scene.BIMProjectProperties.export_schema = "IFC2X3"
+ props = tool.Project.get_project_props()
+ props.export_schema = "IFC2X3"
bpy.ops.bim.create_project()
@@ -742,7 +744,7 @@ def the_object_name_has_a_representation_type_of_context(name, type, context):
def the_object_name_data_is_a_type_representation_of_context(name, type, context):
ifc = an_ifc_file_exists()
context, subcontext, target_view = context.split("/")
- rep = ifc.by_id(the_object_name_exists(name).data.BIMMeshProperties.ifc_definition_id)
+ rep = ifc.by_id(tool.Geometry.get_mesh_props(the_object_name_exists(name).data).ifc_definition_id)
assert rep
assert rep.RepresentationType == type, f"The object {name} is not a {type} representation"
assert rep.ContextOfItems.ContextType == context
@@ -784,14 +786,14 @@ def the_ifc_material_name_does_not_exist(name):
@then("an IFC file does not exist")
def an_ifc_file_does_not_exist():
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
if ifc:
assert False, "An IFC is available"
@then("an IFC file exists")
def an_ifc_file_exists():
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
if not ifc:
assert False, "No IFC file is available"
return ifc
@@ -805,7 +807,7 @@ def the_object_name_should_display_as_mode(name, mode):
@then(parsers.parse('the object "{name}" is voided by "{void}"'))
def the_object_name_is_voided_by_void(name, void):
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id)
assert any((rel for rel in element.HasOpenings if rel.RelatedOpeningElement.Name == void)), "No void found"
@@ -821,14 +823,14 @@ def the_object_name_is_not_voided_by_void(name, void):
@then(parsers.parse('the object "{name}" is not voided'))
def the_object_name_is_not_voided(name):
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id)
assert not element.HasOpenings, "A void was found"
@then(parsers.parse('the object "{name}" is a void'))
def the_object_name_is_a_void(name):
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
obj = the_object_name_exists(name)
element = ifc.by_id(obj.BIMObjectProperties.ifc_definition_id)
assert any((element.VoidsElements)), "No void was found"
@@ -888,7 +890,7 @@ def the_object_name_has_no_data(name):
@then(parsers.parse('the object "{name}" has data which is an IFC representation'))
def the_object_name_has_ifc_representation_data(name):
- id = the_object_name_exists(name).data.BIMMeshProperties.ifc_definition_id
+ id = tool.Geometry.get_mesh_props(the_object_name_exists(name).data).ifc_definition_id
assert id != 0, f"The ID is {id}"
@@ -934,7 +936,7 @@ def the_object_name_has_number_vertices(name, number):
@then(parsers.parse('the void "{name}" is filled by "{filling}"'))
def the_void_name_is_filled_by_filling(name, filling):
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id)
assert any((rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings)), "No filling found"
@@ -951,7 +953,7 @@ def the_void_name_is_not_filled_by_filling(name, filling):
@when(parsers.parse('the object "{name}" is not a filling'))
@then(parsers.parse('the object "{name}" is not a filling'))
def the_object_name_is_not_a_filling(name):
- ifc = IfcStore.get_file()
+ ifc = tool.Ifc.get()
element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id)
assert not any(element.FillsVoids), "A filling was found"
diff --git a/src/bonsai/test/tool/test_brick.py b/src/bonsai/test/tool/test_brick.py
index c2d10d699f..b26a2acd43 100644
--- a/src/bonsai/test/tool/test_brick.py
+++ b/src/bonsai/test/tool/test_brick.py
@@ -122,11 +122,10 @@ class TestAddBrickifcProject(NewFile):
result = subject.add_brickifc_project("http://example.org/digitaltwin#")
assert result == f"http://example.org/digitaltwin#{project.GlobalId}"
brick = URIRef(result)
+ props = tool.Blender.get_bim_props()
assert list(BrickStore.graph.triples((brick, A, REF.ifcProject)))
assert list(BrickStore.graph.triples((brick, REF.ifcProjectID, Literal(project.GlobalId))))
- assert list(
- BrickStore.graph.triples((brick, REF.ifcFileLocation, Literal(bpy.context.scene.BIMProperties.ifc_file)))
- )
+ assert list(BrickStore.graph.triples((brick, REF.ifcFileLocation, Literal(props.ifc_file))))
assert list(
BrickStore.graph.triples(
(brick, URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("My Project"))
diff --git a/src/bonsai/test/tool/test_debug.py b/src/bonsai/test/tool/test_debug.py
index d7d6a95c2c..888a717f4d 100644
--- a/src/bonsai/test/tool/test_debug.py
+++ b/src/bonsai/test/tool/test_debug.py
@@ -55,7 +55,8 @@ class TestLoadExpress(NewFile):
class TestPurgeHdf5Cache(NewFile):
def test_run(self):
- cache_dir = Path(bpy.context.scene.BIMProperties.cache_dir)
+ props = tool.Blender.get_bim_props()
+ cache_dir = Path(props.cache_dir)
test_file = cache_dir / "test.h5"
test_file.parent.mkdir(parents=True, exist_ok=True)
test_file.touch()
diff --git a/src/bonsai/test/tool/test_document.py b/src/bonsai/test/tool/test_document.py
index 30ce8b5721..d60b9fbe31 100644
--- a/src/bonsai/test/tool/test_document.py
+++ b/src/bonsai/test/tool/test_document.py
@@ -36,13 +36,13 @@ class TestAddBreadcrumb(NewFile):
tool.Ifc().set(ifc)
document = ifc.createIfcDocumentInformation()
subject.add_breadcrumb(document)
- props = bpy.context.scene.BIMDocumentProperties
+ props = tool.Document.get_document_props()
assert props.breadcrumbs[0].name == str(document.id())
class TestClearBreadcrumbs(NewFile):
def test_run(self):
- props = bpy.context.scene.BIMDocumentProperties
+ props = tool.Document.get_document_props()
props.breadcrumbs.add()
subject.clear_breadcrumbs()
assert len(props.breadcrumbs) == 0
@@ -50,7 +50,7 @@ class TestClearBreadcrumbs(NewFile):
class TestClearDocumentTree(NewFile):
def test_run(self):
- props = bpy.context.scene.BIMDocumentProperties
+ props = tool.Document.get_document_props()
new = props.documents.add()
subject.clear_document_tree()
assert len(props.documents) == 0
@@ -58,23 +58,26 @@ class TestClearDocumentTree(NewFile):
class TestDisableEditingDocument(NewFile):
def test_run(self):
- bpy.context.scene.BIMDocumentProperties.active_document_id = 1
+ props = tool.Document.get_document_props()
+ props.active_document_id = 1
subject.disable_editing_document()
- assert bpy.context.scene.BIMDocumentProperties.active_document_id == 0
+ assert props.active_document_id == 0
class TestDisableEditingUI(NewFile):
def test_run(self):
- bpy.context.scene.BIMDocumentProperties.is_editing = True
+ props = tool.Document.get_document_props()
+ props.is_editing = True
subject.disable_editing_ui()
- assert bpy.context.scene.BIMDocumentProperties.is_editing == False
+ assert props.is_editing == False
class TestEnableEditingUI(NewFile):
def test_run(self):
- bpy.context.scene.BIMDocumentProperties.is_editing = False
+ props = tool.Document.get_document_props()
+ props.is_editing = False
subject.enable_editing_ui()
- assert bpy.context.scene.BIMDocumentProperties.is_editing == True
+ assert props.is_editing == True
class TestExportDocumentAttributes(NewFile):
@@ -129,22 +132,22 @@ class TestImportDocumentAttributes(NewFile):
document.Confidentiality = "CONFIDENTIAL"
document.Status = "DRAFT"
subject().import_document_attributes(document)
- props = bpy.context.scene.BIMDocumentProperties
- assert props.document_attributes.get("Identification").string_value == "Identification"
- assert props.document_attributes.get("Name").string_value == "Name"
- assert props.document_attributes.get("Description").string_value == "Description"
- assert props.document_attributes.get("Location").string_value == "Location"
- assert props.document_attributes.get("Purpose").string_value == "Purpose"
- assert props.document_attributes.get("IntendedUse").string_value == "IntendedUse"
- assert props.document_attributes.get("Scope").string_value == "Scope"
- assert props.document_attributes.get("Revision").string_value == "Revision"
- assert props.document_attributes.get("CreationTime").string_value == "CreationTime"
- assert props.document_attributes.get("LastRevisionTime").string_value == "LastRevisionTime"
- assert props.document_attributes.get("ElectronicFormat").string_value == "ElectronicFormat"
- assert props.document_attributes.get("ValidFrom").string_value == "ValidFrom"
- assert props.document_attributes.get("ValidUntil").string_value == "ValidUntil"
- assert props.document_attributes.get("Confidentiality").enum_value == "CONFIDENTIAL"
- assert props.document_attributes.get("Status").enum_value == "DRAFT"
+ props = tool.Document.get_document_props()
+ assert props.document_attributes["Identification"].string_value == "Identification"
+ assert props.document_attributes["Name"].string_value == "Name"
+ assert props.document_attributes["Description"].string_value == "Description"
+ assert props.document_attributes["Location"].string_value == "Location"
+ assert props.document_attributes["Purpose"].string_value == "Purpose"
+ assert props.document_attributes["IntendedUse"].string_value == "IntendedUse"
+ assert props.document_attributes["Scope"].string_value == "Scope"
+ assert props.document_attributes["Revision"].string_value == "Revision"
+ assert props.document_attributes["CreationTime"].string_value == "CreationTime"
+ assert props.document_attributes["LastRevisionTime"].string_value == "LastRevisionTime"
+ assert props.document_attributes["ElectronicFormat"].string_value == "ElectronicFormat"
+ assert props.document_attributes["ValidFrom"].string_value == "ValidFrom"
+ assert props.document_attributes["ValidUntil"].string_value == "ValidUntil"
+ assert props.document_attributes["Confidentiality"].enum_value == "CONFIDENTIAL"
+ assert props.document_attributes["Status"].enum_value == "DRAFT"
def test_importing_reference(self):
ifc = ifcopenshell.file()
@@ -155,11 +158,11 @@ class TestImportDocumentAttributes(NewFile):
document.Name = "Name"
document.Description = "Description"
subject().import_document_attributes(document)
- props = bpy.context.scene.BIMDocumentProperties
- assert props.document_attributes.get("Location").string_value == "Location"
- assert props.document_attributes.get("Identification").string_value == "Identification"
- assert props.document_attributes.get("Name").string_value == "Name"
- assert props.document_attributes.get("Description").string_value == "Description"
+ props = tool.Document.get_document_props()
+ assert props.document_attributes["Location"].string_value == "Location"
+ assert props.document_attributes["Identification"].string_value == "Identification"
+ assert props.document_attributes["Name"].string_value == "Name"
+ assert props.document_attributes["Description"].string_value == "Description"
class TestImportProjectDocuments(NewFile):
@@ -169,7 +172,7 @@ class TestImportProjectDocuments(NewFile):
ifc.createIfcProject()
document = ifcopenshell.api.run("document.add_information", ifc)
subject.import_project_documents()
- props = bpy.context.scene.BIMDocumentProperties
+ props = tool.Document.get_document_props()
assert len(props.documents) == 1
assert props.documents[0].ifc_definition_id == document.id()
assert props.documents[0].name == "Unnamed"
@@ -185,7 +188,7 @@ class TestImportReferences(NewFile):
document = ifcopenshell.api.run("document.add_information", ifc)
reference = ifcopenshell.api.run("document.add_reference", ifc, information=document)
subject.import_references(document)
- props = bpy.context.scene.BIMDocumentProperties
+ props = tool.Document.get_document_props()
assert len(props.documents) == 1
assert props.documents[0].ifc_definition_id == reference.id()
assert props.documents[0].name == "Unnamed"
@@ -201,7 +204,7 @@ class TestImportSubdocuments(NewFile):
document = ifcopenshell.api.run("document.add_information", ifc)
subdocument = ifcopenshell.api.run("document.add_information", ifc, parent=document)
subject.import_subdocuments(document)
- props = bpy.context.scene.BIMDocumentProperties
+ props = tool.Document.get_document_props()
assert len(props.documents) == 1
assert props.documents[0].ifc_definition_id == subdocument.id()
assert props.documents[0].name == "Unnamed"
@@ -220,7 +223,7 @@ class TestIsDocumentInformation(NewFile):
class TestRemoveLatestBreadcrumb(NewFile):
def test_run(self):
- props = bpy.context.scene.BIMDocumentProperties
+ props = tool.Document.get_document_props()
props.breadcrumbs.add()
props.breadcrumbs.add()
subject.remove_latest_breadcrumb()
@@ -232,4 +235,5 @@ class TestSetActiveDocument(NewFile):
ifc = ifcopenshell.file()
document = ifc.createIfcDocumentInformation()
subject.set_active_document(document)
- assert bpy.context.scene.BIMDocumentProperties.active_document_id == document.id()
+ props = tool.Document.get_document_props()
+ assert props.active_document_id == document.id()
diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py
index 5e3c239e03..b004a92efb 100644
--- a/src/bonsai/test/tool/test_drawing.py
+++ b/src/bonsai/test/tool/test_drawing.py
@@ -27,7 +27,6 @@ import bonsai.core.tool
import bonsai.tool as tool
from test.bim.bootstrap import NewFile
from bonsai.tool.drawing import Drawing as subject
-from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.drawing.data import DecoratorData
from mathutils import Vector
@@ -113,30 +112,34 @@ class TestDeleteDrawingElements(NewFile):
class TestDisableEditingDrawings(NewFile):
def test_run(self):
- bpy.context.scene.DocProperties.is_editing_drawings = True
+ props = tool.Drawing.get_document_props()
+ props.is_editing_drawings = True
subject.disable_editing_drawings()
- assert bpy.context.scene.DocProperties.is_editing_drawings == False
+ assert props.is_editing_drawings == False
class TestDisableEditingSchedules(NewFile):
def test_run(self):
- bpy.context.scene.DocProperties.is_editing_schedules = True
+ props = tool.Drawing.get_document_props()
+ props.is_editing_schedules = True
subject.disable_editing_schedules()
- assert bpy.context.scene.DocProperties.is_editing_schedules == False
+ assert props.is_editing_schedules == False
class TestDisableEditingReferences(NewFile):
def test_run(self):
- bpy.context.scene.DocProperties.is_editing_references = True
+ props = tool.Drawing.get_document_props()
+ props.is_editing_references = True
subject.disable_editing_references()
- assert bpy.context.scene.DocProperties.is_editing_references == False
+ assert props.is_editing_references == False
class TestDisableEditingSheets(NewFile):
def test_run(self):
- bpy.context.scene.DocProperties.is_editing_sheets = True
+ props = tool.Drawing.get_document_props()
+ props.is_editing_sheets = True
subject.disable_editing_sheets()
- assert bpy.context.scene.DocProperties.is_editing_sheets == False
+ assert props.is_editing_sheets == False
class TestDisableEditingText(NewFile):
@@ -166,30 +169,34 @@ class TestEnableEditing(NewFile):
class TestEnableEditingDrawings(NewFile):
def test_run(self):
- bpy.context.scene.DocProperties.is_editing_drawings = False
+ props = tool.Drawing.get_document_props()
+ props.is_editing_drawings = False
subject.enable_editing_drawings()
- assert bpy.context.scene.DocProperties.is_editing_drawings == True
+ assert props.is_editing_drawings == True
class TestEnableEditingSchedules(NewFile):
def test_run(self):
- bpy.context.scene.DocProperties.is_editing_schedules = False
+ props = tool.Drawing.get_document_props()
+ props.is_editing_schedules = False
subject.enable_editing_schedules()
- assert bpy.context.scene.DocProperties.is_editing_schedules == True
+ assert props.is_editing_schedules == True
class TestEnableEditingReferences(NewFile):
def test_run(self):
- bpy.context.scene.DocProperties.is_editing_references = False
+ props = tool.Drawing.get_document_props()
+ props.is_editing_references = False
subject.enable_editing_references()
- assert bpy.context.scene.DocProperties.is_editing_references == True
+ assert props.is_editing_references == True
class TestEnableEditingSheets(NewFile):
def test_run(self):
- bpy.context.scene.DocProperties.is_editing_sheets = False
+ props = tool.Drawing.get_document_props()
+ props.is_editing_sheets = False
subject.enable_editing_sheets()
- assert bpy.context.scene.DocProperties.is_editing_sheets == True
+ assert props.is_editing_sheets == True
class TestEnableEditingText(NewFile):
@@ -492,7 +499,7 @@ class TestImportDrawings(NewFile):
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=drawing, name="EPset_Drawing")
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"TargetView": "PLAN_VIEW"})
subject.import_drawings()
- props = bpy.context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
for d in props.drawings:
d.is_expanded = True
subject.import_drawings()
@@ -508,7 +515,7 @@ class TestImportSchedules(NewFile):
ifc.createIfcDocumentInformation(Identification="Y", Name="FOOBAZ")
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SCHEDULE")
subject.import_documents("SCHEDULE")
- props = bpy.context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
assert props.schedules[0].ifc_definition_id == document.id()
assert props.schedules[0].identification == "X"
assert props.schedules[0].name == "FOOBAR"
@@ -519,7 +526,7 @@ class TestImportSchedules(NewFile):
ifc.createIfcDocumentInformation(DocumentId="Y", Name="FOOBAZ")
document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="SCHEDULE")
subject.import_documents("SCHEDULE")
- props = bpy.context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
assert props.schedules[0].ifc_definition_id == document.id()
assert props.schedules[0].identification == "X"
assert props.schedules[0].name == "FOOBAR"
@@ -532,7 +539,7 @@ class TestImportReferences(NewFile):
ifc.createIfcDocumentInformation(Identification="Y", Name="FOOBAZ")
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="REFERENCE")
subject.import_documents("REFERENCE")
- props = bpy.context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
assert props.references[0].ifc_definition_id == document.id()
assert props.references[0].identification == "X"
assert props.references[0].name == "FOOBAR"
@@ -543,7 +550,7 @@ class TestImportReferences(NewFile):
ifc.createIfcDocumentInformation(DocumentId="Y", Name="FOOBAZ")
document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="REFERENCE")
subject.import_documents("REFERENCE")
- props = bpy.context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
assert props.references[0].ifc_definition_id == document.id()
assert props.references[0].identification == "X"
assert props.references[0].name == "FOOBAR"
@@ -556,7 +563,7 @@ class TestImportSheets(NewFile):
ifc.createIfcDocumentInformation(Identification="Y", Name="FOOBAZ")
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SHEET")
subject.import_sheets()
- props = bpy.context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
assert props.sheets[0].ifc_definition_id == document.id()
assert props.sheets[0].identification == "X"
assert props.sheets[0].name == "FOOBAR"
@@ -567,7 +574,7 @@ class TestImportSheets(NewFile):
ifc.createIfcDocumentInformation(DocumentId="Y", Name="FOOBAZ")
document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="SHEET")
subject.import_sheets()
- props = bpy.context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
assert props.sheets[0].ifc_definition_id == document.id()
assert props.sheets[0].identification == "X"
assert props.sheets[0].name == "FOOBAR"
@@ -657,9 +664,10 @@ class TestSetName(NewFile):
class TestShowDecorations(NewFile):
def test_run(self):
- bpy.context.scene.DocProperties.should_draw_decorations = False
+ props = tool.Drawing.get_document_props()
+ props.should_draw_decorations = False
subject.show_decorations()
- assert bpy.context.scene.DocProperties.should_draw_decorations is True
+ assert props.should_draw_decorations is True
class TestDrawingMaintainingSheetPosition(NewFile):
@@ -680,7 +688,7 @@ class TestDrawingMaintainingSheetPosition(NewFile):
return drawing_data
def test_run(self):
- props = bpy.context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
sheet_path = Path.cwd() / "layouts" / "A00 - UNTITLED.svg"
@@ -845,10 +853,11 @@ class TestDrawingStyles(NewFile):
ifc = tool.Ifc.get()
drawing = ifc.by_type("IfcAnnotation")[0]
bpy.ops.bim.expand_target_view(target_view="PLAN_VIEW")
- props = bpy.context.scene.DocProperties
+ props = tool.Drawing.get_document_props()
props.active_drawing_index = 2
bpy.ops.bim.activate_drawing(drawing=drawing.id())
- self.drawing_styles = bpy.context.scene.DocProperties.drawing_styles
+ props = tool.Drawing.get_document_props()
+ self.drawing_styles = props.drawing_styles
def test_drawing_styles_not_loaded_if_underlay_is_inactive(self):
self.setup_project_with_drawing()
@@ -867,7 +876,8 @@ class TestDrawingStyles(NewFile):
class TestAddReferenceImage(NewFile):
def test_run(self):
- bpy.context.scene.BIMProjectProperties.template_file = "0"
+ props = tool.Project.get_project_props()
+ props.template_file = "0"
bpy.ops.bim.create_project()
ifc_path = Path("test/files/temp/test.ifc").absolute()
bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True)
diff --git a/src/bonsai/test/tool/test_geometry.py b/src/bonsai/test/tool/test_geometry.py
index f1ca935f30..63721bcdb7 100644
--- a/src/bonsai/test/tool/test_geometry.py
+++ b/src/bonsai/test/tool/test_geometry.py
@@ -171,14 +171,14 @@ class TestGetCartesianPointCoordinateOffset(NewFile):
def test_run(self):
obj = bpy.data.objects.new("Object", None)
obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT"
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
props.has_blender_offset = True
obj.BIMObjectProperties.cartesian_point_offset = "1,2,3"
assert np.allclose(subject.get_cartesian_point_offset(obj), np.array((1.0, 2.0, 3.0)))
def test_get_null_if_not_a_cartesian_point_offset_type(self):
obj = bpy.data.objects.new("Object", None)
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
props.has_blender_offset = True
obj.BIMObjectProperties.cartesian_point_offset = "1,2,3"
assert subject.get_cartesian_point_offset(obj) is None
@@ -186,7 +186,7 @@ class TestGetCartesianPointCoordinateOffset(NewFile):
def test_get_null_if_no_blender_offset(self):
obj = bpy.data.objects.new("Object", None)
obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT"
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
props.has_blender_offset = False
assert subject.get_cartesian_point_offset(obj) is None
@@ -237,14 +237,15 @@ class TestImportRepresentationParameters(NewFile):
item = ifc.createIfcExtrudedAreaSolid(SweptArea=swept_area, Depth=2)
representation = ifc.createIfcShapeRepresentation(Items=[item])
data = bpy.data.meshes.new("Mesh")
- data.BIMMeshProperties.ifc_definition_id = representation.id()
+ mprops = tool.Geometry.get_mesh_props(data)
+ mprops.ifc_definition_id = representation.id()
subject.import_representation_parameters(data)
- assert data.BIMMeshProperties.ifc_parameters[0].name == "IfcExtrudedAreaSolid/Depth"
- assert data.BIMMeshProperties.ifc_parameters[0].step_id == item.id()
- assert data.BIMMeshProperties.ifc_parameters[0].index == 3
- assert data.BIMMeshProperties.ifc_parameters[1].name == "IfcCircleProfileDef/Radius"
- assert data.BIMMeshProperties.ifc_parameters[1].step_id == swept_area.id()
- assert data.BIMMeshProperties.ifc_parameters[1].index == 3
+ assert mprops.ifc_parameters[0].name == "IfcExtrudedAreaSolid/Depth"
+ assert mprops.ifc_parameters[0].step_id == item.id()
+ assert mprops.ifc_parameters[0].index == 3
+ assert mprops.ifc_parameters[1].name == "IfcCircleProfileDef/Radius"
+ assert mprops.ifc_parameters[1].step_id == swept_area.id()
+ assert mprops.ifc_parameters[1].index == 3
class TestIsBodyRepresentation(NewFile):
@@ -293,7 +294,7 @@ class TestLink(NewFile):
element = ifc.createIfcShapeRepresentation()
obj = bpy.data.meshes.new("Mesh")
subject.link(element, obj)
- assert obj.BIMMeshProperties.ifc_definition_id == element.id()
+ assert tool.Geometry.get_mesh_props(obj).ifc_definition_id == element.id()
class TestRecordObjectMaterials(NewFile):
@@ -306,7 +307,7 @@ class TestRecordObjectMaterials(NewFile):
material.BIMStyleProperties.ifc_definition_id = style.id()
obj.data.materials.append(material)
subject.record_object_materials(obj)
- assert obj.data.BIMMeshProperties.material_checksum == str([style.id()])
+ assert tool.Geometry.get_mesh_props(obj).material_checksum == str([style.id()])
class TestRecordObjectPosition(NewFile):
diff --git a/src/bonsai/test/tool/test_georeference.py b/src/bonsai/test/tool/test_georeference.py
index 4b22a84781..96e498482c 100644
--- a/src/bonsai/test/tool/test_georeference.py
+++ b/src/bonsai/test/tool/test_georeference.py
@@ -39,7 +39,7 @@ class TestImportProjectedCRS(NewFile):
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
subject.import_projected_crs()
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
assert len(props.projected_crs) == 0
def test_importing_projected_crs(self):
@@ -58,7 +58,7 @@ class TestImportProjectedCRS(NewFile):
unit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT")
projected_crs.MapUnit = unit
subject.import_projected_crs()
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
assert props.projected_crs.get("Name").string_value == "Name"
assert props.projected_crs.get("Description").string_value == "Description"
assert props.projected_crs.get("GeodeticDatum").string_value == "GeodeticDatum"
@@ -71,7 +71,7 @@ class TestImportProjectedCRS(NewFile):
ifc = ifcopenshell.file(schema="IFC2X3")
tool.Ifc.set(ifc)
subject.import_projected_crs()
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
assert len(props.projected_crs) == 0
@@ -82,7 +82,7 @@ class TestImportCoordinateOperation(NewFile):
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
subject.import_coordinate_operation()
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
assert len(props.coordinate_operation) == 0
def test_importing_coordinate_operation(self):
@@ -99,7 +99,7 @@ class TestImportCoordinateOperation(NewFile):
map_conversion.XAxisOrdinate = 5
map_conversion.Scale = 6
subject.import_coordinate_operation()
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
assert props.coordinate_operation.get("Eastings").string_value == "1.0"
assert props.coordinate_operation.get("Northings").string_value == "2.0"
assert props.coordinate_operation.get("OrthogonalHeight").string_value == "3.0"
@@ -112,7 +112,7 @@ class TestImportCoordinateOperation(NewFile):
ifc = ifcopenshell.file(schema="IFC2X3")
tool.Ifc.set(ifc)
subject.import_coordinate_operation()
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
assert len(props.coordinate_operation) == 0
@@ -123,7 +123,7 @@ class TestImportTrueNorth(NewFile):
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
subject.import_true_north()
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
assert props.true_north_abscissa == "0"
assert props.true_north_ordinate == "1"
assert props.true_north_angle == "0"
@@ -135,7 +135,7 @@ class TestImportTrueNorth(NewFile):
context = ifcopenshell.api.run("context.add_context", ifc, context_type="Model")
context.TrueNorth = ifc.createIfcDirection((1.0, 2.0, 0.0))
subject.import_true_north()
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
assert props.true_north_abscissa == "1.0"
assert props.true_north_ordinate == "2.0"
assert props.true_north_angle == "-26.5650512"
@@ -176,35 +176,39 @@ class TestGetTrueNorthAttributes(NewFile):
class TestEnableEditing(NewFile):
def test_run(self):
- bpy.context.scene.BIMGeoreferenceProperties.is_editing = False
+ props = tool.Georeference.get_georeference_props()
+ props.is_editing = False
subject.enable_editing()
- assert bpy.context.scene.BIMGeoreferenceProperties.is_editing is True
+ assert props.is_editing is True
class TestDisableEditing(NewFile):
def test_run(self):
- bpy.context.scene.BIMGeoreferenceProperties.is_editing = True
+ props = tool.Georeference.get_georeference_props()
+ props.is_editing = True
subject.disable_editing()
- assert bpy.context.scene.BIMGeoreferenceProperties.is_editing is False
+ assert props.is_editing is False
class TestSetCoordinates(NewFile):
def test_run(self):
+ props = tool.Georeference.get_georeference_props()
subject.set_coordinates("local", [1.0, 2.0, 3.0])
- assert bpy.context.scene.BIMGeoreferenceProperties.local_coordinates == "1.0,2.0,3.0"
+ assert props.local_coordinates == "1.0,2.0,3.0"
subject.set_coordinates("blender", [4.0, 5.0, 6.0])
- assert bpy.context.scene.BIMGeoreferenceProperties.blender_coordinates == "4.0,5.0,6.0"
+ assert props.blender_coordinates == "4.0,5.0,6.0"
subject.set_coordinates("map", [7.0, 8.0, 9.0])
- assert bpy.context.scene.BIMGeoreferenceProperties.map_coordinates == "7.0,8.0,9.0"
+ assert props.map_coordinates == "7.0,8.0,9.0"
class TestGetCoordinates(NewFile):
def test_run(self):
- bpy.context.scene.BIMGeoreferenceProperties.local_coordinates = "1.0,2.0,3.0"
+ props = tool.Georeference.get_georeference_props()
+ props.local_coordinates = "1.0,2.0,3.0"
assert subject.get_coordinates("local") == [1.0, 2.0, 3.0]
- bpy.context.scene.BIMGeoreferenceProperties.blender_coordinates = "4.0,5.0,6.0"
+ props.blender_coordinates = "4.0,5.0,6.0"
assert subject.get_coordinates("blender") == [4.0, 5.0, 6.0]
- bpy.context.scene.BIMGeoreferenceProperties.map_coordinates = "7.0,8.0,9.0"
+ props.map_coordinates = "7.0,8.0,9.0"
assert subject.get_coordinates("map") == [7.0, 8.0, 9.0]
@@ -231,7 +235,7 @@ class TestXyz2Enh(NewFile):
ifc = ifcopenshell.file()
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
tool.Ifc.set(ifc)
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
props.has_blender_offset = True
props.blender_offset_x = "1.0"
assert subject.xyz2enh([0.0, 0.0, 0.0]) == (1.0, 0.0, 0.0)
@@ -247,7 +251,7 @@ class TestXyz2Enh(NewFile):
assert subject.xyz2enh([0.0, 0.0, 0.0]) == (1.0, 0.0, 0.0)
def test_applying_both_blender_offset_and_map_conversion(self):
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
props.has_blender_offset = True
props.blender_offset_x = "1.0"
ifc = ifcopenshell.file()
@@ -271,7 +275,7 @@ class TestEnh2Xyz(NewFile):
ifc = ifcopenshell.file()
ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject")
tool.Ifc.set(ifc)
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
props.has_blender_offset = True
props.blender_offset_x = "1.0"
assert subject.enh2xyz([0.0, 0.0, 0.0]) == (-1.0, 0.0, 0.0)
@@ -287,7 +291,7 @@ class TestEnh2Xyz(NewFile):
assert subject.enh2xyz([0.0, 0.0, 0.0]) == (-1.0, 0.0, 0.0)
def test_applying_both_blender_offset_and_map_conversion(self):
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
props.has_blender_offset = True
props.blender_offset_x = "1.0"
ifc = ifcopenshell.file()
diff --git a/src/bonsai/test/tool/test_ifc.py b/src/bonsai/test/tool/test_ifc.py
index 429dd76212..aedcea3465 100644
--- a/src/bonsai/test/tool/test_ifc.py
+++ b/src/bonsai/test/tool/test_ifc.py
@@ -40,12 +40,14 @@ class TestSet(test.bim.bootstrap.NewFile):
class TestGet(test.bim.bootstrap.NewFile):
def test_getting_an_ifc_dataset_from_a_ifc_spf_filepath(self):
assert subject.get() is None
- bpy.context.scene.BIMProperties.ifc_file = "test/files/basic.ifc"
+ props = tool.Blender.get_bim_props()
+ props.ifc_file = "test/files/basic.ifc"
result = subject.get()
assert isinstance(result, ifcopenshell.file)
def test_getting_the_active_ifc_dataset_regardless_of_ifc_path(self):
- bpy.context.scene.BIMProperties.ifc_file = "test/files/basic.ifc"
+ props = tool.Blender.get_bim_props()
+ props.ifc_file = "test/files/basic.ifc"
ifc = ifcopenshell.file()
subject.set(ifc)
assert subject.get() == ifc
@@ -189,7 +191,7 @@ class TestLink(test.bim.bootstrap.NewFile):
element = ifc.create_entity("IfcShapeRepresentation")
obj = bpy.data.meshes.new("Material")
subject.link(element, obj)
- assert obj.BIMMeshProperties.ifc_definition_id == element.id()
+ assert tool.Geometry.get_mesh_props(obj).ifc_definition_id == element.id()
class TestUnlink(test.bim.bootstrap.NewFile):
diff --git a/src/bonsai/test/tool/test_loader.py b/src/bonsai/test/tool/test_loader.py
index c8235ed52b..30ef9e22b9 100644
--- a/src/bonsai/test/tool/test_loader.py
+++ b/src/bonsai/test/tool/test_loader.py
@@ -520,7 +520,8 @@ class TestLoadingIndexedMap(NewFile):
class TestSetupActiveBsddClassification(NewFile):
def run_test(self, schema: ifcopenshell.util.schema.IFC_SCHEMA) -> None:
schema_ = "IFC4X3_ADD2" if schema == "IFC4X3" else schema
- bpy.context.scene.BIMProjectProperties.export_schema = schema_
+ props = tool.Project.get_project_props()
+ props.export_schema = schema_
bpy.ops.bim.create_project()
ifc_file = tool.Ifc.get()
name = "CCI Construction"
diff --git a/src/bonsai/test/tool/test_misc.py b/src/bonsai/test/tool/test_misc.py
index a274c89386..78a3c34cc5 100644
--- a/src/bonsai/test/tool/test_misc.py
+++ b/src/bonsai/test/tool/test_misc.py
@@ -24,7 +24,6 @@ import test.bim.bootstrap
import bonsai.core.tool
import bonsai.tool as tool
from bonsai.tool.misc import Misc as subject
-from bonsai.bim.ifc import IfcStore
class TestImplementsTool(test.bim.bootstrap.NewFile):
diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py
index 13a847f94a..f01399e88c 100644
--- a/src/bonsai/test/tool/test_model.py
+++ b/src/bonsai/test/tool/test_model.py
@@ -378,12 +378,13 @@ class TestGenerateStair2DProfile(NewFile):
class TestUsingArrays(NewFile):
def setup_array(self, add_second_layer=False, sync_children=False):
- bpy.context.scene.BIMProjectProperties.template_file = "0"
+ tool.Project.get_project_props().template_file = "0"
bpy.ops.bim.create_project()
bpy.ops.mesh.primitive_cube_add()
obj = bpy.context.active_object
- bpy.context.scene.BIMRootProperties.ifc_product = "IfcElement"
+ rprops = tool.Root.get_root_props()
+ rprops.ifc_product = "IfcElement"
bpy.ops.bim.assign_class(ifc_class="IfcActuator", predefined_type="ELECTRICACTUATOR", userdefined_type="")
bpy.ops.bim.add_array()
@@ -467,7 +468,8 @@ class TestApplyIfcMaterialChanges(NewFile):
return mesh
def setup_test(self, and_elements: bool = True) -> None:
- bpy.context.scene.BIMProjectProperties.template_file = "0"
+ props = tool.Project.get_project_props()
+ props.template_file = "0"
bpy.context.scene.unit_settings.length_unit = "MILLIMETERS"
bpy.ops.bim.create_project()
ifc_file = tool.Ifc.get()
@@ -519,7 +521,7 @@ class TestApplyIfcMaterialChanges(NewFile):
bpy.ops.bim.add_constr_type_instance(relating_type_id=relating_type_id)
with_opening = bpy.context.active_object
with_opening.name = "With Opening"
- props = bpy.context.scene.BIMRootProperties
+ props = tool.Root.get_root_props()
props.representation_obj = with_opening
bpy.ops.bim.add_element(ifc_product="IfcFeatureElement", ifc_class="IfcOpeningElement")
diff --git a/src/bonsai/test/tool/test_project.py b/src/bonsai/test/tool/test_project.py
index 4af7494a85..a87b00f753 100644
--- a/src/bonsai/test/tool/test_project.py
+++ b/src/bonsai/test/tool/test_project.py
@@ -97,7 +97,8 @@ class TestSetContext(NewFile):
tool.Ifc.set(ifc)
context = ifc.createIfcGeometricRepresentationContext()
subject.set_context(context)
- assert bpy.context.scene.BIMRootProperties.contexts == str(context.id())
+ rprops = tool.Root.get_root_props()
+ assert rprops.contexts == str(context.id())
class TestSetDefaultContext(NewFile):
@@ -115,7 +116,8 @@ class TestSetDefaultContext(NewFile):
target_view="MODEL_VIEW",
)
subject.set_default_context()
- assert bpy.context.scene.BIMRootProperties.contexts == str(body.id())
+ rprops = tool.Root.get_root_props()
+ assert rprops.contexts == str(body.id())
class TestSetDefaultModelingDimensions(NewFile):
@@ -244,25 +246,25 @@ class TestLoadProject(NewFile):
class TestLoadLinkedModels(NewFile):
def test_load_linked_models_no_document(self):
- links = bpy.context.scene.BIMProjectProperties.links
+ props = tool.Project.get_project_props()
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
subject.load_linked_models_from_ifc()
- assert len(links) == 0
+ assert len(props.links) == 0
def test_load_linked_models_document_no_references(self):
ifc = ifcopenshell.file()
- links = bpy.context.scene.BIMProjectProperties.links
+ props = tool.Project.get_project_props()
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
document = ifcopenshell.api.document.add_information(ifc)
document.Name = "BBIM_Linked_Models"
tool.Ifc.set(ifc)
subject.load_linked_models_from_ifc()
- assert len(links) == 0
+ assert len(props.links) == 0
def test_load_linked_models_document_with_references(self):
ifc = ifcopenshell.file()
- links = bpy.context.scene.BIMProjectProperties.links
+ props = tool.Project.get_project_props()
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
document = ifcopenshell.api.document.add_information(ifc)
document.Name = "BBIM_Linked_Models"
@@ -271,8 +273,8 @@ class TestLoadLinkedModels(NewFile):
reference.Location = linked_model_path
tool.Ifc.set(ifc)
subject.load_linked_models_from_ifc()
- assert len(links) == 1
- assert links[0].name == linked_model_path
+ assert len(props.links) == 1
+ assert props.links[0].name == linked_model_path
class TestSaveLinkedModelsToIfc(NewFile):
@@ -286,8 +288,8 @@ class TestSaveLinkedModelsToIfc(NewFile):
def test_save_linked_models_to_ifc_paths_to_add(self):
ifc = ifcopenshell.file()
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
- links = bpy.context.scene.BIMProjectProperties.links
- link = links.add()
+ props = tool.Project.get_project_props()
+ link = props.links.add()
linked_model_path = "test.ifc"
link.name = linked_model_path
tool.Ifc.set(ifc)
@@ -299,7 +301,7 @@ class TestSaveLinkedModelsToIfc(NewFile):
def test_save_linked_models_to_ifc_already_created_references(self):
ifc = ifcopenshell.file()
- links = bpy.context.scene.BIMProjectProperties.links
+ links = tool.Project.get_project_props().links
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
document = ifcopenshell.api.document.add_information(ifc)
@@ -326,7 +328,7 @@ class TestSaveLinkedModelsToIfc(NewFile):
def test_save_linked_models_to_ifc_references_to_remove(self):
ifc = ifcopenshell.file()
- links = bpy.context.scene.BIMProjectProperties.links
+ links = tool.Project.get_project_props().links
ifcopenshell.api.root.create_entity(ifc, "IfcProject")
document = ifcopenshell.api.document.add_information(ifc)
diff --git a/src/bonsai/test/tool/test_root.py b/src/bonsai/test/tool/test_root.py
index 19cba02029..2c89a4e5fa 100644
--- a/src/bonsai/test/tool/test_root.py
+++ b/src/bonsai/test/tool/test_root.py
@@ -120,8 +120,8 @@ class TestGetObjectRepresentation(NewFile):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
representation = ifc.createIfcShapeRepresentation()
- obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
- obj.data.BIMMeshProperties.ifc_definition_id = representation.id()
+ obj = bpy.data.objects.new("Object", (mesh := bpy.data.meshes.new("Mesh")))
+ tool.Geometry.get_mesh_props(mesh).ifc_definition_id = representation.id()
assert subject.get_object_representation(obj) == representation
@@ -175,7 +175,7 @@ class TestSetObjectName(NewFile):
class TestReassignClass(NewFile):
def test_reassigning_multiple_occurrences_of_the_same_type(self):
- bpy.context.scene.BIMProjectProperties.template_file = "IFC4 Demo Template.ifc"
+ tool.Project.get_project_props().template_file = "IFC4 Demo Template.ifc"
bpy.ops.bim.create_project()
ifc_file = tool.Ifc.get()
context = bpy.context
@@ -191,8 +191,10 @@ class TestReassignClass(NewFile):
slabs = [tool.Ifc.get_object(e) for e in ifc_file.by_type("IfcSlab")]
assert len(slabs) == 3
tool.Blender.set_objects_selection(context, slabs[0], (slabs[1],))
- context.scene.BIMRootProperties.ifc_product = "IfcElement"
- context.scene.BIMRootProperties.ifc_class = "IfcWall"
+
+ props = tool.Root.get_root_props()
+ props.ifc_product = "IfcElement"
+ props.ifc_class = "IfcWall"
bpy.ops.bim.reassign_class()
assert len(ifc_file.by_type("IfcWall")) == 3
diff --git a/src/bonsai/test/tool/test_spatial.py b/src/bonsai/test/tool/test_spatial.py
index 2985b458d1..9bcc84c6dc 100644
--- a/src/bonsai/test/tool/test_spatial.py
+++ b/src/bonsai/test/tool/test_spatial.py
@@ -129,7 +129,8 @@ class TestDisableEditing(NewFile):
obj = bpy.data.objects.new("Object", None)
subject.enable_editing(obj)
subject.disable_editing(obj)
- assert obj.BIMObjectSpatialProperties.is_editing is False
+ props = tool.Spatial.get_object_spatial_props(obj)
+ assert props.is_editing is False
class TestDuplicateObjectAndData(NewFile):
@@ -148,7 +149,8 @@ class TestEnableEditing(NewFile):
def test_run(self):
obj = bpy.data.objects.new("Object", None)
subject.enable_editing(obj)
- assert obj.BIMObjectSpatialProperties.is_editing is True
+ props = tool.Spatial.get_object_spatial_props(obj)
+ assert props.is_editing is True
class TestGetContainer(NewFile):
diff --git a/src/bonsai/test/tool/test_surveyor.py b/src/bonsai/test/tool/test_surveyor.py
index 0a12bd3903..bfba0cb480 100644
--- a/src/bonsai/test/tool/test_surveyor.py
+++ b/src/bonsai/test/tool/test_surveyor.py
@@ -20,6 +20,7 @@ import bpy
import numpy as np
import ifcopenshell
import ifcopenshell.api
+import ifcopenshell.util.geolocation
import test.bim.bootstrap
import bonsai.core.tool
@@ -34,7 +35,7 @@ class TestImplementsTool(test.bim.bootstrap.NewFile):
class TestGetGlobalMatrix(test.bim.bootstrap.NewFile):
def test_getting_an_absolute_matrix_if_no_blender_offset(self):
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
props.has_blender_offset = False
obj = bpy.data.objects.new("Object", None)
assert (subject.get_absolute_matrix(obj) == np.array(obj.matrix_world)).all()
@@ -45,7 +46,7 @@ class TestGetGlobalMatrix(test.bim.bootstrap.NewFile):
unit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", prefix="MILLI")
ifcopenshell.api.run("unit.assign_unit", ifc, units=[unit])
tool.Ifc.set(ifc)
- props = bpy.context.scene.BIMGeoreferenceProperties
+ props = tool.Georeference.get_georeference_props()
props.has_blender_offset = True
props.blender_offset_x = "1000"
props.blender_offset_y = "2000"
diff --git a/src/bonsai/test/tool/test_unit.py b/src/bonsai/test/tool/test_unit.py
index 4a8512c360..a3bad12ab3 100644
--- a/src/bonsai/test/tool/test_unit.py
+++ b/src/bonsai/test/tool/test_unit.py
@@ -18,6 +18,9 @@
import bpy
import ifcopenshell
+import ifcopenshell.api.project
+import ifcopenshell.api.root
+import ifcopenshell.api.unit
import bonsai.core.tool
import bonsai.tool as tool
from test.bim.bootstrap import NewFile
@@ -31,23 +34,26 @@ class TestImplementsTool(NewFile):
class TestClearActiveUnit(NewFile):
def test_run(self):
- bpy.context.scene.BIMUnitProperties.active_unit_id = 1
+ props = tool.Unit.get_unit_props()
+ props.active_unit_id = 1
subject.clear_active_unit()
- assert bpy.context.scene.BIMUnitProperties.active_unit_id == 0
+ assert props.active_unit_id == 0
class TestDisableEditingUnits(NewFile):
def test_run(self):
- bpy.context.scene.BIMUnitProperties.is_editing = True
+ props = tool.Unit.get_unit_props()
+ props.is_editing = True
subject.disable_editing_units()
- assert bpy.context.scene.BIMUnitProperties.is_editing == False
+ assert props.is_editing == False
class TestEnableEditingUnits(NewFile):
def test_run(self):
- bpy.context.scene.BIMUnitProperties.is_editing = False
+ props = tool.Unit.get_unit_props()
+ props.is_editing = False
subject.enable_editing_units()
- assert bpy.context.scene.BIMUnitProperties.is_editing == True
+ assert props.is_editing == True
class TestExportUnitAttributes(NewFile):
@@ -98,10 +104,11 @@ class TestExportUnitAttributes(NewFile):
class TestGetSceneUnitName(NewFile):
def test_getting_an_imperial_name(self):
+ props = tool.Blender.get_bim_props()
bpy.context.scene.unit_settings.system = "IMPERIAL"
bpy.context.scene.unit_settings.length_unit = "MILES"
- bpy.context.scene.BIMProperties.area_unit = "square foot"
- bpy.context.scene.BIMProperties.volume_unit = "cubic inch"
+ props.area_unit = "square foot"
+ props.volume_unit = "cubic inch"
assert subject.get_scene_unit_name("LENGTHUNIT") == "mile"
assert subject.get_scene_unit_name("AREAUNIT") == "square foot"
assert subject.get_scene_unit_name("VOLUMEUNIT") == "cubic inch"
@@ -142,13 +149,14 @@ class TestGetSceneUnitSIPrefix:
assert subject.get_scene_unit_si_prefix("LENGTHUNIT") == "KILO"
bpy.context.scene.unit_settings.length_unit = "ADAPTIVE"
assert subject.get_scene_unit_si_prefix("LENGTHUNIT") is None
- bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE"
+ props = tool.Blender.get_bim_props()
+ props.area_unit = "SQUARE_METRE"
assert subject.get_scene_unit_si_prefix("AREAUNIT") is None
- bpy.context.scene.BIMProperties.area_unit = "MILLI/SQUARE_METRE"
+ props.area_unit = "MILLI/SQUARE_METRE"
assert subject.get_scene_unit_si_prefix("AREAUNIT") == "MILLI"
- bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE"
+ props.volume_unit = "CUBIC_METRE"
assert subject.get_scene_unit_si_prefix("VOLUMEUNIT") is None
- bpy.context.scene.BIMProperties.volume_unit = "MILLI/CUBIC_METRE"
+ props.volume_unit = "MILLI/CUBIC_METRE"
assert subject.get_scene_unit_si_prefix("VOLUMEUNIT") == "MILLI"
@@ -159,25 +167,25 @@ class TestImportUnitAttributes(NewFile):
unit.UnitType = "ANGULARVELOCITYUNIT"
unit.UserDefinedType = "UserDefinedType"
subject.import_unit_attributes(unit)
- props = bpy.context.scene.BIMUnitProperties
- assert props.unit_attributes.get("UnitType").enum_value == "ANGULARVELOCITYUNIT"
- assert props.unit_attributes.get("UserDefinedType").string_value == "UserDefinedType"
+ props = tool.Unit.get_unit_props()
+ assert props.unit_attributes["UnitType"].enum_value == "ANGULARVELOCITYUNIT"
+ assert props.unit_attributes["UserDefinedType"].string_value == "UserDefinedType"
def test_importing_monetary_units(self):
tool.Ifc.set(ifc := ifcopenshell.file())
unit = ifc.createIfcMonetaryUnit()
unit.Currency = "Currency"
subject.import_unit_attributes(unit)
- props = bpy.context.scene.BIMUnitProperties
- assert props.unit_attributes.get("Currency").string_value == "Currency"
+ props = tool.Unit.get_unit_props()
+ assert props.unit_attributes["Currency"].string_value == "Currency"
def test_importing_monetary_units_ifc2x3(self):
tool.Ifc.set(ifc := ifcopenshell.file(schema="IFC2X3"))
unit = ifc.createIfcMonetaryUnit()
unit.Currency = "USD"
subject.import_unit_attributes(unit)
- props = bpy.context.scene.BIMUnitProperties
- assert props.unit_attributes.get("Currency").enum_value == "USD"
+ props = tool.Unit.get_unit_props()
+ assert props.unit_attributes["Currency"].enum_value == "USD"
def test_importing_context_dependent_units(self):
ifc = ifcopenshell.file()
@@ -187,10 +195,10 @@ class TestImportUnitAttributes(NewFile):
unit.Name = "Name"
unit.Dimensions = ifc.createIfcDimensionalExponents(1, 2, 3, 4, 5, 6, 7)
subject.import_unit_attributes(unit)
- props = bpy.context.scene.BIMUnitProperties
- assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT"
- assert props.unit_attributes.get("Name").string_value == "Name"
- assert props.unit_attributes.get("Dimensions").string_value == "[1, 2, 3, 4, 5, 6, 7]"
+ props = tool.Unit.get_unit_props()
+ assert props.unit_attributes["UnitType"].enum_value == "ABSORBEDDOSEUNIT"
+ assert props.unit_attributes["Name"].string_value == "Name"
+ assert props.unit_attributes["Dimensions"].string_value == "[1, 2, 3, 4, 5, 6, 7]"
def test_importing_conversion_based_units(self):
ifc = ifcopenshell.file()
@@ -200,10 +208,10 @@ class TestImportUnitAttributes(NewFile):
unit.Name = "Name"
unit.Dimensions = ifc.createIfcDimensionalExponents(1, 2, 3, 4, 5, 6, 7)
subject.import_unit_attributes(unit)
- props = bpy.context.scene.BIMUnitProperties
- assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT"
- assert props.unit_attributes.get("Name").string_value == "Name"
- assert props.unit_attributes.get("Dimensions").string_value == "[1, 2, 3, 4, 5, 6, 7]"
+ props = tool.Unit.get_unit_props()
+ assert props.unit_attributes["UnitType"].enum_value == "ABSORBEDDOSEUNIT"
+ assert props.unit_attributes["Name"].string_value == "Name"
+ assert props.unit_attributes["Dimensions"].string_value == "[1, 2, 3, 4, 5, 6, 7]"
def test_importing_conversion_based_with_offset_units(self):
ifc = ifcopenshell.file()
@@ -214,11 +222,11 @@ class TestImportUnitAttributes(NewFile):
unit.Dimensions = ifc.createIfcDimensionalExponents(1, 2, 3, 4, 5, 6, 7)
unit.ConversionOffset = 1
subject.import_unit_attributes(unit)
- props = bpy.context.scene.BIMUnitProperties
- assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT"
- assert props.unit_attributes.get("Name").string_value == "Name"
- assert props.unit_attributes.get("Dimensions").string_value == "[1, 2, 3, 4, 5, 6, 7]"
- assert props.unit_attributes.get("ConversionOffset").float_value == 1
+ props = tool.Unit.get_unit_props()
+ assert props.unit_attributes["UnitType"].enum_value == "ABSORBEDDOSEUNIT"
+ assert props.unit_attributes["Name"].string_value == "Name"
+ assert props.unit_attributes["Dimensions"].string_value == "[1, 2, 3, 4, 5, 6, 7]"
+ assert props.unit_attributes["ConversionOffset"].float_value == 1
def test_importing_si_units(self):
ifc = ifcopenshell.file()
@@ -228,11 +236,11 @@ class TestImportUnitAttributes(NewFile):
unit.Prefix = "EXA"
unit.Name = "AMPERE"
subject.import_unit_attributes(unit)
- props = bpy.context.scene.BIMUnitProperties
- assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT"
- assert props.unit_attributes.get("Prefix").enum_value == "EXA"
- assert props.unit_attributes.get("Name").enum_value == "AMPERE"
- assert props.unit_attributes.get("Dimensions") is None
+ props = tool.Unit.get_unit_props()
+ assert props.unit_attributes["UnitType"].enum_value == "ABSORBEDDOSEUNIT"
+ assert props.unit_attributes["Prefix"].enum_value == "EXA"
+ assert props.unit_attributes["Name"].enum_value == "AMPERE"
+ assert props.unit_attributes["Dimensions"] is None
class TestImportUnits(NewFile):
@@ -248,7 +256,7 @@ class TestImportUnits(NewFile):
ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject")
ifcopenshell.api.unit.assign_unit(ifc, units=[unit2])
subject.import_units()
- props = bpy.context.scene.BIMUnitProperties
+ props = tool.Unit.get_unit_props()
assert len(props.units) == 6
assert props.units[0].ifc_definition_id == unit1.id()
@@ -311,4 +319,5 @@ class TestSetActiveUnit(NewFile):
ifc = ifcopenshell.file()
unit = ifc.createIfcSIUnit()
subject.set_active_unit(unit)
- assert bpy.context.scene.BIMUnitProperties.active_unit_id == unit.id()
+ props = tool.Unit.get_unit_props()
+ assert props.active_unit_id == unit.id()
diff --git a/src/ifcgeom/ConversionSettings.h b/src/ifcgeom/ConversionSettings.h
index 65b11ba394..ed172cad69 100644
--- a/src/ifcgeom/ConversionSettings.h
+++ b/src/ifcgeom/ConversionSettings.h
@@ -407,7 +407,11 @@ namespace ifcopenshell {
static constexpr TriangulationMethod defaultvalue = TRIANGLE_MESH;
};
-
+ struct CgalEmitOriginalEdges : public SettingBase {
+ static constexpr const char* const name = "cgal-original-edges";
+ static constexpr const char* const description = "Try to emit original edge face boundary edges instead of recomputed ones based on face normal. Falls back to triangulated data in case of boolean operands and faces with holes.";
+ static constexpr bool defaultvalue = false;
+ };
}
template
@@ -500,7 +504,7 @@ namespace ifcopenshell {
};
class IFC_GEOM_API Settings : public SettingsContainer<
- std::tuple
+ std::tuple
>
{};
}
diff --git a/src/ifcgeom/function_item_evaluator.cpp b/src/ifcgeom/function_item_evaluator.cpp
index 9ca71b3450..1d5c0d186a 100644
--- a/src/ifcgeom/function_item_evaluator.cpp
+++ b/src/ifcgeom/function_item_evaluator.cpp
@@ -1,8 +1,16 @@
#include "function_item_evaluator.h"
#include "profile_helper.h"
+#include
+
using namespace ifcopenshell::geometry;
+double ifcopenshell::geometry::polynomial_length(double A, double B, double C, double horizontal_length) {
+ auto fn = [A, B, C](double x) -> double { return sqrt(pow(B + 2 * C * x, 2.0) + 1.0); };
+ auto l = boost::math::quadrature::trapezoidal(fn, 0.0, horizontal_length);
+ return l;
+}
+
struct functor_fn_evaluator : public fn_evaluator {
functor_fn_evaluator(taxonomy::functor_item::const_ptr fn, const ifcopenshell::geometry::Settings& settings) : fn_evaluator(settings),
diff --git a/src/ifcgeom/function_item_evaluator.h b/src/ifcgeom/function_item_evaluator.h
index 0e29c6149c..3107b615dc 100644
--- a/src/ifcgeom/function_item_evaluator.h
+++ b/src/ifcgeom/function_item_evaluator.h
@@ -7,6 +7,17 @@
namespace ifcopenshell { namespace geometry {
+/// @brief Computes the curve length of a polynomial of the form y = A + Bx + Cx^2
+/// This function is needed on the python side. To do this computation, a large library like scipy
+/// is needed. That is too much overhead. For this reason, a simple function is here on the C++ side
+/// that the python side can call
+/// @param A constant term
+/// @param B linear term
+/// @param C quadradic term
+/// @param horizontal_length length of the polynomal projected onto the horizontal axis
+/// @return curve length
+double polynomial_length(double A, double B, double C,double horizontal_length);
+
/// @brief Abstract class for evaluating a function_item. This class is specialized for each of the function_item types.
struct fn_evaluator {
fn_evaluator(const ifcopenshell::geometry::Settings& settings) : settings_(settings) {
diff --git a/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp b/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp
index ac991e40a3..e7a3b2a9ce 100644
--- a/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp
+++ b/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp
@@ -144,10 +144,15 @@ ifcopenshell::geometry::CgalShape::CgalShape(const cgal_shape_t& shape, bool con
}
if (shape.size_of_facets() != 1) {
- // this is for handling the specical case of storing a single point in a polyhedron,
+ // the size_of_facets() == 1 check is for handling the specical case of
+ // storing a single point in a polyhedron as a degenerate triangle
+ //
// @todo come up with a proper variant for storing lower dimensional entities
- CGAL::Polygon_mesh_processing::triangulate_faces(*shape_);
- CGAL::Polygon_mesh_processing::remove_degenerate_faces(*shape_);
+
+ // @todo we don't have access to settings here so we don't know whether we should triangulate
+ // remove_degenerate_faces() is also called in the triangulate() call below though...
+ // CGAL::Polygon_mesh_processing::triangulate_faces(*shape_);
+ // CGAL::Polygon_mesh_processing::remove_degenerate_faces(*shape_);
}
}
@@ -183,6 +188,15 @@ void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Sett
// ... also becuase of transforming the vertex positions, right?
cgal_shape_t s = *this;
+ const bool setting_use_original_edges = settings.get().get();
+
+ std::set> original_edges;
+ if (setting_use_original_edges) {
+ for (auto it = s.edges_begin(); it != s.edges_end(); ++it) {
+ original_edges.insert({ it->vertex()->point(), it->prev()->vertex()->point() });
+ }
+ }
+
if (!place.is_identity()) {
const auto& m = place.ccomponents();
@@ -199,14 +213,11 @@ void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Sett
}
if (!std::all_of(s.facets_begin(), s.facets_end(), [](auto f) { return f.is_triangle(); })) {
-
if (!s.is_valid()) {
Logger::Message(Logger::LOG_ERROR, "Invalid Polyhedron_3 in object (before triangulation)");
return;
}
- CGAL::Polygon_mesh_processing::remove_degenerate_faces(s);
-
bool success = false;
try {
success = CGAL::Polygon_mesh_processing::triangulate_faces(s);
@@ -215,27 +226,29 @@ void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Sett
return;
}
+ CGAL::Polygon_mesh_processing::remove_degenerate_faces(s);
+
if (!success) {
Logger::Message(Logger::LOG_ERROR, "Triangulation failed");
return;
}
- // std::cout << "Triangulated model: " << s.size_of_facets() << " facets and " << s.size_of_vertices() << " vertices" << std::endl;
if (!s.is_valid()) {
Logger::Message(Logger::LOG_ERROR, "Invalid Polyhedron_3 in object (after triangulation)");
// return;
}
-
}
// Facet -> planar component map for determining which
// edges are to be registered.
std::vector> components;
- partition_coplanar_components(s, components);
std::map facet_to_component;
- for (auto it = components.begin(); it != components.end(); ++it) {
- for (auto& f : *it) {
- facet_to_component[f] = it;
+ if (!setting_use_original_edges) {
+ partition_coplanar_components(s, components);
+ for (auto it = components.begin(); it != components.end(); ++it) {
+ for (auto& f : *it) {
+ facet_to_component[f] = it;
+ }
}
}
@@ -305,8 +318,10 @@ void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Sett
}
vertexidx[i] = (int)vidx;
- is_face_boundary[i] = facet_to_component[face] != facet_to_component[current_halfedge->opposite()->face()];
-
+ is_face_boundary[i] = setting_use_original_edges
+ ? original_edges.find({ current_halfedge->vertex()->point(), current_halfedge->prev()->vertex()->point() }) != original_edges.end()
+ : facet_to_component[face] != facet_to_component[current_halfedge->opposite()->face()];
+
++i;
++num_vertices;
++current_halfedge;
diff --git a/src/ifcgeom/mapping/IfcArbitraryClosedProfileDef.cpp b/src/ifcgeom/mapping/IfcArbitraryClosedProfileDef.cpp
index 6a51091f99..3cce6f31dc 100644
--- a/src/ifcgeom/mapping/IfcArbitraryClosedProfileDef.cpp
+++ b/src/ifcgeom/mapping/IfcArbitraryClosedProfileDef.cpp
@@ -26,6 +26,10 @@ using namespace ifcopenshell::geometry;
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcArbitraryClosedProfileDef* inst) {
auto loop = taxonomy::cast(map(inst->OuterCurve()));
if (loop) {
+ if (inst->ProfileType() == IfcSchema::IfcProfileTypeEnum::IfcProfileType_CURVE) {
+ return loop;
+ }
+
auto face = taxonomy::make();
loop->external = true;
face->children = { loop };
diff --git a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp
index 14ee9899ad..52187d7e5e 100644
--- a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp
+++ b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp
@@ -31,10 +31,10 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in
std::vector cross_sections;
auto dir = map(inst->Directrix());
- auto pwf = taxonomy::dcast(dir);
- if (!pwf) {
+ auto fn = taxonomy::dcast(dir);
+ if (!fn) {
// Only implement on alignment curves
- Logger::Warning("IfcSectionedSolidHorizontal is only implemented for piecewise function Directrix curves", inst);
+ Logger::Warning("IfcSectionedSolidHorizontal is only implemented for Directrix curves based on taxonomy::function_item", inst);
return nullptr;
}
@@ -70,9 +70,6 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in
profile_offsets.push_back(po);
}
-#else
- return nullptr;
-#endif
if (faces.size() != profile_offsets.size()) {
Logger::Warning("Expected CrossSections and CrossSectionPositions to be equal length, but got " + std::to_string(faces.size()) + " and " + std::to_string(profile_offsets.size()) + " respectively", inst);
return nullptr;
@@ -85,9 +82,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in
for (size_t i = 0; i < faces.size(); ++i) {
cross_sections.push_back({ longitudes[i], faces[i], profile_offsets[i] });
}
- }
+#else
+ return nullptr;
+#endif
+ }
- return make_loft(settings_, inst, pwf, cross_sections);
+ return make_loft(settings_, inst, fn, cross_sections);
}
#endif
diff --git a/src/ifcgeom/mapping/IfcSweptDiskSolid.cpp b/src/ifcgeom/mapping/IfcSweptDiskSolid.cpp
index 1eec8c6bc7..bd779fd6cd 100644
--- a/src/ifcgeom/mapping/IfcSweptDiskSolid.cpp
+++ b/src/ifcgeom/mapping/IfcSweptDiskSolid.cpp
@@ -58,8 +58,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSweptDiskSolid* inst) {
auto ep = inst->EndParam();
#else
boost::optional sp, ep;
- sp = inst->StartParam();
- ep = inst->EndParam();
+ try {
+ sp = inst->StartParam();
+ ep = inst->EndParam();
+ } catch (const IfcParse::IfcException& e) {
+ Logger::Warning(e);
+ }
#endif
const double tol = settings_.get().get();
diff --git a/src/ifcgeom/taxonomy.cpp b/src/ifcgeom/taxonomy.cpp
index b2d48478f3..6fc7288dda 100644
--- a/src/ifcgeom/taxonomy.cpp
+++ b/src/ifcgeom/taxonomy.cpp
@@ -800,13 +800,14 @@ boost::optional ifcopenshell::geometry::taxonomy::curve_to_face_upgra
}
-boost::optional ifcopenshell::geometry::taxonomy::loop_to_piecewise_function_upgrade_impl(ptr item) {
- boost::optional pwf_;
+boost::optional ifcopenshell::geometry::taxonomy::loop_to_function_item_upgrade_impl(ptr item) {
+ boost::optional fi_;
auto loop_ = dcast(item);
if (loop_) {
- if (loop_->pwf.is_initialized()) {
- pwf_ = loop_->pwf;
+ if (loop_->fi.is_initialized()) {
+ fi_ = loop_->fi;
} else {
+ // piecewise_function is a specialization of function_item - callers don't need to know this detail
piecewise_function::spans_t spans;
spans.reserve(loop_->children.size());
for (auto& edge_ : loop_->children) {
@@ -828,9 +829,9 @@ boost::optional ifcopenshell::geometry::taxonomy::loop_
};
spans.emplace_back(taxonomy::make(l, fn));
}
- pwf_ = make(0.0,spans);
- loop_->pwf = pwf_;
+ fi_ = make(0.0,spans);
+ loop_->fi = fi_;
}
}
- return pwf_;
+ return fi_;
}
diff --git a/src/ifcgeom/taxonomy.h b/src/ifcgeom/taxonomy.h
index 4f73f10a66..703504abc2 100644
--- a/src/ifcgeom/taxonomy.h
+++ b/src/ifcgeom/taxonomy.h
@@ -892,7 +892,7 @@ typedef item const* ptr;
DECLARE_PTR(loop)
boost::optional external, closed;
- boost::optional pwf;
+ boost::optional fi;
bool is_polyhedron() const {
for (auto& e : children) {
@@ -1374,27 +1374,27 @@ typedef item const* ptr;
}
};
- boost::optional loop_to_piecewise_function_upgrade_impl(ptr item);
+ boost::optional loop_to_function_item_upgrade_impl(ptr item);
template
- class loop_to_piecewise_function_upgrade {
+ class loop_to_function_item_upgrade {
private:
- boost::optional pwf_;
+ boost::optional fi_;
public:
- loop_to_piecewise_function_upgrade(taxonomy::ptr item) {
- if constexpr (std::is_same_v) {
- pwf_ = loop_to_piecewise_function_upgrade_impl(item);
+ loop_to_function_item_upgrade(taxonomy::ptr item) {
+ if constexpr (std::is_same_v) {
+ fi_ = loop_to_function_item_upgrade_impl(item);
}
}
operator bool() const {
- return pwf_.is_initialized();
+ return fi_.is_initialized();
}
operator typename T::ptr() const {
- if constexpr (std::is_same_v) {
- if (pwf_) {
- return *pwf_;
+ if constexpr (std::is_same_v) {
+ if (fi_) {
+ return *fi_;
}
}
return nullptr;
@@ -1435,7 +1435,7 @@ typedef item const* ptr;
}
}
{
- loop_to_piecewise_function_upgrade upg(u);
+ loop_to_function_item_upgrade upg(u);
if (upg) {
return upg;
}
@@ -1479,7 +1479,7 @@ typedef item const* ptr;
}
}
{
- loop_to_piecewise_function_upgrade upg(u);
+ loop_to_function_item_upgrade upg(u);
if (upg) {
return upg;
}
diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst
index df7ae3321e..0c4753807a 100644
--- a/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst
+++ b/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst
@@ -306,39 +306,43 @@ In addition to geometry settings, serialisation has its own set of
.. code-block:: python
- import ifcopenshell
- import ifcopenshell.geom
- import multiprocessing
-
- settings = ifcopenshell.geom.settings()
-
- # Settings for glTF / glb
- settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
- # Note that applying default materials is required in glTF serialisation.
- settings.set("apply-default-materials", True)
-
- # Settings for obj
- # settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
- # settings.set("apply-default-materials", True)
- # settings.set("use-world-coords", True)
-
- # Serialise to glTF / glb
- serialiser = ifcopenshell.geom.serializers.gltf("output.glb", settings)
- self.serialiser_settings = ifcopenshell.geom.serializer_settings()
- # Setting element GUIDs is optional, but useful to uniquely identify objects in non-semantic formats.
- serialiser_settings.set("use-element-guids", True)
-
- # Serialise to obj
- # serialiser = ifcopenshell.geom.serializers.obj('output.obj', 'output.mtl', settings, serialiser_settings)
-
- serialiser.setFile(self.file)
- serialiser.setUnitNameAndMagnitude("METER", 1.0)
- serialiser.writeHeader()
-
- iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count())
- if iterator.initialize():
- while True:
- serialiser.write(iterator.get())
- if not iterator.next():
- break
- serialiser.finalize()
+ import multiprocessing
+
+ import ifcopenshell
+ import ifcopenshell.geom
+
+ ifc_file = ifcopenshell.open("model.ifc")
+
+ settings = ifcopenshell.geom.settings()
+
+ # Settings for glTF / glb
+ settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
+ # Note that applying default materials is required in glTF serialisation.
+ settings.set("apply-default-materials", True)
+
+ # Settings for obj
+ # settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
+ # settings.set("apply-default-materials", True)
+ # settings.set("use-world-coords", True)
+
+ serialiser_settings = ifcopenshell.geom.serializer_settings()
+ # Setting element GUIDs is optional, but useful to uniquely identify objects in non-semantic formats.
+ serialiser_settings.set("use-element-guids", True)
+
+ # Serialise to glTF / glb
+ serialiser = ifcopenshell.geom.serializers.gltf("output.glb", settings, serialiser_settings)
+
+ # Serialise to obj
+ # serialiser = ifcopenshell.geom.serializers.obj('output.obj', 'output.mtl', settings, serialiser_settings)
+
+ serialiser.setFile(ifc_file)
+ serialiser.setUnitNameAndMagnitude("METER", 1.0)
+ serialiser.writeHeader()
+
+ iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count())
+ if iterator.initialize():
+ while True:
+ serialiser.write(iterator.get())
+ if not iterator.next():
+ break
+ serialiser.finalize()
diff --git a/src/ifcopenshell-python/ifcopenshell/alignment.py b/src/ifcopenshell-python/ifcopenshell/alignment.py
index a40151068d..45eb8bc054 100644
--- a/src/ifcopenshell-python/ifcopenshell/alignment.py
+++ b/src/ifcopenshell-python/ifcopenshell/alignment.py
@@ -28,6 +28,8 @@ import ifcopenshell.guid
import ifcopenshell.template
from ifcopenshell import entity_instance
from ifcopenshell import ifcopenshell_wrapper
+import ifcopenshell.util
+import ifcopenshell.util.stationing
def evaluate_representation(shape_rep: entity_instance, dist_along: float) -> np.ndarray:
@@ -46,10 +48,10 @@ def evaluate_representation(shape_rep: entity_instance, dist_along: float) -> np
# TODO: confirm point is not beyond limits of alignment
s = ifcopenshell.geom.settings()
- piecewise_function = ifcopenshell_wrapper.map_shape(s, shape_rep.wrapped_data)
- pwf_evaluator = ifcopenshell_wrapper.piecewise_function_evaluator(piecewise_function, s)
+ function_item = ifcopenshell_wrapper.map_shape(s, shape_rep.wrapped_data)
+ evaluator = ifcopenshell_wrapper.function_item_evaluator(s, function_item)
- trans_matrix = pwf_evaluator.evaluate(dist_along)
+ trans_matrix = evaluator.evaluate(dist_along)
return np.array(trans_matrix, dtype=np.float64).T
@@ -68,10 +70,10 @@ def evaluate_segment(segment: entity_instance, dist_along: float) -> np.ndarray:
raise ValueError(f"Provided value {dist_along=} is beyond the end of the segment ({segment.SegmentLength}).")
s = ifcopenshell.geom.settings()
- piecewise_function = ifcopenshell_wrapper.map_shape(s, segment.wrapped_data)
- pwf_evaluator = ifcopenshell_wrapper.piecewise_function_evaluator(piecewise_function, s)
+ function_item = ifcopenshell_wrapper.map_shape(s, segment.wrapped_data)
+ evaluator = ifcopenshell_wrapper.function_item_evaluator(s, function_item)
- trans_matrix = pwf_evaluator.evaluate(dist_along)
+ trans_matrix = evaluator.evaluate(dist_along)
return np.array(trans_matrix, dtype=np.float64).T
@@ -90,7 +92,8 @@ def generate_vertices(rep_curve: entity_instance, distance_interval: float = 5.0
raise ValueError("Alignment representation not found.")
s = ifcopenshell.geom.settings()
- s.set("PIECEWISE_STEP_PARAM", distance_interval)
+ s.set("piecewise-step-type", 0) # 0 = step-size is maximum step size, 1 = step-size is mininimum number of steps
+ s.set("piecewise-step-size", distance_interval)
shape = ifcopenshell.geom.create_shape(s, rep_curve)
vertices = shape.verts
if len(vertices) == 0:
@@ -185,6 +188,148 @@ class IfcAlignmentHelper:
alignment_segment.ObjectPlacement = global_placement
alignment_segment.Representation = product
+ def _map_alignment_vertical_segment(self, segment: entity_instance) -> Sequence[entity_instance]:
+ segment_type = segment.is_a().upper()
+ expected_type = "IFCALIGNMENTVERTICALSEGMENT"
+ if not segment_type == expected_type:
+ raise TypeError(f"Expected to see type '{expected_type}', instead received '{segment_type}'.")
+
+ start_distance_along = segment.StartDistAlong
+ horizontal_length = segment.HorizontalLength
+ start_height = segment.StartHeight
+ start_gradient = segment.StartGradient
+ end_gradient = segment.EndGradient
+ radius_of_curvature = segment.RadiusOfCurvature
+
+ if math.isclose(horizontal_length, 0):
+ # set transition value based on whether this is the final zero-length segment
+ transition = "DISCONTINUOUS"
+ else:
+ transition = "CONTSAMEGRADIENTSAMECURVATURE"
+
+ _type = segment.PredefinedType
+
+ match _type:
+ case "CONSTANTGRADIENT":
+ parent_curve = self._file.create_entity(
+ type="IfcLine",
+ Pnt=self._file.create_entity(
+ type="IfcCartesianPoint",
+ Coordinates=(0.0, 0.0),
+ ),
+ Dir=self._file.create_entity(
+ type="IfcVector",
+ Orientation=self._file.create_entity(
+ type="IfcDirection",
+ DirectionRatios=(1.0, 0.0),
+ ),
+ Magnitude=1.0,
+ ),
+ )
+
+ dx = math.cos(math.atan(start_gradient))
+ dy = math.sin(math.atan(start_gradient))
+ curve_segment_length = horizontal_length / dx
+
+ curve_segment = self._file.create_entity(
+ type="IfcCurveSegment",
+ Transition=transition,
+ Placement=self._file.create_entity(
+ type="IfcAxis2Placement2D",
+ Location=self._file.create_entity(
+ type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height)
+ ),
+ RefDirection=self._file.createIfcDirection((dx, dy)),
+ ),
+ SegmentStart=self._file.createIfcLengthMeasure(0.0),
+ SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length),
+ ParentCurve=parent_curve,
+ )
+ result = (curve_segment, None)
+
+ case "PARABOLICARC":
+ A = start_height
+ B = start_gradient
+ C = (end_gradient - start_gradient) / (2.0 * horizontal_length)
+
+ parent_curve = self._file.create_entity(
+ type="IfcPolynomialCurve",
+ Position=self._file.create_entity(
+ type="IfcAxis2Placement2D",
+ Location=self._file.create_entity(type="IfcCartesianPoint", Coordinates=(0.0, 0.0)),
+ RefDirection=self._file.createIfcDirection(
+ (1.0, 0.0),
+ ),
+ ),
+ CoefficientsX=(0.0, 1.0),
+ CoefficientsY=(A, B, C),
+ )
+
+ dx = math.cos(math.atan(start_gradient))
+ dy = math.sin(math.atan(start_gradient))
+ curve_segment_length = ifcopenshell_wrapper.polynomial_length(A, B, C, horizontal_length)
+
+ curve_segment = self._file.create_entity(
+ type="IfcCurveSegment",
+ Transition=transition,
+ Placement=self._file.create_entity(
+ type="IfcAxis2Placement2D",
+ Location=self._file.create_entity(
+ type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height)
+ ),
+ RefDirection=self._file.createIfcDirection((dx, dy)),
+ ),
+ SegmentStart=self._file.createIfcLengthMeasure(0.0),
+ SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length),
+ ParentCurve=parent_curve,
+ )
+ result = (curve_segment, None)
+
+ case "CIRCULARARC":
+ start_angle = math.atan(start_gradient)
+ end_angle = math.atan(end_gradient)
+ if start_angle < end_angle:
+ radius = horizontal_length / (math.sin(end_angle) - math.sin(start_angle))
+ else:
+ radius = horizontal_length / (math.sin(start_angle) - math.sin(end_angle))
+
+ parent_curve = self._file.create_entity(
+ type="IfcCircle",
+ Position=self._file.create_entity(
+ type="IfcAxis2Placement2D",
+ Location=self._file.create_entity(type="IfcCartesianPoint", Coordinates=(0.0, 0.0)),
+ RefDirection=self._file.createIfcDirection(
+ (1.0, 0.0),
+ ),
+ ),
+ Radius=radius,
+ )
+
+ segment_curve_length = radius * math.fabs(end_angle - start_angle)
+
+ curve_segment = self._file.create_entity(
+ type="IfcCurveSegment",
+ Transition=transition,
+ Placement=self._file.create_entity(
+ type="IfcAxis2Placement2D",
+ Location=self._file.create_entity(
+ type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height)
+ ),
+ RefDirection=self._file.createIfcDirection(
+ (1.0, 0.0),
+ ),
+ ),
+ SegmentStart=self._file.createIfcLengthMeasure(0.0),
+ SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length),
+ ParentCurve=parent_curve,
+ )
+ result = (curve_segment, None)
+
+ case _:
+ result = (None, None)
+
+ return result
+
def _map_alignment_horizontal_segment(self, segment: entity_instance) -> Sequence[entity_instance]:
segment_type = segment.is_a().upper()
expected_type = "IFCALIGNMENTHORIZONTALSEGMENT"
@@ -203,67 +348,62 @@ class IfcAlignmentHelper:
else:
transition = "CONTSAMEGRADIENTSAMECURVATURE"
- match _type:
- case "LINE":
- parent_curve = self._file.create_entity(
- type="IfcLine",
- Pnt=self._file.create_entity(
- type="IfcCartesianPoint",
- Coordinates=(0.0, 0.0),
+ if _type == "LINE":
+ parent_curve = self._file.create_entity(
+ type="IfcLine",
+ Pnt=self._file.create_entity(
+ type="IfcCartesianPoint",
+ Coordinates=(0.0, 0.0),
+ ),
+ Dir=self._file.create_entity(
+ type="IfcVector",
+ Orientation=self._file.create_entity(
+ type="IfcDirection",
+ DirectionRatios=(1.0, 0.0),
),
- Dir=self._file.create_entity(
- type="IfcVector",
- Orientation=self._file.create_entity(
- type="IfcDirection",
- DirectionRatios=(1.0, 0.0),
- ),
- Magnitude=1.0,
+ Magnitude=1.0,
+ ),
+ )
+ curve_segment = self._file.create_entity(
+ type="IfcCurveSegment",
+ Transition=transition,
+ Placement=self._file.create_entity(
+ type="IfcAxis2Placement2D",
+ Location=start_point,
+ RefDirection=self._file.createIfcDirection(
+ (math.cos(start_direction), math.sin(start_direction)),
),
- )
- curve_segment = self._file.create_entity(
- type="IfcCurveSegment",
- Transition=transition,
- Placement=self._file.create_entity(
- type="IfcAxis2Placement2D",
- Location=start_point,
- RefDirection=self._file.createIfcDirection(
- (math.cos(start_direction), math.sin(start_direction)),
- ),
- ),
- SegmentStart=self._file.createIfcLengthMeasure(0.0),
- SegmentLength=self._file.createIfcLengthMeasure(length),
- ParentCurve=parent_curve,
- )
- result = (curve_segment, None)
- case "CIRCULARARC":
- parent_curve = self._file.createIfcCircle(
- Position=self._file.createIfcAxis2Placement2D(
- Location=self._file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)),
- RefDirection=self._file.createIfcDirection(
- (math.cos(start_direction), math.sin(start_direction))
- ),
- ),
- Radius=abs(start_radius),
- )
+ ),
+ SegmentStart=self._file.createIfcLengthMeasure(0.0),
+ SegmentLength=self._file.createIfcLengthMeasure(length),
+ ParentCurve=parent_curve,
+ )
+ result = (curve_segment, None)
+ elif _type == "CIRCULARARC":
+ parent_curve = self._file.createIfcCircle(
+ Position=self._file.createIfcAxis2Placement2D(
+ Location=self._file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)),
+ RefDirection=self._file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))),
+ ),
+ Radius=abs(start_radius),
+ )
- curve_segment = self._file.create_entity(
- type="IfcCurveSegment",
- Transition=transition,
- Placement=self._file.create_entity(
- type="IfcAxis2Placement2D",
- Location=start_point,
- RefDirection=self._file.createIfcDirection(
- (math.cos(start_direction), math.sin(start_direction))
- ),
- ),
- SegmentStart=self._file.createIfcLengthMeasure(0.0),
- SegmentLength=self._file.createIfcLengthMeasure(length * start_radius / abs(start_radius)),
- ParentCurve=parent_curve,
- )
- result = (curve_segment, None)
+ curve_segment = self._file.create_entity(
+ type="IfcCurveSegment",
+ Transition=transition,
+ Placement=self._file.create_entity(
+ type="IfcAxis2Placement2D",
+ Location=start_point,
+ RefDirection=self._file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))),
+ ),
+ SegmentStart=self._file.createIfcLengthMeasure(0.0),
+ SegmentLength=self._file.createIfcLengthMeasure(length * start_radius / abs(start_radius)),
+ ParentCurve=parent_curve,
+ )
+ result = (curve_segment, None)
- case _:
- result = (None, None)
+ else:
+ result = (None, None)
return result
@@ -555,10 +695,13 @@ class IfcAlignmentHelper:
alignment.Representation = product_definition_shape
# create referent for start station
+ start_station_name = "Start Station ({})".format(
+ ifcopenshell.util.stationing.station_as_string(start_station)
+ )
start_referent = self._file.createIfcReferent(
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
- Name="Start Station",
+ Name=start_station_name,
Description=None,
ObjectType=None,
ObjectPlacement=self._file.createIfcLinearPlacement(
@@ -576,6 +719,8 @@ class IfcAlignmentHelper:
Representation=None,
PredefinedType="STATION",
)
+ pset_stationing = ifcopenshell.api.pset.add_pset(self._file, product=start_referent, name="Pset_Stationing")
+ ifcopenshell.api.pset.edit_pset(self._file, pset=pset_stationing, properties={"Station": start_station})
# nest the horizontal and the referent under the alignment
nesting_of_alignment = self._file.create_entity(
@@ -599,12 +744,11 @@ class IfcAlignmentHelper:
return alignment
- def add_vertical_alignment(
+ def _create_vertical_alignment(
self,
- name: str,
- description: str,
+ composite_curve: entity_instance,
vpoints: Sequence[Sequence[float]],
- vclengths: Sequence[Sequence[float]],
+ lengths: Sequence[float],
include_geometry: bool = True,
):
"""
@@ -613,12 +757,358 @@ class IfcAlignmentHelper:
@param name: value for Name attribute
@param description: value for Description attribute
@param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end.
- @param vclengths: radii values to use for transition
+ @param vclengths: horizontal length of parabolic vertical curves
@param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic
"""
- pass
+ vertical_segments = list() # business logic
+ vertical_curve_segments = list() # geometry
+ xPBG, yPBG = vpoints[0]
+ xPVI, yPVI = vpoints[1]
+ i = 1
+ for length in lengths:
+ # back gradient
+ dxBG = xPVI - xPBG
+ dyBG = yPVI - yPBG
+ start_slope = math.tan(math.atan2(dyBG, dxBG))
- def add_alignment(
+ # forward gradient
+ i += 1
+ xPFG, yPFG = vpoints[i]
+ dxFG = xPFG - xPVI
+ dyFG = yPFG - yPVI
+ end_slope = math.tan(math.atan2(dyFG, dxFG))
+
+ xEVC = xPVI + length / 2.0
+ yEVC = yPVI + end_slope * length / 2.0
+
+ # create gradient
+ gradient_length = dxBG - length / 2.0
+ design_parameters = self._file.create_entity(
+ type="IfcAlignmentVerticalSegment",
+ StartTag=None,
+ EndTag=None,
+ StartDistAlong=xPBG,
+ HorizontalLength=gradient_length,
+ StartHeight=yPBG,
+ StartGradient=start_slope,
+ EndGradient=start_slope,
+ RadiusOfCurvature=None,
+ PredefinedType="CONSTANTGRADIENT",
+ )
+ alignment_segment = self._file.create_entity(
+ type="IfcAlignmentSegment",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name=None,
+ Description=None,
+ ObjectType=None,
+ ObjectPlacement=None,
+ Representation=None,
+ DesignParameters=design_parameters,
+ )
+ vertical_segments.append(alignment_segment)
+
+ if include_geometry:
+ vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0])
+
+ # create vertical curve
+ k = (end_slope - start_slope) / length
+ xBVC = xPVI - length / 2.0
+ yBVC = yPVI - start_slope * length / 2.0
+
+ design_parameters = self._file.create_entity(
+ type="IfcAlignmentVerticalSegment",
+ StartTag=None,
+ EndTag=None,
+ StartDistAlong=xBVC,
+ HorizontalLength=length,
+ StartHeight=yBVC,
+ StartGradient=start_slope,
+ EndGradient=end_slope,
+ RadiusOfCurvature=1 / k,
+ PredefinedType="PARABOLICARC",
+ )
+ alignment_segment = self._file.create_entity(
+ type="IfcAlignmentSegment",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name=None,
+ Description=None,
+ ObjectType=None,
+ ObjectPlacement=None,
+ Representation=None,
+ DesignParameters=design_parameters,
+ )
+ vertical_segments.append(alignment_segment)
+
+ if include_geometry:
+ vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0])
+
+ # start of next curve is end of this curve
+ xPBG = xEVC
+ yPBG = yEVC
+ xPVI = xPFG
+ yPVI = yPFG
+
+ # create last gradient run
+ dx = xPVI - xPBG
+ dy = yPVI - yPBG
+ slope = math.tan(math.atan2(dy, dx))
+ gradient_length = dx
+
+ design_parameters = self._file.create_entity(
+ type="IfcAlignmentVerticalSegment",
+ StartTag=None,
+ EndTag=None,
+ StartDistAlong=xPBG,
+ HorizontalLength=gradient_length,
+ StartHeight=yPBG,
+ StartGradient=slope,
+ EndGradient=slope,
+ RadiusOfCurvature=None,
+ PredefinedType="CONSTANTGRADIENT",
+ )
+ alignment_segment = self._file.create_entity(
+ type="IfcAlignmentSegment",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name=None,
+ Description=None,
+ ObjectType=None,
+ ObjectPlacement=None,
+ Representation=None,
+ DesignParameters=design_parameters,
+ )
+ vertical_segments.append(alignment_segment)
+
+ if include_geometry:
+ vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0])
+
+ # create zero length terminator segment
+ design_parameters = self._file.create_entity(
+ type="IfcAlignmentVerticalSegment",
+ StartTag="VPOE",
+ EndTag="VPOE",
+ StartDistAlong=xPVI,
+ HorizontalLength=0.0,
+ StartHeight=yPVI,
+ StartGradient=slope,
+ EndGradient=slope,
+ RadiusOfCurvature=None,
+ PredefinedType="CONSTANTGRADIENT",
+ )
+ alignment_segment = self._file.create_entity(
+ type="IfcAlignmentSegment",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name=None,
+ Description=None,
+ ObjectType=None,
+ ObjectPlacement=None,
+ Representation=None,
+ DesignParameters=design_parameters,
+ )
+ vertical_segments.append(alignment_segment)
+
+ if include_geometry:
+ vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0])
+
+ if include_geometry:
+ gradient_curve = self._file.create_entity(
+ type="IfcGradientCurve",
+ Segments=vertical_curve_segments,
+ SelfIntersect=False,
+ BaseCurve=composite_curve,
+ EndPoint=None,
+ )
+ else:
+ gradient_curve = None
+
+ return vertical_segments, vertical_curve_segments, gradient_curve
+
+ def create_alignment_by_pi_method(
+ self,
+ alignment_name: str,
+ points: Sequence[Sequence[float]],
+ radii: Sequence[float],
+ vpoints: Sequence[Sequence[float]],
+ lengths: Sequence[float],
+ alignment_description: str = None,
+ start_station: float = 1000.0,
+ include_geometry: bool = True,
+ ):
+ """
+ Create an alignment using the PI layout method for both horizontal and vertical alignments.
+
+ @param alignment_name: value for Name attribute
+ @param alignment_description: value for Description attribute
+ @param points: (X,Y) pairs denoting the location of the horizontal PIs, including start and end
+ @param radii: radii values to use for transition
+ @param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end.
+ @param lengths: parabolic vertical curve horizontal length values to use for transition
+ @param start_station: ??? NOT USED AT THIS TIME ???
+ @param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic
+ """
+
+ horizontal_segments, horizontal_curve_segments, composite_curve = self._create_horizontal_alignment(
+ alignment_name, alignment_description, points, radii, include_geometry
+ )
+ vertical_segments, vertical_curve_segments, gradient_curve = self._create_vertical_alignment(
+ composite_curve, vpoints, lengths
+ )
+
+ name_segments(prefix="H", segments=horizontal_segments)
+ name_segments(prefix="V", segments=vertical_segments)
+
+ # Create the horizontal alignment (IfcAlignmentHorizontal) and nest alignment segments
+ horizontal_alignment = self._file.create_entity(
+ type="IfcAlignmentHorizontal",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name=f"{alignment_name} - Horizontal",
+ Description=alignment_description,
+ ObjectType=None,
+ ObjectPlacement=None,
+ Representation=None,
+ )
+
+ nests_horizontal_segments = self._file.create_entity(
+ type="IfcRelNests",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name="Nests horizontal alignment segments under horizontal alignment",
+ RelatingObject=horizontal_alignment,
+ RelatedObjects=horizontal_segments,
+ )
+
+ # Create the vertical alignment (IfcAlignmentVertical) and nest alignment segments
+ vertical_alignment = self._file.create_entity(
+ type="IfcAlignmentVertical",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name=f"{alignment_name} - Vertical",
+ Description=alignment_description,
+ ObjectType=None,
+ ObjectPlacement=None,
+ Representation=None,
+ )
+
+ nests_vertical_segments = self._file.create_entity(
+ type="IfcRelNests",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name="Nests vertical alignment segments under vertical alignment",
+ RelatingObject=vertical_alignment,
+ RelatedObjects=vertical_segments,
+ )
+
+ # create the alignment
+ placement = self._file.createIfcLocalPlacement(
+ PlacementRelTo=None,
+ RelativePlacement=self._file.createIfcAxis2Placement2D(
+ Location=self._file.createIfcCartesianPoint(Coordinates=(0.0, 0.0))
+ ),
+ )
+
+ alignment = self._file.create_entity(
+ type="IfcAlignment",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name=alignment_name,
+ Description=alignment_description,
+ ObjectType=None,
+ ObjectPlacement=placement,
+ Representation=None,
+ PredefinedType=None,
+ )
+
+ # create referent for start station
+ start_station_name = "Start Station ({})".format(ifcopenshell.util.stationing.station_as_string(start_station))
+ start_referent = self._file.createIfcReferent(
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name=start_station_name,
+ Description=None,
+ ObjectType=None,
+ ObjectPlacement=self._file.createIfcLinearPlacement(
+ RelativePlacement=self._file.createIfcAxis2PlacementLinear(
+ Location=self._file.createIfcPointByDistanceExpression(
+ DistanceAlong=self._file.createIfcLengthMeasure(0.0),
+ OffsetLateral=None,
+ OffsetVertical=None,
+ OffsetLongitudinal=None,
+ BasisCurve=composite_curve,
+ ),
+ ),
+ CartesianPosition=None,
+ ),
+ Representation=None,
+ PredefinedType="STATION",
+ )
+ pset_stationing = ifcopenshell.api.pset.add_pset(self._file, product=start_referent, name="Pset_Stationing")
+ ifcopenshell.api.pset.edit_pset(self._file, pset=pset_stationing, properties={"Station": start_station})
+
+ # nest the horizontal, vertical and the referent under the alignment
+ nesting_of_alignment = self._file.create_entity(
+ type="IfcRelNests",
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name="Nests horizontal alignment, vertical alginment, and referents under overall alignment",
+ RelatingObject=alignment,
+ RelatedObjects=(horizontal_alignment, vertical_alignment, start_referent),
+ )
+
+ # aggregate the alignment under the project
+ project = self._file.by_type("IfcProject")[0]
+ alignment_within_project = self._file.createIfcRelAggregates(
+ GlobalId=ifcopenshell.guid.new(),
+ OwnerHistory=None,
+ Name="Aggregates alignment under the project",
+ RelatingObject=project,
+ RelatedObjects=(alignment,),
+ )
+
+ # create geometric representation
+ if include_geometry:
+ # create the footprint representation
+ footprint_shape_representation = self._file.create_entity(
+ type="IfcShapeRepresentation",
+ ContextOfItems=self._axis_geom_subcontext,
+ RepresentationIdentifier="FootPrint",
+ RepresentationType="Curve2D",
+ Items=(composite_curve,),
+ )
+
+ # create the Curve3D representation
+ axis3d_shape_representation = self._file.create_entity(
+ type="IfcShapeRepresentation",
+ ContextOfItems=self._axis_geom_subcontext,
+ RepresentationIdentifier="Axis",
+ RepresentationType="Curve3D",
+ Items=(gradient_curve,),
+ )
+
+ # create the alignment product definition
+ product_definition_shape = self._file.create_entity(
+ type="IfcProductDefinitionShape",
+ Name="Alignment Product Definition Shape",
+ Description=None,
+ Representations=(
+ footprint_shape_representation,
+ axis3d_shape_representation,
+ ),
+ )
+
+ # create representations for each segment
+ self._create_segment_representations(placement, horizontal_curve_segments, horizontal_segments)
+ self._create_segment_representations(placement, vertical_curve_segments, vertical_segments)
+
+ # add the representation to the alignment
+ alignment.Representation = product_definition_shape
+
+ return alignment
+
+ def create_horizontal_alignment_by_pi_method(
self,
name: str,
hpoints: Sequence[Sequence[float]],
@@ -630,7 +1120,7 @@ class IfcAlignmentHelper:
"""
Create a new alignment with a horizontal alignment using the PI layout method
"""
- self._add_horizontal_alignment(
+ return self._add_horizontal_alignment(
alignment_name=name,
points=hpoints,
radii=radii,
@@ -647,7 +1137,17 @@ if __name__ == "__main__":
import sys
from matplotlib import pyplot as plt
- f = ifcopenshell.open(sys.argv[1])
+ f = ifcopenshell.file(schema="IFC4X3_ADD2")
+ project = f.create_entity(type="IfcProject", GlobalId=ifcopenshell.guid.new())
+ context = f.create_entity(type="IfcGeometricRepresentationContext")
+
+ points = [(0.0, 0.0), (100.0, 0.0), (200.0, 150.0)]
+ radii = [50.0]
+
+ helper = IfcAlignmentHelper(f)
+ helper.create_horizontal_alignment_by_pi_method(name="MyAlignment", hpoints=points, radii=radii)
+
+ # f = ifcopenshell.open(sys.argv[1])
print_structure(f.by_type("IfcAlignment")[0])
al_hor_rep = f.by_type("IfcCompositeCurve")[0]
diff --git a/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py b/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py
index 3cbab7f38d..ff0e2b11b7 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py
@@ -39,11 +39,8 @@ def edit_attributes(file: ifcopenshell.file, product: ifcopenshell.entity_instan
:param product: The product you want to edit. This may be any rooted IFC
entity.
- :type product: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
diff --git a/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py b/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py
index d77776f04a..b9a2630442 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py
@@ -17,17 +17,20 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell.util.unit
+import numpy as np
+import numpy.typing as npt
from typing import Optional
+from ifcopenshell.util.shape_builder import SequenceOfVectors, V, ifc_safe_vector_type
def assign_connection_geometry(
file: ifcopenshell.file,
rel_space_boundary: ifcopenshell.entity_instance,
- outer_boundary: list[tuple[float, float]],
+ outer_boundary: SequenceOfVectors,
location: tuple[float, float, float],
axis: tuple[float, float, float],
ref_direction: tuple[float, float, float],
- inner_boundaries: Optional[list[list[tuple[float, float]]]] = None,
+ inner_boundaries: Optional[SequenceOfVectors] = None,
unit_scale: Optional[float] = None,
) -> None:
"""Create and assign a connection geometry to a space boundary relationship
@@ -40,35 +43,28 @@ def assign_connection_geometry(
:param rel_space_boundary: The space boundary relationship to assign the
connection geometry to.
- :type rel_space_boundary: ifcopenshell.entity_instance
:param outer_boundary: A list of 2D points representing an open
polyline. The last point will connect to the first point. Each
point is represented by an interable of 2 floats. The coordinates of
the points are relative to the positional matrix arguments.
- :type outer_boundary: list[tuple[float, float]]
:param inner_boundaries: A list of zero or more inner boundaries to use
for the plane. Each boundary is represented by an open polyline, as
defined by the outer_boundary argument.
- :type inner_boundaries: list[list[tuple[float, float]]], optional
:param location: The local origin of the connection geometry, defined as
an XYZ coordinate relative to the placement of the space that is
being bounded.
- :type location: tuple[float, float, float]
:param axis: The local X axis of the connection geometry, defined as an
XYZ vector relative to the placement of the space that is being
bounded.
- :type axis: tuple[float, float, float]
:param ref_direction: The local Z axis of the connection geometry,
defined as an XYZ vector relative to the placement of the space that
is being bounded. The Y vector is automatically derived using the
right hand rule.
- :type ref_direction: tuple[float, float, float]
:param unit_scale: The unit scale as calculated by
ifcopenshell.util.unit.calculate_unit_scale. If not provided, it
will be automatically calculated for you.
:type unit_scale: float, optional
:return: None
- :rtype: None
Example:
@@ -83,19 +79,26 @@ def assign_connection_geometry(
usecase = Usecase()
usecase.file = file
usecase.rel_space_boundary = rel_space_boundary
- usecase.outer_boundary = outer_boundary
- usecase.inner_boundaries = inner_boundaries or ()
- usecase.location = location
- usecase.axis = axis
- usecase.ref_direction = ref_direction
- usecase.unit_scale = unit_scale
+ usecase.outer_boundary = V(outer_boundary)
+ usecase.inner_boundaries = V(inner_boundaries or [])
+ usecase.location = V(location)
+ usecase.axis = V(axis)
+ usecase.ref_direction = V(ref_direction)
+ usecase.unit_scale = unit_scale if unit_scale is not None else ifcopenshell.util.unit.calculate_unit_scale(file)
return usecase.execute()
class Usecase:
+ file: ifcopenshell.file
+ rel_space_boundary: ifcopenshell.entity_instance
+ outer_boundary: npt.NDArray
+ inner_boundaries: npt.NDArray
+ location: npt.NDArray
+ axis: npt.NDArray
+ ref_direction: npt.NDArray
+ unit_scale: float
+
def execute(self):
- if self.unit_scale is None:
- self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
outer_boundary = self.create_polyline(self.outer_boundary)
inner_boundaries = tuple(self.create_polyline(boundary) for boundary in self.inner_boundaries)
plane = self.create_plane(self.location, self.axis, self.ref_direction)
@@ -103,18 +106,23 @@ class Usecase:
connection_geometry = self.file.createIfcConnectionSurfaceGeometry(curve_bounded_plane)
self.rel_space_boundary.ConnectionGeometry = connection_geometry
- def create_point(self, point):
- return self.file.createIfcCartesianPoint(point / self.unit_scale)
+ def create_point(self, point: npt.NDArray) -> ifcopenshell.entity_instance:
+ return self.file.create_enitty("IfcCartesianPoint", ifc_safe_vector_type(point / self.unit_scale))
- def close_polyline(self, points):
+ def close_polyline(
+ self, points: tuple[ifcopenshell.entity_instance, ...]
+ ) -> tuple[ifcopenshell.entity_instance, ...]:
return points + (points[0],)
- def create_polyline(self, points):
- if points[0] == points[-1]:
+ def create_polyline(self, points: npt.NDArray) -> ifcopenshell.entity_instance:
+ if np.allclose(points[0], points[-1]):
points = points[0 : len(points) - 1]
- return self.file.createIfcPolyline(self.close_polyline(tuple(self.create_point(point) for point in points)))
+ ifc_points = tuple(self.create_point(point) for point in points)
+ return self.file.createIfcPolyline(self.close_polyline(ifc_points))
- def create_plane(self, location, axis, ref_direction):
+ def create_plane(
+ self, location: npt.NDArray, axis: npt.NDArray, ref_direction: npt.NDArray
+ ) -> ifcopenshell.entity_instance:
return self.file.createIfcPlane(
self.file.createIfcAxis2Placement3D(
self.create_point(location),
diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py b/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py
index af886a0014..2932a5bb93 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py
@@ -20,7 +20,7 @@ import ifcopenshell
import ifcopenshell.guid
import ifcopenshell.util.schema
import ifcopenshell.util.date
-from typing import Union
+from typing import Union, Any
def add_classification(
@@ -61,9 +61,7 @@ def add_classification(
classification library. The latter approach is preferred if you are
using a commonly known system such as Uniclass, as this will ensure
all metadata is added correctly.
- :type classification: str,ifcopenshell.entity_instance
:return: The added IfcClassification element
- :rtype: ifcopenshell.entity_instance
Example:
@@ -81,28 +79,28 @@ def add_classification(
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {
- "classification": classification,
- }
- return usecase.execute()
+ return usecase.execute(classification)
class Usecase:
- def execute(self):
- if isinstance(self.settings["classification"], str):
- classification = self.file.createIfcClassification(Name=self.settings["classification"])
+ file: ifcopenshell.file
+
+ def execute(self, classification: Union[str, ifcopenshell.entity_instance]) -> ifcopenshell.entity_instance:
+ self.classification = classification
+ if isinstance(self.classification, str):
+ classification = self.file.create_entity("IfcClassification", Name=self.classification)
self.relate_to_project(classification)
return classification
return self.add_from_library()
- def add_from_library(self):
+ def add_from_library(self) -> ifcopenshell.entity_instance:
edition_date = None
- if self.settings["classification"].EditionDate:
- edition_date = ifcopenshell.util.date.ifc2datetime(self.settings["classification"].EditionDate)
- self.settings["classification"].EditionDate = None
+ if self.classification.EditionDate:
+ edition_date = ifcopenshell.util.date.ifc2datetime(self.classification.EditionDate)
+ self.classification.EditionDate = None
migrator = ifcopenshell.util.schema.Migrator()
- result = migrator.migrate(self.settings["classification"], self.file)
+ result = migrator.migrate(self.classification, self.file)
# TODO: should auto date migration be part of the migrator?
if self.file.schema == "IFC2X3" and edition_date:
@@ -118,7 +116,7 @@ class Usecase:
return result
- def relate_to_project(self, classification):
+ def relate_to_project(self, classification: ifcopenshell.entity_instance) -> None:
self.file.create_entity(
"IfcRelAssociatesClassification",
GlobalId=ifcopenshell.guid.new(),
diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py
index 1e0ae329bf..817d15b5cf 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py
@@ -21,7 +21,7 @@ import ifcopenshell.api.owner
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.schema
-from typing import Optional, Union
+from typing import Optional, Union, Any
def add_reference(
@@ -65,23 +65,18 @@ def add_reference(
:param product: The list of IFC objects, properties, or resources you want to
associate the classification reference to.
- :type product: list[ifcopenshell.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 reference: ifcopenshell.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
the levels of hierarchy in the classificaion (e.g. Pr_12_23_34).
- :type identification: str, optional
:param name: If you choose option 1 and do not specify a reference, you
may manually specify a name. The name is typically human readable.
- :type name: str, optional
: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 classification: ifcopenshell.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
@@ -91,13 +86,11 @@ def add_reference(
references merely help describe the "tree" of classifications, but
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
or `None` if `products` was empty list.
- :rtype: Union[ifcopenshell.entity_instance, None]
Example:
@@ -136,6 +129,9 @@ def add_reference(
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
if not self.settings["products"]:
return
diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/edit_classification.py b/src/ifcopenshell-python/ifcopenshell/api/classification/edit_classification.py
index d6c8ef8bc4..b2091c0eca 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/classification/edit_classification.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/classification/edit_classification.py
@@ -28,11 +28,8 @@ def edit_classification(
IfcClassification, consult the IFC documentation.
:param classification: The IfcClassification entity you want to edit
- :type classification: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -43,7 +40,5 @@ def edit_classification(
ifcopenshell.api.classification.edit_classification(model,
classification=classification, attributes={"Name": "Foo"})
"""
- settings = {"classification": classification, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["classification"], name, value)
+ for name, value in attributes.items():
+ setattr(classification, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/edit_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/edit_reference.py
index 80d978e7d3..f5a85d40f9 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/classification/edit_reference.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/classification/edit_reference.py
@@ -28,11 +28,8 @@ def edit_reference(
IfcClassificationReference, consult the IFC documentation.
:param reference: The IfcClassificationReference entity you want to edit
- :type reference: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -43,7 +40,5 @@ def edit_reference(
ifcopenshell.api.classification.edit_reference(model,
reference=reference, attributes={"Name": "Foo"})
"""
- settings = {"reference": reference, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["reference"], name, value)
+ for name, value in attributes.items():
+ setattr(reference, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_classification.py b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_classification.py
index 90e6abbbe7..38382eda91 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_classification.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_classification.py
@@ -28,9 +28,7 @@ def remove_classification(file: ifcopenshell.file, classification: ifcopenshell.
removed from a project.
:param classification: The IfcClassification entity you want to remove
- :type classification: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -42,16 +40,17 @@ def remove_classification(file: ifcopenshell.file, classification: ifcopenshell.
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"classification": classification}
- return usecase.execute()
+ return usecase.execute(classification)
class Usecase:
- def execute(self):
- references = self.get_references(self.settings["classification"])
+ file: ifcopenshell.file
+
+ def execute(self, classification: ifcopenshell.entity_instance) -> None:
+ references = self.get_references(classification)
for reference in references:
self.file.remove(reference)
- self.file.remove(self.settings["classification"])
+ self.file.remove(classification)
for rel in self.file.by_type("IfcRelAssociatesClassification"):
if not rel.RelatingClassification:
history = rel.OwnerHistory
diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py
index 0bdd347bfa..55dfd11b08 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py
@@ -33,15 +33,12 @@ def remove_reference(
:param reference: The IfcClassificationReference entity of the
relationship you want to remove.
- :type reference: ifcopenshell.entity_instance
:param product: The list fo object entities of the relationship you want to
remove.
- :type product: list[ifcopenshell.entity_instance]
:raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements.
:return: None
- :rtype: None
Example:
@@ -56,20 +53,18 @@ def remove_reference(
ifcopenshell.api.classification.remove_reference(model,
reference=reference, products=[wall_type])
"""
- settings = {"reference": reference, "products": products}
-
is_ifc2x3 = file.schema == "IFC2X3"
- products = set(settings["products"])
- referenced = ifcopenshell.util.element.get_referenced_elements(settings["reference"])
- products -= products.difference(referenced)
+ products_set = set(products)
+ referenced = ifcopenshell.util.element.get_referenced_elements(reference)
+ products_set -= products_set.difference(referenced)
# all products are already unassigned from a reference
- if not products:
+ if not products_set:
return
rooted_products: set[ifcopenshell.entity_instance] = set()
non_rooted_products: set[ifcopenshell.entity_instance] = set()
- for product in settings["products"]:
+ for product in products:
if product.is_a("IfcRoot"):
rooted_products.add(product)
else:
@@ -86,7 +81,7 @@ def remove_reference(
reference_rels = {
rel
for rel in reference_rels
- if rel.is_a("IfcRelAssociatesClassification") and rel.RelatingClassification == settings["reference"]
+ if rel.is_a("IfcRelAssociatesClassification") and rel.RelatingClassification == reference
}
for rel in reference_rels:
@@ -108,7 +103,7 @@ def remove_reference(
rels = getattr(product, "HasExternalReference", [])
reference_rels.update(rels)
- reference_rels = {rel for rel in reference_rels if rel.RelatingReference == settings["reference"]}
+ reference_rels = {rel for rel in reference_rels if rel.RelatingReference == reference}
for rel in reference_rels:
related_objects = set(rel.RelatedResourceObjects) - non_rooted_products
if related_objects:
@@ -117,6 +112,6 @@ def remove_reference(
file.remove(rel)
# TODO: we only handle lightweight classifications here
- referenced_elements = ifcopenshell.util.element.get_referenced_elements(settings["reference"])
+ referenced_elements = ifcopenshell.util.element.get_referenced_elements(reference)
if not referenced_elements:
- file.remove(settings["reference"])
+ file.remove(reference)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py
index fdcdf50f6f..63ecfeb3c0 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py
@@ -26,16 +26,14 @@ def add_metric_reference(
Adds a chain of references to a metric. The reference path is a string of the form "attribute.attribute.attribute"
Used to reference a value of an attribute of an instance through a metric objective entity.
"""
- settings = {"metric": metric, "reference_path": reference_path}
-
references_created = []
- if settings["reference_path"]:
- attributes = settings["reference_path"].split(".")
+ if reference_path:
+ attributes = reference_path.split(".")
for i in range(len(attributes)):
if i == 0:
reference = file.create_entity("IfcReference")
reference.AttributeIdentifier = attributes[i]
- settings["metric"].ReferencePath = reference
+ metric.ReferencePath = reference
references_created.append(reference)
else:
reference = file.create_entity("IfcReference")
diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py
index 74bf3d9a04..f4daad0abb 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py
@@ -39,36 +39,29 @@ def assign_constraint(
:param products: The list of products the constraint applies to. This is anything
which can have properties or quantities.
- :type products: list[ifcopenshell.entity_instance]
:param constraint: The IfcObjective constraint
- :type constraint: ifcopenshell.entity_instance
:return: The new or updated IfcRelAssociatesConstraint relationship
or `None` if `products` was an empty list.
- :rtype: ifcopenshell.entity_instance
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {
- "products": products,
- "constraint": constraint,
- }
- return usecase.execute()
+ return usecase.execute(products, constraint)
class Usecase:
- def execute(self):
- products = set(self.settings["products"])
+ file: ifcopenshell.file
+
+ def execute(self, products: list[ifcopenshell.entity_instance], constraint: ifcopenshell.entity_instance):
if not products:
return
+ products_set = set(products)
- self.constraint = self.settings["constraint"]
-
- rels = self.get_constraint_rels()
+ rels = self.get_constraint_rels(constraint)
related_objects = set()
for rel in rels:
related_objects.update(rel.RelatedObjects)
- products_to_assign = products - related_objects
+ products_to_assign = products_set - related_objects
if not products_to_assign:
return rels[0]
@@ -85,14 +78,14 @@ class Usecase:
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(self.file),
- "RelatingConstraint": self.constraint,
+ "RelatingConstraint": constraint,
"RelatedObjects": list(products_to_assign),
}
)
- def get_constraint_rels(self) -> list[ifcopenshell.entity_instance]:
+ def get_constraint_rels(self, constraint: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
rels = []
- for rel in self.file.get_inverse(self.constraint):
+ for rel in self.file.get_inverse(constraint):
if rel.is_a("IfcRelAssociatesConstraint"):
rels.append(rel)
return rels
diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_metric.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_metric.py
index 89a0cbef7d..3967846759 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_metric.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_metric.py
@@ -26,11 +26,8 @@ def edit_metric(file: ifcopenshell.file, metric: ifcopenshell.entity_instance, a
IfcMetric, consult the IFC documentation.
:param metric: The IfcMetric you want to edit.
- :type metric: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -42,7 +39,5 @@ def edit_metric(file: ifcopenshell.file, metric: ifcopenshell.entity_instance, a
ifcopenshell.api.constraint.edit_metric(model,
metric=metric, attributes={"ConstraintGrade": "HARD"})
"""
- settings = {"metric": metric, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["metric"], name, value)
+ for name, value in attributes.items():
+ setattr(metric, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_objective.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_objective.py
index ce4e76ba9e..3cd75356a7 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_objective.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_objective.py
@@ -28,11 +28,8 @@ def edit_objective(
IfcObjective, consult the IFC documentation.
:param objective: The IfcObjective you want to edit.
- :type objective: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -42,7 +39,5 @@ def edit_objective(
ifcopenshell.api.constraint.edit_objective(model,
objective=objective, attributes={"ConstraintGrade": "HARD"})
"""
- settings = {"objective": objective, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["objective"], name, value)
+ for name, value in attributes.items():
+ setattr(objective, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py
index f700a2c150..76668f714d 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.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 ifcopenshell
+import ifcopenshell.util.element
def remove_metric(file: ifcopenshell.file, metric: ifcopenshell.entity_instance) -> None:
@@ -41,17 +42,18 @@ def remove_metric(file: ifcopenshell.file, metric: ifcopenshell.entity_instance)
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"metric": metric}
- return usecase.execute()
+ return usecase.execute(metric)
class Usecase:
- def execute(self):
- if self.settings["metric"].ReferencePath:
- reference = self.settings["metric"].ReferencePath
+ file: ifcopenshell.file
+
+ def execute(self, metric: ifcopenshell.entity_instance) -> None:
+ if metric.ReferencePath:
+ reference = metric.ReferencePath
self.delete_reference(reference)
- self.file.remove(self.settings["metric"])
+ self.file.remove(metric)
for rel in self.file.by_type("IfcRelAssociatesConstraint"):
if not rel.RelatingConstraint:
history = rel.OwnerHistory
@@ -62,7 +64,7 @@ class Usecase:
if not resource_rel.RelatingConstraint:
self.file.remove(resource_rel)
- def delete_reference(self, reference):
+ def delete_reference(self, reference: ifcopenshell.entity_instance) -> None:
if reference.InnerReference:
self.delete_reference(reference.InnerReference)
self.file.remove(reference)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py
index cff9716809..d9e84cc826 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py
@@ -32,41 +32,35 @@ def unassign_constraint(
other products.
:param products: The list of products the constraint applies to.
- :type products: list[ifcopenshell.entity_instance]
:param constraint: The IfcObjective constraint
- :type constraint: ifcopenshell.entity_instance
:return: None
- :rtype: None
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {
- "products": products,
- "constraint": constraint,
- }
- return usecase.execute()
+ return usecase.execute(products, constraint)
class Usecase:
- def execute(self):
- products = set(self.settings["products"])
- if not products:
- return
+ file: ifcopenshell.file
- self.constraint = self.settings["constraint"]
- rels = self.get_constraint_rels()
+ def execute(self, products_: list[ifcopenshell.entity_instance], constraint: ifcopenshell.entity_instance):
+ if not products_:
+ return
+ products_set = set(products_)
+
+ rels = self.get_constraint_rels(constraint)
related_objects = set()
for rel in rels:
related_objects.update(rel.RelatedObjects)
- if not related_objects.intersection(products):
+ if not related_objects.intersection(products_set):
return
for rel in rels:
related_objects = set(rel.RelatedObjects)
- if not related_objects.intersection(products):
+ if not related_objects.intersection(products_set):
continue
- related_objects -= products
+ related_objects -= products_set
if related_objects:
rel.RelatedObjects = list(related_objects)
ifcopenshell.api.owner.update_owner_history(self.file, **{"element": rel})
@@ -77,9 +71,9 @@ class Usecase:
if history:
ifcopenshell.util.element.remove_deep2(self.file, history)
- def get_constraint_rels(self) -> list[ifcopenshell.entity_instance]:
+ def get_constraint_rels(self, cosntraint: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
rels = []
- for rel in self.file.get_inverse(self.constraint):
+ for rel in self.file.get_inverse(cosntraint):
if rel.is_a("IfcRelAssociatesConstraint"):
rels.append(rel)
return rels
diff --git a/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py b/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py
index ba52e83a9e..1a7d567c58 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py
@@ -27,11 +27,8 @@ def edit_context(file: ifcopenshell.file, context: ifcopenshell.entity_instance,
IfcGeometricRepresentationContext, consult the IFC documentation.
:param context: The IfcGeometricRepresentationContext entity you want to edit
- :type context: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -47,7 +44,5 @@ def edit_context(file: ifcopenshell.file, context: ifcopenshell.entity_instance,
ifcopenshell.api.context.edit_context(model,
context=body, attributes={"ContextIdentifier": "Body"})
"""
- settings = {"context": context, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["context"], name, value)
+ for name, value in attributes.items():
+ setattr(context, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py
index ac6b563960..fcf298ff7c 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py
@@ -25,8 +25,6 @@ from typing import Union
def copy_cost_item(
file: ifcopenshell.file, cost_item: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]:
- # TODO: currently it never returns list of duplicated cost items
- # though it is stated in the docs
"""Copies all cost items and related relationships
The following relationships are also duplicated:
@@ -36,9 +34,7 @@ def copy_cost_item(
* The copy will have duplicated nested cost items
:param cost_item: The cost item to be duplicated
- :type cost_item: ifcopenshell.entity_instance
:return: The duplicated cost item or the list of duplicated cost items if the latter has children
- :rtype: ifcopenshell.entity_instance or list[ifcopenshell.entity_instance]
Example:
.. code:: python
@@ -53,22 +49,29 @@ def copy_cost_item(
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"cost_item": cost_item}
- return usecase.execute()
+ return usecase.execute(cost_item)
class Usecase:
- def execute(self):
- self.new_cost_items = []
- return self.duplicate_cost_item(self.settings["cost_item"])
+ file: ifcopenshell.file
+ new_cost_items: list[ifcopenshell.entity_instance]
- def duplicate_cost_item(self, cost_item):
+ def execute(
+ self, cost_item: ifcopenshell.entity_instance
+ ) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]:
+ self.new_cost_items = []
+ self.duplicate_cost_item(cost_item)
+ return self.new_cost_items[0] if len(self.new_cost_items) == 1 else self.new_cost_items
+
+ def duplicate_cost_item(self, cost_item: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
new_cost_item = ifcopenshell.util.element.copy_deep(self.file, cost_item)
self.new_cost_items.append(new_cost_item)
self.copy_indirect_attributes(cost_item, new_cost_item)
return new_cost_item
- def copy_indirect_attributes(self, from_element, to_element):
+ def copy_indirect_attributes(
+ self, from_element: ifcopenshell.entity_instance, to_element: ifcopenshell.entity_instance
+ ) -> None:
for inverse in self.file.get_inverse(from_element):
if inverse.is_a("IfcRelDefinesByProperties"):
inverse = ifcopenshell.util.element.copy(self.file, inverse)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item_values.py b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item_values.py
index 6903f41bfe..e62b105c50 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item_values.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item_values.py
@@ -30,11 +30,8 @@ def copy_cost_item_values(
parametrically linked, so if one value changes, the other will not.
:param source: The IfcCostItem to copy cost values from
- :type source: ifcopenshell.entity_instance
:param destination: The IfcCostItem to copy cost values from
- :type destination: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -53,11 +50,9 @@ def copy_cost_item_values(
# Let's copy the value from one item to another
ifcopenshell.api.cost.copy_cost_item_values(model, source=item1, destination=item2)
"""
- settings = {"source": source, "destination": destination}
-
- for cost_value in settings["destination"].CostValues or []:
+ for cost_value in destination.CostValues or []:
ifcopenshell.api.cost.remove_cost_item_value(file, cost_value=cost_value)
copied_cost_values = []
- for cost_value in settings["source"].CostValues or []:
+ for cost_value in source.CostValues or []:
copied_cost_values.append(ifcopenshell.util.element.copy_deep(file, cost_value))
- settings["destination"].CostValues = copied_cost_values
+ destination.CostValues = copied_cost_values
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py
index 2417c9ac65..813e306bc8 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py
@@ -28,11 +28,8 @@ def edit_cost_item(
IfcCostItem, consult the IFC documentation.
:param cost_item: The IfcCostItem entity you want to edit
- :type cost_item: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -42,7 +39,5 @@ def edit_cost_item(
item = ifcopenshell.api.cost.add_cost_item(model, cost_schedule=schedule)
ifcopenshell.api.cost.edit_cost_item(model, cost_item=item, attributes={"Name": "Foo"})
"""
- settings = {"cost_item": cost_item, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["cost_item"], name, value)
+ for name, value in attributes.items():
+ setattr(cost_item, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item_quantity.py
index cb265318ac..697d028a37 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item_quantity.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item_quantity.py
@@ -28,11 +28,8 @@ def edit_cost_item_quantity(
IfcPhysicalQuantity, consult the IFC documentation.
:param physical_quantity: The IfcPhysicalQuantity entity you want to edit
- :type physical_quantity: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -50,7 +47,5 @@ def edit_cost_item_quantity(
ifcopenshell.api.cost.edit_cost_item_quantity(model,
physical_quantity=quantity, "attributes": {"VolumeValue": 3.0})
"""
- settings = {"physical_quantity": physical_quantity, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["physical_quantity"], name, value)
+ for name, value in attributes.items():
+ setattr(physical_quantity, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py
index 1c6f42b123..d55e93330e 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py
@@ -28,11 +28,8 @@ def edit_cost_schedule(
IfcCostSchedule, consult the IFC documentation.
:param cost_schedule: The IfcCostSchedule entity you want to edit
- :type cost_schedule: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -42,8 +39,5 @@ def edit_cost_schedule(
ifcopenshell.api.cost.edit_cost_schedule(model,
cost_schedule=schedule, attributes={"Name": "Foo"})
"""
-
- settings = {"cost_schedule": cost_schedule, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["cost_schedule"], name, value)
+ for name, value in attributes.items():
+ setattr(cost_schedule, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py
index f3e4e01acd..ebc09ed34c 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py
@@ -31,11 +31,8 @@ def edit_cost_value(
IfcCostValue, consult the IFC documentation.
:param cost_value: The IfcCostValue entity you want to edit
- :type cost_value: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -49,20 +46,18 @@ def edit_cost_value(
ifcopenshell.api.cost.edit_cost_value(model, cost_value=value,
attributes={"AppliedValue": 42.0})
"""
- settings = {"cost_value": cost_value, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
+ for name, value in attributes.items():
if name == "AppliedValue" and value is not None:
# TODO: support all applied value select types
value = file.createIfcMonetaryMeasure(value)
elif name == "UnitBasis":
- old_unit_basis = settings["cost_value"].UnitBasis
+ old_unit_basis = cost_value.UnitBasis
if value:
value_component = file.create_entity(
ifcopenshell.util.unit.get_unit_measure_class(value["UnitComponent"].UnitType),
value["ValueComponent"],
)
value = file.create_entity("IfcMeasureWithUnit", value_component, value["UnitComponent"])
- if old_unit_basis and len(file.get_inverse(old_unit_basis)) == 0:
+ if old_unit_basis and file.get_total_inverses(old_unit_basis) == 0:
ifcopenshell.util.element.remove_deep(file, old_unit_basis)
- setattr(settings["cost_value"], name, value)
+ setattr(cost_value, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py
index c9ef89455e..80f330ce0b 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py
@@ -28,11 +28,8 @@ def remove_cost_item_quantity(
removed.
:param cost_item: The IfcCostItem that the quantity is assigned to
- :type cost_item: ifcopenshell.entity_instance
:param physical_quantity: The IfcPhysicalQuantity to remove
- :type physical_quantity: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -46,11 +43,9 @@ def remove_cost_item_quantity(
ifcopenshell.api.cost.remove_cost_item(model,
cost_item=item, physical_quantity=quantity)
"""
- settings = {"cost_item": cost_item, "physical_quantity": physical_quantity}
-
- if len(file.get_inverse(settings["physical_quantity"])) == 1:
- file.remove(settings["physical_quantity"])
+ if file.get_total_inverses(physical_quantity) == 1:
+ file.remove(physical_quantity)
return
- quantities = list(settings["cost_item"].CostQuantities or [])
- quantities.remove(settings["physical_quantity"])
- settings["cost_item"].CostQuantities = quantities
+ quantities = list(cost_item.CostQuantities or [])
+ quantities.remove(physical_quantity)
+ cost_item.CostQuantities = quantities
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py
index 2c7745d7f9..b578cd7134 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py
@@ -28,11 +28,8 @@ def remove_cost_value(
:param parent: The IfcCostItem, IfcConstructionResource, or IfcCostValue
that the IfcCostValue is assigned to.
- :type parent: ifcopenshell.entity_instance
:param cost_value: The IfcCostValue that you want to remove
- :type parent: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -48,20 +45,18 @@ def remove_cost_value(
ifcopenshell.api.cost.remove_cost_value(model, parent=item, cost_value=value)
"""
- settings = {"parent": parent, "cost_value": cost_value}
-
- if len(file.get_inverse(settings["cost_value"])) == 1:
- file.remove(settings["cost_value"])
+ if file.get_total_inverses(cost_value) == 1:
+ file.remove(cost_value)
# TODO deep purge
- elif settings["parent"].is_a("IfcCostItem"):
- values = list(settings["parent"].CostValues)
- values.remove(settings["cost_value"])
- settings["parent"].CostValues = values if values else None
- elif settings["parent"].is_a("IfcConstructionResource"):
- values = list(settings["parent"].BaseCosts)
- values.remove(settings["cost_value"])
- settings["parent"].BaseCosts = values if values else None
- elif settings["parent"].is_a("IfcCostValue"):
- components = list(settings["parent"].Components)
- components.remove(settings["cost_value"])
- settings["parent"].Components = components if components else None
+ elif parent.is_a("IfcCostItem"):
+ values = list(parent.CostValues)
+ values.remove(cost_value)
+ parent.CostValues = values if values else None
+ elif parent.is_a("IfcConstructionResource"):
+ values = list(parent.BaseCosts)
+ values.remove(cost_value)
+ parent.BaseCosts = values if values else None
+ elif parent.is_a("IfcCostValue"):
+ components = list(parent.Components)
+ components.remove(cost_value)
+ parent.Components = components if components else None
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py
index bb815647fd..de180d65ef 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py
@@ -30,12 +30,9 @@ def unassign_cost_item_quantity(
have any impact on the cost item.
:param cost_item: The IfcCostItem to remove quantities from
- :type cost_item: ifcopenshell.entity_instance
:param products: A list of IfcProducts that may have parametrically
connected quantities to the cost item
- :type products: list[ifcopenshell.entity_instance]
:return: None
- :rtype: None
Example:
@@ -69,38 +66,39 @@ def unassign_cost_item_quantity(
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"cost_item": cost_item, "products": products or []}
- return usecase.execute()
+ return usecase.execute(cost_item, products or [])
class Usecase:
- def execute(self):
- self.quantities = set(self.settings["cost_item"].CostQuantities or [])
- for quantity in self.settings["cost_item"].CostQuantities or []:
+ file: ifcopenshell.file
+
+ def execute(self, cost_item: ifcopenshell.entity_instance, products: list[ifcopenshell.entity_instance]) -> None:
+ quantities = set(cost_item.CostQuantities or [])
+ for quantity in cost_item.CostQuantities or []:
for inverse in self.file.get_inverse(quantity):
if not inverse.is_a("IfcElementQuantity"):
continue
for rel in inverse.DefinesOccurrence or []:
for related_object in rel.RelatedObjects:
- if related_object in self.settings["products"]:
- self.quantities.remove(quantity)
- self.settings["cost_item"].CostQuantities = list(self.quantities)
- for product in self.settings["products"]:
+ if related_object in products:
+ quantities.remove(quantity)
+ cost_item.CostQuantities = list(quantities)
+ for product in products:
ifcopenshell.api.control.unassign_control(
self.file,
related_object=product,
- relating_control=self.settings["cost_item"],
+ relating_control=cost_item,
)
- self.update_cost_item_count()
+ self.update_cost_item_count(cost_item)
- def update_cost_item_count(self):
+ def update_cost_item_count(self, cost_item: ifcopenshell.entity_instance) -> None:
# This is a bold assumption
# https://forums.buildingsmart.org/t/how-does-a-cost-item-know-that-it-is-counting-a-controlled-product/3564
- if len(self.settings["cost_item"].CostQuantities) == 1:
- quantity = self.settings["cost_item"].CostQuantities[0]
+ if len(cost_item.CostQuantities) == 1:
+ quantity = cost_item.CostQuantities[0]
if quantity.is_a("IfcQuantityCount"):
count = 0
- for rel in self.settings["cost_item"].Controls:
+ for rel in cost_item.Controls:
count += len(rel.RelatedObjects)
if count:
quantity[3] = count
diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py b/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py
index 5627cc1c84..a86a178e86 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py
@@ -41,15 +41,12 @@ def assign_document(
:param product: The list of objects to associate the document to. This could be
almost any sensible object in IFC.
- :type product: list[ifcopenshell.entity_instance]
:param document: The IfcDocumentReference to associate to, or
alternatively an IfcDocumentInformation, though this is not
recommended.
- :type document: ifcopenshell.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
Example:
@@ -65,43 +62,41 @@ def assign_document(
# Let's imagine storey represents an IfcBuildingStorey for the ground floor
ifcopenshell.api.document.assign_document(model, products=[storey], document=reference)
"""
- settings = {
- "products": products,
- "document": document,
- }
# 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(settings["document"])
- products: set[ifcopenshell.entity_instance] = set(settings["products"])
- products = products - referenced_elements
+ referenced_elements = ifcopenshell.util.element.get_referenced_elements(document)
+ products_set: set[ifcopenshell.entity_instance] = set(products)
+ products_set = products_set - referenced_elements
- if not products:
+ if not products_set:
return
if file.schema == "IFC2X3":
rel = next(
- (r for r in file.by_type("IfcRelAssociatesDocument") if r.RelatingDocument == settings["document"]),
+ (r for r in file.by_type("IfcRelAssociatesDocument") if r.RelatingDocument == document),
None,
)
else:
- ifc_class = settings["document"].is_a()
+ ifc_class = document.is_a()
if ifc_class == "IfcDocumentReference":
- rel = next(iter(settings["document"].DocumentRefForObjects), None)
+ rel = next(iter(document.DocumentRefForObjects), None)
elif ifc_class == "IfcDocumentInformation":
- rel = next(iter(settings["document"].DocumentInfoForObjects), None)
+ rel = next(iter(document.DocumentInfoForObjects), None)
+ else:
+ assert False, f"Unexpected document type: {ifc_class}"
if not rel:
return file.create_entity(
"IfcRelAssociatesDocument",
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=ifcopenshell.api.owner.create_owner_history(file),
- RelatedObjects=list(products),
- RelatingDocument=settings["document"],
+ RelatedObjects=list(products_set),
+ RelatingDocument=document,
)
- related_objects = set(rel.RelatedObjects) | products
+ related_objects = set(rel.RelatedObjects) | products_set
rel.RelatedObjects = list(related_objects)
ifcopenshell.api.owner.update_owner_history(file, element=rel)
return rel
diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py b/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py
index 09d697f7db..56c1d40df4 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py
@@ -30,11 +30,8 @@ def edit_information(
IfcDocumentInformation, consult the IFC documentation.
:param reference: The IfcDocumentInformation entity you want to edit
- :type reference: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -46,7 +43,5 @@ def edit_information(
attributes={"Identification": "A-GA-6100", "Name": "Overall Plan",
"Location": "A-GA-6100 - Overall Plan.pdf"})
"""
- settings = {"information": information, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["information"], name, value)
+ for name, value in attributes.items():
+ setattr(information, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py b/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py
index 5a00827305..51ad195bf3 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py
@@ -30,11 +30,8 @@ def edit_reference(
IfcDocumentReference, consult the IFC documentation.
:param reference: The IfcDocumentReference entity you want to edit
- :type reference: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -49,7 +46,5 @@ def edit_reference(
ifcopenshell.api.document.edit_reference(model,
reference=reference, attributes={"Identification": "2.1.15"})
"""
- settings = {"reference": reference, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["reference"], name, value)
+ for name, value in attributes.items():
+ setattr(reference, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/drawing/edit_text_literal.py b/src/ifcopenshell-python/ifcopenshell/api/drawing/edit_text_literal.py
index 1f7356e989..c083960e59 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/drawing/edit_text_literal.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/drawing/edit_text_literal.py
@@ -28,11 +28,8 @@ def edit_text_literal(
IfcTextLiteral, consult the IFC documentation.
:param reference: The IfcTextLiteral entity you want to edit
- :type reference: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -42,7 +39,5 @@ def edit_text_literal(
ifcopenshell.api.drawing.edit_text_literal(model,
text_literal=text, attributes={"Literal": "MY ANNOTATION"})
"""
- settings = {"text_literal": text_literal, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["text_literal"], name, value)
+ for name, value in attributes.items():
+ setattr(text_literal, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py
index 1883374b97..174c475f92 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py
@@ -17,7 +17,7 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell.util.unit
-from typing import Union
+from typing import Union, Any
COORD = Union[tuple[float, float], tuple[float, float, float]]
@@ -82,6 +82,9 @@ def add_axis_representation(
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
is_2d = len(self.settings["axis"][0]) == 2
diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py
index 6b48845e4e..9d5fe9f20b 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py
@@ -26,14 +26,9 @@ def add_footprint_representation(
# A list of IFC curves to include in the curve set
curves: list[ifcopenshell.entity_instance],
) -> ifcopenshell.entity_instance:
- settings = {
- "context": context,
- "curves": curves,
- }
-
return file.createIfcShapeRepresentation(
- settings["context"],
- settings["context"].ContextIdentifier,
+ context,
+ context.ContextIdentifier,
"GeometricCurveSet",
- [file.createIfcGeometricCurveSet(settings["curves"])],
+ [file.createIfcGeometricCurveSet(curves)],
)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py
index efe6aa5c89..940315621a 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py
@@ -17,7 +17,7 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell.util.unit
-from typing import Optional
+from typing import Optional, Any
COORD_3D = tuple[float, float, float]
@@ -60,6 +60,9 @@ def add_mesh_representation(
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
if self.settings["unit_scale"] is None:
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py
index 17cdec58c8..77bb515996 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py
@@ -52,6 +52,9 @@ def add_profile_representation(
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
self.settings["clippings"] = [Clipping.parse(c) for c in self.settings["clippings"]]
diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py
index 13d06e59c8..8244c3e434 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py
@@ -111,10 +111,12 @@ class Usecase:
settings: dict[str, Any]
ifc_vertices: list[ifcopenshell.entity_instance]
coordinate_offset: Union[npt.NDArray[np.float64], None]
+ geometry: Union[bpy.types.Mesh, bpy.types.Curve]
def execute(self) -> Union[ifcopenshell.entity_instance, None]:
self.is_manifold = None
self.coordinate_offset = self.settings["coordinate_offset"]
+ self.geometry = self.settings["geometry"]
if (
isinstance(self.settings["geometry"], bpy.types.Mesh)
and self.settings["geometry"] == self.settings["blender_object"].data
@@ -636,9 +638,7 @@ class Usecase:
dim = (lambda v: v.xy) if is_2d else (lambda v: v.xyz)
results = []
for spline in curve_object_data.splines:
- points = spline.bezier_points[:] + spline.points[:]
- if spline.use_cyclic_u:
- points.append(points[0])
+ points = self.get_spline_points(spline)
ifc_points = [self.create_cartesian_point(*dim(point.co)) for point in points]
results.append(self.file.createIfcPolyline(ifc_points))
return results
@@ -981,12 +981,12 @@ class Usecase:
)
def create_structural_reference_representation(self) -> ifcopenshell.entity_instance:
- if len(self.settings["geometry"].vertices) == 1:
+ if isinstance(self.geometry, bpy.types.Mesh) and len(self.geometry.vertices) == 1:
return self.file.createIfcTopologyRepresentation(
self.settings["context"],
self.settings["context"].ContextIdentifier,
"Vertex",
- [self.create_vertex_point(self.settings["geometry"].vertices[0].co)],
+ [self.create_vertex_point(self.geometry.vertices[0].co)],
)
return self.file.createIfcTopologyRepresentation(
self.settings["context"],
@@ -998,11 +998,22 @@ class Usecase:
def create_vertex_point(self, point: Vector) -> ifcopenshell.entity_instance:
return self.file.createIfcVertexPoint(self.create_cartesian_point(point.x, point.y, point.z))
+ def get_spline_points(
+ self, spline: bpy.types.Spline
+ ) -> list[Union[bpy.types.SplinePoint, bpy.types.BezierSplinePoint]]:
+ points = spline.bezier_points[:] + spline.points[:]
+ if spline.use_cyclic_u:
+ points.append(points[0])
+ return points
+
def create_edge(self) -> Union[ifcopenshell.entity_instance, None]:
- if hasattr(self.settings["geometry"], "splines"):
- points = self.get_spline_points(self.settings["geometry"].splines[0])
+ geometry = self.geometry
+ if isinstance(geometry, bpy.types.Curve):
+ points = self.get_spline_points(geometry.splines[0])
+ elif isinstance(geometry, bpy.types.Mesh):
+ points = geometry.vertices
else:
- points = self.settings["geometry"].vertices
+ assert False, type(geometry)
if not points:
return
return self.file.createIfcEdge(self.create_vertex_point(points[0].co), self.create_vertex_point(points[1].co))
diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py
index 22fb99d583..fdc457367b 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py
@@ -426,6 +426,9 @@ def add_window_representation(
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
builder = ShapeBuilder(self.file)
np_X, np_Y, np_Z = 0, 1, 2
diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py
index 571c5212ba..81fa276a2c 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py
@@ -31,53 +31,33 @@ def connect_path(
related_connection: str = "NOTDEFINED",
description: Optional[str] = None,
) -> ifcopenshell.entity_instance:
- settings = {
- "relating_element": relating_element,
- "related_element": related_element,
- "relating_connection": relating_connection,
- "related_connection": related_connection,
- "description": description,
- }
-
- incompatible_connections = []
- for rel in settings["relating_element"].ConnectedTo:
+ incompatible_connections: list[ifcopenshell.entity_instance] = []
+ for rel in relating_element.ConnectedTo:
if not rel.is_a("IfcRelConnectsPathElements"):
continue
- if rel.RelatedElement == settings["related_element"]:
+ if rel.RelatedElement == related_element:
incompatible_connections.append(rel)
- elif (
- rel.RelatingConnectionType in ["ATSTART", "ATEND"]
- and rel.RelatingConnectionType == settings["relating_connection"]
- ):
+ elif rel.RelatingConnectionType in ["ATSTART", "ATEND"] and rel.RelatingConnectionType == relating_connection:
incompatible_connections.append(rel)
- for rel in settings["relating_element"].ConnectedFrom:
+ for rel in relating_element.ConnectedFrom:
if not rel.is_a("IfcRelConnectsPathElements"):
continue
- if (
- rel.RelatedConnectionType in ["ATSTART", "ATEND"]
- and rel.RelatedConnectionType == settings["relating_connection"]
- ):
+ if rel.RelatedConnectionType in ["ATSTART", "ATEND"] and rel.RelatedConnectionType == relating_connection:
incompatible_connections.append(rel)
- for rel in settings["related_element"].ConnectedFrom:
+ for rel in related_element.ConnectedFrom:
if not rel.is_a("IfcRelConnectsPathElements"):
continue
- if (
- rel.RelatedConnectionType in ["ATSTART", "ATEND"]
- and rel.RelatedConnectionType == settings["related_connection"]
- ):
+ if rel.RelatedConnectionType in ["ATSTART", "ATEND"] and rel.RelatedConnectionType == related_connection:
incompatible_connections.append(rel)
- for rel in settings["related_element"].ConnectedTo:
+ for rel in related_element.ConnectedTo:
if not rel.is_a("IfcRelConnectsPathElements"):
continue
- if rel.RelatedElement == settings["relating_element"]:
+ if rel.RelatedElement == relating_element:
incompatible_connections.append(rel)
- elif (
- rel.RelatingConnectionType in ["ATSTART", "ATEND"]
- and rel.RelatingConnectionType == settings["related_connection"]
- ):
+ elif rel.RelatingConnectionType in ["ATSTART", "ATEND"] and rel.RelatingConnectionType == related_connection:
incompatible_connections.append(rel)
if incompatible_connections:
@@ -87,14 +67,15 @@ def connect_path(
if history:
ifcopenshell.util.element.remove_deep2(file, history)
- return file.createIfcRelConnectsPathElements(
+ return file.create_entity(
+ "IfcRelConnectsPathElements",
ifcopenshell.guid.new(),
OwnerHistory=ifcopenshell.api.owner.create_owner_history(file),
- Description=settings["description"],
- RelatingElement=settings["relating_element"],
- RelatedElement=settings["related_element"],
- RelatingConnectionType=settings["relating_connection"],
- RelatedConnectionType=settings["related_connection"],
+ Description=description,
+ RelatingElement=relating_element,
+ RelatedElement=related_element,
+ RelatingConnectionType=relating_connection,
+ RelatedConnectionType=related_connection,
RelatingPriorities=[],
RelatedPriorities=[],
)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py
index e8cd98aefc..0618b407ec 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py
@@ -37,8 +37,10 @@ def remove_representation(
:param should_keep_named_profiles: If true, named profile defs will not be
removed as they are assumed to be significant.
"""
+ is_ifc2x3 = file.schema == "IFC2X3"
styled_items = set()
- presentation_layer_assignments = set()
+ presentation_layer_assignments_items: set[ifcopenshell.entity_instance] = set()
+ presentation_layer_assignments_reps: set[ifcopenshell.entity_instance] = set()
textures = set()
colours = set()
named_profiles = set()
@@ -46,16 +48,14 @@ def remove_representation(
if subelement.is_a("IfcRepresentationItem"):
[styled_items.add(s) for s in subelement.StyledByItem or []]
# IFC2X3 is using LayerAssignments
- for s in (
- subelement.LayerAssignment if hasattr(subelement, "LayerAssignment") else subelement.LayerAssignments
- ):
- presentation_layer_assignments.add(s)
+ for s in subelement.LayerAssignment if not is_ifc2x3 else subelement.LayerAssignments:
+ presentation_layer_assignments_items.add(s)
# IfcTessellatedFaceSet inverses
[textures.add(t) for t in getattr(subelement, "HasTextures", []) or []]
[colours.add(t) for t in getattr(subelement, "HasColours", []) or []]
elif subelement.is_a("IfcRepresentation"):
for layer in subelement.LayerAssignments:
- presentation_layer_assignments.add(layer)
+ presentation_layer_assignments_reps.add(layer)
elif subelement.is_a("IfcProfileDef") and subelement.ProfileName:
named_profiles.add(subelement)
@@ -63,11 +63,16 @@ def remove_representation(
if should_keep_named_profiles:
do_not_delete += named_profiles
+ # Order matters - layer assignments may reference representation directly.
+ also_consider = list(presentation_layer_assignments_reps)
+ also_consider.extend(presentation_layer_assignments_items - presentation_layer_assignments_reps)
+ also_consider.extend(styled_items)
+ also_consider.extend(textures)
ifcopenshell.util.element.remove_deep2(
file,
representation,
- also_consider=list(styled_items | presentation_layer_assignments | colours),
- do_not_delete=do_not_delete,
+ also_consider=also_consider,
+ do_not_delete=set(do_not_delete),
)
for texture in textures:
@@ -77,8 +82,10 @@ def remove_representation(
to_delete = file.to_delete or set()
for element in styled_items:
- if not element.Item or element.Item in to_delete:
+ item = element.Item
+ if not item or item in to_delete:
file.remove(element)
+ presentation_layer_assignments = presentation_layer_assignments_reps | presentation_layer_assignments_items
for element in presentation_layer_assignments:
if all(item in to_delete for item in element.AssignedItems):
file.remove(element)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py
index 63d46b840b..3cbcae5d60 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py
@@ -43,7 +43,7 @@ def remove_georeferencing(file: ifcopenshell.file) -> None:
ifcopenshell.api.pset.remove_pset(file, project, file.by_id(pset["id"]))
return
for projected_crs in file.by_type("IfcProjectedCRS"):
- if (unit := projected_crs.MapUnit) and len(file.get_inverse(unit)) == 1:
+ if (unit := projected_crs.MapUnit) and file.get_total_inverses(unit) == 1:
projected_crs.MapUnit = None
ifcopenshell.util.element.remove_deep2(file, unit)
file.remove(projected_crs)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py b/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py
index 8239ab9614..652fc89f56 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py
@@ -23,9 +23,7 @@ def remove_grid_axis(file: ifcopenshell.file, axis: ifcopenshell.entity_instance
"""Removes a grid axis from a grid
:param axis: The IfcGridAxis you want to remove.
- :type axis: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -44,7 +42,7 @@ def remove_grid_axis(file: ifcopenshell.file, axis: ifcopenshell.entity_instance
ifcopenshell.api.grid.remove_grid_axis(model, axis=axis_2)
"""
axis_curve = axis.AxisCurve
- if len(file.get_inverse(axis_curve)) == 1:
+ if file.get_total_inverses(axis_curve) == 1:
ifcopenshell.util.element.remove_deep(file, axis_curve)
file.remove(axis_curve)
file.remove(axis)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py
index 64a4f571dc..4972058fb4 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py
@@ -46,29 +46,25 @@ def assign_group(
ifcopenshell.api.group.assign_group(model,
products=model.by_type("IfcFurniture"), group=group)
"""
- settings = {
- "products": products,
- "group": group,
- }
-
- if not settings["products"]:
+ if not products:
return
- if not settings["group"].IsGroupedBy:
+ is_grouped_by: tuple[ifcopenshell.entity_instance, ...]
+ if not (is_grouped_by := group.IsGroupedBy):
return file.create_entity(
"IfcRelAssignsToGroup",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
- "RelatedObjects": settings["products"],
- "RelatingGroup": settings["group"],
- }
+ "RelatedObjects": products,
+ "RelatingGroup": group,
+ },
)
- rel = settings["group"].IsGroupedBy[0]
+ rel = is_grouped_by[0]
related_objects = set(rel.RelatedObjects) or set()
- products = set(settings["products"])
- if products.issubset(related_objects):
+ products_set = set(products)
+ if products_set.issubset(related_objects):
return rel
- rel.RelatedObjects = list(related_objects | products)
+ rel.RelatedObjects = list(related_objects | products_set)
ifcopenshell.api.owner.update_owner_history(file, **{"element": rel})
return rel
diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py
index a404ef0f88..5a7a2b2018 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py
@@ -26,11 +26,8 @@ def edit_group(file: ifcopenshell.file, group: ifcopenshell.entity_instance, att
IfcGroup, consult the IFC documentation.
:param group: The IfcGroup entity you want to edit
- :type group: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -40,7 +37,5 @@ def edit_group(file: ifcopenshell.file, group: ifcopenshell.entity_instance, att
ifcopenshell.api.group.edit_group(model,
group=group, attributes={"Description": "All furniture and joinery included in the unit"})
"""
- settings = {"group": group, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["group"], name, value)
+ for name, value in attributes.items():
+ setattr(group, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py
index ee31336eec..cac87106aa 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py
@@ -46,17 +46,12 @@ def unassign_group(
bad_furniture = furniture[0]
ifcopenshell.api.group.unassign_group(model, products=[bad_furniture], group=group)
"""
- settings = {
- "products": products,
- "group": group,
- }
-
- if not settings["group"].IsGroupedBy:
+ if not group.IsGroupedBy:
return
- rel = settings["group"].IsGroupedBy[0]
+ rel = group.IsGroupedBy[0]
related_objects = set(rel.RelatedObjects) or set()
- products = set(settings["products"])
- related_objects -= products
+ products_set = set(products)
+ related_objects -= products_set
if related_objects:
rel.RelatedObjects = list(related_objects)
ifcopenshell.api.owner.update_owner_history(file, **{"element": rel})
diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py b/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py
index 61b3ec7657..468c193ff7 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py
@@ -45,24 +45,19 @@ def update_group_products(
ifcopenshell.api.group.update_group_products(model,
products=model.by_type("IfcFurniture"), group=group)
"""
- settings = {
- "group": group,
- "products": products,
- }
-
- if not settings["group"].IsGroupedBy:
+ if not group.IsGroupedBy:
return file.create_entity(
"IfcRelAssignsToGroup",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
- "RelatedObjects": settings["products"],
- "RelatingGroup": settings["group"],
+ "RelatedObjects": products,
+ "RelatingGroup": group,
}
)
else:
- rels = settings["group"].IsGroupedBy
- objects = set(settings["products"])
+ rels = group.IsGroupedBy
+ objects = set(products)
for rel in rels:
objects.update([g for g in rel.RelatedObjects if g.is_a("IfcGroup")])
to_purge = rels[1:]
diff --git a/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py b/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py
index fa57e526cc..57117b6625 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py
@@ -62,15 +62,10 @@ def assign_layer(
# only one item) to the layer.
ifcopenshell.api.layer.assign_layer(model, items=[representation.Items[0]], layer=layer)
"""
- settings = {
- "items": items,
- "layer": layer,
- }
-
# support AssignedItems == None since layer might just got created
- layer = settings["layer"]
+ assigned_items: set[ifcopenshell.entity_instance]
assigned_items = set(layer.AssignedItems or [])
- items = set(settings["items"])
- if items.issubset(assigned_items):
+ items_set = set(items)
+ if items_set.issubset(assigned_items):
return
- layer.AssignedItems = list(assigned_items | items)
+ layer.AssignedItems = list(assigned_items | items_set)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/layer/edit_layer.py b/src/ifcopenshell-python/ifcopenshell/api/layer/edit_layer.py
index 6f55f5da1d..7203fabb0c 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/layer/edit_layer.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/layer/edit_layer.py
@@ -37,7 +37,5 @@ def edit_layer(file: ifcopenshell.file, layer: ifcopenshell.entity_instance, att
ifcopenshell.api.layer.edit_layer(model,
layer=layer, attributes={"Description": "All walls, based on the AIA standard."})
"""
- settings = {"layer": layer, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["layer"], name, value)
+ for name, value in attributes.items():
+ setattr(layer, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py b/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py
index 97ae094a50..3eb0ed8fa1 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py
@@ -61,17 +61,11 @@ def unassign_layer(
# Let's undo it!
ifcopenshell.api.layer.unassign_layer(model, items=[representation.Items[0]], layer=layer)
"""
- settings = {
- "items": items,
- "layer": layer,
- }
-
- layer = settings["layer"]
assigned_items = set(layer.AssignedItems) or set()
- items = set(settings["items"])
- if not items.issubset(assigned_items):
+ items_set = set(items)
+ if not items_set.issubset(assigned_items):
return
- assigned_items = list(assigned_items - items)
+ assigned_items = list(assigned_items - items_set)
# keep IFC valid in case if there are no items left
if assigned_items:
diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/edit_library.py b/src/ifcopenshell-python/ifcopenshell/api/library/edit_library.py
index b3bc87b1c3..4e12195bda 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/library/edit_library.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/library/edit_library.py
@@ -28,11 +28,8 @@ def edit_library(file: ifcopenshell.file, library: ifcopenshell.entity_instance,
IfcLibraryInformation, consult the IFC documentation.
:param library: The IfcLibraryInformation entity you want to edit
- :type library: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/edit_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/edit_reference.py
index f3016fd9cf..e6acacbd8f 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/library/edit_reference.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/library/edit_reference.py
@@ -28,11 +28,8 @@ def edit_reference(
IfcLibraryReference, consult the IFC documentation.
:param reference: The IfcLibraryReference entity you want to edit
- :type reference: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -44,7 +41,5 @@ def edit_reference(
ifcopenshell.api.library.edit_reference(model,
reference=reference, attributes={"Identification": "http://example.org/digitaltwin#AHU01"})
"""
- settings = {"reference": reference, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["reference"], name, value)
+ for name, value in attributes.items():
+ setattr(reference, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py
index 7719fd86c8..1e7c209e6c 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py
@@ -17,6 +17,7 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell.util.representation
+from typing import Any
def assign_profile(
@@ -91,18 +92,17 @@ def assign_profile(
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"material_profile": material_profile, "profile": profile}
- return usecase.execute()
+ return usecase.execute(material_profile, profile)
class Usecase:
file: ifcopenshell.file
- def execute(self) -> None:
+ def execute(self, material_profile: ifcopenshell.entity_instance, profile: ifcopenshell.entity_instance) -> None:
# TODO: handle composite profiles
- old_profile = self.settings["material_profile"].Profile
- self.settings["material_profile"].Profile = self.settings["profile"]
- for profile_set in self.settings["material_profile"].ToMaterialProfileSet:
+ old_profile = material_profile.Profile
+ material_profile.Profile = profile
+ for profile_set in material_profile.ToMaterialProfileSet:
for inverse in self.file.get_inverse(profile_set):
if not inverse.is_a("IfcMaterialProfileSetUsage"):
continue
@@ -111,20 +111,20 @@ class Usecase:
if not rel.is_a("IfcRelAssociatesMaterial"):
continue
for element in rel.RelatedObjects:
- self.change_profile(element)
+ self.change_profile(element, profile)
else:
for rel in inverse.AssociatedTo:
for element in rel.RelatedObjects:
- self.change_profile(element)
+ self.change_profile(element, profile)
- if old_profile and len(self.file.get_inverse(old_profile)) == 0:
+ if old_profile and self.file.get_total_inverses(old_profile) == 0:
# TODO: check remove deep
self.file.remove(old_profile)
- def change_profile(self, element: ifcopenshell.entity_instance) -> None:
+ def change_profile(self, element: ifcopenshell.entity_instance, profile: ifcopenshell.entity_instance) -> None:
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
return
for subelement in self.file.traverse(representation):
if subelement.is_a("IfcSweptAreaSolid"):
- subelement.SweptArea = self.settings["profile"]
+ subelement.SweptArea = profile
diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py
index 939e995a06..9a01322743 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py
@@ -28,11 +28,8 @@ def edit_assigned_material(
IfcMaterial, consult the IFC documentation.
:param element: The IfcMaterial entity you want to edit
- :type element: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -42,7 +39,5 @@ def edit_assigned_material(
ifcopenshell.api.material.edit_assigned_material(model,
element=concrete, attributes={"Description": "40MPA concrete with broom finish"})
"""
- settings = {"element": element, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["element"], name, value)
+ for name, value in attributes.items():
+ setattr(element, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_constituent.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_constituent.py
index eda33d349b..a7398ca5f0 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_constituent.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_constituent.py
@@ -31,13 +31,9 @@ def edit_constituent(
IfcMaterialConstituent, consult the IFC documentation.
:param constituent: The IfcMaterialConstituent entity you want to edit
- :type constituent: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict, optional
:param material: The IfcMaterial entity you want to change the constituent to
- :type material: ifcopenshell.entity_instance, optional
:return: None
- :rtype: None
Example:
@@ -65,8 +61,6 @@ def edit_constituent(
ifcopenshell.api.material.edit_constituent(model,
constituent=constituent, attributes={"Name": "Glazing"})
"""
- settings = {"constituent": constituent, "attributes": attributes or {}, "material": material}
-
- for name, value in settings["attributes"].items():
- setattr(settings["constituent"], name, value)
- settings["constituent"].Material = settings["material"]
+ for name, value in (attributes or {}).items():
+ setattr(constituent, name, value)
+ constituent.Material = material
diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer.py
index d691d6b154..68f3c6d832 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer.py
@@ -31,14 +31,10 @@ def edit_layer(
IfcMaterialLayer, consult the IFC documentation.
:param layer: The IfcMaterialLayer entity you want to edit
- :type layer: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict, optional
:param material: The IfcMaterial entity you want the layer to be made
from.
- :type material: ifcopenshell.entity_instance, optional
:return: None
- :rtype: None
Example:
@@ -63,9 +59,7 @@ def edit_layer(
layer = ifcopenshell.api.material.add_layer(model, layer_set=material_set, material=gypsum)
ifcopenshell.api.material.edit_layer(model, layer=layer, attributes={"LayerThickness": 13})
"""
- settings = {"layer": layer, "attributes": attributes or {}, "material": material}
-
- for name, value in settings["attributes"].items():
- setattr(settings["layer"], name, value)
- if settings["material"]:
- settings["layer"].Material = settings["material"]
+ for name, value in (attributes or {}).items():
+ setattr(layer, name, value)
+ if material:
+ layer.Material = material
diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py
index 6c581c1374..9d1ffb14ec 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py
@@ -29,11 +29,8 @@ def edit_layer_usage(file: ifcopenshell.file, usage: ifcopenshell.entity_instanc
IfcMaterialLayerSetUsage, consult the IFC documentation.
:param usage: The IfcMaterialLayerSetUsage entity you want to edit
- :type usage: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -75,7 +72,5 @@ def edit_layer_usage(file: ifcopenshell.file, usage: ifcopenshell.entity_instanc
ifcopenshell.api.material.edit_layer_usage(model,
usage=rel.RelatingMaterial, attributes={"OffsetFromReferenceLine": 200})
"""
- settings = {"usage": usage, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["usage"], name, value)
+ for name, value in attributes.items():
+ setattr(usage, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py
index 17f925997e..c35755a960 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py
@@ -34,17 +34,12 @@ def edit_profile(
IfcMaterialProfile, consult the IFC documentation.
:param profile: The IfcMaterialProfile entity you want to edit
- :type profile: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict, optional
:param profile_def: The IfcProfileDef entity the profile curve should be
extruded from.
- :type profile_def: ifcopenshell.entity_instance, optional
:param material: The IfcMaterial entity you want to change the profile
to be made from.
- :type material: ifcopenshell.entity_instance, optional
:return: None
- :rtype: None
Example:
@@ -80,16 +75,9 @@ def edit_profile(
ifcopenshell.api.material.edit_profile(model,
profile=profile_item, profile_def=hea200, material=steel2)
"""
- settings = {
- "profile": profile,
- "attributes": attributes or {},
- "profile_def": profile_def,
- "material": material,
- }
-
- for name, value in settings["attributes"].items():
- setattr(settings["profile"], name, value)
- if settings["material"]:
- settings["profile"].Material = settings["material"]
- if settings["profile_def"]:
- settings["profile"].Profile = settings["profile_def"]
+ for name, value in (attributes or {}).items():
+ setattr(profile, name, value)
+ if material:
+ profile.Material = material
+ if profile_def:
+ profile.Profile = profile_def
diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py
index f9894db5b3..8a583d64b4 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py
@@ -17,6 +17,7 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell.geom
import ifcopenshell.util.representation
+from ifcopenshell.geom import ShapeType
from typing import Any
@@ -34,11 +35,8 @@ def edit_profile_usage(
IfcMaterialProfileSetUsage, consult the IFC documentation.
:param usage: The IfcMaterialProfileSetUsage entity you want to edit
- :type usage: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -93,21 +91,23 @@ def edit_profile_usage(
usecase = Usecase()
usecase.file = file
- usecase.settings = {"usage": usage, "attributes": attributes}
- return usecase.execute()
+ return usecase.execute(usage, attributes)
class Usecase:
- def execute(self):
- self.cardinal_point = self.settings["attributes"].get("CardinalPoint")
- if self.cardinal_point and self.cardinal_point != self.settings["usage"].CardinalPoint:
+ file: ifcopenshell.file
+
+ def execute(self, usage: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
+ self.attributes = attributes
+ self.cardinal_point = attributes.get("CardinalPoint")
+ if self.cardinal_point and self.cardinal_point != usage.CardinalPoint:
self.update_cardinal_point()
- for name, value in self.settings["attributes"].items():
- setattr(self.settings["usage"], name, value)
+ for name, value in attributes.items():
+ setattr(usage, name, value)
def update_cardinal_point(self):
- material_set = self.settings["usage"].ForProfileSet
+ material_set = self.attributes["usage"].ForProfileSet
self.profile = material_set.CompositeProfile
if not self.profile and material_set.MaterialProfiles:
self.profile = material_set.MaterialProfiles[0].Profile
@@ -117,13 +117,13 @@ class Usecase:
self.position = self.calculate_position()
if self.file.schema == "IFC2X3":
- for rel in self.file.get_inverse(self.settings["usage"]):
+ for rel in self.file.get_inverse(self.attributes["usage"]):
if not rel.is_a("IfcRelAssociatesMaterial"):
continue
for element in rel.RelatedObjects:
self.update_representation(element)
else:
- for rel in self.settings["usage"].AssociatedTo:
+ for rel in self.attributes["usage"].AssociatedTo:
for element in rel.RelatedObjects:
self.update_representation(element)
@@ -166,7 +166,7 @@ class Usecase:
elif self.cardinal_point == 9:
return self.get_top_right(shape)
- def get_bottom_left(self, shape):
+ def get_bottom_left(self, shape: ShapeType) -> ifcopenshell.entity_instance:
v = shape.verts
x = [v[i] for i in range(0, len(v), 3)]
y = [v[i + 1] for i in range(0, len(v), 3)]
@@ -174,13 +174,13 @@ class Usecase:
height = max(y) - min(y)
return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((-width / 2, height / 2, 0.0)))
- def get_bottom_centre(self, shape):
+ def get_bottom_centre(self, shape: ShapeType) -> ifcopenshell.entity_instance:
v = shape.verts
y = [v[i + 1] for i in range(0, len(v), 3)]
height = max(y) - min(y)
return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((0.0, height / 2, 0.0)))
- def get_bottom_right(self, shape):
+ def get_bottom_right(self, shape: ShapeType) -> ifcopenshell.entity_instance:
v = shape.verts
x = [v[i] for i in range(0, len(v), 3)]
y = [v[i + 1] for i in range(0, len(v), 3)]
@@ -188,22 +188,22 @@ class Usecase:
height = max(y) - min(y)
return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((width / 2, height / 2, 0.0)))
- def get_mid_depth_left(self, shape):
+ def get_mid_depth_left(self, shape: ShapeType) -> ifcopenshell.entity_instance:
v = shape.verts
x = [v[i] for i in range(0, len(v), 3)]
width = max(x) - min(x)
return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((-width / 2, 0.0, 0.0)))
- def get_mid_depth_centre(self, shape):
+ def get_mid_depth_centre(self, shape: ShapeType) -> ifcopenshell.entity_instance:
return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)))
- def get_mid_depth_right(self, shape):
+ def get_mid_depth_right(self, shape: ShapeType) -> ifcopenshell.entity_instance:
v = shape.verts
x = [v[i] for i in range(0, len(v), 3)]
width = max(x) - min(x)
return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((width / 2, 0.0, 0.0)))
- def get_top_left(self, shape):
+ def get_top_left(self, shape: ShapeType) -> ifcopenshell.entity_instance:
v = shape.verts
x = [v[i] for i in range(0, len(v), 3)]
y = [v[i + 1] for i in range(0, len(v), 3)]
@@ -211,13 +211,13 @@ class Usecase:
height = max(y) - min(y)
return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((-width / 2, -height / 2, 0.0)))
- def get_top_centre(self, shape):
+ def get_top_centre(self, shape: ShapeType) -> ifcopenshell.entity_instance:
v = shape.verts
y = [v[i + 1] for i in range(0, len(v), 3)]
height = max(y) - min(y)
return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((0.0, -height / 2, 0.0)))
- def get_top_right(self, shape):
+ def get_top_right(self, shape: ShapeType) -> ifcopenshell.entity_instance:
v = shape.verts
x = [v[i] for i in range(0, len(v), 3)]
y = [v[i + 1] for i in range(0, len(v), 3)]
@@ -225,7 +225,7 @@ class Usecase:
height = max(y) - min(y)
return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((width / 2, -height / 2, 0.0)))
- def update_representation(self, element):
+ def update_representation(self, element: ifcopenshell.entity_instance) -> None:
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if not representation:
return
@@ -234,5 +234,5 @@ class Usecase:
if subelement.is_a("IfcSweptAreaSolid") and subelement.SweptArea == self.profile:
self.update_swept_area_solid(subelement)
- def update_swept_area_solid(self, element):
+ def update_swept_area_solid(self, element: ifcopenshell.entity_instance) -> None:
element.Position = self.position
diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py
index 7521c48a15..8367f24d0e 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.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 ifcopenshell
+import ifcopenshell.util.element
def remove_constituent(
@@ -51,7 +52,7 @@ def remove_constituent(
# invalid.
ifcopenshell.api.material.remove_constituent(model, constituent=glazing)
"""
- material = layer.Material
+ material = constituent.Material
file.remove(constituent)
if material and should_remove_material:
- ifcopenshell.util.element.remove_deep2(file, subelement)
+ ifcopenshell.util.element.remove_deep2(file, material)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py
index 7055beaf26..443ad122c0 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py
@@ -57,4 +57,4 @@ def remove_layer(
material = layer.Material
file.remove(layer)
if material and should_remove_material:
- ifcopenshell.util.element.remove_deep2(file, subelement)
+ ifcopenshell.util.element.remove_deep2(file, material)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_list_item.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_list_item.py
index cd4f2f1cd6..6af15a7d3a 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_list_item.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_list_item.py
@@ -54,8 +54,6 @@ def remove_list_item(
# Let's remove the glass
ifcopenshell.api.material.remove_list_item(model, material_list=material_set, material_index=1)
"""
- settings = {"material_list": material_list, "material_index": material_index}
-
- materials = list(settings["material_list"].Materials)
- materials.pop(settings["material_index"])
- settings["material_list"].Materials = materials
+ materials = list(material_list.Materials)
+ materials.pop(material_index)
+ material_list.Materials = materials
diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/reorder_set_item.py b/src/ifcopenshell-python/ifcopenshell/api/material/reorder_set_item.py
index 0015d3558a..aa06433e66 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/material/reorder_set_item.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/material/reorder_set_item.py
@@ -57,16 +57,17 @@ def reorder_set_item(
ifcopenshell.api.material.reorder_set_item(model,
material_set=material_set, old_index=0, new_index=1)
"""
- settings = {"material_set": material_set, "old_index": old_index, "new_index": new_index}
-
- if settings["material_set"].is_a("IfcMaterialConstituentSet"):
+ if material_set.is_a("IfcMaterialConstituentSet"):
set_name = "MaterialConstituents"
- elif settings["material_set"].is_a("IfcMaterialLayerSet"):
+ elif material_set.is_a("IfcMaterialLayerSet"):
set_name = "MaterialLayers"
- elif settings["material_set"].is_a("IfcMaterialProfileSet"):
+ elif material_set.is_a("IfcMaterialProfileSet"):
set_name = "MaterialProfiles"
- elif settings["material_set"].is_a("IfcMaterialList"):
+ elif material_set.is_a("IfcMaterialList"):
set_name = "Materials"
- items = list(getattr(settings["material_set"], set_name) or [])
- items.insert(settings["new_index"], items.pop(settings["old_index"]))
- setattr(settings["material_set"], set_name, items)
+ else:
+ raise ValueError(f"Unexpected material set type: '{material_set.is_a()}'.")
+
+ items = list(getattr(material_set, set_name) or [])
+ items.insert(new_index, items.pop(old_index))
+ setattr(material_set, set_name, items)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py
index 8db2060df0..f8386490c7 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py
@@ -19,6 +19,7 @@
import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.util.element
+from typing import Any
def unassign_material(file: ifcopenshell.file, products: list[ifcopenshell.entity_instance]) -> None:
@@ -58,6 +59,9 @@ def unassign_material(file: ifcopenshell.file, products: list[ifcopenshell.entit
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
self.products = set(self.settings["products"])
if not self.products:
diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/add_application.py b/src/ifcopenshell-python/ifcopenshell/api/owner/add_application.py
index 991006be01..3f18754287 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/owner/add_application.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/owner/add_application.py
@@ -17,7 +17,7 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell.api
-from typing import Optional
+from typing import Optional, Any
def add_application(
@@ -68,6 +68,9 @@ def add_application(
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
if not self.settings["application_developer"]:
self.settings["application_developer"] = self.create_application_organisation()
diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py b/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py
index 7105e5e78d..75bb5eb517 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py
@@ -48,16 +48,14 @@ def add_role(
identification="AWB", name="Architects Without Ballpens")
ifcopenshell.api.owner.add_role(model, assigned_object=organisation, role="ARCHITECT")
"""
- settings = {"assigned_object": assigned_object, "role": role}
-
- element = file.createIfcActorRole("ARCHITECT")
- if settings["role"]:
+ element = file.create_entity("IfcActorRole", Role="ARCHITECT")
+ if role:
try:
- element.Role = settings["role"]
+ element.Role = role
except:
element.Role = "USERDEFINED"
- element.UserDefinedRole = settings["role"]
- roles = list(settings["assigned_object"].Roles) if settings["assigned_object"].Roles else []
+ element.UserDefinedRole = role
+ roles = list(assigned_object.Roles) if assigned_object.Roles else []
roles.append(element)
- settings["assigned_object"].Roles = roles
+ assigned_object.Roles = roles
return element
diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_actor.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_actor.py
index 799b37462c..8ad238f994 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_actor.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_actor.py
@@ -26,11 +26,8 @@ def edit_actor(file: ifcopenshell.file, actor: ifcopenshell.entity_instance, att
IfcActor, consult the IFC documentation.
:param actor: The IfcActor entity you want to edit
- :type actor: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -49,7 +46,5 @@ def edit_actor(file: ifcopenshell.file, actor: ifcopenshell.entity_instance, att
ifcopenshell.api.actor.edit_actor(model,
actor=actor, attributes={"Description": "Responsible for buildings A, B, and C."})
"""
- settings = {"actor": actor, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["actor"], name, value)
+ for name, value in attributes.items():
+ setattr(actor, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_address.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_address.py
index 43776fa711..52e80d6ef1 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_address.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_address.py
@@ -26,11 +26,8 @@ def edit_address(file: ifcopenshell.file, address: ifcopenshell.entity_instance,
IfcAddress, consult the IFC documentation.
:param address: The IfcAddress entity you want to edit
- :type address: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -51,7 +48,5 @@ def edit_address(file: ifcopenshell.file, address: ifcopenshell.entity_instance,
"ElectronicMailAddresses": ["bobthebuilder@example.com"],
"WWWHomePageURL": "https://thinkmoult.com"})
"""
- settings = {"address": address, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["address"], name, value)
+ for name, value in attributes.items():
+ setattr(address, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py
index 9cfbfff48d..a2bb5e3eb6 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py
@@ -28,11 +28,8 @@ def edit_organisation(
IfcOrganization, consult the IFC documentation.
:param organisation: The IfcOrganization entity you want to edit
- :type organisation: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -43,7 +40,5 @@ def edit_organisation(
ifcopenshell.api.owner.edit_organisation(model, organisation=organisation,
attributes={"name": "Architects Without Ballpens"})
"""
- settings = {"organisation": organisation, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["organisation"], name, value)
+ for name, value in attributes.items():
+ setattr(organisation, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_person.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_person.py
index 0e4bd15293..6b697bd674 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_person.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_person.py
@@ -26,11 +26,8 @@ def edit_person(file: ifcopenshell.file, person: ifcopenshell.entity_instance, a
IfcPerson, consult the IFC documentation.
:param person: The IfcPerson entity you want to edit
- :type person: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -41,7 +38,5 @@ def edit_person(file: ifcopenshell.file, person: ifcopenshell.entity_instance, a
ifcopenshell.api.owner.edit_person(model, person=person,
attributes={"MiddleNames": ["The"], "FamilyName": "Builder"})
"""
- settings = {"person": person, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["person"], name, value)
+ for name, value in attributes.items():
+ setattr(person, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_role.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_role.py
index 885d4b9b9b..fca80a16b0 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_role.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_role.py
@@ -26,11 +26,8 @@ def edit_role(file: ifcopenshell.file, role: ifcopenshell.entity_instance, attri
IfcActorRole, consult the IFC documentation.
:param role: The IfcActorRole entity you want to edit
- :type role: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -45,7 +42,5 @@ def edit_role(file: ifcopenshell.file, role: ifcopenshell.entity_instance, attri
# But Bob is not an architect
ifcopenshell.api.owner.edit_role(model, role=role, attributes={"Role": "CONSTRUCTIONMANAGER"})
"""
- settings = {"role": role, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["role"], name, value)
+ for name, value in attributes.items():
+ setattr(role, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py
index fbc25adcbe..acee11d9f5 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py
@@ -27,9 +27,7 @@ def remove_organisation(file: ifcopenshell.file, organisation: ifcopenshell.enti
removed.
:param organisation: The IfcOrganization to remove
- :type organisation: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -39,31 +37,29 @@ def remove_organisation(file: ifcopenshell.file, organisation: ifcopenshell.enti
identification="AWB", name="Architects Without Ballpens")
ifcopenshell.api.owner.remove_organisation(model, organisation=organisation)
"""
- settings = {"organisation": organisation}
-
- for role in settings["organisation"].Roles or []:
- if len(file.get_inverse(role)) == 1:
+ for role in organisation.Roles or []:
+ if (file.get_total_inverses(role)) == 1:
ifcopenshell.api.owner.remove_role(file, role=role)
- for address in settings["organisation"].Addresses or []:
- if len(file.get_inverse(address)) == 1:
+ for address in organisation.Addresses or []:
+ if (file.get_total_inverses(address)) == 1:
ifcopenshell.api.owner.remove_address(file, address=address)
- for inverse in file.get_inverse(settings["organisation"]):
+ for inverse in file.get_inverse(organisation):
if inverse.is_a("IfcOrganizationRelationship"):
- if inverse.RelatingOrganization == settings["organisation"]:
+ if inverse.RelatingOrganization == organisation:
file.remove(inverse)
- elif inverse.RelatedOrganizations == (settings["organisation"],):
+ elif inverse.RelatedOrganizations == (organisation,):
file.remove(inverse)
elif inverse.is_a("IfcDocumentInformation"):
- if inverse.Editors == (settings["organisation"],):
+ if inverse.Editors == (organisation,):
inverse.Editors = None
elif inverse.is_a("IfcPersonAndOrganization"):
ifcopenshell.api.owner.remove_person_and_organisation(file, person_and_organisation=inverse)
elif inverse.is_a("IfcActor"):
ifcopenshell.api.root.remove_product(file, product=inverse)
elif inverse.is_a("IfcResourceLevelRelationship") and not inverse.is_a("IfcOrganizationRelationship"):
- if inverse.RelatedResourceObjects == (settings["organisation"],):
+ if inverse.RelatedResourceObjects == (organisation,):
file.remove(inverse)
elif inverse.is_a("IfcApplication"):
ifcopenshell.api.owner.remove_application(file, application=inverse)
- file.remove(settings["organisation"])
+ file.remove(organisation)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py
index a4f4d64f1f..0c618c1e74 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py
@@ -29,9 +29,7 @@ def remove_person(file: ifcopenshell.file, person: ifcopenshell.entity_instance)
the only responsile person for them.
:param person: The IfcPerson to remove
- :type person: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -41,31 +39,30 @@ def remove_person(file: ifcopenshell.file, person: ifcopenshell.entity_instance)
identification="bobthebuilder", family_name="Thebuilder", given_name="Bob")
ifcopenshell.api.owner.remove_person(model, person=person)
"""
- settings = {"person": person}
- for role in settings["person"].Roles or []:
- if len(file.get_inverse(role)) == 1:
+ for role in person.Roles or []:
+ if file.get_total_inverses(role) == 1:
ifcopenshell.api.owner.remove_role(file, role=role)
- for address in settings["person"].Addresses or []:
- if len(file.get_inverse(address)) == 1:
+ for address in person.Addresses or []:
+ if file.get_total_inverses(address) == 1:
ifcopenshell.api.owner.remove_address(file, address=address)
- for inverse in file.get_inverse(settings["person"]):
+ for inverse in file.get_inverse(person):
if inverse.is_a("IfcWorkControl"):
- if inverse.Creators == (settings["person"],):
+ if inverse.Creators == (person,):
inverse.Creators = None
elif inverse.is_a("IfcInventory"):
- if inverse.ResponsiblePersons == (settings["person"],):
+ if inverse.ResponsiblePersons == (person,):
# in IFC2X3 ResponsiblePersons is not optional and without it IfcInventory is not valid
if file.schema == "IFC2X3":
ifcopenshell.api.root.remove_product(file, product=inverse)
elif inverse.is_a("IfcDocumentInformation"):
- if inverse.Editors == (settings["person"],):
+ if inverse.Editors == (person,):
inverse.Editors = None
elif inverse.is_a("IfcPersonAndOrganization"):
ifcopenshell.api.owner.remove_person_and_organisation(file, person_and_organisation=inverse)
elif inverse.is_a("IfcActor"):
ifcopenshell.api.root.remove_product(file, product=inverse)
elif inverse.is_a("IfcResourceLevelRelationship"):
- if inverse.RelatedResourceObjects == (settings["person"],):
+ if inverse.RelatedResourceObjects == (person,):
file.remove(inverse)
- file.remove(settings["person"])
+ file.remove(person)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py b/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py
index 493f4077f4..b5dedfff49 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py
@@ -30,17 +30,15 @@ def get_application(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instanc
IfcApplication. See ifcopenshell.api.owner.create_owner_history for details.
:param ifc: The IFC file object that is being edited.
- :type ifc: ifcopenshell.file
:return: The IfcApplication with metadata of the authoring software.
- :rtype: ifcopenshell.entity_instance
"""
- app = ifc.by_type("IfcApplication")
+ app = next(iter(ifc.by_type("IfcApplication")), None)
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://docs.ifcopenshell.org/autoapi/ifcopenshell/api/owner/create_owner_history/index.html"
)
- return (app or [None])[0]
+ return app
def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]:
@@ -50,17 +48,15 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None
IfcApplication. See ifcopenshell.api.owner.create_owner_history for details.
:param ifc: The IFC file object that is being edited.
- :type ifc: ifcopenshell.file
:return: The IfcPersonAndOrganization with metadata of the authoring user.
- :rtype: ifcopenshell.entity_instance
"""
- pao = ifc.by_type("IfcPersonAndOrganization")
+ pao = next(iter(ifc.by_type("IfcPersonAndOrganization")), 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://docs.ifcopenshell.org/autoapi/ifcopenshell/api/owner/create_owner_history/index.html"
)
- return (pao or [None])[0]
+ return pao
get_application_factory = get_application
diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile.py b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile.py
index 08892a555b..80f3b8cf71 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile.py
@@ -16,12 +16,14 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see .
+import numpy.typing as npt
import ifcopenshell.util.unit
-from typing import Optional
+from ifcopenshell.util.shape_builder import V, SequenceOfVectors, ifc_safe_vector_type
+from typing import Optional, Union
def add_arbitrary_profile(
- file: ifcopenshell.file, profile: list[tuple[float, float]], name: Optional[str] = None
+ file: ifcopenshell.file, profile: SequenceOfVectors, name: Optional[str] = None
) -> ifcopenshell.entity_instance:
"""Adds a new arbitrary polyline-based profile
@@ -33,13 +35,10 @@ def add_arbitrary_profile(
identical.
:param profile: A list of coordinates
- :type profile: list[tuple[float, float]]
:param name: If the profile is semantically significant (i.e. to be
managed and reused by the user) then it must be named. Otherwise,
this may be left as none.
- :type name: str, optional
:return: The newly created IfcArbitraryClosedProfileDef
- :rtype: ifcopenshell.entity_instance
Example:
@@ -53,26 +52,30 @@ def add_arbitrary_profile(
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"profile": profile, "name": name}
- return usecase.execute()
+ return usecase.execute(V(profile), name)
class Usecase:
- def execute(self):
- self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
- points = [self.convert_si_to_unit(p) for p in self.settings["profile"]]
- if self.file.schema == "IFC2X3":
- curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points])
- else:
- dimensions = len(points[0])
- if dimensions == 2:
- ifc_points = self.file.createIfcCartesianPointList2D(points)
- elif dimensions == 3:
- ifc_points = self.file.createIfcCartesianPointList3D(points)
- curve = self.file.createIfcIndexedPolyCurve(ifc_points)
- return self.file.createIfcArbitraryClosedProfileDef("AREA", self.settings["name"], curve)
+ file: ifcopenshell.file
- def convert_si_to_unit(self, co):
- if isinstance(co, (tuple, list)):
- return [self.convert_si_to_unit(o) for o in co]
- return co / self.settings["unit_scale"]
+ def execute(self, profile: npt.NDArray, name: Union[str, None]):
+ self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
+ points = self.convert_si_to_unit(profile)
+ if self.file.schema == "IFC2X3":
+ curve = self.file.create_entity(
+ "IfcPolyline",
+ [self.file.create_entity("IfcCartesianPoint", ifc_safe_vector_type(p)) for p in points],
+ )
+ else:
+ dimensions = points.shape[1]
+ if dimensions == 2:
+ ifc_points = self.file.create_entity("IfcCartesianPointList2D", ifc_safe_vector_type(points))
+ elif dimensions == 3:
+ ifc_points = self.file.create_entity("IfcCartesianPointList3D", ifc_safe_vector_type(points))
+ else:
+ assert False, f"Invalid dimensions: {dimensions}."
+ curve = self.file.create_entity("IfcIndexedPolyCurve", ifc_points)
+ return self.file.create_entity("IfcArbitraryClosedProfileDef", "AREA", name, curve)
+
+ def convert_si_to_unit(self, co: npt.NDArray) -> npt.NDArray:
+ return co / self.unit_scale
diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py
index 8412d40e23..b813a3c65f 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py
@@ -17,13 +17,15 @@
# along with IfcOpenShell. If not, see .
import ifcopenshell.util.unit
-from typing import Optional
+import numpy.typing as npt
+from ifcopenshell.util.shape_builder import SequenceOfVectors, ifc_safe_vector_type, V
+from typing import Optional, Union
def add_arbitrary_profile_with_voids(
file: ifcopenshell.file,
- outer_profile: list[tuple[float, float]],
- inner_profiles: list[list[tuple[float, float]]],
+ outer_profile: SequenceOfVectors,
+ inner_profiles: list[SequenceOfVectors],
name: Optional[str] = None,
) -> ifcopenshell.entity_instance:
"""Adds a new arbitrary polyline-based profile with voids
@@ -41,15 +43,11 @@ def add_arbitrary_profile_with_voids(
provided in SI meters.
:param outer_profile: A list of coordinates
- :type profile: list[tuple[float, float]]
:param inner_profiles: A list of polylines
- :type profile: list[list[tuple[float, float]]]
:param name: If the profile is semantically significant (i.e. to be
managed and reused by the user) then it must be named. Otherwise,
this may be left as none.
- :type name: str, optional
:return: The newly created IfcArbitraryProfileDefWithVoids
- :rtype: ifcopenshell.entity_instance
Example:
@@ -63,37 +61,52 @@ def add_arbitrary_profile_with_voids(
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"outer_profile": outer_profile, "inner_profiles": inner_profiles, "name": name}
- return usecase.execute()
+ return usecase.execute(V(outer_profile), [V(p) for p in inner_profiles], name)
class Usecase:
- def execute(self):
- self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file)
- outer_points = [self.convert_si_to_unit(p) for p in self.settings["outer_profile"]]
- inner_points = []
- for inner_profile in self.settings["inner_profiles"]:
- inner_points.append([self.convert_si_to_unit(p) for p in inner_profile])
+ file: ifcopenshell.file
+
+ def execute(
+ self,
+ outer_profile: npt.NDArray,
+ inner_profiles: list[npt.NDArray],
+ name: Union[str, None],
+ ):
+ self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
+ outer_points = self.convert_si_to_unit(outer_profile)
+ inner_points: list[npt.NDArray] = []
+ for inner_profile in inner_profiles:
+ inner_points.append(self.convert_si_to_unit(inner_profile))
+
+ inner_curves: list[ifcopenshell.entity_instance] = []
if self.file.schema == "IFC2X3":
- outer_curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in outer_points])
- inner_curves = []
+ outer_curve = self.file.create_entity(
+ "IfcPolyline",
+ [self.file.create_entity("IfcCartesianPoint", ifc_safe_vector_type(p)) for p in outer_points],
+ )
for inner_point in inner_points:
inner_curves.append(
- self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in inner_point])
+ self.file.create_entity(
+ "IfcPolyline",
+ [self.file.create_entity("IfcCartesianPoint", ifc_safe_vector_type(p)) for p in inner_point],
+ )
)
else:
- outer_curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList3D(outer_points))
- inner_curves = []
+ outer_curve = self.file.create_entity(
+ "IfcIndexedPolyCurve",
+ (self.file.create_entity("IfcCartesianPointList3D", ifc_safe_vector_type(outer_points))),
+ )
for inner_point in inner_points:
- dimensions = len(inner_point[0])
+ dimensions = inner_point.shape[1]
if dimensions == 2:
- ifc_points = self.file.createIfcCartesianPointList2D(inner_point)
+ ifc_points = self.file.create_entity("IfcCartesianPointList2D", ifc_safe_vector_type(inner_point))
elif dimensions == 3:
- ifc_points = self.file.createIfcCartesianPointList3D(inner_point)
- inner_curves.append(self.file.createIfcIndexedPolyCurve(ifc_points))
- return self.file.createIfcArbitraryProfileDefWithVoids("AREA", self.settings["name"], outer_curve, inner_curves)
+ ifc_points = self.file.create_entity("IfcCartesianPointList3D", ifc_safe_vector_type(inner_point))
+ else:
+ assert False, f"Invalid dimensions: {dimensions}."
+ inner_curves.append(self.file.create_entity("IfcIndexedPolyCurve", ifc_points))
+ return self.file.create_entity("IfcArbitraryProfileDefWithVoids", "AREA", name, outer_curve, inner_curves)
- def convert_si_to_unit(self, co):
- if isinstance(co, (tuple, list)):
- return [self.convert_si_to_unit(o) for o in co]
- return co / self.settings["unit_scale"]
+ def convert_si_to_unit(self, co: npt.NDArray) -> npt.NDArray:
+ return co / self.unit_scale
diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/edit_profile.py b/src/ifcopenshell-python/ifcopenshell/api/profile/edit_profile.py
index b1fda065f0..ba7bc3cfeb 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/profile/edit_profile.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/profile/edit_profile.py
@@ -26,11 +26,8 @@ def edit_profile(file: ifcopenshell.file, profile: ifcopenshell.entity_instance,
IfcProfileDef, consult the IFC documentation.
:param profile: The IfcProfileDef entity you want to edit
- :type profile: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -43,7 +40,5 @@ def edit_profile(file: ifcopenshell.file, profile: ifcopenshell.entity_instance,
ifcopenshell.api.profile.edit_profile(model,
profile=circle, attributes={"ProfileName": "1000mm Dia"})
"""
- settings = {"profile": profile, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["profile"], name, value)
+ for name, value in attributes.items():
+ setattr(profile, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py
index ffa31faa8e..db585c3276 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py
@@ -20,7 +20,7 @@ import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.api.pset
import ifcopenshell.guid
-from typing import Optional
+from typing import Optional, Any
def add_pset(
@@ -92,12 +92,11 @@ def add_pset(
# Add a fire rating property standardised by buildingSMART.
ifcopenshell.api.pset.edit_pset(model, pset=pset, properties={"FireRating": "2HR"})
"""
- settings = {"product": product, "name": name}
is_ifc2x3 = file.schema == "IFC2X3"
- if settings["product"].is_a("IfcObject") or settings["product"].is_a("IfcContext"):
- for rel in settings["product"].IsDefinedBy or []:
- if rel.is_a("IfcRelDefinesByProperties") and rel.RelatingPropertyDefinition.Name == settings["name"]:
+ if product.is_a("IfcObject") or product.is_a("IfcContext"):
+ for rel in product.IsDefinedBy or []:
+ if rel.is_a("IfcRelDefinesByProperties") and rel.RelatingPropertyDefinition.Name == name:
return rel.RelatingPropertyDefinition
pset = file.create_entity(
@@ -105,15 +104,15 @@ def add_pset(
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
- "Name": settings["name"],
+ "Name": name,
},
)
- ifcopenshell.api.pset.assign_pset(file, [settings["product"]], pset)
+ ifcopenshell.api.pset.assign_pset(file, [product], pset)
return pset
- elif settings["product"].is_a("IfcTypeObject"):
- for definition in settings["product"].HasPropertySets or []:
- if definition.Name == settings["name"]:
+ elif product.is_a("IfcTypeObject"):
+ for definition in product.HasPropertySets or []:
+ if definition.Name == name:
return definition
pset = file.create_entity(
@@ -121,42 +120,43 @@ def add_pset(
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
- "Name": settings["name"],
+ "Name": name,
},
)
- ifcopenshell.api.pset.assign_pset(file, [settings["product"]], pset)
+ ifcopenshell.api.pset.assign_pset(file, [product], pset)
return pset
# in IFC2X3 IfcMaterialDefinition not yet existed
- elif settings["product"].is_a("IfcMaterialDefinition") or settings["product"].is_a("IfcMaterial"):
- kwargs = {"Material": settings["product"]}
+ elif product.is_a("IfcMaterialDefinition") or product.is_a("IfcMaterial"):
+ kwargs: dict[str, Any]
+ kwargs = {"Material": product}
if file.schema == "IFC2X3":
ifc_class = ifc2x3_subclass or "IfcExtendedMaterialProperties"
- definitions = (d for d in file.by_type("IfcMaterialProperties") if d.Material == settings["product"])
+ definitions = (d for d in file.by_type("IfcMaterialProperties") if d.Material == product)
if ifc_class == "IfcExtendedMaterialProperties":
- kwargs["Name"] = settings["name"]
+ kwargs["Name"] = name
else:
ifc_class = "IfcMaterialProperties"
- definitions = settings["product"].HasProperties
- kwargs["Name"] = settings["name"]
+ definitions = product.HasProperties
+ kwargs["Name"] = name
for definition in definitions:
# In IFC2X3 not all IfcMaterialProperties has Name
- if getattr(definition, "Name", None) == settings["name"]:
+ if getattr(definition, "Name", None) == name:
return definition
return file.create_entity(ifc_class, **kwargs)
- elif settings["product"].is_a("IfcProfileDef"):
+ elif product.is_a("IfcProfileDef"):
# in IFC2X3 IfcProfileProperties doesn't have Name and we cannot identify them
if file.schema != "IFC2X3":
- for definition in settings["product"].HasProperties or []:
- if definition.Name == settings["name"]:
+ for definition in product.HasProperties or []:
+ if definition.Name == name:
return definition
kwargs = {}
- kwargs["ProfileDefinition"] = settings["product"]
+ kwargs["ProfileDefinition"] = product
if file.schema != "IFC2X3":
- kwargs["Name"] = settings["name"]
+ kwargs["Name"] = name
if is_ifc2x3:
ifc_class = ifc2x3_subclass or "IfcGeneralProfileProperties"
@@ -164,4 +164,4 @@ def add_pset(
ifc_class = "IfcProfileProperties"
return file.create_entity(ifc_class, **kwargs)
- raise TypeError(f"Class '{settings['product'].is_a(True)}' doesn't support adding a property set.")
+ raise TypeError(f"Class '{product.is_a(True)}' doesn't support adding a property set.")
diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py b/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py
index 15646fe9c7..415b8ec187 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py
@@ -19,6 +19,7 @@
import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.guid
+from typing import Any
def add_qto(file: ifcopenshell.file, product: ifcopenshell.entity_instance, name: str) -> ifcopenshell.entity_instance:
@@ -83,9 +84,15 @@ def add_qto(file: ifcopenshell.file, product: ifcopenshell.entity_instance, name
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
- if self.settings["product"].is_a("IfcObject") or self.settings["product"].is_a("IfcContext"):
- for rel in self.settings["product"].IsDefinedBy or []:
+ product: ifcopenshell.entity_instance = self.settings["product"]
+ name: str = self.settings["name"]
+
+ if product.is_a("IfcObject") or product.is_a("IfcContext"):
+ for rel in product.IsDefinedBy or []:
if (
rel.is_a("IfcRelDefinesByProperties")
and rel.RelatingPropertyDefinition.Name == self.settings["name"]
@@ -103,14 +110,14 @@ class Usecase:
}
)
return qto
- elif self.settings["product"].is_a("IfcTypeObject"):
- for definition in self.settings["product"].HasPropertySets or []:
- if definition.Name == self.settings["name"]:
+ elif product.is_a("IfcTypeObject"):
+ for definition in product.HasPropertySets or []:
+ if definition.Name == name:
return definition
qto = self.create_qto()
- has_property_sets = list(self.settings["product"].HasPropertySets or [])
+ has_property_sets = list(product.HasPropertySets or [])
has_property_sets.append(qto)
- self.settings["product"].HasPropertySets = has_property_sets
+ product.HasPropertySets = has_property_sets
return qto
def create_qto(self):
diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py b/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py
index 1984506c30..3677a8e63e 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py
@@ -28,11 +28,8 @@ def edit_prop_template(
IfcSimplePropertyTemplate, consult the IFC documentation.
:param prop_template: The IfcSimplePropertyTemplate entity you want to edit
- :type prop_template: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_pset_template.py b/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_pset_template.py
index 1682fa150d..d143b5554e 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_pset_template.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_pset_template.py
@@ -28,11 +28,8 @@ def edit_pset_template(
IfcPropertySetTemplate, consult the IFC documentation.
:param pset_template: The IfcPropertySetTemplate entity you want to edit
- :type pset_template: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -45,7 +42,5 @@ def edit_pset_template(
ifcopenshell.api.pset_template.edit_pset_template(model,
pset_template=template, attributes={"Name": "ABC_RiskFactors"})
"""
- settings = {"pset_template": pset_template, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["pset_template"], name, value)
+ for name, value in attributes.items():
+ setattr(pset_template, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource_quantity.py
index b445c1d1ad..fc67a8d3c7 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource_quantity.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource_quantity.py
@@ -36,15 +36,12 @@ def add_resource_quantity(
This base quantity is then used in other calculations.
:param resource: The IfcConstructionResource to add a quantity to.
- :type resource: ifcopenshell.entity_instance
:param ifc_class: The type of quantity to add, chosen from
IfcQuantityArea (for material), IfcQuantityCount (for products),
IfcQuantityLength (for material), IfcQuantityTime (for equipment or
labour), IfcQuantityVolume (for material), and IfcQuantityWeight
(for material).
- :type ifc_class: str,optional
:return: The newly created quantity depending on the IFC class
- :rtype: ifcopenshell.entity_instance
Example:
@@ -65,8 +62,6 @@ def add_resource_quantity(
ifcopenshell.api.resource.edit_resource_quantity(model,
physical_quantity=quantity, attributes={"TimeValue": 8.0})
"""
- settings = {"resource": resource, "ifc_class": ifc_class}
-
resource_type = resource.is_a()
supported_quantities = ifcopenshell.util.resource.RESOURCES_TO_QUANTITIES[resource_type]
if ifc_class not in supported_quantities:
@@ -75,14 +70,14 @@ def add_resource_quantity(
f"Supported quantities: {','.join(supported_quantities)}"
)
- quantity = file.create_entity(settings["ifc_class"], Name="Unnamed")
+ quantity = file.create_entity(ifc_class, Name="Unnamed")
# 3 IfcPhysicalSimpleQuantity Value
- if settings["ifc_class"] == "IfcQuantityCount":
+ if ifc_class == "IfcQuantityCount":
quantity[3] = 0
else:
quantity[3] = 0.0
- old_quantity = settings["resource"].BaseQuantity
- settings["resource"].BaseQuantity = quantity
+ old_quantity = resource.BaseQuantity
+ resource.BaseQuantity = quantity
if old_quantity:
ifcopenshell.util.element.remove_deep(file, old_quantity)
return quantity
diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource.py b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource.py
index b5bb899ead..3476032c1a 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource.py
@@ -26,11 +26,8 @@ def edit_resource(file: ifcopenshell.file, resource: ifcopenshell.entity_instanc
IfcResource, consult the IFC documentation.
:param resource: The IfcResource entity you want to edit
- :type resource: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -42,7 +39,5 @@ def edit_resource(file: ifcopenshell.file, resource: ifcopenshell.entity_instanc
# Change the name of the resource to "Zone A Crew"
ifcopenshell.api.resource.edit_resource(model, resource=resource, attributes={"Name": "Foo"})
"""
- settings = {"resource": resource, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["resource"], name, value)
+ for name, value in attributes.items():
+ setattr(resource, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_quantity.py
index af8f55f94f..87a14774d8 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_quantity.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_quantity.py
@@ -28,11 +28,8 @@ def edit_resource_quantity(
IfC quantity, consult the IFC documentation.
:param physical_quantity: The IfC quantity entity you want to edit
- :type physical_quantity: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -53,10 +50,5 @@ def edit_resource_quantity(
ifcopenshell.api.resource.edit_resource_quantity(model,
physical_quantity=time, attributes={"TimeValue": 8.0})
"""
- settings = {
- "physical_quantity": physical_quantity,
- "attributes": attributes,
- }
-
- for name, value in settings["attributes"].items():
- setattr(settings["physical_quantity"], name, value)
+ for name, value in attributes.items():
+ setattr(physical_quantity, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py
index 687fe0490d..7326a5fd90 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py
@@ -18,6 +18,9 @@
import ifcopenshell
import ifcopenshell.api.sequence
+import ifcopenshell.util.constraint
+import ifcopenshell.util.date
+import ifcopenshell.util.resource
from typing import Any
@@ -30,11 +33,8 @@ def edit_resource_time(
IfcResourceTime, consult the IFC documentation.
:param resource_time: The IfcResourceTime entity you want to edit
- :type resource_time: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -62,25 +62,23 @@ def edit_resource_time(
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"resource_time": resource_time, "attributes": attributes}
- return usecase.execute()
+ return usecase.execute(resource_time, attributes)
class Usecase:
- def execute(self):
- self.resource = self.get_resource()
+ file: ifcopenshell.file
+
+ def execute(self, resource_time: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
+ resource = self.get_resource(resource_time)
# If the user specifies both an end date and a duration, the duration takes priority
- if (
- self.settings["attributes"].get("ScheduleWork", None)
- and "ScheduleFinish" in self.settings["attributes"].keys()
- ):
- del self.settings["attributes"]["ScheduleFinish"]
- if self.settings["attributes"].get("ActualWork", None) and "ActualFinish" in self.settings["attributes"].keys():
- del self.settings["attributes"]["ActualFinish"]
+ if attributes.get("ScheduleWork", None) and "ScheduleFinish" in attributes.keys():
+ del attributes["ScheduleFinish"]
+ if attributes.get("ActualWork", None) and "ActualFinish" in attributes.keys():
+ del attributes["ActualFinish"]
- for name, value in self.settings["attributes"].items():
- metrics = ifcopenshell.util.constraint.get_metric_constraints(self.resource, "Usage." + name)
+ for name, value in attributes.items():
+ metrics = ifcopenshell.util.constraint.get_metric_constraints(resource, "Usage." + name)
if metrics and ifcopenshell.util.constraint.is_hard_constraint(metrics[0]):
continue
if value:
@@ -88,13 +86,13 @@ class Usecase:
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime")
elif name == "ScheduleWork" or name == "ActualWork" or name == "RemainingTime":
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration")
- setattr(self.settings["resource_time"], name, value)
+ setattr(resource_time, name, value)
if name == "ScheduleUsage" and ifcopenshell.util.constraint.get_metric_constraints(
- self.resource, "Usage.ScheduleWork"
+ resource, "Usage.ScheduleWork"
):
- task = ifcopenshell.util.resource.get_task_assignments(self.resource)
+ task = ifcopenshell.util.resource.get_task_assignments(resource)
if task:
ifcopenshell.api.sequence.calculate_task_duration(self.file, task=task)
- def get_resource(self):
- return [e for e in self.file.get_inverse(self.settings["resource_time"]) if e.is_a("IfcResource")][0]
+ def get_resource(self, resource_time: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
+ return next(e for e in self.file.get_inverse(resource_time) if e.is_a("IfcResource"))
diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py b/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py
index a55c711e53..6f02a4531d 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py
@@ -23,6 +23,7 @@ import ifcopenshell.api.geometry
import ifcopenshell.util.system
import ifcopenshell.util.element
import ifcopenshell.util.placement
+from typing import Any
def copy_class(file: ifcopenshell.file, product: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
@@ -74,6 +75,9 @@ def copy_class(file: ifcopenshell.file, product: ifcopenshell.entity_instance) -
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
result = ifcopenshell.util.element.copy(self.file, self.settings["product"])
self.copy_direct_attributes(result)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py b/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py
index a31dc4c00a..67d67ed7f2 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py
@@ -19,7 +19,7 @@
import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.guid
-from typing import Optional
+from typing import Optional, Any
def create_entity(
@@ -80,6 +80,9 @@ def create_entity(
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
element = self.file.create_entity(
self.settings["ifc_class"],
diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py
index 3e4cd3faf4..7682856864 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py
@@ -27,7 +27,7 @@ import ifcopenshell.util.representation
import ifcopenshell.util.type
import ifcopenshell.util.schema
import ifcopenshell.util.element
-from typing import Optional, Union, Literal
+from typing import Optional, Union, Literal, Any
def reassign_class(
@@ -87,6 +87,7 @@ def reassign_class(
class Usecase:
file: ifcopenshell.file
+ settings: dict[str, Any]
def execute(self):
ifc_class: str = self.settings["ifc_class"]
diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py
index 2df1b4eaa5..1ab4bc432e 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py
@@ -62,42 +62,38 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc
# No we don't.
ifcopenshell.api.root.remove_product(model, product=wall)
"""
- settings = {"product": product}
-
- representations = []
- if settings["product"].is_a("IfcProduct"):
- if settings["product"].Representation:
- representations = settings["product"].Representation.Representations or []
+ representations: list[ifcopenshell.entity_instance] = []
+ if product.is_a("IfcProduct"):
+ if product.Representation:
+ representations = product.Representation.Representations or []
else:
representations = []
# remove object placements
- object_placement = settings["product"].ObjectPlacement
+ object_placement = product.ObjectPlacement
if object_placement:
if file.get_total_inverses(object_placement) == 1:
- settings["product"].ObjectPlacement = None # remove the inverse for remove_deep2 to work
+ product.ObjectPlacement = None # remove the inverse for remove_deep2 to work
ifcopenshell.util.element.remove_deep2(file, object_placement)
- elif settings["product"].is_a("IfcTypeProduct"):
- representations = [rm.MappedRepresentation for rm in settings["product"].RepresentationMaps or []]
+ elif product.is_a("IfcTypeProduct"):
+ representations = [rm.MappedRepresentation for rm in product.RepresentationMaps or []]
# remove psets
- psets = settings["product"].HasPropertySets or []
+ psets = product.HasPropertySets or []
for pset in psets:
if file.get_total_inverses(pset) != 1:
continue
- ifcopenshell.api.pset.remove_pset(file, product=settings["product"], pset=pset)
+ ifcopenshell.api.pset.remove_pset(file, product=product, pset=pset)
for representation in representations:
- ifcopenshell.api.geometry.unassign_representation(
- file, product=settings["product"], representation=representation
- )
- ifcopenshell.api.geometry.remove_representation(file, **{"representation": representation})
- for opening in getattr(settings["product"], "HasOpenings", []) or []:
+ ifcopenshell.api.geometry.unassign_representation(file, product=product, representation=representation)
+ ifcopenshell.api.geometry.remove_representation(file, representation=representation)
+ for opening in getattr(product, "HasOpenings", []) or []:
ifcopenshell.api.feature.remove_feature(file, feature=opening.RelatedOpeningElement)
- if settings["product"].is_a("IfcGrid"):
- for axis in settings["product"].UAxes + settings["product"].VAxes + (settings["product"].WAxes or ()):
+ if product.is_a("IfcGrid"):
+ for axis in product.UAxes + product.VAxes + (product.WAxes or ()):
ifcopenshell.api.grid.remove_grid_axis(file, axis=axis)
def element_exists(element_id):
@@ -108,22 +104,20 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc
return False
# TODO: remove object placement and other relationships
- for inverse_id in [i.id() for i in file.get_inverse(settings["product"])]:
+ for inverse_id in [i.id() for i in file.get_inverse(product)]:
try:
inverse = file.by_id(inverse_id)
except:
continue
if inverse.is_a("IfcRelDefinesByProperties"):
- ifcopenshell.api.pset.remove_pset(
- file, product=settings["product"], pset=inverse.RelatingPropertyDefinition
- )
+ ifcopenshell.api.pset.remove_pset(file, product=product, pset=inverse.RelatingPropertyDefinition)
elif inverse.is_a("IfcRelAssociatesMaterial"):
- ifcopenshell.api.material.unassign_material(file, products=[settings["product"]])
+ ifcopenshell.api.material.unassign_material(file, products=[product])
elif inverse.is_a("IfcRelDefinesByType"):
- if inverse.RelatingType == settings["product"]:
+ if inverse.RelatingType == product:
ifcopenshell.api.type.unassign_type(file, related_objects=inverse.RelatedObjects)
else:
- ifcopenshell.api.type.unassign_type(file, related_objects=[settings["product"]])
+ ifcopenshell.api.type.unassign_type(file, related_objects=[product])
elif inverse.is_a("IfcRelSpaceBoundary"):
ifcopenshell.api.boundary.remove_boundary(file, boundary=inverse)
elif inverse.is_a("IfcRelFillsElement"):
@@ -142,7 +136,7 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc
if history:
ifcopenshell.util.element.remove_deep2(file, history)
elif inverse.is_a("IfcRelNests"):
- if inverse.RelatingObject == settings["product"]:
+ if inverse.RelatingObject == product:
inverse_id = inverse.id()
for subelement in inverse.RelatedObjects:
if subelement.is_a("IfcDistributionPort"):
@@ -153,27 +147,27 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
- elif inverse.RelatedObjects == (settings["product"],):
+ elif inverse.RelatedObjects == (product,):
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
elif inverse.is_a("IfcRelAggregates"):
- if inverse.RelatingObject == settings["product"] or len(inverse.RelatedObjects) == 1:
+ if inverse.RelatingObject == product or len(inverse.RelatedObjects) == 1:
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
elif inverse.is_a("IfcRelContainedInSpatialStructure"):
- if inverse.RelatingStructure == settings["product"] or len(inverse.RelatedElements) == 1:
+ if inverse.RelatingStructure == product or len(inverse.RelatedElements) == 1:
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
elif inverse.is_a("IfcRelConnectsElements"):
if inverse.is_a("IfcRelConnectsWithRealizingElements"):
- if settings["product"] not in (inverse.RelatingElement, inverse.RelatedElement) and any(
- el for el in inverse.RealizingElements if el != settings["product"]
+ if product not in (inverse.RelatingElement, inverse.RelatedElement) and any(
+ el for el in inverse.RealizingElements if el != product
):
continue
history = inverse.OwnerHistory
@@ -181,15 +175,15 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc
if history:
ifcopenshell.util.element.remove_deep2(file, history)
elif inverse.is_a("IfcRelConnectsPortToElement"):
- if inverse.RelatedElement == settings["product"]:
+ if inverse.RelatedElement == product:
ifcopenshell.api.root.remove_product(file, product=inverse.RelatingPort)
- elif inverse.RelatingPort == settings["product"]:
+ elif inverse.RelatingPort == product:
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
elif inverse.is_a("IfcRelConnectsPorts"):
- if settings["product"] not in (inverse.RelatingPort, inverse.RelatedPort):
+ if product not in (inverse.RelatingPort, inverse.RelatedPort):
# if it's not RelatingPort/RelatedPort then it's optional RealizingElement
# so we keep the relationship
continue
@@ -204,7 +198,7 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc
if history:
ifcopenshell.util.element.remove_deep2(file, history)
elif inverse.is_a("IfcRelAssignsToProduct"):
- if inverse.RelatingProduct == settings["product"]:
+ if inverse.RelatingProduct == product:
history = inverse.OwnerHistory
file.remove(inverse)
if history:
@@ -215,17 +209,17 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc
if history:
ifcopenshell.util.element.remove_deep2(file, history)
elif inverse.is_a("IfcRelFlowControlElements"):
- if inverse.RelatingFlowElement == settings["product"]:
+ if inverse.RelatingFlowElement == product:
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
- elif inverse.RelatedControlElements == (settings["product"],):
+ elif inverse.RelatedControlElements == (product,):
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
- history = settings["product"].OwnerHistory
- file.remove(settings["product"])
+ history = product.OwnerHistory
+ file.remove(product)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py
index 89037d4eb0..38d64aa1bc 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py
@@ -29,11 +29,8 @@ def add_task_time(
(especially for maintenance tasks).
:param task: The task to add time data to.
- :type task: ifcopenshell.entity_instance
:param is_recurring: Whether or not the time should recur.
- :type is_recurring: bool
:return: The newly created IfcTaskTime.
- :rtype: ifcopenshell.entity_instance
Example:
@@ -61,11 +58,9 @@ def add_task_time(
ifcopenshell.api.sequence.edit_task_time(model,
task_time=time, attributes={"ScheduleStart": "2000-01-01", "ScheduleDuration": "P2D"})
"""
- settings = {"task": task, "is_recurring": is_recurring}
-
- if settings["is_recurring"]:
+ if is_recurring:
task_time = file.create_entity("IfcTaskTimeRecurring")
else:
task_time = file.create_entity("IfcTaskTime")
- settings["task"].TaskTime = task_time
+ task.TaskTime = task_time
return task_time
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py
index 65ea1db9bd..2c08ab00a3 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py
@@ -34,17 +34,13 @@ def assign_lag_time(
are allowed.
:param rel_sequence: The IfcRelSequence to assign the lag time to.
- :type rel_sequence: ifcopenshell.entity_instance
:param lag_value: An ISO standardised duration string.
- :type lag_value: str
:param duration_type: Choose from WORKTIME for the associated
calendar-based lag times (this is the most common scenario and is
recommended as a default), or ELAPSEDTIME to not follow the
calendar. You may also choose NOTDEFINED but the behaviour of this
is unclear.
- :type duration_type: str
:return: The newly created IfcLagTime
- :rtype: ifcopenshell.entity_instance
Example:
@@ -84,16 +80,10 @@ def assign_lag_time(
# for whatever reason.
ifcopenshell.api.sequence.assign_lag_time(model, rel_sequence=sequence, lag_value="P1D")
"""
- settings = {
- "rel_sequence": rel_sequence,
- "lag_value": lag_value,
- "duration_type": duration_type,
- }
-
- lag_value = file.createIfcDuration(ifcopenshell.util.date.datetime2ifc(settings["lag_value"], "IfcDuration"))
- lag_time = file.create_entity("IfcLagTime", DurationType=settings["duration_type"], LagValue=lag_value)
- if settings["rel_sequence"].is_a("IfcRelSequence"):
- if settings["rel_sequence"].TimeLag and len(file.get_inverse(settings["rel_sequence"].TimeLag)) == 1:
- file.remove(settings["rel_sequence"].TimeLag)
- settings["rel_sequence"].TimeLag = lag_time
+ duration = file.create_entity("IfcDuration", ifcopenshell.util.date.datetime2ifc(lag_value, "IfcDuration"))
+ lag_time = file.create_entity("IfcLagTime", DurationType=duration_type, LagValue=duration)
+ if rel_sequence.is_a("IfcRelSequence"):
+ if (current_lag_time := rel_sequence.TimeLag) and file.get_total_inverses(current_lag_time) == 1:
+ file.remove(current_lag_time)
+ rel_sequence.TimeLag = lag_time
return lag_time
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py
index 0c64fb09e2..7f74fb300b 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py
@@ -65,11 +65,8 @@ def assign_recurrence_pattern(
:param parent: Either an IfcTaskTimeRecurring if you are defining a
recurring schedule for a task, or IfcWorkTime if you are defining a
recurring pattern for a workdays or holidays in a calendar.
- :type parent: ifcopenshell.entity_instance
:param recurrence_type: One of the types of recurrences.
- :type recurrence_type: str
:return: The newly created IfcRecurrencePattern
- :rtype: ifcopenshell.entity_instance
Example:
@@ -108,16 +105,14 @@ def assign_recurrence_pattern(
ifcopenshell.api.sequence.edit_recurrence_pattern(model,
recurrence_pattern=pattern, attributes={"DayComponent": [1], "Interval": 6})
"""
- settings = {"parent": parent, "recurrence_type": recurrence_type}
+ recurrence = file.create_entity("IfcRecurrencePattern", recurrence_type)
- recurrence = file.createIfcRecurrencePattern(settings["recurrence_type"])
-
- if settings["parent"].is_a("IfcWorkTime"):
- if settings["parent"].RecurrencePattern and len(file.get_inverse(settings["parent"].RecurrencePattern)) == 1:
- file.remove(settings["parent"].RecurrencePattern)
- settings["parent"].RecurrencePattern = recurrence
- elif settings["parent"].is_a("IfcTaskTimeRecurring"):
- if recurrence_old := settings["parent"].Recurrence and len(file.get_inverse(recurrence_old)) == 1:
+ if parent.is_a("IfcWorkTime"):
+ if (old_recurrence := parent.RecurrencePattern) and file.get_total_inverses(old_recurrence) == 1:
+ file.remove(old_recurrence)
+ parent.RecurrencePattern = recurrence
+ elif parent.is_a("IfcTaskTimeRecurring"):
+ if (recurrence_old := parent.Recurrence) and file.get_total_inverses(recurrence_old) == 1:
file.remove(recurrence_old)
- settings["parent"].Recurrence = recurrence
+ parent.Recurrence = recurrence
return recurrence
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py
index 664d5780ec..65491604aa 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py
@@ -51,13 +51,10 @@ def assign_sequence(
predecessor and successor tasks in the planning profession.
:param relating_process: The previous / predecessor task.
- :type relating_process: ifcopenshell.entity_instance
:param related_process: The next / successor task.
- :type related_process: ifcopenshell.entity_instance
:param sequence_type: Choose from FINISH_START, FINISH_FINISH,
START_START, or START_FINISH.
:return: The newly created IfcRelSequence
- :rtype: ifcopenshell.entity_instance
Example:
@@ -109,24 +106,18 @@ def assign_sequence(
# to be 2000-01-05.
ifcopenshell.api.sequence.cascade_schedule(model, task=formwork)
"""
- settings = {
- "relating_process": relating_process,
- "related_process": related_process,
- "sequence_type": sequence_type,
- }
-
- for rel in settings["related_process"].IsSuccessorFrom or []:
- if rel.RelatingProcess == settings["relating_process"]:
+ for rel in related_process.IsSuccessorFrom or []:
+ if rel.RelatingProcess == relating_process:
return rel
rel = file.create_entity(
"IfcRelSequence",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
- "RelatingProcess": settings["relating_process"],
- "RelatedProcess": settings["related_process"],
- "SequenceType": settings["sequence_type"],
+ "RelatingProcess": relating_process,
+ "RelatedProcess": related_process,
+ "SequenceType": sequence_type,
}
)
- ifcopenshell.api.sequence.cascade_schedule(file, task=settings["relating_process"])
+ ifcopenshell.api.sequence.cascade_schedule(file, task=relating_process)
return rel
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py
index 7c3f821e7e..d5f0e5947e 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py
@@ -20,6 +20,7 @@ import math
import ifcopenshell.api.sequence
import ifcopenshell.util.date
import ifcopenshell.util.element
+from typing import Union
def calculate_task_duration(file: ifcopenshell.file, task: ifcopenshell.entity_instance) -> None:
@@ -35,9 +36,7 @@ def calculate_task_duration(file: ifcopenshell.file, task: ifcopenshell.entity_i
then nothing happens.
:param task: The IfcTask to calculate the duration for.
- :type task: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -82,18 +81,20 @@ def calculate_task_duration(file: ifcopenshell.file, task: ifcopenshell.entity_i
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"task": task}
- return usecase.execute()
+ return usecase.execute(task)
class Usecase:
- def execute(self):
+ file: ifcopenshell.file
+
+ def execute(self, task: ifcopenshell.entity_instance) -> None:
+ self.task = task
self.seconds_per_workday = self.calculate_seconds_per_workday()
duration = self.calculate_max_resource_usage_duration()
if duration:
self.set_task_duration(duration)
- def calculate_seconds_per_workday(self):
+ def calculate_seconds_per_workday(self) -> float:
def get_work_schedule(task):
for rel in task.HasAssignments or []:
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkSchedule"):
@@ -102,7 +103,7 @@ class Usecase:
return get_work_schedule(rel.RelatingObject)
default_seconds_per_workday = 8 * 60 * 60
- work_schedule = get_work_schedule(self.settings["task"])
+ work_schedule = get_work_schedule(self.task)
if not work_schedule:
return default_seconds_per_workday
psets = ifcopenshell.util.element.get_psets(work_schedule)
@@ -115,9 +116,9 @@ class Usecase:
work_day_duration = ifcopenshell.util.date.ifc2datetime(psets["Pset_WorkControlCommon"]["WorkDayDuration"])
return work_day_duration.seconds
- def calculate_max_resource_usage_duration(self):
+ def calculate_max_resource_usage_duration(self) -> float:
max_duration = 0
- for rel in self.settings["task"].OperatesOn or []:
+ for rel in self.task.OperatesOn or []:
for related_object in rel.RelatedObjects:
if related_object.is_a("IfcConstructionResource"):
duration = self.calculate_duration_in_days(related_object)
@@ -125,7 +126,7 @@ class Usecase:
max_duration = duration
return max_duration
- def calculate_duration_in_days(self, resource):
+ def calculate_duration_in_days(self, resource: ifcopenshell.entity_instance) -> Union[float, None]:
def is_hourly_work(schedule_work):
return "T" in schedule_work
@@ -140,7 +141,7 @@ class Usecase:
schedule_seconds = (schedule_duration.days + partial_days) * self.seconds_per_workday
return math.ceil((schedule_seconds / self.seconds_per_workday) / schedule_usage)
- def set_task_duration(self, duration):
- if not self.settings["task"].TaskTime:
- ifcopenshell.api.sequence.add_task_time(self.file, task=self.settings["task"])
- self.settings["task"].TaskTime.ScheduleDuration = f"P{duration}D"
+ def set_task_duration(self, duration: float) -> None:
+ if not (task_time := self.task.TaskTime):
+ task_time = ifcopenshell.api.sequence.add_task_time(self.file, task=self.task)
+ task_time.ScheduleDuration = f"P{duration}D"
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py
index 8a989ecb55..65cbd5a58a 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py
@@ -19,6 +19,8 @@
import datetime
import ifcopenshell.util.date
import ifcopenshell.util.sequence
+from ifcopenshell.util.sequence import DURATION_TYPE
+from typing import Union, Optional
def cascade_schedule(file: ifcopenshell.file, task: ifcopenshell.entity_instance) -> None:
@@ -41,9 +43,7 @@ def cascade_schedule(file: ifcopenshell.file, task: ifcopenshell.entity_instance
be equivalent to be Tuesday 8am, for instance.
:param task: The start task to begin cascading from.
- :type task: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -103,16 +103,22 @@ def cascade_schedule(file: ifcopenshell.file, task: ifcopenshell.entity_instance
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"task": task}
- return usecase.execute()
+ return usecase.execute(task)
class Usecase:
- def execute(self):
- self.calendar_cache = {}
- self.cascade_task(self.settings["task"], is_first_task=True)
+ file: ifcopenshell.file
- def cascade_task(self, task, is_first_task=False, task_sequence=None):
+ def execute(self, task: ifcopenshell.entity_instance):
+ self.calendar_cache = {}
+ self.cascade_task(task, is_first_task=True)
+
+ def cascade_task(
+ self,
+ task: ifcopenshell.entity_instance,
+ is_first_task: bool = False,
+ task_sequence: Optional[list[ifcopenshell.entity_instance]] = None,
+ ) -> None:
if task_sequence is None:
task_sequence = []
@@ -316,18 +322,22 @@ class Usecase:
for nested_task in rel.RelatedObjects or []
]
- def get_lag_time_days(self, lag_time):
+ def get_lag_time_days(self, lag_time: ifcopenshell.entity_instance) -> int:
return ifcopenshell.util.date.ifc2datetime(lag_time.LagValue.wrappedValue).days
- def get_calendar(self, task):
+ def get_calendar(self, task: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
if task.id() not in self.calendar_cache:
self.calendar_cache[task.id()] = ifcopenshell.util.sequence.derive_calendar(task)
return self.calendar_cache[task.id()]
- def offset_date(self, date, days, duration_type, calendar):
+ def offset_date(
+ self, date: datetime.datetime, days: int, duration_type: DURATION_TYPE, calendar: ifcopenshell.entity_instance
+ ) -> datetime.datetime:
return ifcopenshell.util.sequence.offset_date(date, datetime.timedelta(days=days), duration_type, calendar)
- def get_task_time_attribute(self, task, attribute):
+ def get_task_time_attribute(
+ self, task: ifcopenshell.entity_instance, attribute: str
+ ) -> Union[datetime.datetime, None]:
if task.TaskTime:
value = getattr(task.TaskTime, attribute)
if value:
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py
index d0670b76e8..ab1035bbab 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py
@@ -24,7 +24,7 @@ import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.sequence
import ifcopenshell.util.system
-from typing import Optional
+from typing import Optional, Union
def create_baseline(
@@ -44,11 +44,8 @@ def create_baseline(
* Same Resource Relationships
:param work_schedule: The planned work_schedule to baseline
- :type work_schedule: ifcopenshell.entity_instance
:param name: baseline work schedule name
- :type name: str, optional
:return: The baseline work_schedule
- :rtype: ifcopenshell.entity_instance
Example:
@@ -62,33 +59,34 @@ def create_baseline(
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"work_schedule": work_schedule, "name": name}
- return usecase.execute()
+ return usecase.execute(work_schedule, name)
class Usecase:
- def execute(self):
- result = self.create_baseline_work_schedule(self.settings["work_schedule"])
- return result
+ file: ifcopenshell.file
- def create_baseline_work_schedule(self, work_schedule):
+ def execute(self, work_schedule: ifcopenshell.entity_instance, name: Union[str, None]) -> None:
# create work schedule
if not work_schedule.PredefinedType == "PLANNED":
return
baseline_work_schedule = ifcopenshell.api.sequence.add_work_schedule(
self.file, name=work_schedule.Name, predefined_type="BASELINE"
)
- baseline_work_schedule.Name = self.settings["name"]
+ baseline_work_schedule.Name = name
self.create_baseline_reference(work_schedule, baseline_work_schedule)
for summary_task in ifcopenshell.util.sequence.get_root_tasks(work_schedule):
- current, duplicate = ifcopenshell.api.sequence.duplicate_task(self.file, task=summary_task)
+ res = ifcopenshell.api.sequence.duplicate_task(self.file, task=summary_task)
+ assert isinstance(res, list)
+ current, duplicate = res
ifcopenshell.api.control.assign_control(
self.file, relating_control=baseline_work_schedule, related_object=duplicate[0]
)
for i, task in enumerate(current):
self.create_baseline_reference(task, duplicate[i])
- def create_baseline_reference(self, relating_object, related_object):
+ def create_baseline_reference(
+ self, relating_object: ifcopenshell.entity_instance, related_object: ifcopenshell.entity_instance
+ ) -> ifcopenshell.entity_instance:
referenced_by = None
if relating_object.Declares:
referenced_by = relating_object.Declares[0]
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py
index db47ccd5ec..7f974bb84e 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py
@@ -23,9 +23,12 @@ import ifcopenshell.api.owner
import ifcopenshell.api.sequence
import ifcopenshell.util.element
import ifcopenshell.util.sequence
+from typing import Union, Any
-def duplicate_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
+def duplicate_task(
+ file: ifcopenshell.file, task: ifcopenshell.entity_instance
+) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]:
"""Duplicates a task in the project
The following relationships are also duplicated:
@@ -35,9 +38,7 @@ def duplicate_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance)
* The copy will have duplicated nested tasks
:param task: The task to be duplicated
- :type task: ifcopenshell.entity_instance
:return: The duplicated task or the list of duplicated tasks if the latter has children
- :rtype: ifcopenshell.entity_instance or list of ifcopenshell.entity_instance
Example:
.. code:: python
@@ -55,6 +56,9 @@ def duplicate_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance)
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
self.tracker = {"current": [], "duplicate": []}
self.duplicate_task(self.settings["task"])
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_lag_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_lag_time.py
index d3c8a4418a..d55bf0e4b5 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_lag_time.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_lag_time.py
@@ -28,11 +28,8 @@ def edit_lag_time(file: ifcopenshell.file, lag_time: ifcopenshell.entity_instanc
IfcLagTime, consult the IFC documentation.
:param lag_time: The IfcLagTime entity you want to edit
- :type lag_time: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -75,14 +72,12 @@ def edit_lag_time(file: ifcopenshell.file, lag_time: ifcopenshell.entity_instanc
# Or, let's make it 2 days instead.
ifcopenshell.api.sequence.edit_lag_time(model, lag_time=lag, attributes={"LagValue": "P2D"})
"""
- settings = {"lag_time": lag_time, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
+ for name, value in attributes.items():
if name == "LagValue" and value is not None:
if isinstance(value, float):
value = file.createIfcRatioMeasure(value)
else:
value = file.createIfcDuration(ifcopenshell.util.date.datetime2ifc(value, "IfcDuration"))
- setattr(settings["lag_time"], name, value)
- for rel in [r for r in file.get_inverse(settings["lag_time"]) if r.is_a("IfcRelSequence")]:
+ setattr(lag_time, name, value)
+ for rel in [r for r in file.get_inverse(lag_time) if r.is_a("IfcRelSequence")]:
ifcopenshell.api.sequence.cascade_schedule(file, task=rel.RelatedProcess)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_recurrence_pattern.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_recurrence_pattern.py
index 6b165f716a..519c5cdf25 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_recurrence_pattern.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_recurrence_pattern.py
@@ -30,11 +30,8 @@ def edit_recurrence_pattern(
IfcRecurrencePattern, consult the IFC documentation.
:param recurrence_pattern: The IfcRecurrencePattern entity you want to edit
- :type recurrence_pattern: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -55,13 +52,8 @@ def edit_recurrence_pattern(
ifcopenshell.api.sequence.edit_recurrence_pattern(model,
recurrence_pattern=pattern, attributes={"WeekdayComponent": [1, 2, 3, 4, 5]})
"""
- settings = {
- "recurrence_pattern": recurrence_pattern,
- "attributes": attributes,
- }
-
- for name, value in settings["attributes"].items():
- setattr(settings["recurrence_pattern"], name, value)
+ for name, value in attributes.items():
+ setattr(recurrence_pattern, name, value)
ifcopenshell.util.sequence.is_working_day.cache_clear()
ifcopenshell.util.sequence.is_calendar_applicable.cache_clear()
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_sequence.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_sequence.py
index dee8a32e64..2686ec5ace 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_sequence.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_sequence.py
@@ -30,11 +30,8 @@ def edit_sequence(
IfcRelSequence, consult the IFC documentation.
:param rel_sequence: The IfcRelSequence entity you want to edit
- :type rel_sequence: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -62,9 +59,7 @@ def edit_sequence(
ifcopenshell.api.sequence.edit_sequence(model,
rel_sequence=sequence, attributes={"SequenceType": "START_START"})
"""
- settings = {"rel_sequence": rel_sequence, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["rel_sequence"], name, value)
- if "SequenceType" in settings["attributes"].keys():
- ifcopenshell.api.sequence.cascade_schedule(file, task=settings["rel_sequence"].RelatedProcess)
+ for name, value in attributes.items():
+ setattr(rel_sequence, name, value)
+ if "SequenceType" in attributes.keys():
+ ifcopenshell.api.sequence.cascade_schedule(file, task=rel_sequence.RelatedProcess)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py
index dcd218059f..3fa1b88caf 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py
@@ -26,11 +26,8 @@ def edit_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance, attri
IfcTask, consult the IFC documentation.
:param task: The IfcTask entity you want to edit
- :type task: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -48,7 +45,5 @@ def edit_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance, attri
# Change the identification
ifcopenshell.api.sequence.edit_task(model, task=task, attributes={"Identification": "M"})
"""
- settings = {"task": task, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["task"], name, value)
+ for name, value in attributes.items():
+ setattr(task, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py
index 6d9d3404d1..119ab82a14 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py
@@ -36,11 +36,8 @@ def edit_task_time(
IfcTaskTime, consult the IFC documentation.
:param task_time: The IfcTaskTime entity you want to edit
- :type task_time: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -61,94 +58,89 @@ def edit_task_time(
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"task_time": task_time, "attributes": attributes}
- return usecase.execute()
+ return usecase.execute(task_time, attributes)
class Usecase:
- def execute(self):
+ file: ifcopenshell.file
+
+ def execute(self, task_time: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
+ self.task_time = task_time
self.task = self.get_task()
self.calendar = ifcopenshell.util.sequence.derive_calendar(self.task)
# If the user specifies both an end date and a duration, the duration takes priority
- if (
- self.settings["attributes"].get("ScheduleDuration", None)
- and "ScheduleFinish" in self.settings["attributes"].keys()
- ):
- del self.settings["attributes"]["ScheduleFinish"]
+ if attributes.get("ScheduleDuration", None) and "ScheduleFinish" in attributes.keys():
+ del attributes["ScheduleFinish"]
- duration_type = self.settings["attributes"].get("DurationType", self.settings["task_time"].DurationType)
- finish = self.settings["attributes"].get("ScheduleFinish", None)
+ duration_type = attributes.get("DurationType", self.task_time.DurationType)
+ finish = attributes.get("ScheduleFinish", None)
if finish:
if isinstance(finish, str):
finish = datetime.datetime.fromisoformat(finish)
- self.settings["attributes"]["ScheduleFinish"] = datetime.datetime.combine(
+ attributes["ScheduleFinish"] = datetime.datetime.combine(
ifcopenshell.util.sequence.get_soonest_working_day(finish, duration_type, self.calendar),
datetime.time(17),
)
- start = self.settings["attributes"].get("ScheduleStart", None)
+ start = attributes.get("ScheduleStart", None)
if start:
if isinstance(start, str):
start = datetime.datetime.fromisoformat(start)
- self.settings["attributes"]["ScheduleStart"] = datetime.datetime.combine(
+ attributes["ScheduleStart"] = datetime.datetime.combine(
ifcopenshell.util.sequence.get_soonest_working_day(start, duration_type, self.calendar),
datetime.time(9),
)
- for name, value in self.settings["attributes"].items():
+ for name, value in attributes.items():
if value is not None:
if "Start" in name or "Finish" in name or name == "StatusTime":
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime")
elif name == "ScheduleDuration" or name == "ActualDuration" or name == "RemainingTime":
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration")
- setattr(self.settings["task_time"], name, value)
+ setattr(self.task_time, name, value)
- if (
- "ScheduleDuration" in self.settings["attributes"].keys()
- and self.settings["task_time"].ScheduleDuration
- and self.settings["task_time"].ScheduleStart
- ):
+ if "ScheduleDuration" in attributes.keys() and task_time.ScheduleDuration and task_time.ScheduleStart:
self.calculate_finish()
- elif self.settings["attributes"].get("ScheduleStart", None) and self.settings["task_time"].ScheduleDuration:
+ elif attributes.get("ScheduleStart", None) and task_time.ScheduleDuration:
self.calculate_finish()
- elif self.settings["attributes"].get("ScheduleFinish", None) and self.settings["task_time"].ScheduleStart:
+ elif attributes.get("ScheduleFinish", None) and task_time.ScheduleStart:
self.calculate_duration()
- if self.settings["task_time"].ScheduleDuration and (
- "ScheduleStart" in self.settings["attributes"].keys()
- or "ScheduleFinish" in self.settings["attributes"].keys()
- or "ScheduleDuration" in self.settings["attributes"].keys()
+ if task_time.ScheduleDuration and (
+ "ScheduleStart" in attributes.keys()
+ or "ScheduleFinish" in attributes.keys()
+ or "ScheduleDuration" in attributes.keys()
):
ifcopenshell.api.sequence.cascade_schedule(self.file, task=self.task)
- if self.settings["task_time"].ScheduleDuration:
+ if task_time.ScheduleDuration:
self.handle_resource_calculation()
def calculate_finish(self):
finish = ifcopenshell.util.sequence.get_start_or_finish_date(
- ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleStart),
- ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleDuration),
- self.settings["task_time"].DurationType,
+ ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleStart),
+ ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleDuration),
+ self.task_time.DurationType,
self.calendar,
date_type="FINISH",
)
- self.settings["task_time"].ScheduleFinish = ifcopenshell.util.date.datetime2ifc(finish, "IfcDateTime")
+ self.task_time.ScheduleFinish = ifcopenshell.util.date.datetime2ifc(finish, "IfcDateTime")
def calculate_duration(self):
- start = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleStart)
- finish = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleFinish)
+ start = ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleStart)
+ finish = ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleFinish)
current_date = datetime.date(start.year, start.month, start.day)
finish_date = datetime.date(finish.year, finish.month, finish.day)
duration = datetime.timedelta(days=1)
while current_date < finish_date:
- if self.settings["task_time"].DurationType == "ELAPSEDTIME" or not self.calendar:
+ if self.task_time.DurationType == "ELAPSEDTIME" or not self.calendar:
duration += datetime.timedelta(days=1)
elif ifcopenshell.util.sequence.is_working_day(current_date, self.calendar):
duration += datetime.timedelta(days=1)
current_date += datetime.timedelta(days=1)
- self.settings["task_time"].ScheduleDuration = ifcopenshell.util.date.datetime2ifc(duration, "IfcDuration")
+ self.task_time.ScheduleDuration = ifcopenshell.util.date.datetime2ifc(duration, "IfcDuration")
def get_task(self) -> ifcopenshell.entity_instance:
- return next(e for e in self.file.get_inverse(self.settings["task_time"]) if e.is_a("IfcTask"))
+ return next(e for e in self.file.get_inverse(self.task_time) if e.is_a("IfcTask"))
def handle_resource_calculation(self):
resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_deep=False)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_calendar.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_calendar.py
index f82f53e534..83e56a4372 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_calendar.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_calendar.py
@@ -28,11 +28,8 @@ def edit_work_calendar(
IfcWorkCalendar, consult the IFC documentation.
:param work_calendar: The IfcWorkCalendar entity you want to edit
- :type work_calendar: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -45,7 +42,5 @@ def edit_work_calendar(
ifcopenshell.api.sequence.edit_work_calendar(model,
work_calendar=calendar, attributes={"Description": "Monday to Friday 8 hour days"})
"""
- settings = {"work_calendar": work_calendar, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
- setattr(settings["work_calendar"], name, value)
+ for name, value in attributes.items():
+ setattr(work_calendar, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_plan.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_plan.py
index 920e2c671e..145460e11c 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_plan.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_plan.py
@@ -29,11 +29,8 @@ def edit_work_plan(
IfcWorkPlan, consult the IFC documentation.
:param work_plan: The IfcWorkPlan entity you want to edit
- :type work_plan: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -46,12 +43,10 @@ def edit_work_plan(
ifcopenshell.api.sequence.edit_work_plan(model,
work_plan=work_plan, attributes={"Description": "Construction of phase 1"})
"""
- settings = {"work_plan": work_plan, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
+ for name, value in attributes.items():
if value:
if "Date" in name or "Time" in name:
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime")
elif name == "Duration" or name == "TotalFloat":
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration")
- setattr(settings["work_plan"], name, value)
+ setattr(work_plan, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py
index 5ed392ddff..98905b9a1f 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py
@@ -29,11 +29,8 @@ def edit_work_schedule(
IfcWorkSchedule, consult the IFC documentation.
:param work_schedule: The IfcWorkSchedule entity you want to edit
- :type work_schedule: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -50,12 +47,10 @@ def edit_work_schedule(
ifcopenshell.api.sequence.edit_work_schedule(model,
work_schedule=work_schedule, attributes={"Description": "3 crane design option"})
"""
- settings = {"work_schedule": work_schedule, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
+ for name, value in attributes.items():
if value:
if "Date" in name or "Time" in name:
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime")
elif name == "Duration" or name == "TotalFloat":
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration")
- setattr(settings["work_schedule"], name, value)
+ setattr(work_schedule, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py
index 07c1f818d5..7fae8b4c18 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py
@@ -31,11 +31,8 @@ def edit_work_time(
IfcWorkTime, consult the IFC documentation.
:param work_time: The IfcWorkTime entity you want to edit
- :type work_time: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -54,16 +51,14 @@ def edit_work_time(
ifcopenshell.api.sequence.edit_work_time(model,
work_time=work_time, attributes={"StartDate": "2000-01-01", "FinishDate": "2000-01-02"})
"""
- settings = {"work_time": work_time, "attributes": attributes}
-
- for name, value in settings["attributes"].items():
+ for name, value in attributes.items():
if name in ("Start", "StartDate"):
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate")
# 4 IfcWorktime Start
- settings["work_time"][4] = value
+ work_time[4] = value
elif name in ("Finish", "FinishDate"):
value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate")
# 5 IfcWorktime Finish
- settings["work_time"][5] = value
+ work_time[5] = value
else:
- setattr(settings["work_time"], name, value)
+ setattr(work_time, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py
index a7fb9d01e6..14e82822e3 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py
@@ -35,9 +35,7 @@ def recalculate_schedule(file: ifcopenshell.file, work_schedule: ifcopenshell.en
error.
:param work_schedule: The IfcWorkSchedule to perform the calculation on.
- :type work_schedule: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -50,12 +48,14 @@ def recalculate_schedule(file: ifcopenshell.file, work_schedule: ifcopenshell.en
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"work_schedule": work_schedule}
- return usecase.execute()
+ return usecase.execute(work_schedule)
class Usecase:
- def execute(self):
+ file: ifcopenshell.file
+
+ def execute(self, work_schedule: ifcopenshell.entity_instance) -> None:
+ self.work_schedule = work_schedule
# The method implemented is the same as shown here:
# https://www.youtube.com/watch?v=qTErIV6OqLg
self.start_dates = []
@@ -88,7 +88,6 @@ class Usecase:
if is_cyclic:
raise RecursionError("Task graph is cyclic and so critical path method cannot be performed.")
- return
self.pending_nodes = set(self.g.nodes)
while self.pending_nodes:
@@ -100,7 +99,7 @@ class Usecase:
self.update_task_times()
- def build_network_graph(self):
+ def build_network_graph(self) -> None:
self.sequence_type_map = {
None: "FS",
"START_START": "SS",
@@ -114,14 +113,14 @@ class Usecase:
self.edges = []
self.g.add_node("start", duration=0, duration_type="ELAPSEDTIME", calendar=None)
self.g.add_node("finish", duration=0, duration_type="ELAPSEDTIME", calendar=None)
- for rel in self.settings["work_schedule"].Controls:
+ for rel in self.work_schedule.Controls:
for related_object in rel.RelatedObjects:
if not related_object.is_a("IfcTask"):
continue
self.add_node(related_object)
self.g.add_edges_from(self.edges)
- def add_node(self, task):
+ def add_node(self, task: ifcopenshell.entity_instance) -> None:
if task.IsNestedBy:
for rel in task.IsNestedBy:
[self.add_node(o) for o in rel.RelatedObjects]
@@ -176,7 +175,7 @@ class Usecase:
if not successor_types:
self.edges.append((task.id(), "finish", {"lag_time": 0, "type": "FF"}))
- def update_task_times(self):
+ def update_task_times(self) -> None:
for ifc_definition_id in self.g.nodes:
if ifc_definition_id in ("start", "finish"):
continue
@@ -198,12 +197,12 @@ class Usecase:
},
)
- def offset_date(self, date, days, node):
+ def offset_date(self, date: datetime.datetime, days: int, node: dict) -> datetime.datetime:
return ifcopenshell.util.sequence.offset_date(
date, datetime.timedelta(days=days), node["duration_type"], node["calendar"]
)
- def forward_pass(self, node):
+ def forward_pass(self, node) -> bool:
successors = self.g.successors(node)
predecessors = list(self.g.predecessors(node))
data = self.g.nodes[node]
@@ -326,7 +325,7 @@ class Usecase:
return True
- def backward_pass(self, node):
+ def backward_pass(self, node) -> bool:
successors = list(self.g.successors(node))
predecessors = self.g.predecessors(node)
data = self.g.nodes[node]
@@ -496,12 +495,12 @@ class Usecase:
def calculate_free_float(
self,
- predecessor_date,
- successor_date,
- lag_time,
- predecessor_data,
- successor_data,
- ):
+ predecessor_date: datetime.datetime,
+ successor_date: datetime.datetime,
+ lag_time: int,
+ predecessor_data: dict,
+ successor_data: dict,
+ ) -> datetime.timedelta:
if not lag_time:
min_successor_date = successor_date
else:
diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py
index 08ffcd9f40..b07a630684 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py
@@ -25,9 +25,7 @@ def unassign_lag_time(file: ifcopenshell.file, rel_sequence: ifcopenshell.entity
The schedule is cascaded afterwards.
:param rel_sequence: The sequence to remove the lag time from.
- :type rel_sequence: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
@@ -57,12 +55,8 @@ def unassign_lag_time(file: ifcopenshell.file, rel_sequence: ifcopenshell.entity
# What if you didn't?
ifcopenshell.api.sequence.unassign_lag_time(model, rel_sequence=sequence)
"""
- settings = {
- "rel_sequence": rel_sequence,
- }
-
- if len(file.get_inverse(settings["rel_sequence"].TimeLag)) == 1:
- file.remove(settings["rel_sequence"].TimeLag)
+ if file.get_total_inverses((current_lag_time := rel_sequence.TimeLag)) == 1:
+ file.remove(current_lag_time)
else:
- settings["rel_sequence"].TimeLag = None
- ifcopenshell.api.sequence.cascade_schedule(file, task=settings["rel_sequence"].RelatedProcess)
+ rel_sequence.TimeLag = None
+ ifcopenshell.api.sequence.cascade_schedule(file, task=rel_sequence.RelatedProcess)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py
index bb31cd7c7d..abd66e7fa8 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py
@@ -29,12 +29,10 @@ def dereference_structure(
"""Dereferences a list of products and space
:param products: The list of physical IfcElements that exists in the space.
- :type products: list[ifcopenshell.entity_instance]
:param relating_structure: The IfcSpatialStructureElement element, such
as IfcBuilding, IfcBuildingStorey, or IfcSpace that the element
exists in.
:return: None
- :rtype: None
Example:
@@ -68,14 +66,12 @@ def dereference_structure(
# Actually, it only goes up to storey 2.
ifcopenshell.api.spatial.dereference_structure(model, products=[column], relating_structure=storey3)
"""
- settings = {"products": products, "relating_structure": relating_structure}
-
- products = set(settings["products"])
- for rel in settings["relating_structure"].ReferencesElements:
+ products_set = set(products)
+ for rel in relating_structure.ReferencesElements:
related_elements = set(rel.RelatedElements)
- if not related_elements.intersection(products):
+ if not related_elements.intersection(products_set):
continue
- related_elements = related_elements - products
+ related_elements = related_elements - products_set
if related_elements:
rel.RelatedElements = list(related_elements)
ifcopenshell.api.owner.update_owner_history(file, **{"element": rel})
diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py
index 2897219e68..41452244d1 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py
@@ -46,14 +46,11 @@ def reference_structure(
spaces simultaneously.
:param products: The list of physical IfcElements that exists in the space.
- :type products: list[ifcopenshell.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
:return: The IfcRelReferencedInSpatialStructure relationship instance
or `None` if `products` was an empty list.
- :rtype: Union[ifcopenshell.entity_instance, None]
Example:
@@ -85,19 +82,16 @@ def reference_structure(
model, products=[column], relating_structure=[storey2, storey3]
)
"""
- settings = {
- "products": products,
- "relating_structure": relating_structure,
- }
- structure = settings["relating_structure"]
- products = set(settings["products"])
+ structure = relating_structure
+ products_set = set(products)
- if not products:
+ if not products_set:
return
referenced = ifcopenshell.util.element.get_structure_referenced_elements(structure)
- products_to_assign = products - referenced
+ products_to_assign = products_set - referenced
+ rel: Union[ifcopenshell.entity_instance, None]
rel = next(iter(structure.ReferencesElements), None)
if not products_to_assign:
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_analysis_model.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_analysis_model.py
index 4b7d94a4a2..65c03eb238 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_analysis_model.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_analysis_model.py
@@ -28,14 +28,8 @@ def edit_structural_analysis_model(
IfcStructuralAnalysisModel, consult the IFC documentation.
:param structural_analysis_model: The IfcStructuralAnalysisModel entity you want to edit
- :type structural_analysis_model: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
"""
- settings = {"structural_analysis_model": structural_analysis_model, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["structural_analysis_model"], name, value)
- return settings["structural_analysis_model"]
+ for name, value in attributes.items():
+ setattr(structural_analysis_model, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py
index 54963415df..3d33e4e8ad 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py
@@ -28,19 +28,14 @@ def edit_structural_boundary_condition(
IfcBoundaryCondition, consult the IFC documentation.
:param condition: The IfcBoundaryCondition entity you want to edit
- :type condition: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
"""
- settings = {"condition": condition, "attributes": attributes}
-
- for name, data in settings["attributes"].items():
+ for name, data in attributes.items():
if data["type"] == "string" or data["type"] == "null":
value = data["value"]
elif data["type"] == "IfcBoolean":
value = file.createIfcBoolean(data["value"])
else:
value = file.create_entity(data["type"], data["value"])
- setattr(settings["condition"], name, value)
+ setattr(condition, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py
index 0d569c5f56..38befd9c6f 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py
@@ -16,42 +16,33 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see .
import ifcopenshell
+from ifcopenshell.util.shape_builder import VectorType, ifc_safe_vector_type
def edit_structural_connection_cs(
file: ifcopenshell.file,
structural_item: ifcopenshell.entity_instance,
- axis: tuple[float, float, float] = (0.0, 0.0, 1.0),
- ref_direction: tuple[float, float, float] = (1.0, 0.0, 0.0),
+ axis: VectorType = (0.0, 0.0, 1.0),
+ ref_direction: VectorType = (1.0, 0.0, 0.0),
) -> None:
"""Edits the coordinate system of a structural connection
:param structural_item: The IfcStructuralItem you want to modify.
- :type structural_item: ifcopenshell.entity_instance
:param axis: The unit Z axis vector defined as a list of 3 floats.
Defaults to (0., 0., 1.).
- :type axis: tuple[float, float, float]
:param ref_direction: The unit X axis vector defined as a list of 3
floats. Defaults to (1., 0., 0.).
- :type ref_direction: tuple[float, float, float]
:return: None
- :rtype: None
"""
- settings = {
- "structural_item": structural_item,
- "axis": axis,
- "ref_direction": ref_direction,
- }
-
- if settings["structural_item"].ConditionCoordinateSystem is None:
+ if structural_item.ConditionCoordinateSystem is None:
point = file.createIfcCartesianPoint((0.0, 0.0, 0.0))
ccs = file.createIfcAxis2Placement3D(point, None, None)
- settings["structural_item"].ConditionCoordinateSystem = ccs
+ structural_item.ConditionCoordinateSystem = ccs
- ccs = settings["structural_item"].ConditionCoordinateSystem
- if ccs.Axis and len(file.get_inverse(ccs.Axis)) == 1:
- file.remove(ccs.Axis)
- ccs.Axis = file.createIfcDirection(settings["axis"])
- if ccs.RefDirection and len(file.get_inverse(ccs.RefDirection)) == 1:
- file.remove(ccs.RefDirection)
- ccs.RefDirection = file.createIfcDirection(settings["ref_direction"])
+ ccs = structural_item.ConditionCoordinateSystem
+ if (current_axis := ccs.Axis) and file.get_total_inverses(current_axis) == 1:
+ file.remove(current_axis)
+ ccs.Axis = file.create_entity("IfcDirection", ifc_safe_vector_type(axis))
+ if (prev_ref_direction := ccs.RefDirection) and file.get_total_inverses(prev_ref_direction) == 1:
+ file.remove(prev_ref_direction)
+ ccs.RefDirection = file.create_entity("IfcDirection", ifc_safe_vector_type(ref_direction))
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py
index 8b90457a47..d3858bec95 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py
@@ -16,25 +16,21 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see .
import ifcopenshell
+from ifcopenshell.util.shape_builder import VectorType, ifc_safe_vector_type
def edit_structural_item_axis(
file: ifcopenshell.file,
structural_item: ifcopenshell.entity_instance,
- axis: tuple[float, float, float] = (0.0, 0.0, 1.0),
+ axis: VectorType = (0.0, 0.0, 1.0),
) -> None:
"""Edits the coordinate system of a structural connection
:param structural_item: The IfcStructuralItem you want to modify.
- :type structural_item: ifcopenshell.entity_instance
:param axis: The unit Z axis vector defined as a list of 3 floats.
Defaults to (0., 0., 1.).
- :type axis: tuple[float, float, float]
:return: None
- :rtype: None
"""
- settings = {"structural_item": structural_item, "axis": axis}
-
- if len(file.get_inverse(settings["structural_item"].Axis)) == 1:
- file.remove(settings["structural_item"].Axis)
- settings["structural_item"].Axis = file.createIfcDirection(settings["axis"])
+ if file.get_total_inverses(axis_dir := structural_item.Axis) == 1:
+ file.remove(axis_dir)
+ structural_item.Axis = file.create_entity("IfcDirection", ifc_safe_vector_type(axis))
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load.py
index 20298d1c0f..faa218a7a3 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load.py
@@ -28,13 +28,8 @@ def edit_structural_load(
IfcStructuralLoad, consult the IFC documentation.
:param structural_load: The IfcStructuralLoad entity you want to edit
- :type structural_load: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
"""
- settings = {"structural_load": structural_load, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["structural_load"], name, value)
+ for name, value in attributes.items():
+ setattr(structural_load, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load_case.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load_case.py
index 59231fb9e4..02948c637d 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load_case.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load_case.py
@@ -28,13 +28,8 @@ def edit_structural_load_case(
IfcStructuralLoadCase, consult the IFC documentation.
:param load_case: The IfcStructuralLoadCase entity you want to edit
- :type load_case: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
"""
- settings = {"load_case": load_case, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["load_case"], name, value)
+ for name, value in attributes.items():
+ setattr(load_case, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py
index 8135bc0255..144b96f5a9 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py
@@ -29,18 +29,14 @@ def remove_structural_analysis_model(
:param structural_analysis_model: The IfcStructuralAnalysisModel to
remove.
- :type structural_analysis_model: ifcopenshell.entity_instance
:return: None
- :rtype: None
"""
- settings = {"structural_analysis_model": structural_analysis_model}
-
- for rel in settings["structural_analysis_model"].IsGroupedBy or []:
+ for rel in structural_analysis_model.IsGroupedBy or []:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
- history = settings["structural_analysis_model"].OwnerHistory
- file.remove(settings["structural_analysis_model"])
+ history = structural_analysis_model.OwnerHistory
+ file.remove(structural_analysis_model)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py
index bd9018c1e2..d31811a739 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py
@@ -24,27 +24,25 @@ def remove_structural_boundary_condition(
connection: Optional[ifcopenshell.entity_instance] = None,
boundary_condition: Optional[ifcopenshell.entity_instance] = None,
) -> None:
- """Removes a condition from a connection, or an orphased boundary condition
+ """Removes a condition from a connection, or an orphaned boundary condition
:param connection: The IfcStructuralConnection to remove the condition
from. If omitted, it is assumed to be an orphaned condition.
- :type connection: ifcopenshell.entity_instance,optional
:param boundary_condition: The IfcBoundaryCondition to remove.
- :type boundary_condition: ifcopenshell.entity_instance, optional.
:return: None
- :rtype: None
"""
- settings = {"connection": connection, "boundary_condition": boundary_condition}
- if settings["connection"]:
+ if connection:
# remove boundary condition from a connection
- if not settings["connection"].AppliedCondition:
+ if not connection.AppliedCondition:
return
- if len(file.get_inverse(settings["connection"].AppliedCondition)) == 1:
- file.remove(settings["connection"].AppliedCondition)
- settings["connection"].AppliedCondition = None
+ applied_condition = connection.AppliedCondition
+ if file.get_total_inverses(applied_condition) == 1:
+ file.remove(applied_condition)
+ connection.AppliedCondition = None
else:
+ assert boundary_condition, "Either connection or boundary_condition must be provided."
# remove the boundary condition
- for conn in file.get_inverse(settings["boundary_condition"]):
+ for conn in file.get_inverse(boundary_condition):
conn.AppliedCondition = None
- file.remove(settings["boundary_condition"])
+ file.remove(boundary_condition)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py
index 1d406a16e8..0c1a6ad3a1 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py
@@ -22,10 +22,6 @@ def remove_structural_load(file: ifcopenshell.file, structural_load: ifcopenshel
"""Removes a structural load
:param structural_load: The IfcStructuralLoad to remove.
- :type structural_load: ifcopenshell.entity_instance
:return: None
- :rtype: None
"""
- settings = {"structural_load": structural_load}
-
- file.remove(settings["structural_load"])
+ file.remove(structural_load)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py
index 1d9473515a..401db40dbc 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py
@@ -25,18 +25,14 @@ def remove_structural_load_case(file: ifcopenshell.file, load_case: ifcopenshell
"""Removes a structural load case
:param load_case: The IfcStructuralLoadCase to remove.
- :type load_case: ifcopenshell.entity_instance
:return: None
- :rtype: None
"""
- settings = {"load_case": load_case}
-
# TODO: do a deep purge
- for rel in settings["load_case"].IsGroupedBy or []:
+ for rel in load_case.IsGroupedBy or []:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
- history = settings["load_case"].OwnerHistory
- file.remove(settings["load_case"])
+ history = load_case.OwnerHistory
+ file.remove(load_case)
ifcopenshell.util.element.remove_deep2(file, history)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py
index 281630fd7a..25acbbe9cc 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py
@@ -25,20 +25,16 @@ def remove_structural_load_group(file: ifcopenshell.file, load_group: ifcopenshe
"""Removes a structural load group
:param load_group: The IfcStructuralLoadGroup to remove.
- :type load_group: ifcopenshell.entity_instance
:return: None
- :rtype: None
"""
- settings = {"load_group": load_group}
-
# TODO: do a deep purge
- for inverse in file.get_inverse(settings["load_group"]):
+ for inverse in file.get_inverse(load_group):
if inverse.is_a("IfcRelAssignsToGroup") and len(inverse.RelatedObjects) == 1:
history = inverse.OwnerHistory
file.remove(inverse)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
- history = settings["load_group"].OwnerHistory
- file.remove(settings["load_group"])
+ history = load_group.OwnerHistory
+ file.remove(load_group)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py
index 408f8e7906..944240e618 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py
@@ -35,7 +35,7 @@ def add_surface_style(
style: ifcopenshell.entity_instance,
ifc_class: SURFACE_STYLE_TYPES = "IfcSurfaceStyleShading",
attributes: Optional[dict[str, Any]] = None,
-) -> None:
+) -> ifcopenshell.entity_instance:
"""Adds a new presentation item to a surface style
A surface style can have multiple different types of presentation items
@@ -82,17 +82,13 @@ def add_surface_style(
:param style: The IfcSurfaceStyle you want to add to presentation item
to. See ifcopenshell.api.style.add_style.
- :type style: ifcopenshell.entity_instance
:param ifc_class: Choose from IfcSurfaceStyleShading,
IfcSurfaceStyleRendering, IfcSurfaceStyleWithTextures,
IfcSurfaceStyleLighting, IfcSurfaceStyleReflectance, or
IfcExternallyDefinedSurfaceStyle.
- :type ifc_class: str
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict, optional
:return: The newly created presentation item based on the provided
ifc_class.
- :rtype: ifcopenshell.entity_instance
Example:
@@ -127,20 +123,20 @@ def add_surface_style(
"SpecularHighlight": {"SpecularRoughness": 0.5}, # Roughness factor
})
"""
- settings = {"style": style, "ifc_class": ifc_class, "attributes": attributes or {}}
+ attributes = attributes or {}
+ style_item = file.create_entity(ifc_class)
+ ifcopenshell.api.style.edit_surface_style(file, style=style_item, attributes=attributes)
+ styles: list[ifcopenshell.entity_instance]
+ styles = list(style.Styles or [])
- style_item = file.create_entity(settings["ifc_class"])
- ifcopenshell.api.style.edit_surface_style(file, style=style_item, attributes=settings["attributes"])
- styles = list(settings["style"].Styles or [])
-
- select_class = settings["ifc_class"]
+ select_class = ifc_class
if select_class == "IfcSurfaceStyleRendering":
select_class = "IfcSurfaceStyleShading"
duplicate_items = [s for s in styles if s.is_a(select_class)]
for duplicate_item in duplicate_items:
ifcopenshell.api.style.remove_surface_style(file, style=duplicate_item)
- styles = list(settings["style"].Styles or [])
+ styles = list(style.Styles or [])
styles.append(style_item)
- settings["style"].Styles = styles
+ style.Styles = styles
return style_item
diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py
index 3ecc91177f..fbd706df68 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py
@@ -19,6 +19,7 @@
import ifcopenshell
import ifcopenshell.api.style
import ifcopenshell.util.element
+from typing import Any
def assign_material_style(
@@ -115,6 +116,9 @@ def assign_material_style(
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
self.style = self.settings["style"]
if self.file.schema == "IFC2X3" or self.settings["should_use_presentation_style_assignment"]:
@@ -170,7 +174,7 @@ class Usecase:
new_items.append(self.create_styled_item(item_to_reuse))
representation.Items = new_items
for item in same_style_items:
- if len(self.file.get_inverse(item)) == 0:
+ if self.file.get_total_inverses(item) == 0:
self.file.remove(item)
else:
representations = list(definition_representation.Representations)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/edit_presentation_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/edit_presentation_style.py
index d1a42e5d9e..7a39629528 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/style/edit_presentation_style.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/style/edit_presentation_style.py
@@ -28,11 +28,8 @@ def edit_presentation_style(
IfcPresentationStyle, consult the IFC documentation.
:param style: The IfcPresentationStyle entity you want to edit
- :type style: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -44,7 +41,5 @@ def edit_presentation_style(
# Change the name of the style to "Foo"
ifcopenshell.api.style.edit_presentation_style(model, style=style, attributes={"Name": "Foo"})
"""
- settings = {"style": style, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["style"], name, value)
+ for name, value in attributes.items():
+ setattr(style, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py
index c5be1d757f..7f7f05948d 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py
@@ -16,7 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see .
import ifcopenshell
-from typing import Any
+from typing import Any, Union
def edit_surface_style(
@@ -36,11 +36,8 @@ def edit_surface_style(
example below.
:param style: The IfcPresentationStyle entity you want to edit
- :type style: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -74,24 +71,27 @@ def edit_surface_style(
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"style": style, "attributes": attributes or {}}
- return usecase.execute()
+ return usecase.execute(style, attributes)
class Usecase:
- def execute(self):
- attributes = {}
- for attribute in self.settings["style"].wrapped_data.declaration().as_entity().all_attributes():
+ file: ifcopenshell.file
+
+ def execute(self, style: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None:
+ self.style = style
+
+ attribute_types: dict[str, str] = {}
+ for attribute in style.wrapped_data.declaration().as_entity().all_attributes():
attribute_type = attribute.type_of_attribute()
if attribute_type.as_aggregation_type() is None:
attribute_type = attribute_type.declared_type().name()
else:
# doesn't have .declared_type()
attribute_type = attribute_type.type_of_element()
- attributes[attribute.name()] = attribute_type
+ attribute_types[attribute.name()] = attribute_type
- for key, value in self.settings["attributes"].items():
- attribute_class = attributes.get(key)
+ for key, value in attributes.items():
+ attribute_class = attribute_types.get(key)
if attribute_class == "IfcColourRgb":
self.edit_colour_rgb(key, value)
elif key == "SpecularHighlight":
@@ -99,41 +99,39 @@ class Usecase:
elif attribute_class == "IfcColourOrFactor":
self.edit_colour_or_factor(key, value)
else:
- setattr(self.settings["style"], key, value)
+ setattr(style, key, value)
- def edit_colour_rgb(self, name, value: dict):
- if (attribute := getattr(self.settings["style"], name)) is None:
+ def edit_colour_rgb(self, name: str, value: dict[str, Any]):
+ if (attribute := getattr(self.style, name)) is None:
attribute = self.file.createIfcColourRgb()
- setattr(self.settings["style"], name, attribute)
+ setattr(self.style, name, attribute)
attribute.Name = value.get("Name", None)
attribute.Red = value["Red"]
attribute.Green = value["Green"]
attribute.Blue = value["Blue"]
- def edit_colour_or_factor(self, name, value):
+ def edit_colour_or_factor(self, name: str, value: Union[dict[str, Any], ifcopenshell.entity_instance, None]):
if isinstance(value, dict):
- attribute = getattr(self.settings["style"], name)
+ attribute = getattr(self.style, name)
if not attribute or not attribute.is_a("IfcColourRgb"):
colour = self.file.createIfcColourRgb(None, 0, 0, 0)
- setattr(self.settings["style"], name, colour)
- attribute = getattr(self.settings["style"], name)
+ setattr(self.style, name, colour)
+ attribute = getattr(self.style, name)
attribute[1] = value["Red"]
attribute[2] = value["Green"]
attribute[3] = value["Blue"]
else: # assume it's float value for IfcNormalisedRatioMeasure or None
- existing_value = getattr(self.settings["style"], name)
+ existing_value = getattr(self.style, name)
if existing_value and existing_value.id():
self.file.remove(existing_value)
if value is not None:
- value = self.file.createIfcNormalisedRatioMeasure(value)
- setattr(self.settings["style"], name, value)
+ value = self.file.create_entity("IfcNormalisedRatioMeasure", value)
+ setattr(self.style, name, value)
- def edit_specular_highlight(self, value):
+ def edit_specular_highlight(self, value: Union[dict[str, Any], None]) -> None:
if value is None:
- self.settings["style"].SpecularHighlight = None
+ self.style.SpecularHighlight = None
elif value.get("IfcSpecularExponent", None):
- self.settings["style"].SpecularHighlight = self.file.createIfcSpecularExponent(value["IfcSpecularExponent"])
+ self.style.SpecularHighlight = self.file.createIfcSpecularExponent(value["IfcSpecularExponent"])
elif value.get("IfcSpecularRoughness", None):
- self.settings["style"].SpecularHighlight = self.file.createIfcSpecularRoughness(
- value["IfcSpecularRoughness"]
- )
+ self.style.SpecularHighlight = self.file.createIfcSpecularRoughness(value["IfcSpecularRoughness"])
diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/remove_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/remove_style.py
index 3ee4cbf6d5..c72cca657e 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/style/remove_style.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/style/remove_style.py
@@ -41,7 +41,6 @@ def remove_style(file: ifcopenshell.file, style: ifcopenshell.entity_instance) -
"""
usecase = Usecase()
usecase.file = file
- usecase.settings = {"style": style}
return usecase.execute(style)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/remove_surface_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/remove_surface_style.py
index bad29c878f..19bb9e1b79 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/style/remove_surface_style.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/style/remove_surface_style.py
@@ -24,9 +24,7 @@ def remove_surface_style(file: ifcopenshell.file, style: ifcopenshell.entity_ins
"""Removes a presentation item from a presentation style
:param style: The IfcPresentationItem to remove.
- :type style: ifcopenshell.entity_instance
:return: None
- :rtype: None
Example:
diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/unassign_representation_styles.py b/src/ifcopenshell-python/ifcopenshell/api/style/unassign_representation_styles.py
index f56fad763c..4141adbacc 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/style/unassign_representation_styles.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/style/unassign_representation_styles.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 ifcopenshell
+from typing import Any
def unassign_representation_styles(
@@ -63,6 +64,9 @@ def unassign_representation_styles(
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
if not self.settings["styles"]:
return []
diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py
index 9a1bc76f43..b0b39cfd2b 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py
@@ -20,7 +20,7 @@ import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.guid
import ifcopenshell.util.element
-from typing import Optional
+from typing import Optional, Any
def connect_port(
@@ -110,6 +110,9 @@ def connect_port(
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
# Note: there are a number of ambiguities with port connectivity. We
# assume system topology is represented by a directed graph. In other
diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/edit_system.py b/src/ifcopenshell-python/ifcopenshell/api/system/edit_system.py
index 00311b7ac0..c90894ec7f 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/system/edit_system.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/system/edit_system.py
@@ -26,11 +26,8 @@ def edit_system(file: ifcopenshell.file, system: ifcopenshell.entity_instance, a
IfcSystem, consult the IFC documentation.
:param system: The IfcSystem entity you want to edit
- :type system: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -42,8 +39,5 @@ def edit_system(file: ifcopenshell.file, system: ifcopenshell.entity_instance, a
# Change the name of the system to "HW" for Hot Water
ifcopenshell.api.system.edit_system(model, system=system, attributes={"Name": "HW"})
"""
-
- settings = {"system": system, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["system"], name, value)
+ for name, value in attributes.items():
+ setattr(system, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py
index 1bf459920f..b5b69e276c 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py
@@ -19,6 +19,7 @@
import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.util.element
+from typing import Any
def unassign_port(
@@ -62,6 +63,9 @@ def unassign_port(
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
if self.file.schema == "IFC2X3":
return self.execute_ifc2x3()
diff --git a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py
index 2a20f57f2f..f4bfae022e 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py
@@ -22,7 +22,7 @@ import ifcopenshell.api.owner
import ifcopenshell.api.material
import ifcopenshell.guid
import ifcopenshell.util.element
-from typing import Union, Iterable
+from typing import Union, Iterable, Any
def assign_type(
@@ -186,6 +186,9 @@ def assign_type(
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
if not self.settings["related_objects"]:
return
diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/add_derived_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/add_derived_unit.py
index 8a68e5727f..0fba187c75 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/unit/add_derived_unit.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/unit/add_derived_unit.py
@@ -46,11 +46,8 @@ def add_derived_unit(
:type unit_type: str
:param userdefinedtype: The user defined type in case of choosing USERDEFINED, or None for no
user defined type.
- :type userdefinedtype: str or None
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: The newly created IfcDerivedUnit
- :rtype: ifcopenshell.entity_instance
Example:
@@ -69,15 +66,13 @@ def add_derived_unit(
#12=IfcDerivedUnit((#10,#11),.LINEARVELOCITY.,$)
"""
- settings = {"unit_type": unit_type, "attributes": attributes}
-
derive_unit_elements = []
- for named_unit in settings["attributes"]:
+ for named_unit in attributes:
derive_unit_elements.append(
- file.create_entity("IfcDerivedUnitElement", Unit=named_unit, Exponent=settings["attributes"][named_unit])
+ file.create_entity("IfcDerivedUnitElement", Unit=named_unit, Exponent=attributes[named_unit])
)
return file.create_entity(
- "IfcDerivedUnit", Elements=derive_unit_elements, UnitType=settings["unit_type"], UserDefinedType=userdefinedtype
+ "IfcDerivedUnit", Elements=derive_unit_elements, UnitType=unit_type, UserDefinedType=userdefinedtype
)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py
index 4e2dc3f848..d57acbd435 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py
@@ -18,7 +18,7 @@
import ifcopenshell
import ifcopenshell.util.unit
-from typing import Optional
+from typing import Optional, Any
def assign_unit(
@@ -75,6 +75,9 @@ def assign_unit(
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self):
# We're going to refactor this to split unit creation and assignment
if self.settings["units"]:
diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_derived_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_derived_unit.py
index a8b3316dbd..5bc9bbd316 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_derived_unit.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_derived_unit.py
@@ -26,13 +26,8 @@ def edit_derived_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instanc
IfcDerivedUnit, consult the IFC documentation.
:param unit: The IfcDerivedUnit entity you want to edit
- :type unit: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
"""
- settings = {"unit": unit, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["unit"], name, value)
+ for name, value in attributes.items():
+ setattr(unit, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_monetary_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_monetary_unit.py
index 4fdfe6ec29..bccc61afc0 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_monetary_unit.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_monetary_unit.py
@@ -26,11 +26,8 @@ def edit_monetary_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instan
IfcMonetaryUnit, consult the IFC documentation.
:param unit: The IfcMonetaryUnit entity you want to edit
- :type unit: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -43,7 +40,5 @@ def edit_monetary_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instan
# Ah who are we kidding
ifcopenshell.api.unit.edit_monetary_unit(model, unit=zwl, attributes={"Currency": "USD"})
"""
- settings = {"unit": unit, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
- setattr(settings["unit"], name, value)
+ for name, value in attributes.items():
+ setattr(unit, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py
index e439e2c476..683feb7216 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py
@@ -29,11 +29,8 @@ def edit_named_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instance,
IfcNamedUnit, consult the IFC documentation.
:param unit: The IfcNamedUnit entity you want to edit
- :type unit: ifcopenshell.entity_instance
:param attributes: a dictionary of attribute names and values.
- :type attributes: dict
:return: None
- :rtype: None
Example:
@@ -45,15 +42,13 @@ def edit_named_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instance,
# Uh, crates? Boxes? Whatever.
ifcopenshell.api.unit.edit_named_unit(model, unit=unit, attibutes={"Name": "CRATES"})
"""
- settings = {"unit": unit, "attributes": attributes or {}}
-
- for name, value in settings["attributes"].items():
+ for name, value in attributes.items():
if name == "Dimensions":
- dimensions = settings["unit"].Dimensions
- if len(file.get_inverse(dimensions)) > 1:
- settings["unit"].Dimensions = file.createIfcDimensionalExponents(*value)
+ dimensions = unit.Dimensions
+ if file.get_total_inverses(dimensions) > 1:
+ unit.Dimensions = file.createIfcDimensionalExponents(*value)
else:
for i, exponent in enumerate(value):
dimensions[i] = exponent
continue
- setattr(settings["unit"], name, value)
+ setattr(unit, name, value)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py
index 17eee6d4be..9651bfa87e 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py
@@ -23,9 +23,7 @@ def unassign_unit(file: ifcopenshell.file, units: Optional[list[ifcopenshell.ent
"""Unassigns units as default units for the project
:param units: A list of units to assign as project defaults.
- :type units: list[ifcopenshell.entity_instance],optional
:return: None
- :rtype: None
Example:
@@ -44,15 +42,13 @@ def unassign_unit(file: ifcopenshell.file, units: Optional[list[ifcopenshell.ent
# Actually, we don't need areas.
ifcopenshell.api.unit.unassign_unit(model, units=[area])
"""
- settings = {"units": units}
-
- unit_assignment = file.by_type("IfcUnitAssignment")
- if not unit_assignment:
+ unit_assignments = file.by_type("IfcUnitAssignment")
+ if not unit_assignments:
+ return
+ unit_assignment = unit_assignments[0]
+ units_set = set(unit_assignment.Units or [])
+ units_set = units_set - set(units or [])
+ if units_set:
+ unit_assignment.Units = list(units_set)
return
- unit_assignment = unit_assignment[0]
- units = set(unit_assignment.Units or [])
- units = units - set(settings["units"])
- if units:
- unit_assignment.Units = list(units)
- return unit_assignment
file.remove(unit_assignment)
diff --git a/src/ifcopenshell-python/ifcopenshell/entity_instance.py b/src/ifcopenshell-python/ifcopenshell/entity_instance.py
index dcd3b74cb0..15e5980d41 100644
--- a/src/ifcopenshell-python/ifcopenshell/entity_instance.py
+++ b/src/ifcopenshell-python/ifcopenshell/entity_instance.py
@@ -248,14 +248,10 @@ class entity_instance:
:param f: A callable that takes a single argument and returns a boolean
value. It represents the condition.
- :type f: Callable
:param g: A callable that takes a single argument and returns a
transformed value. It represents the transformation.
- :type g: Callable
:param value: Any object, the input value to be processed
- :type value: Any
:return: Transformed value
- :rtype: Any
Example:
@@ -304,8 +300,6 @@ class entity_instance:
"""Return the data type of a positional attribute of the element
:param attr: The index or name of the attribute
- :type attr: Union[int, str]
- :rtype: string
"""
attr_idx = attr if isinstance(attr, numbers.Integral) else self.wrapped_data.get_argument_index(attr)
return self.wrapped_data.get_argument_type(attr_idx)
@@ -314,8 +308,6 @@ class entity_instance:
"""Return the name of a positional attribute of the element
:param attr_idx: The index of the attribute
- :type attr_idx: int
- :rtype: string
"""
return self.wrapped_data.get_argument_name(attr_idx)
@@ -405,9 +397,7 @@ class entity_instance:
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: Union[str, bool]
Example:
@@ -423,10 +413,7 @@ class entity_instance:
return self.wrapped_data.is_a(*args)
def id(self) -> int:
- """Return the STEP numerical identifier
-
- :rtype: int
- """
+ """Return the STEP numerical identifier"""
return self.wrapped_data.id()
def __eq__(self, other: "entity_instance") -> bool:
diff --git a/src/ifcopenshell-python/ifcopenshell/file.py b/src/ifcopenshell-python/ifcopenshell/file.py
index 04c6c2ce50..328d98b27f 100644
--- a/src/ifcopenshell-python/ifcopenshell/file.py
+++ b/src/ifcopenshell-python/ifcopenshell/file.py
@@ -24,6 +24,7 @@ import zipfile
import functools
import ifcopenshell
from pathlib import Path
+from typing import Any
from typing import Callable
from typing import Generator
from typing import Optional
@@ -358,7 +359,6 @@ class file:
of those methods.
:param type: Case insensitive name of the IFC class
- :type type: string
:param args: The positional arguments of the IFC class
:param kwargs: The keyword arguments of the IFC class
:returns: An entity instance
@@ -481,12 +481,10 @@ class file:
"""Return an IFC entity instance filtered by IFC ID.
:param id: STEP numerical identifier
- :type id: int
:raises RuntimeError: If `id` is not found.
:returns: An ifcopenshell.entity_instance
- :rtype: ifcopenshell.entity_instance
"""
return self[id]
@@ -494,13 +492,10 @@ class file:
"""Return an IFC entity instance filtered by IFC GUID.
:param guid: GlobalId value in 22-character encoded form
- :type guid: string
:raises RuntimeError: If `guid` is not found.
:returns: An ifcopenshell.entity_instance
- :rtype: ifcopenshell.entity_instance
-
"""
return self[guid]
@@ -509,9 +504,7 @@ class file:
If the entity already exists, it is not re-added. Existence of entity is checked by it's `.identity()`.
:param inst: The entity instance to add
- :type inst: ifcopenshell.entity_instance
:returns: An ifcopenshell.entity_instance
- :rtype: ifcopenshell.entity_instance
"""
if self.transaction:
@@ -529,14 +522,11 @@ class file:
If an IFC type class has subclasses, all entities of those subclasses are also returned.
:param type: The case insensitive type of IFC class to return.
- :type type: string
:param include_subtypes: Whether or not to return subtypes of the IFC class
- :type include_subtypes: bool
:raises RuntimeError: If `type` is not found in IFC schema.
:returns: A list of ifcopenshell.entity_instance objects
- :rtype: list[ifcopenshell.entity_instance]
"""
if include_subtypes:
return [entity_instance(e, self) for e in self.wrapped_data.by_type(type)]
@@ -624,9 +614,7 @@ class file:
significantly faster.
:param inst: The entity instance to get inverse relationships
- :type inst: ifcopenshell.entity_instance
:returns: The total number of references
- :rtype: int
"""
return self.wrapped_data.get_total_inverses(inst.wrapped_data)
@@ -638,8 +626,6 @@ class file:
the reference to the deleted will be removed from the aggregate.
:param inst: The entity instance to delete
- :type inst: ifcopenshell.entity_instance
- :rtype: None
"""
if self.transaction:
self.transaction.store_delete(inst)
@@ -675,9 +661,7 @@ class file:
if None. Supported formats : .ifc, .ifcXML, .ifcZIP (equivalent to
format=".ifc" with zipped=True) For zipped .ifcXML use
format=".ifcXML" with zipped=True
- :type format: str
:param zipped: zip the file after it is written
- :type zipped: bool
Example:
diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py
index b8ef0711d5..9034e382be 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/element.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/element.py
@@ -454,7 +454,7 @@ def get_elements_by_pset(pset: ifcopenshell.entity_instance) -> set[ifcopenshell
return elements
-def get_predefined_type(element: ifcopenshell.entity_instance) -> str:
+def get_predefined_type(element: ifcopenshell.entity_instance) -> Union[str, None]:
"""Retrieves the PrefefinedType attribute of an element.
If the predefined type is user defined, the custom type (such as object
@@ -1333,7 +1333,7 @@ def get_referenced_elements(reference: ifcopenshell.entity_instance) -> set[ifco
def replace_element(element: ifcopenshell.entity_instance, replacement: ifcopenshell.entity_instance) -> None:
- for inverse in element.file:
+ for inverse in element.file.get_inverse(element):
replace_attribute(inverse, element, replacement)
@@ -1434,7 +1434,7 @@ def remove_deep2(
ifc_file: ifcopenshell.file,
element: ifcopenshell.entity_instance,
also_consider: list[ifcopenshell.entity_instance] = [],
- do_not_delete: list[ifcopenshell.entity_instance] = [],
+ do_not_delete: set[ifcopenshell.entity_instance] = set(),
) -> None:
"""Recursively purges a subgraph safely, starting at an element
@@ -1462,28 +1462,34 @@ def remove_deep2(
:param ifc_file: The IFC file object
:param also_consider: elements to also consider as a part of a subgraph
+ Order could matter for perfomance - elements that reference `element`
+ directly should go first for the better performance.
:param do_not_delete: elements to protect from deletion
:param element: The starting element that defines the subgraph
"""
# ifc_file.batch()
- also_considered_inverses = 0
+ total_inverses = ifc_file.get_total_inverses(element)
+ if total_inverses > 0:
- def increment_considered_inverses(_):
- nonlocal also_considered_inverses
- also_considered_inverses += 1
+ def are_inverses_contained() -> bool:
+ also_considered_inverses = 0
- for considered_element in also_consider:
- for attribute in considered_element:
- considered_element.walk(lambda x: x == element, increment_considered_inverses, attribute)
+ for considered_element in also_consider:
+ traverse = ifc_file.traverse(considered_element, max_levels=1)
+ if element in traverse:
+ also_considered_inverses += 1
+ if total_inverses == also_considered_inverses:
+ return True
+ return False
- if ifc_file.get_total_inverses(element) > 0 + also_considered_inverses:
- return
+ if not are_inverses_contained():
+ return
to_delete = set()
subgraph = list(ifc_file.traverse(element, breadth_first=True))
subgraph.extend(also_consider)
subgraph_set = set(subgraph)
- subelement_queue = ifc_file.traverse(element, max_levels=1)
+ subelement_queue = [element]
while subelement_queue:
subelement = subelement_queue.pop(0)
if (
diff --git a/src/ifcopenshell-python/ifcopenshell/util/placement.py b/src/ifcopenshell-python/ifcopenshell/util/placement.py
index d6caa1b657..5f8ad154b6 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/placement.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/placement.py
@@ -137,9 +137,7 @@ def get_cartesiantransformationoperator3d(inst: ifcopenshell.entity_instance) ->
``get_mappeditem_transformation`` instead.
:param item: The IfcCartesianTransformationOperator entity
- :type item: ifcopenshell.entity_instance
:return: A 4x4 numpy transformation matrix
- :rtype: MatrixType
"""
origin = np.array(inst.LocalOrigin.Coordinates)
axis1 = np.array((1.0, 0.0, 0.0))
diff --git a/src/ifcopenshell-python/ifcopenshell/util/schema.py b/src/ifcopenshell-python/ifcopenshell/util/schema.py
index f709af6b5a..70eff36eb9 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/schema.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/schema.py
@@ -80,12 +80,7 @@ def is_a(declaration: ifcopenshell.ifcopenshell_wrapper.entity, ifc_class: str)
declaration = ifcopenshell.util.schema.get_declaration(wall)
ifcopenshell.util.schema.is_a(declaration, "IfcRoot") # True
"""
- ifc_class = ifc_class.upper()
- if declaration.name_uc() == ifc_class:
- return True
- if declaration.supertype():
- return is_a(declaration.supertype(), ifc_class)
- return False
+ return declaration._is(ifc_class)
def get_supertypes(
diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py
index 37919b9408..f0a1025583 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/selector.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py
@@ -17,7 +17,7 @@
# along with IfcOpenShell. If not, see .
import re
-import types
+import sys
import lark
import numpy as np
import ifcopenshell.api.pset
@@ -37,6 +37,11 @@ import ifcopenshell.util.unit
from decimal import Decimal
from typing import Optional, Any, Union, Iterable
+if sys.version_info >= (3, 10):
+ from types import EllipsisType
+else:
+ EllipsisType = type(...)
+
filter_elements_grammar = lark.Lark(
"""start: filter_group
@@ -659,7 +664,7 @@ def set_element_value(
def process_pset_prop_value(
pset: ifcopenshell.entity_instance, prop: str, value: Any
- ) -> Union[Any, types.EllipsisType]:
+ ) -> Union[Any, EllipsisType]:
"""Try to process value for edit_pset.
`edit_pset` is expecting a sequence of values
@@ -672,7 +677,7 @@ def set_element_value(
current_value = element.get(key, ...)
# Check if previous value is a list as a fast way to identify enum properties.
- if not isinstance(current_value, (types.EllipsisType, list)):
+ if not isinstance(current_value, (EllipsisType, list)):
return value
if isinstance(current_value, list):
diff --git a/src/ifcopenshell-python/ifcopenshell/util/stationing.py b/src/ifcopenshell-python/ifcopenshell/util/stationing.py
new file mode 100644
index 0000000000..1e0517f177
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/util/stationing.py
@@ -0,0 +1,50 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2021 Thomas Krijnen
+#
+# 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 math
+
+
+def station_as_string(station: float, plus_seperator=3, accuracy=3):
+ """
+ Returns a stringized version of a station. Example 100.0 is 1+00.00 as a stationing string
+ @param station: the station to be stringized
+ @param plus_seperator: location of the '+' symbol relative to the decimal place (typically 2 for US units and 3 for SI units)
+ @param accuracy: number of digits following the decimal place
+ """
+ value = math.fabs(station)
+
+ shifter = math.pow(10.0, plus_seperator)
+ v1 = math.floor(value / shifter)
+ v2 = value - v1 * shifter
+
+ # Check to make sure that v2 is not basically the same as shifter
+ # If station = 69500.00000, we sometimes get 694+100.00 instead of 695+00.00
+ if math.isclose(v2 - shifter, 5.0 * math.pow(10.0, -(accuracy + 1))):
+ v2 = 0.0
+ v1 += 1
+
+ v1 = -1 * v1 if station < 0 else v1
+
+ station_string = "{:d}+{:0{}.{}f}".format(v1, v2, plus_seperator + accuracy + 1, accuracy)
+
+ # special case when v1 is 0 and station is negative, the string above doesn't get the leading
+ # negative sign. this snippet fixes that
+ if v1 == 0 and station < 0:
+ station_string = "-" + station_string
+
+ return station_string
diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py
index b19b8154c9..47adbc284e 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/unit.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py
@@ -835,6 +835,7 @@ def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = "
import ifcopenshell.util.element
import ifcopenshell.util.geolocation
import ifcopenshell.api.georeference
+ import ifcopenshell.api.unit
prefix = get_prefix(target_units)
si_unit = get_unit_name(target_units)
diff --git a/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py b/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py
index ba8385fa3e..07a4400f5a 100644
--- a/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py
+++ b/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py
@@ -105,7 +105,7 @@ class TestAddBoolean(test.bootstrap.IFC4):
assert len(booleans) == 1
assert len(rep.Items) == 2
- assert len(self.file.get_inverse(first1)) == 1
+ assert self.file.get_total_inverses(first1) == 1
result = list(self.file.get_inverse(first1))[0]
assert result.FirstOperand == first1
assert result.SecondOperand == second1
@@ -114,7 +114,7 @@ class TestAddBoolean(test.bootstrap.IFC4):
# Second2 is now used twice. Reusing is OK (albeit confusing), so long as things don't get recursive.
assert result2.SecondOperand == second2
- assert len(self.file.get_inverse(first2)) == 1
+ assert self.file.get_total_inverses(first2) == 1
result3 = list(self.file.get_inverse(first2))[0]
assert result3.FirstOperand == first2
assert result3.SecondOperand == second2
diff --git a/src/ifcopenshell-python/test/api/structural/test_edit_structural_analysis_model.py b/src/ifcopenshell-python/test/api/structural/test_edit_structural_analysis_model.py
index 18f389f488..fddb8a9993 100644
--- a/src/ifcopenshell-python/test/api/structural/test_edit_structural_analysis_model.py
+++ b/src/ifcopenshell-python/test/api/structural/test_edit_structural_analysis_model.py
@@ -23,7 +23,7 @@ import ifcopenshell.api.structural
class TestEditStructuralAnalysisModel(test.bootstrap.IFC4):
def test_editing_a_structural_analysis_model(self):
subject = ifcopenshell.api.structural.add_structural_analysis_model(self.file)
- subject = ifcopenshell.api.structural.edit_structural_analysis_model(
+ ifcopenshell.api.structural.edit_structural_analysis_model(
self.file,
structural_analysis_model=subject,
attributes={"Name": "My edited model", "Description": "Description of my model"},
diff --git a/src/ifcopenshell-python/test/geom/original_edges.py b/src/ifcopenshell-python/test/geom/original_edges.py
new file mode 100644
index 0000000000..41fd6b8aff
--- /dev/null
+++ b/src/ifcopenshell-python/test/geom/original_edges.py
@@ -0,0 +1,98 @@
+import ifcopenshell
+import ifcopenshell.geom
+
+contents = """ISO-10303-21;
+HEADER;
+FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1');
+FILE_NAME('untitled.ifc','2025-02-17T08:13:34+11:00',(''),(''),'IfcOpenShell 0.0.0','Bonsai 0.8.1-alpha250208-8f261be','Nobody');
+FILE_SCHEMA(('IFC4'));
+ENDSEC;
+DATA;
+#1=IFCPROJECT('3IbgqFUY99IejU9tnNSVCj',$,'My Project',$,$,$,$,(#14,#26),#9);
+#2=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
+#3=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
+#4=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
+#5=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
+#6=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
+#7=IFCMEASUREWITHUNIT(IFCREAL(0.0174532925199433),#6);
+#8=IFCCONVERSIONBASEDUNIT(#5,.PLANEANGLEUNIT.,'degree',#7);
+#9=IFCUNITASSIGNMENT((#3,#4,#2,#8));
+#10=IFCCARTESIANPOINT((0.,0.,0.));
+#11=IFCDIRECTION((0.,0.,1.));
+#12=IFCDIRECTION((1.,0.,0.));
+#13=IFCAXIS2PLACEMENT3D(#10,#11,#12);
+#14=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#13,$);
+#15=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#14,$,.MODEL_VIEW.,$);
+#16=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Model',*,*,*,*,#14,$,.GRAPH_VIEW.,$);
+#17=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Box','Model',*,*,*,*,#14,$,.MODEL_VIEW.,$);
+#18=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.SECTION_VIEW.,$);
+#19=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.ELEVATION_VIEW.,$);
+#20=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.MODEL_VIEW.,$);
+#21=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.PLAN_VIEW.,$);
+#22=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Profile','Model',*,*,*,*,#14,$,.ELEVATION_VIEW.,$);
+#23=IFCCARTESIANPOINT((0.,0.));
+#24=IFCDIRECTION((1.,0.));
+#25=IFCAXIS2PLACEMENT2D(#23,#24);
+#26=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Plan',2,1.E-05,#25,$);
+#27=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Plan',*,*,*,*,#26,$,.GRAPH_VIEW.,$);
+#28=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Plan',*,*,*,*,#26,$,.PLAN_VIEW.,$);
+#29=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Plan',*,*,*,*,#26,$,.PLAN_VIEW.,$);
+#30=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Plan',*,*,*,*,#26,$,.REFLECTED_PLAN_VIEW.,$);
+#31=IFCSITE('0RWRVKEyv0w8rD30dnxZgC',$,'My Site',$,$,#54,$,$,$,$,$,$,$,$);
+#37=IFCBUILDING('39X8AnBtP4gvIIEtolio1r',$,'My Building',$,$,#60,$,$,$,$,$,$);
+#43=IFCBUILDINGSTOREY('3u3kxyF4r8PPWfqNxA77Mg',$,'My Storey',$,$,#66,$,$,$,$);
+#49=IFCRELAGGREGATES('254xcZnL5DnQg9572XWoGw',$,$,$,#1,(#31));
+#50=IFCCARTESIANPOINT((0.,0.,0.));
+#51=IFCDIRECTION((0.,0.,1.));
+#52=IFCDIRECTION((1.,0.,0.));
+#53=IFCAXIS2PLACEMENT3D(#50,#51,#52);
+#54=IFCLOCALPLACEMENT($,#53);
+#55=IFCRELAGGREGATES('3mOs7xn1P8ZutyBJCrBHy6',$,$,$,#31,(#37));
+#56=IFCCARTESIANPOINT((0.,0.,0.));
+#57=IFCDIRECTION((0.,0.,1.));
+#58=IFCDIRECTION((1.,0.,0.));
+#59=IFCAXIS2PLACEMENT3D(#56,#57,#58);
+#60=IFCLOCALPLACEMENT(#54,#59);
+#61=IFCRELAGGREGATES('061R4J$_v02e5$uJ_Zcpwu',$,$,$,#37,(#43));
+#62=IFCCARTESIANPOINT((0.,0.,0.));
+#63=IFCDIRECTION((0.,0.,1.));
+#64=IFCDIRECTION((1.,0.,0.));
+#65=IFCAXIS2PLACEMENT3D(#62,#63,#64);
+#66=IFCLOCALPLACEMENT(#60,#65);
+#67=IFCACTUATOR('3XPXM$jnf6gQHxVSs1SU0h',$,'Cube',$,$,#83,#78,$,.ELECTRICACTUATOR.);
+#68=IFCRELCONTAINEDINSPATIALSTRUCTURE('2uCE41OAzCIh07v_z205CZ',$,$,$,(#67),#43);
+#77=IFCSHAPEREPRESENTATION(#15,'Body','Tessellation',(#95));
+#78=IFCPRODUCTDEFINITIONSHAPE($,$,(#77));
+#79=IFCCARTESIANPOINT((0.,0.,0.));
+#80=IFCDIRECTION((0.,0.,1.));
+#81=IFCDIRECTION((1.,0.,0.));
+#82=IFCAXIS2PLACEMENT3D(#79,#80,#81);
+#83=IFCLOCALPLACEMENT(#66,#82);
+#84=IFCCARTESIANPOINTLIST3D(((-1.,1.,-1.),(-1.,-1.,-1.),(-1.,-1.,1.),(1.,1.,-1.),(-1.,1.,1.),(1.,-1.,-1.),(1.,1.,1.),(1.,-1.,1.),(1.,1.,2.),(1.,-1.,2.),(-1.,1.,2.),(-1.,-1.,2.)));
+#85=IFCINDEXEDPOLYGONALFACE((5,7,4,1));
+#86=IFCINDEXEDPOLYGONALFACE((7,8,6,4));
+#87=IFCINDEXEDPOLYGONALFACE((8,3,2,6));
+#88=IFCINDEXEDPOLYGONALFACE((4,6,2,1));
+#89=IFCINDEXEDPOLYGONALFACE((5,3,12,11));
+#90=IFCINDEXEDPOLYGONALFACE((3,5,1,2));
+#91=IFCINDEXEDPOLYGONALFACE((11,12,10,9));
+#92=IFCINDEXEDPOLYGONALFACE((8,7,9,10));
+#93=IFCINDEXEDPOLYGONALFACE((3,8,10,12));
+#94=IFCINDEXEDPOLYGONALFACE((7,5,11,9));
+#95=IFCPOLYGONALFACESET(#84,$,(#85,#86,#87,#88,#89,#90,#91,#92,#93,#94),$);
+ENDSEC;
+END-ISO-10303-21;
+"""
+
+
+def test_original_edges():
+ ifc_file = ifcopenshell.file.from_string(contents)
+ element = ifc_file.by_id(95)
+ settings = ifcopenshell.geom.settings()
+ shape = ifcopenshell.geom.create_shape(settings, element, geometry_library="opencascade")
+ assert (len(shape.edges) // 2) == 20
+ shape = ifcopenshell.geom.create_shape(settings, element, geometry_library="cgal")
+ assert (len(shape.edges) // 2) == 16
+ settings.set("cgal-original-edges", True)
+ shape = ifcopenshell.geom.create_shape(settings, element, geometry_library="cgal")
+ assert (len(shape.edges) // 2) == 20
diff --git a/src/ifcopenshell-python/test/util/test_stationing.py b/src/ifcopenshell-python/test/util/test_stationing.py
new file mode 100644
index 0000000000..a72a1fb6c1
--- /dev/null
+++ b/src/ifcopenshell-python/test/util/test_stationing.py
@@ -0,0 +1,46 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2021 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 ifcopenshell.util.stationing as sta
+
+
+def test_station_as_string():
+ # test with a bunch of "random" station values
+ s = sta.station_as_string(0.0)
+ assert s == "0+000.000"
+
+ s = sta.station_as_string(0.0, 2, 2)
+ assert s == "0+00.00"
+
+ s = sta.station_as_string(0.0, 2)
+ assert s == "0+00.000"
+
+ s = sta.station_as_string(100.00)
+ assert s == "0+100.000"
+
+ s = sta.station_as_string(-100.00)
+ assert s == "-0+100.000"
+
+ s = sta.station_as_string(123456.789, 2, 2)
+ assert s == "1234+56.79"
+
+ s = sta.station_as_string(-123456.789, 2, 2)
+ assert s == "-1234+56.79"
+
+ s = sta.station_as_string(123456.789, 3, 4)
+ assert s == "123+456.7890"
diff --git a/src/ifcparse/IfcAlignmentHelper.cpp b/src/ifcparse/IfcAlignmentHelper.cpp
index 8e26375ab5..a2953c16c2 100644
--- a/src/ifcparse/IfcAlignmentHelper.cpp
+++ b/src/ifcparse/IfcAlignmentHelper.cpp
@@ -30,6 +30,7 @@
// @todo use std::numbers::pi when upgrading to C++ 20
static const double PI = boost::math::constants::pi();
+#include
#ifdef HAS_SCHEMA_4x3_add2
@@ -207,7 +208,7 @@ Ifc4x3_add2::IfcAlignment* addHorizontalAlignment(IfcHierarchyHelper::ptr alignment_representations(new aggregate_of());
alignment_representations->push(footprint_shape_representation); // 2D footprint
@@ -243,21 +244,20 @@ std::tuple::ptr, typenam
// back gradient
auto dxBG = xPVI - xPBG;
auto dyBG = yPVI - yPBG;
- auto start_slope = atan2(dyBG, dxBG);
+ auto start_slope = tan(atan2(dyBG,dxBG));
// forward gradient
point_iter++;
std::tie(xPFG, yPFG) = *point_iter;
auto dxFG = xPFG - xPVI;
auto dyFG = yPFG - yPVI;
- auto end_slope = atan2(dyFG, dxFG);
+ auto end_slope = tan(atan2(dyFG,dxFG));
double xEVC = xPVI + length / 2;
double yEVC = yPVI + end_slope * length / 2;
// create gradient
{
- file.addDoublet(xPBG, yPBG);
auto gradient_length = dxBG - length/2;
auto design_parameters = new Ifc4x3_add2::IfcAlignmentVerticalSegment(boost::none, boost::none, xPBG, gradient_length, yPBG, start_slope, start_slope, boost::none, Ifc4x3_add2::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_CONSTANTGRADIENT);
auto alignment_segment = new Ifc4x3_add2::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters);
@@ -273,8 +273,6 @@ std::tuple::ptr, typenam
double xBVC = xPVI - length / 2;
double yBVC = yPVI - start_slope * length / 2;
- file.addDoublet(xBVC, yBVC);
-
auto design_parameters = new Ifc4x3_add2::IfcAlignmentVerticalSegment(boost::none, boost::none, xBVC, length, yBVC, start_slope, end_slope, 1 / k, Ifc4x3_add2::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_PARABOLICARC);
auto alignment_segment = new Ifc4x3_add2::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters);
vertical_segments->push(alignment_segment);
@@ -292,9 +290,9 @@ std::tuple::ptr, typenam
// create last tangent run
auto dx = xPVI - xPBG;
auto dy = yPVI - yPBG;
- auto slope = atan2(dy, dx);
- auto gradient_length = sqrt(dx * dx + dy * dy);
- file.addDoublet(xPBG, yPBG);
+ auto slope = tan(atan2(dy,dx));
+ auto gradient_length = dx;
+
auto design_parameters = new Ifc4x3_add2::IfcAlignmentVerticalSegment(boost::none, boost::none, xPBG, gradient_length, yPBG, slope, slope, boost::none, Ifc4x3_add2::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_CONSTANTGRADIENT);
auto alignment_segment = new Ifc4x3_add2::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters);
vertical_segments->push(alignment_segment);
@@ -303,7 +301,6 @@ std::tuple::ptr, typenam
}
// create zero length terminator segment
- file.addDoublet(xPVI, yPVI);
design_parameters = new Ifc4x3_add2::IfcAlignmentVerticalSegment(boost::none, boost::none, xPVI, 0.0, yPVI, slope, slope, boost::none, Ifc4x3_add2::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_CONSTANTGRADIENT);
alignment_segment = new Ifc4x3_add2::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters);
vertical_segments->push(alignment_segment);
@@ -356,14 +353,14 @@ Ifc4x3_add2::IfcAlignment* addAlignment(IfcHierarchyHelper& file, c
typename aggregate_of::ptr alignment_representation_items(new aggregate_of());
alignment_representation_items->push(composite_curve);
- // create the footprint representation
- auto footprint_shape_representation = new Ifc4x3_add2::IfcShapeRepresentation(axis_model_representation_subcontext, std::string("FootPrint"), std::string("Curve2D"), alignment_representation_items);
- file.addEntity(footprint_shape_representation);
-
// the gradient curve is a representation item
typename aggregate_of::ptr profile_representation_items(new aggregate_of());
profile_representation_items->push(gradient_curve);
+ // create footprint representation
+ auto footprint_shape_representation = new Ifc4x3_add2::IfcShapeRepresentation(axis_model_representation_subcontext, std::string("FootPrint"), std::string("Curve2D"), alignment_representation_items);
+ file.addEntity(footprint_shape_representation);
+
// create the axis representation
auto axis3d_shape_representation = new Ifc4x3_add2::IfcShapeRepresentation(axis_model_representation_subcontext, std::string("Axis"), std::string("Curve3D"), profile_representation_items);
file.addEntity(axis3d_shape_representation);
@@ -373,10 +370,10 @@ Ifc4x3_add2::IfcAlignment* addAlignment(IfcHierarchyHelper& file, c
_createSegmentRepresentations(file, placement, axis_model_representation_subcontext, horizontal_curve_segments, horizontal_segments);
_createSegmentRepresentations(file, placement, axis_model_representation_subcontext, vertical_curve_segments, vertical_segments);
- // the alignment has two representations, a plan view footprint and a 3d curve
+ // the alignment has a 3d curve representation
typename aggregate_of::ptr alignment_representations(new aggregate_of());
- alignment_representations->push(footprint_shape_representation); // 2D footprint
- alignment_representations->push(axis3d_shape_representation); // 3D curve
+ alignment_representations->push(footprint_shape_representation); // 2D curve
+ alignment_representations->push(axis3d_shape_representation); // 3D curve
// create the alignment product definition
product_definition_shape = new Ifc4x3_add2::IfcProductDefinitionShape(std::string("Alignment Product Definition Shape"), boost::none, alignment_representations);
@@ -688,13 +685,18 @@ std::pair mapAlign
new Ifc4x3_add2::IfcCartesianPoint(std::vector({0, 0})),
new Ifc4x3_add2::IfcVector(new Ifc4x3_add2::IfcDirection(std::vector{1, 0}), 1.0));
+ // IfcCurveSegment.SegmentLength is the length of the curve segment, not the horizontal length.
+ auto dx = cos(atan(start_gradient));
+ auto dy = sin(atan(start_gradient));
+ auto segment_curve_length = horizontal_length / dx;
+
auto curve_segment = new Ifc4x3_add2::IfcCurveSegment(
Ifc4x3_add2::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT,
new Ifc4x3_add2::IfcAxis2Placement2D(
new Ifc4x3_add2::IfcCartesianPoint({start_distance_along, start_height}),
- new Ifc4x3_add2::IfcDirection({sqrt(1.0 - start_gradient * start_gradient), start_gradient})),
+ new Ifc4x3_add2::IfcDirection({dx,dy})),
new Ifc4x3_add2::IfcLengthMeasure(0.0), // start
- new Ifc4x3_add2::IfcLengthMeasure(horizontal_length),
+ new Ifc4x3_add2::IfcLengthMeasure(segment_curve_length),
parent_curve);
result.first = curve_segment;
@@ -710,11 +712,22 @@ std::pair mapAlign
std::vector{A, B, C},
boost::none);
+ // IfcCurveSegment.SegmentLength is the length of the curve segment, not the horizontal length.
+ // The curve length is calculated by integrating the differential curve length equation sqrt(1 + (dy/dx)^2) from 0 to horizontal_length.
+ // y = A + Bx + Cx^2
+ // dy/dx = B + 2Cx
+ auto dx = cos(atan(start_gradient));
+ auto dy = sin(atan(start_gradient));
+ auto curve_length_fn = [B, C](double x) { return sqrt(1 + pow(B + C * x, 2)); };
+ auto segment_curve_length = boost::math::quadrature::trapezoidal(curve_length_fn, 0.0, horizontal_length);
+
auto curve_segment = new Ifc4x3_add2::IfcCurveSegment(
Ifc4x3_add2::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT,
- new Ifc4x3_add2::IfcAxis2Placement2D(new Ifc4x3_add2::IfcCartesianPoint({start_distance_along, start_height}), new Ifc4x3_add2::IfcDirection({sqrt(1.0 - start_gradient * start_gradient), start_gradient})),
+ new Ifc4x3_add2::IfcAxis2Placement2D(
+ new Ifc4x3_add2::IfcCartesianPoint({start_distance_along, start_height}),
+ new Ifc4x3_add2::IfcDirection({dx,dy})),
new Ifc4x3_add2::IfcLengthMeasure(0.0),
- new Ifc4x3_add2::IfcLengthMeasure(horizontal_length),
+ new Ifc4x3_add2::IfcLengthMeasure(segment_curve_length),
parent_curve);
result.first = curve_segment;
@@ -735,11 +748,14 @@ std::pair mapAlign
new Ifc4x3_add2::IfcDirection(std::vector{1, 0})),
radius);
+
+ auto segment_curve_length = radius * fabs(end_angle - start_angle);
+
Ifc4x3_add2::IfcCurveSegment* curve_segment = new Ifc4x3_add2::IfcCurveSegment(
Ifc4x3_add2::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT,
new Ifc4x3_add2::IfcAxis2Placement2D(new Ifc4x3_add2::IfcCartesianPoint({start_distance_along, start_height}), new Ifc4x3_add2::IfcDirection({1.0, 0.0})),
new Ifc4x3_add2::IfcLengthMeasure(0.0),
- new Ifc4x3_add2::IfcLengthMeasure(horizontal_length),
+ new Ifc4x3_add2::IfcLengthMeasure(segment_curve_length),
parent_curve);
result.first = curve_segment;
diff --git a/src/ifcparse/IfcHierarchyHelper.h b/src/ifcparse/IfcHierarchyHelper.h
index a8fbdf444b..eff35e7259 100644
--- a/src/ifcparse/IfcHierarchyHelper.h
+++ b/src/ifcparse/IfcHierarchyHelper.h
@@ -404,50 +404,79 @@ class IFC_PARSE_API IfcHierarchyHelper : public IfcParse::IfcFile {
template
void addRelatedObject(typename Schema::IfcObjectDefinition* relating_object,
typename Schema::IfcObjectDefinition* related_object,
- typename Schema::IfcOwnerHistory* owner_hist = 0) {
- typename T::list::ptr li = instances_by_type();
- bool found = false;
- for (typename T::list::it i = li->begin(); i != li->end(); ++i) {
- T* rel = *i;
- try {
- if (get_parent_of_relation(rel) == relating_object) {
- aggregate_of_instance::ptr products = get_children_of_relation(rel);
- products->push(related_object);
- set_children_of_relation(rel, products);
+ typename Schema::IfcOwnerHistory* owner_hist = 0)
+ {
+ if constexpr (std::is_same_v) {
+ typename Schema::IfcRelDefinesByType::list::ptr li = instances_by_type();
+ bool found = false;
+ for (typename Schema::IfcRelDefinesByType::list::it i = li->begin(); i != li->end(); ++i) {
+ typename Schema::IfcRelDefinesByType* rel = *i;
+ if (rel->RelatingType() == related_object) {
+ typename Schema::IfcObject::list::ptr objects = rel->RelatedObjects();
+ objects->push((typename Schema::IfcObject*)related_object);
+ rel->setRelatedObjects(objects);
found = true;
break;
}
- } catch (std::exception& e) {
- Logger::Error(e);
- } catch (...) {
- Logger::Error("Unknown error in addRelatedObject()");
- }
- }
- if (!found) {
- if (!owner_hist) {
- owner_hist = getSingle();
- }
- if (!owner_hist) {
- owner_hist = addOwnerHistory();
}
+ if (!found) {
+ if (!owner_hist) {
+ owner_hist = getSingle();
+ }
+ if (!owner_hist) {
+ owner_hist = addOwnerHistory();
+ }
+ typename Schema::IfcObject::list::ptr related_objects(new aggregate_of());
+ related_objects->push((typename Schema::IfcObject*)related_object);
+ typename Schema::IfcRelDefinesByType* t = new typename Schema::IfcRelDefinesByType(IfcParse::IfcGlobalId(), owner_hist, boost::none, boost::none, related_objects, related_object->template as);
- aggregate_of_instance::ptr related_objects(new aggregate_of_instance);
- related_objects->push(related_object);
-
- IfcEntityInstanceData data = IfcEntityInstanceData(storage_t(T::Class().attribute_count()));
- data.storage_.set(0, (std::string)IfcParse::IfcGlobalId());
- data.storage_.set(1, owner_hist);
- int relating_index = 4;
- int related_index = 5;
- if (T::Class().name() == "IfcRelContainedInSpatialStructure" || std::is_base_of::value) {
- // some classes have attributes reversed.
- std::swap(relating_index, related_index);
+ addEntity(t);
}
- data.storage_.set(relating_index, relating_object);
- data.storage_.set(related_index, related_objects);
+ } else {
+ typename T::list::ptr li = instances_by_type();
+ bool found = false;
+ for (typename T::list::it i = li->begin(); i != li->end(); ++i) {
+ T* rel = *i;
+ try {
+ if (get_parent_of_relation(rel) == relating_object) {
+ aggregate_of_instance::ptr products = get_children_of_relation(rel);
+ products->push(related_object);
+ set_children_of_relation(rel, products);
+ found = true;
+ break;
+ }
+ } catch (std::exception& e) {
+ Logger::Error(e);
+ } catch (...) {
+ Logger::Error("Unknown error in addRelatedObject()");
+ }
+ }
+ if (!found) {
+ if (!owner_hist) {
+ owner_hist = getSingle();
+ }
+ if (!owner_hist) {
+ owner_hist = addOwnerHistory();
+ }
- T* t = (T*)Schema::get_schema().instantiate(&T::Class(), std::move(data));
- addEntity(t);
+ aggregate_of_instance::ptr related_objects(new aggregate_of_instance);
+ related_objects->push(related_object);
+
+ IfcEntityInstanceData data = IfcEntityInstanceData(storage_t(T::Class().attribute_count()));
+ data.storage_.set(0, (std::string)IfcParse::IfcGlobalId());
+ data.storage_.set(1, owner_hist);
+ int relating_index = 4;
+ int related_index = 5;
+ if (T::Class().name() == "IfcRelContainedInSpatialStructure" || std::is_base_of::value) {
+ // some classes have attributes reversed.
+ std::swap(relating_index, related_index);
+ }
+ data.storage_.set(relating_index, relating_object);
+ data.storage_.set(related_index, related_objects);
+
+ T* t = (T*)Schema::get_schema().instantiate(&T::Class(), std::move(data));
+ addEntity(t);
+ }
}
}
@@ -589,70 +618,4 @@ IFC_PARSE_API void setSurfaceColour(IfcHierarchyHelper& file, Ifc4x
IFC_PARSE_API void setSurfaceColour(IfcHierarchyHelper& file, Ifc4x3_add1::IfcRepresentation* shape, Ifc4x3_add1::IfcPresentationStyle* style);
#endif
-/*
-template <>
-inline void IfcHierarchyHelper::addRelatedObject (typename Schema::IfcObjectDefinition* relating_structure,
- typename Schema::IfcObjectDefinition* related_object, typename Schema::IfcOwnerHistory* owner_hist)
-{
- typename Schema::IfcRelContainedInSpatialStructure::list::ptr li = instances_by_type();
- bool found = false;
- for (typename Schema::IfcRelContainedInSpatialStructure::list::it i = li->begin(); i != li->end(); ++i) {
- typename Schema::IfcRelContainedInSpatialStructure* rel = *i;
- if (rel->RelatingStructure() == relating_structure) {
- typename Schema::IfcProduct::list::ptr products = rel->RelatedElements();
- products->push((typename Schema::IfcProduct*)related_object);
- rel->setRelatedElements(products);
- found = true;
- break;
- }
- }
- if (! found) {
- if (! owner_hist) {
- owner_hist = getSingle();
- }
- if (! owner_hist) {
- owner_hist = addOwnerHistory();
- }
- typename Schema::IfcProduct::list::ptr related_objects (new aggregate_of());
- related_objects->push((typename Schema::IfcProduct*)related_object);
- typename Schema::IfcRelContainedInSpatialStructure* t = new typename Schema::IfcRelContainedInSpatialStructure(IfcParse::IfcGlobalId(), owner_hist,
- boost::none, boost::none, related_objects, (typename Schema::IfcSpatialStructureElement*)relating_structure);
-
- addEntity(t);
- }
-}
-
-template <>
-inline void IfcHierarchyHelper::addRelatedObject (typename Schema::IfcObjectDefinition* relating_type,
- typename Schema::IfcObjectDefinition* related_object, typename Schema::IfcOwnerHistory* owner_hist)
-{
- typename Schema::IfcRelDefinesByType::list::ptr li = instances_by_type();
- bool found = false;
- for (typename Schema::IfcRelDefinesByType::list::it i = li->begin(); i != li->end(); ++i) {
- typename Schema::IfcRelDefinesByType* rel = *i;
- if (rel->RelatingType() == relating_type) {
- typename Schema::IfcObject::list::ptr objects = rel->RelatedObjects();
- objects->push((typename Schema::IfcObject*)related_object);
- rel->setRelatedObjects(objects);
- found = true;
- break;
- }
- }
- if (! found) {
- if (! owner_hist) {
- owner_hist = getSingle();
- }
- if (! owner_hist) {
- owner_hist = addOwnerHistory();
- }
- typename Schema::IfcObject::list::ptr related_objects (new aggregate_of());
- related_objects->push((typename Schema::IfcObject*)related_object);
- typename Schema::IfcRelDefinesByType* t = new typename Schema::IfcRelDefinesByType(IfcParse::IfcGlobalId(), owner_hist,
- boost::none, boost::none, related_objects, (typename Schema::IfcTypeObject*)relating_type);
-
- addEntity(t);
- }
-}
-*/
-
#endif
diff --git a/src/ifcpatch/ifcpatch/recipes/FixArchiCADToRevitSpaces.py b/src/ifcpatch/ifcpatch/recipes/FixArchiCADToRevitSpaces.py
index a025709674..be7fac9d71 100644
--- a/src/ifcpatch/ifcpatch/recipes/FixArchiCADToRevitSpaces.py
+++ b/src/ifcpatch/ifcpatch/recipes/FixArchiCADToRevitSpaces.py
@@ -71,7 +71,6 @@ class Patcher:
import bonsai.tool as tool
import ifcopenshell
import ifcopenshell.util.element
- from bonsai.bim.ifc import IfcStore
from mathutils import Vector, Matrix
if len(bpy.data.objects) > 0:
@@ -114,8 +113,8 @@ class Patcher:
bpy.ops.bim.update_representation(
ifc_representation_class="IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids"
)
- for context in IfcStore.get_file().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
+ for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if context.Precision:
context.Precision = 10
- self.file = IfcStore.get_file()
+ self.file = tool.Ifc.get()
diff --git a/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py b/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py
index 07184d1cb2..6a52abd742 100644
--- a/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py
+++ b/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py
@@ -84,7 +84,8 @@ class Patcher:
import bonsai.tool as tool
from math import degrees
- bpy.context.scene.BIMProjectProperties.should_use_native_meshes = True
+ props = tool.Project.get_project_props()
+ props.should_use_native_meshes = True
bpy.ops.bim.load_project(filepath=self.filepath)
old_history_size = tool.Ifc.get().history_size
diff --git a/src/ifcwrap/IfcPython.i b/src/ifcwrap/IfcPython.i
index b6f344da1a..196416586c 100644
--- a/src/ifcwrap/IfcPython.i
+++ b/src/ifcwrap/IfcPython.i
@@ -54,6 +54,11 @@
%include "exception.i"
%include "std_shared_ptr.i"
+%{
+ #include
+%}
+%template(DoubleArray3) std::array;
+
%ignore IfcGeom::NumberNativeDouble;
%ignore ifcopenshell::geometry::Converter;
@@ -86,7 +91,7 @@
%ignore curve_to_loop_upgrade_impl;
%ignore edge_to_loop_upgrade_impl;
%ignore curve_to_face_upgrade_impl;
-%ignore loop_to_piecewise_function_upgrade_impl;
+%ignore loop_to_function_item_upgrade_impl;
// settings, can this done more generally?
%ignore UseElementNames;
diff --git a/src/serializers/GltfSerializer.cpp b/src/serializers/GltfSerializer.cpp
index 8f694801be..8b1c0ec03b 100644
--- a/src/serializers/GltfSerializer.cpp
+++ b/src/serializers/GltfSerializer.cpp
@@ -141,13 +141,12 @@ size_t write_accessor(json& j, std::ofstream& ofs, It begin, It end, int bufferV
accessor["componentType"] = component_type::value;
accessor["count"] = num;
- if (N == 1) {
+ if constexpr (N == 1) {
j["bufferViews"].push_back({ {"buffer", 0}, {"byteOffset", (size_t)ofs.tellp()}, { "byteLength", num * 4}, {"target", ELEMENT_ARRAY_BUFFER} });
} else {
j["bufferViews"].push_back({ {"buffer", 0}, {"byteStride", 12}, { "byteOffset", (size_t)ofs.tellp()}, { "byteLength", num * 12}, {"target", ARRAY_BUFFER}});
}
-
std::array min, max;
min.fill(std::numeric_limits::max());
max.fill(std::numeric_limits::lowest());