diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index 287e0bb09d..4ed12d74f0 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -228,7 +228,7 @@ def refresh_ui_data(): @persistent -def loadIfcStore(scene): +def loadIfcStore(scene: bpy.types.Scene) -> None: IfcStore.purge() refresh_ui_data() if not tool.Ifc.get(): @@ -238,19 +238,21 @@ def loadIfcStore(scene): @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() @@ -283,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" @@ -341,7 +343,8 @@ 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 = tool.Georeference.get_georeference_props() diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index 2d19fa6e1f..3ac994739a 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -32,6 +32,7 @@ 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 @@ -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 044bf74fd2..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) @@ -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 5dd519de28..21c8d16e3e 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -879,17 +879,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 + 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"): @@ -909,19 +911,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] diff --git a/src/bonsai/bonsai/bim/module/augin/operator.py b/src/bonsai/bonsai/bim/module/augin/operator.py index c4999ab08f..b578beb554 100644 --- a/src/bonsai/bonsai/bim/module/augin/operator.py +++ b/src/bonsai/bonsai/bim/module/augin/operator.py @@ -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/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index d8eb640551..c3d87e4759 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -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 diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 68e3525a87..4b16c3fb12 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -106,7 +106,8 @@ class ConvertToBlender(bpy.types.Operator): if material.library: continue tool.Ifc.unlink(obj=material) - context.scene.BIMProperties.ifc_file = "" + 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() @@ -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 diff --git a/src/bonsai/bonsai/bim/module/debug/ui.py b/src/bonsai/bonsai/bim/module/debug/ui.py index 4fbd5c8ea6..c2c43bd4bb 100644 --- a/src/bonsai/bonsai/bim/module/debug/ui.py +++ b/src/bonsai/bonsai/bim/module/debug/ui.py @@ -34,9 +34,10 @@ class BIM_PT_debug(Panel): layout = self.layout 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="") 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/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 13c494d092..2ed1a0a8d5 100644 --- a/src/bonsai/bonsai/bim/module/document/operator.py +++ b/src/bonsai/bonsai/bim/module/document/operator.py @@ -115,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 c2aff57ed4..4bd935c8f6 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -39,7 +39,7 @@ class BIM_PT_documents(Panel): 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") @@ -102,7 +102,7 @@ class BIM_PT_object_documents(Panel): obj = context.active_object self.oprops = obj.BIMObjectProperties - self.props = context.scene.BIMDocumentProperties + 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/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 3d1c3f46ec..6014015995 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -712,7 +712,8 @@ 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()} props = tool.Project.get_project_props() for link in props.links: @@ -730,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) @@ -1618,7 +1619,8 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator): def poll(cls, context): 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.") @@ -1722,7 +1724,8 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator): 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 @@ -2523,7 +2526,8 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator): 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 = tool.Drawing.get_document_props() @@ -2589,7 +2593,8 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator): 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 = tool.Drawing.get_document_props() diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 3408bd8baa..b91e0d8158 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -76,34 +76,35 @@ 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": 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" + bim_props.area_unit = "SQUARE_METRE" + bim_props.volume_unit = "CUBIC_METRE" pprops.template_file = "0" elif self.preset == "metric_mm": 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" + bim_props.area_unit = "SQUARE_METRE" + bim_props.volume_unit = "CUBIC_METRE" pprops.template_file = "0" elif self.preset == "imperial_ft": 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" + bim_props.area_unit = "square foot" + bim_props.volume_unit = "cubic foot" pprops.template_file = "0" elif self.preset == "demo": 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" + bim_props.area_unit = "SQUARE_METRE" + bim_props.volume_unit = "CUBIC_METRE" pprops.template_file = "IFC4 Demo Template.ifc" if self.preset != "wizard": @@ -979,7 +980,8 @@ 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"}, @@ -1039,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"} @@ -1070,7 +1074,8 @@ class LoadProjectElements(bpy.types.Operator): 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": @@ -1560,7 +1565,8 @@ class ExportIFC(bpy.types.Operator): return {"FINISHED"} self.use_relative_path = tool.Project.get_project_props().use_relative_project_path - if (filepath := context.scene.BIMProperties.ifc_file) and not self.should_save_as: + 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: @@ -1621,7 +1627,6 @@ 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 props = tool.Drawing.get_document_props() if not props.ifc_files: new = props.ifc_files.add() @@ -1629,12 +1634,13 @@ class ExportIFC(bpy.types.Operator): 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"}, diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index e49cdb8910..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,7 +151,7 @@ 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 = tool.Ifc.get() if pprops.is_loading: @@ -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() @@ -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/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 4022c1d5ff..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" 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/unit/prop.py b/src/bonsai/bonsai/bim/module/unit/prop.py index b01fab4ddf..f0f04e16ae 100644 --- a/src/bonsai/bonsai/bim/module/unit/prop.py +++ b/src/bonsai/bonsai/bim/module/unit/prop.py @@ -30,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"] @@ -57,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") @@ -67,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 83637845f7..f7b55a96e3 100644 --- a/src/bonsai/bonsai/bim/module/unit/ui.py +++ b/src/bonsai/bonsai/bim/module/unit/ui.py @@ -41,7 +41,7 @@ class BIM_PT_units(Panel): 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/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 2bd6f445f4..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 @@ -575,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") @@ -639,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) @@ -814,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): @@ -852,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"} diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index c9b8c759d8..e335af9a0e 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) @@ -331,10 +335,31 @@ class Attribute(PropertyGroup): 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: @@ -439,6 +464,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 @@ -449,6 +481,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) @@ -517,6 +554,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") @@ -547,6 +599,7 @@ class PsetQto(PropertyGroup): class GlobalId(PropertyGroup): name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") class BIMCollectionProperties(PropertyGroup): @@ -556,11 +609,14 @@ class BIMCollectionProperties(PropertyGroup): 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", ) @@ -570,6 +626,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 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 a77650a72b..4223706b8a 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -121,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") @@ -381,12 +381,13 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): layout.prop(props, "occurrence_name_function") 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.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.prop(props, "cache_dir") row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.cache_dir" row = layout.row(align=True) @@ -394,7 +395,8 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): 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: - layout.prop(context.scene.BIMProperties, "pset_dir") + 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") @@ -510,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) @@ -557,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 + 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 @@ -579,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 + 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 diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index b9699de172..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", @@ -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 ec4490b2c3..7752423ce4 100644 --- a/src/bonsai/bonsai/tool/debug.py +++ b/src/bonsai/bonsai/tool/debug.py @@ -54,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/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/polyline.py b/src/bonsai/bonsai/tool/polyline.py index db8f4e7922..56eab4a288 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -456,7 +456,8 @@ class Polyline(bonsai.core.tool.Polyline): dprops = tool.Drawing.get_document_props() precision = dprops.imperial_precision if is_area: - area_unit = bpy.context.scene.BIMProperties.area_unit + 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 diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index 6a2bbb0140..3f4d77f0d7 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)) 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/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/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/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_ifc.py b/src/bonsai/test/tool/test_ifc.py index 641a8adfa0..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 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/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/style/edit_surface_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py index 28419b8d86..dba4dd48ea 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py @@ -81,7 +81,7 @@ class Usecase: def execute(self, style: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: self.style = style - attribute_types = {} + 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: 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 d3c94b616d..328d98b27f 100644 --- a/src/ifcopenshell-python/ifcopenshell/file.py +++ b/src/ifcopenshell-python/ifcopenshell/file.py @@ -359,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 @@ -482,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] @@ -495,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] @@ -510,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: @@ -530,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)] @@ -625,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) @@ -639,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) @@ -676,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: