diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml new file mode 100644 index 0000000000..57a4d8f707 --- /dev/null +++ b/.github/workflows/build_all.yml @@ -0,0 +1,21 @@ +name: Dispatch Build IfcOpenShell + +on: + workflow_dispatch: + +jobs: + trigger-workflows: + runs-on: ubuntu-latest + strategy: + matrix: + workflow: + - 'Build IfcOpenShell Linux' + - 'Build IfcOpenShell Linux ARM' + - 'Build IfcOpenShell OSX' + - 'Build IfcOpenShell WASM / Pyodide' + - 'Build IfcOpenShell Windows' + steps: + - name: Trigger binary build workflows + uses: benc-uk/workflow-dispatch@v1 + with: + workflow: ${{ matrix.workflow }} diff --git a/.github/workflows/build_osx.yml b/.github/workflows/build_osx.yml index d70cf0930c..4b968c8516 100644 --- a/.github/workflows/build_osx.yml +++ b/.github/workflows/build_osx.yml @@ -128,10 +128,8 @@ jobs: # # To force the link and overwrite all conflicting files: # brew link --overwrite python@3.13 - brew link --overwrite python@3.12 - brew link --overwrite python@3.13 # https://github.com/rust-lang/rustup/pull/3989/files - brew install --overwrite awscli + brew install --overwrite awscli | true - name: Upload .zip archives to S3 run: | diff --git a/nix/build-all.py b/nix/build-all.py index cb821a5d6d..5f2f2f0a79 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -350,7 +350,7 @@ def git_clone_or_pull_repository(clone_url, target_dir, revision=None): run([git, "clone", "--recursive", clone_url, target_dir]) else: logger.info(f"directory '{target_dir}' already cloned. Pulling latest changes.") - run([git, "-C", target_dir, "fetch", "--all", "--tags"]) + run([git, "-C", target_dir, "fetch", "--all", "--tags", "--force"]) # detect whether we are on a branch and pull if run([git, "rev-parse", "--abbrev-ref", "HEAD"], cwd=target_dir) != "HEAD": diff --git a/src/bonsai/bonsai/bim/__init__.py b/src/bonsai/bonsai/bim/__init__.py index 3755a8d12a..5985459e2a 100644 --- a/src/bonsai/bonsai/bim/__init__.py +++ b/src/bonsai/bonsai/bim/__init__.py @@ -115,10 +115,8 @@ classes = [ operator.ReloadIfcFile, operator.RemoveIfcFile, operator.RevertClippingPlaneCut, - operator.SelectDataDir, - operator.SelectCacheDir, + operator.SelectDir, operator.SelectIfcFile, - operator.SelectSchemaDir, operator.SelectURIAttribute, operator.SetTab, operator.SwitchTab, diff --git a/src/bonsai/bonsai/bim/export_ifc.py b/src/bonsai/bonsai/bim/export_ifc.py index deafc5b3ff..84173a9ec5 100644 --- a/src/bonsai/bonsai/bim/export_ifc.py +++ b/src/bonsai/bonsai/bim/export_ifc.py @@ -132,7 +132,8 @@ class IfcExporter: bpy.ops.bim.update_representation(obj=obj.name) def has_changed_materials(self, obj: bpy.types.Object) -> bool: - checksum = obj.data.BIMMeshProperties.material_checksum + mprops = tool.Geometry.get_mesh_props(obj.data) + checksum = mprops.material_checksum return checksum != tool.Geometry.get_material_checksum(obj) def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]: diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index d65017243f..4ed12d74f0 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -60,7 +60,7 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) - if props.is_renaming: props.is_renmaing = False return - IfcStore.get_file().by_id(ifc_definition_id).Name = obj.name + tool.Ifc.get().by_id(ifc_definition_id).Name = obj.name refresh_ui_data() return @@ -71,7 +71,7 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) - obj.BIMObjectProperties.is_renaming = False return - element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) + element = tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id) if "/" in obj.name: object_name = obj.name element_name = obj.name.split("/", 1)[1] @@ -221,35 +221,38 @@ def refresh_ui_data(): if isinstance(tool.Ifc.get(), ifcopenshell.sqlite): tool.Ifc.get().clear_cache() - bpy.context.scene.DocProperties.should_draw_decorations = bpy.context.scene.DocProperties.should_draw_decorations + props = tool.Drawing.get_document_props() + props.should_draw_decorations = props.should_draw_decorations if bpy.context.scene.WebProperties.is_connected: tool.Web.send_webui_data() @persistent -def loadIfcStore(scene): +def loadIfcStore(scene: bpy.types.Scene) -> None: IfcStore.purge() refresh_ui_data() - if not IfcStore.get_file(): + if not tool.Ifc.get(): return - IfcStore.get_schema() + tool.Ifc.schema() IfcStore.relink_all_objects() @persistent -def undo_post(scene): - if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction: - IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction - IfcStore.undo(until_key=bpy.context.scene.BIMProperties.last_transaction) +def undo_post(scene: bpy.types.Scene) -> None: + props = tool.Blender.get_bim_props() + if IfcStore.last_transaction != props.last_transaction: + IfcStore.last_transaction = props.last_transaction + IfcStore.undo(until_key=props.last_transaction) refresh_ui_data() tool.Ifc.rebuild_element_maps() @persistent -def redo_post(scene): - if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction: - IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction - IfcStore.redo(until_key=bpy.context.scene.BIMProperties.last_transaction) +def redo_post(scene: bpy.types.Scene) -> None: + props = tool.Blender.get_bim_props() + if IfcStore.last_transaction != props.last_transaction: + IfcStore.last_transaction = props.last_transaction + IfcStore.redo(until_key=props.last_transaction) refresh_ui_data() tool.Ifc.rebuild_element_maps() @@ -282,7 +285,7 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None return pao -def viewport_shading_changed_callback(area): +def viewport_shading_changed_callback(area: bpy.types.Area) -> None: shading = area.spaces.active.shading.type if shading == "RENDERED": bpy.context.scene.BIMStylesProperties.active_style_type = "External" @@ -340,10 +343,11 @@ def load_post(scene): tool.Blender.setup_tabs() if tool.Ifc.get() and bpy.data.is_saved: - bpy.context.scene.BIMProperties.has_blend_warning = True + props = tool.Blender.get_bim_props() + props.has_blend_warning = True # Bonsai overlays - georeference_props = bpy.context.scene.BIMGeoreferenceProperties + georeference_props = tool.Georeference.get_georeference_props() aggregate_props = bpy.context.scene.BIMAggregateProperties nest_props = bpy.context.scene.BIMNestProperties model_props = tool.Model.get_model_props() diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index a8b25bca1a..3ac994739a 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -27,12 +27,12 @@ import ifcopenshell.util.element import ifcopenshell.util.unit from ifcopenshell.util.doc import get_attribute_doc, get_predefined_type_doc, get_property_doc import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore from typing import Optional, Callable, Any, Union, Iterable, TYPE_CHECKING if TYPE_CHECKING: import bonsai.bim.prop from bonsai.bim.prop import Attribute + from bonsai.bim.module.search.prop import BIMFilterGroup # ImportCallback return values: # - None - property should be imported by default workflow @@ -119,7 +119,8 @@ def import_attributes( data: dict[str, Any], callback: Optional[ImportCallback] = None, ) -> None: - for attribute in IfcStore.get_schema().declaration_by_name(ifc_class).all_attributes(): + schema = tool.Ifc.schema() + for attribute in schema.declaration_by_name(ifc_class).all_attributes(): import_attribute(attribute, props, data, callback=callback) @@ -372,11 +373,16 @@ def convert_property_group_from_si(property_group: bpy.types.PropertyGroup, skip setattr(property_group, prop_name, prop_value) -def draw_filter(layout: bpy.types.UILayout, filter_groups, data, module: str) -> None: +def draw_filter( + layout: bpy.types.UILayout, + filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup], + data, + module: str, +) -> None: if not data.is_loaded: data.load() - sprops = bpy.context.scene.BIMSearchProperties + sprops = tool.Search.get_search_props() if tool.Ifc.get(): row = layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index 1c5d7800b1..35b40b7b3c 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -105,7 +105,8 @@ class IfcStore: @staticmethod def get_file(): if IfcStore.file is None: - IfcStore.path = cast(str, bpy.context.scene.BIMProperties.ifc_file) + props = tool.Blender.get_bim_props() + IfcStore.path = props.ifc_file # Interpret relative paths as relative to .blend file. if IfcStore.path and not os.path.isabs(IfcStore.path): IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path)) @@ -119,10 +120,11 @@ class IfcStore: @staticmethod def get_cache(): if IfcStore.cache is None and IfcStore.path: + props = tool.Blender.get_bim_props() ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest() - os.makedirs(bpy.context.scene.BIMProperties.cache_dir, exist_ok=True) - IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5") + os.makedirs(props.cache_dir, exist_ok=True) + IfcStore.cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5") cache_path = Path(IfcStore.cache_path) cache_settings = ifcopenshell.geom.settings() serializer_settings = ifcopenshell.geom.serializer_settings() @@ -162,7 +164,8 @@ class IfcStore: assert IfcStore.file ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest() - new_cache_path = os.path.join(bpy.context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5") + props = tool.Blender.get_bim_props() + new_cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5") IfcStore.cache = None try: shutil.move(IfcStore.cache_path, new_cache_path) @@ -178,6 +181,7 @@ class IfcStore: if not os.path.isfile(path): return extension = path.split(".")[-1] + props = tool.Project.get_project_props() if extension.lower() == "ifczip": with tempfile.TemporaryDirectory() as unzipped_path: with zipfile.ZipFile(path, "r") as zip_ref: @@ -187,7 +191,7 @@ class IfcStore: return elif extension.lower() == "ifcxml": IfcStore.file = ifcopenshell.file(ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(path)) - elif bpy.context.scene.BIMProjectProperties.should_stream: + elif props.should_stream: IfcStore.file = ifcopenshell.open(path, should_stream=True) else: IfcStore.file = ifcopenshell.open(path) @@ -195,9 +199,8 @@ class IfcStore: @staticmethod def get_schema() -> ifcopenshell.ifcopenshell_wrapper.schema_definition: if IfcStore.file is None: - IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name( - bpy.context.scene.BIMProjectProperties.export_schema - ) + props = tool.Project.get_project_props() + IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(props.export_schema) elif IfcStore.schema is None: IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(IfcStore.file.schema_identifier) return IfcStore.schema @@ -247,7 +250,7 @@ class IfcStore: # refactor this class and deprecate usage of IfcStore in favour of # tools. if not isinstance(obj, (bpy.types.Object, bpy.types.Material)): - obj.BIMMeshProperties.ifc_definition_id = element.id() + tool.Geometry.get_mesh_props(obj).ifc_definition_id = element.id() return existing_obj = IfcStore.id_map.get(element.id(), None) @@ -414,7 +417,8 @@ class IfcStore: method: Literal["EXECUTE", "INVOKE", "MODAL"] = "EXECUTE", ) -> set[str]: bonsai.last_actions.append({"type": "operator", "name": operator.bl_idname}) - bpy.context.scene.BIMProperties.is_dirty = True + props = tool.Blender.get_bim_props() + props.is_dirty = True # Modals don't nest, and Blender handles the loop that continuously calls modal() is_top_level_operator = not bool(IfcStore.current_transaction) or (method == "MODAL") @@ -493,7 +497,8 @@ class IfcStore: ) -> None: key = getattr(operator, "transaction_key", None) data = getattr(operator, "transaction_data", None) - bpy.context.scene.BIMProperties.last_transaction = key + props = tool.Blender.get_bim_props() + props.last_transaction = key IfcStore.last_transaction = key rollback = rollback or getattr(operator, "rollback", lambda data: True) commit = commit or getattr(operator, "commit", lambda data: True) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index d2bdc2ed70..2a3a5f97a2 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -69,6 +69,9 @@ class MaterialCreator: if isinstance(mesh, bpy.types.Curve): return + if mesh.get("has_layer_styles", None) == True: + return + self.mesh = mesh self.obj = obj if element.is_a("IfcTypeProduct"): @@ -90,7 +93,8 @@ class MaterialCreator: return # Already has materials assign to the representation itself # Otherwise, we need to check for material styles on the element, since # create_shape on types only works on representations. - context = tool.Ifc.get().by_id(self.mesh.BIMMeshProperties.ifc_definition_id).ContextOfItems + mprops = tool.Geometry.get_mesh_props(self.mesh) + context = tool.Ifc.get().by_id(mprops.ifc_definition_id).ContextOfItems for material in ifcopenshell.util.element.get_materials(element): if style := ifcopenshell.util.representation.get_material_style(material, context): self.mesh["ios_materials"] = (style.id(),) @@ -218,7 +222,7 @@ class IfcImporter: self.progress = 0 self.material_creator = MaterialCreator(ifc_import_settings, self) - classes_to_wireframe_str = bpy.context.scene.DocProperties.classes_to_wireframe + classes_to_wireframe_str = tool.Drawing.get_document_props().classes_to_wireframe self.classes_to_wireframe_list = [word.strip() for word in classes_to_wireframe_str.split(",")] def profile_code(self, message: str) -> None: @@ -434,7 +438,7 @@ class IfcImporter: return False def calculate_model_offset(self) -> None: - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if self.ifc_import_settings.false_origin_mode == "MANUAL": tool.Loader.set_manual_blender_offset(self.file) elif self.ifc_import_settings.false_origin_mode == "AUTOMATIC": @@ -878,17 +882,19 @@ class IfcImporter: def load_file(self): self.ifc_import_settings.logger.info("loading file %s", self.ifc_import_settings.input_file) - if not bpy.context.scene.BIMProperties.ifc_file: - bpy.context.scene.BIMProperties.ifc_file = self.ifc_import_settings.input_file - self.file = IfcStore.get_file() + props = tool.Blender.get_bim_props() + if not props.ifc_file: + props.ifc_file = self.ifc_import_settings.input_file + self.file = tool.Ifc.get() def calculate_unit_scale(self): self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) tool.Loader.set_unit_scale(self.unit_scale) - def set_units(self): + def set_units(self) -> None: if not (assignment := self.file.by_type("IfcProject")[0].UnitsInContext): return # Geometry is optional in IFC + props = tool.Blender.get_bim_props() for unit in assignment.Units: if unit.is_a("IfcNamedUnit") and unit.UnitType == "LENGTHUNIT": if unit.is_a("IfcSIUnit"): @@ -908,19 +914,19 @@ class IfcImporter: elif unit.is_a("IfcNamedUnit") and unit.UnitType == "AREAUNIT": name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower() try: - bpy.context.scene.BIMProperties.area_unit = "{}{}".format( + props.area_unit = "{}{}".format( unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name ) except: # Probably an invalid unit. - bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE" + props.area_unit = "SQUARE_METRE" elif unit.is_a("IfcNamedUnit") and unit.UnitType == "VOLUMEUNIT": name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower() try: - bpy.context.scene.BIMProperties.volume_unit = "{}{}".format( + props.volume_unit = "{}{}".format( unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name ) except: # Probably an invalid unit. - bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE" + props.volume_unit = "CUBIC_METRE" def create_project(self): project = self.file.by_type("IfcProject")[0] @@ -964,10 +970,10 @@ class IfcImporter: tool.Collector.assign(obj, should_clean_users_collection=False) def is_curve_annotation(self, element: ifcopenshell.entity_instance) -> bool: - object_type = element.ObjectType + object_type = ifcopenshell.util.element.get_predefined_type(element) return ( object_type in tool.Drawing.ANNOTATION_TYPES_DATA - and tool.Drawing.ANNOTATION_TYPES_DATA[object_type][3] == "curve" + and tool.Drawing.ANNOTATION_TYPES_DATA[object_type].data_type == "curve" ) def get_drawing_group(self, element): @@ -1056,12 +1062,13 @@ class IfcImporter: else: mesh["has_cartesian_point_offset"] = False - return tool.Loader.convert_geometry_to_mesh( + mesh = tool.Loader.convert_geometry_to_mesh( geometry, mesh, verts=verts, load_indexed_maps=self.ifc_import_settings.load_indexed_maps, ) + return tool.Loader.slice_layerset_mesh(element, mesh) except: self.ifc_import_settings.logger.error("Could not create mesh for %s", element) import traceback @@ -1069,9 +1076,10 @@ class IfcImporter: print(traceback.format_exc()) def set_default_context(self): + rprops = tool.Root.get_root_props() for subcontext in self.file.by_type("IfcGeometricRepresentationSubContext"): if subcontext.ContextIdentifier == "Body": - bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id()) + rprops.contexts = str(subcontext.id()) break def link_element(self, element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None: diff --git a/src/bonsai/bonsai/bim/module/aggregate/operator.py b/src/bonsai/bonsai/bim/module/aggregate/operator.py index c2e0cb16c2..c324037b89 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/operator.py +++ b/src/bonsai/bonsai/bim/module/aggregate/operator.py @@ -23,7 +23,6 @@ import ifcopenshell.util.element import bonsai.tool as tool import bonsai.core.aggregate as core import bonsai.core.spatial -from bonsai.bim.ifc import IfcStore class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/aggregate/ui.py b/src/bonsai/bonsai/bim/module/aggregate/ui.py index 7b9503f530..74389c723d 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/ui.py +++ b/src/bonsai/bonsai/bim/module/aggregate/ui.py @@ -39,7 +39,7 @@ class BIM_PT_aggregate(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"): return False @@ -120,9 +120,9 @@ class BIM_PT_linked_aggregate(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"): + if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"): return False return True diff --git a/src/bonsai/bonsai/bim/module/attribute/operator.py b/src/bonsai/bonsai/bim/module/attribute/operator.py index 3282c66ec5..2b279895e3 100644 --- a/src/bonsai/bonsai/bim/module/attribute/operator.py +++ b/src/bonsai/bonsai/bim/module/attribute/operator.py @@ -26,7 +26,6 @@ import bonsai.bim.helper import bonsai.tool as tool import bonsai.core.attribute as core import bonsai.core.spatial -from bonsai.bim.ifc import IfcStore def get_objs_for_operation(operator_properties, context): @@ -117,7 +116,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() obj = tool.Blender.get_active_object(is_selected=False) if not (element := tool.Ifc.get_entity(obj)): return diff --git a/src/bonsai/bonsai/bim/module/attribute/prop.py b/src/bonsai/bonsai/bim/module/attribute/prop.py index 207fd927fd..fd2dab7cd2 100644 --- a/src/bonsai/bonsai/bim/module/attribute/prop.py +++ b/src/bonsai/bonsai/bim/module/attribute/prop.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import bpy -from bonsai.bim.ifc import IfcStore from bonsai.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( diff --git a/src/bonsai/bonsai/bim/module/attribute/ui.py b/src/bonsai/bonsai/bim/module/attribute/ui.py index 6f0f5a5599..0fce2b07d4 100644 --- a/src/bonsai/bonsai/bim/module/attribute/ui.py +++ b/src/bonsai/bonsai/bim/module/attribute/ui.py @@ -18,7 +18,6 @@ import bonsai.bim.helper from bpy.types import Panel -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.attribute.data import AttributesData import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/augin/operator.py b/src/bonsai/bonsai/bim/module/augin/operator.py index 965279e9b7..b578beb554 100644 --- a/src/bonsai/bonsai/bim/module/augin/operator.py +++ b/src/bonsai/bonsai/bim/module/augin/operator.py @@ -113,7 +113,7 @@ class AuginCreateNewModel(bpy.types.Operator): context.scene.collection.objects.link(cam_obj) context.scene.camera = cam_obj - tmpdir = tempfile.mkdtemp() + tmpdir = tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None) thumb_path = os.path.join(tmpdir, "thumb.png") context.scene.render.image_settings.file_format = "PNG" context.scene.render.filepath = thumb_path @@ -122,15 +122,16 @@ class AuginCreateNewModel(bpy.types.Operator): context.scene.render.image_settings.file_format = old_file_format context.scene.render.filepath = old_filepath - client.upload_file(context.scene.BIMProperties.ifc_file, result["s3_bucket"], result["model_path"]) + bim_props = tool.Blender.get_bim_props() + client.upload_file(bim_props.ifc_file, result["s3_bucket"], result["model_path"]) client.upload_file(thumb_path, result["s3_bucket"], result["thumb_path"]) # Notify done url = "https://server.auge.pro.br/API/v3/augin_rest.php/files_uploaded" payload = { "user_token": props.token, - "ifc_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file), - "model_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file), + "ifc_filesize": os.path.getsize(bim_props.ifc_file), + "model_filesize": os.path.getsize(bim_props.ifc_file), "thumb_filesize": os.path.getsize(thumb_path), "model_upload_path": result["model_path"], "thumb_upload_path": result["thumb_path"], diff --git a/src/bonsai/bonsai/bim/module/augin/ui.py b/src/bonsai/bonsai/bim/module/augin/ui.py index 0da722f7a6..65ca3ac7f9 100644 --- a/src/bonsai/bonsai/bim/module/augin/ui.py +++ b/src/bonsai/bonsai/bim/module/augin/ui.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy.types +import bonsai.tool as tool class BIM_PT_augin(bpy.types.Panel): @@ -47,7 +48,8 @@ class BIM_PT_augin(bpy.types.Panel): row = layout.row() row.label(text="Logged in as " + props.username) - if not context.scene.BIMProperties.ifc_file: + bim_props = tool.Blender.get_bim_props() + if not bim_props.ifc_file: row = layout.row() row.label(text="No IFC Found") return diff --git a/src/bonsai/bonsai/bim/module/bcf/operator.py b/src/bonsai/bonsai/bim/module/bcf/operator.py index bfcc40b8a9..0c145d98f8 100644 --- a/src/bonsai/bonsai/bim/module/bcf/operator.py +++ b/src/bonsai/bonsai/bim/module/bcf/operator.py @@ -43,7 +43,6 @@ import bonsai.tool as tool import bonsai.bim.module.bcf.prop as bcf_prop import bonsai.bim.module.bcf.bcfstore as bcfstore from pathlib import Path -from bonsai.bim.ifc import IfcStore from math import radians, degrees, atan, tan, cos, sin from mathutils import Vector, Matrix, Euler, geometry from xsdata.models.datatype import XmlDateTime @@ -1206,7 +1205,7 @@ class ActivateBcfViewpoint(bpy.types.Operator): return True def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml @@ -1316,7 +1315,7 @@ class ActivateBcfViewpoint(bpy.types.Operator): [0, 0, 0, 1], ) ) - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) matrix = ifcopenshell.util.geolocation.global2local( @@ -1355,7 +1354,7 @@ class ActivateBcfViewpoint(bpy.types.Operator): objs: list[bpy.types.Object] = [] for global_id in exception_global_ids: - obj = IfcStore.get_element(global_id) + obj = tool.Ifc.get_object_by_identifier(global_id) if obj and context.view_layer.objects.get(obj.name): assert isinstance(obj, bpy.types.Object) objs.append(obj) @@ -1414,7 +1413,7 @@ class ActivateBcfViewpoint(bpy.types.Operator): return bpy.ops.object.select_all(action="DESELECT") for global_id in selected_global_ids: - obj = IfcStore.get_element(global_id) + obj = tool.Ifc.get_object_by_identifier(global_id) if obj: obj.select_set(True) obj.hide_set(False) @@ -1427,7 +1426,7 @@ class ActivateBcfViewpoint(bpy.types.Operator): for acomponent in acoloring.component: global_id_colours.setdefault(acomponent.ifc_guid, acoloring.color) for global_id, color in global_id_colours.items(): - obj = IfcStore.get_element(global_id) + obj = tool.Ifc.get_object_by_identifier(global_id) if obj: obj.color = self.hex_to_rgb(color) diff --git a/src/bonsai/bonsai/bim/module/boundary/operator.py b/src/bonsai/bonsai/bim/module/boundary/operator.py index e3bc51683c..75cf32cb66 100644 --- a/src/bonsai/bonsai/bim/module/boundary/operator.py +++ b/src/bonsai/bonsai/bim/module/boundary/operator.py @@ -25,6 +25,7 @@ import mathutils import numpy as np import multiprocessing import ifcopenshell.api +import ifcopenshell.api.boundary import ifcopenshell.geom import ifcopenshell.util.unit import ifcopenshell.util.shape @@ -113,7 +114,7 @@ class Loader: bm.edges.new((verts[-1], verts[0])) bm.to_mesh(mesh) bm.free() - mesh.BIMMeshProperties.ifc_definition_id = surface.id() + tool.Ifc.link(surface, mesh) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) matrix = mathutils.Matrix( ifcopenshell.util.placement.get_axis2placement(surface.BasisSurface.Position).tolist() @@ -416,7 +417,7 @@ class UpdateBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): tool.Boundary.move_origin_to_space_origin(context.active_object) settings = tool.Boundary.get_assign_connection_geometry_settings(context.active_object) - ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings) + ifcopenshell.api.boundary.assign_connection_geometry(tool.Ifc.get(), **settings) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/boundary/ui.py b/src/bonsai/bonsai/bim/module/boundary/ui.py index e3c7d8936f..e7332d50c6 100644 --- a/src/bonsai/bonsai/bim/module/boundary/ui.py +++ b/src/bonsai/bonsai/bim/module/boundary/ui.py @@ -18,7 +18,6 @@ import bpy from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore import bonsai.tool as tool from bonsai.bim.module.boundary.data import SpaceBoundariesData @@ -34,7 +33,7 @@ class BIM_PT_SceneBoundaries(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): row = self.layout.row(align=True) @@ -58,9 +57,9 @@ class BIM_PT_Boundary(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - entity = IfcStore.get_file().by_id(props.ifc_definition_id) + entity = tool.Ifc.get().by_id(props.ifc_definition_id) return entity.is_a("IfcRelSpaceBoundary") def draw(self, context): @@ -134,7 +133,7 @@ class BIM_PT_SpaceBoundaries(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False element = tool.Ifc.get_entity(context.active_object) for ifc_class in ("IfcSpace", "IfcExternalSpatialElement"): diff --git a/src/bonsai/bonsai/bim/module/bsdd/ui.py b/src/bonsai/bonsai/bim/module/bsdd/ui.py index ac3e633d8a..046bc46fec 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/ui.py +++ b/src/bonsai/bonsai/bim/module/bsdd/ui.py @@ -18,7 +18,6 @@ import bonsai.tool as tool from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore class BIM_PT_bsdd(Panel): diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py index 854b34bb7d..4b70d04df1 100644 --- a/src/bonsai/bonsai/bim/module/cad/workspace.py +++ b/src/bonsai/bonsai/bim/module/cad/workspace.py @@ -69,10 +69,12 @@ class CadTool(WorkSpaceTool): ("bim.cad_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}), ) - def draw_settings(context, layout, workspace_tool): + def draw_settings( + context: bpy.types.Context, layout: bpy.types.UILayout, workspace_tool: bpy.types.WorkSpaceTool + ) -> None: ui_context = str(context.region.type) obj = context.active_object - if not obj or not obj.data: + if not obj or not (data := obj.data): return is_profile = tool.Geometry.is_profile_object_active() if is_profile: @@ -122,7 +124,10 @@ class CadTool(WorkSpaceTool): row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) add_layout_hotkey_operator(row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__, ui_context) - elif hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "AXIS": + elif ( + isinstance(data, tool.Geometry.TYPES_WITH_MESH_PROPERTIES) + and tool.Geometry.get_mesh_props(data).subshape_type == "AXIS" + ): add_header_apply_button( layout, "Edit Axis", "bim.edit_extrusion_axis", "bim.disable_editing_extrusion_axis", ui_context ) @@ -141,7 +146,7 @@ class CadTool(WorkSpaceTool): if ( (RailingData.is_loaded or not RailingData.load()) and RailingData.data["pset_data"] - and context.active_object.BIMRailingProperties.is_editing_path + and obj.BIMRailingProperties.is_editing_path ): add_header_apply_button( layout, @@ -154,7 +159,7 @@ class CadTool(WorkSpaceTool): elif ( (RoofData.is_loaded or not RoofData.load()) and RoofData.data["pset_data"] - and context.active_object.BIMRoofProperties.is_editing_path + and obj.BIMRoofProperties.is_editing_path ): add_header_apply_button( layout, "Edit Roof Path", "bim.finish_editing_roof_path", "bim.cancel_editing_roof_path", ui_context @@ -252,15 +257,27 @@ class CadHotkey(bpy.types.Operator): bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion) def hotkey_S_Q(self): - element = tool.Ifc.get_entity(bpy.context.active_object) - if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE": + obj = bpy.context.active_object + + if not obj: + return + + if not tool.Geometry.has_mesh_properties(data := obj.data): + return + + element = tool.Ifc.get_entity(obj) + if not element: + return + + mprops = tool.Geometry.get_mesh_props(data) + if mprops.subshape_type == "PROFILE": if element.is_a("IfcProfileDef"): bpy.ops.bim.edit_arbitrary_profile() elif element.is_a("IfcRelSpaceBoundary"): bpy.ops.bim.edit_boundary_geometry() else: bpy.ops.bim.edit_extrusion_profile() - elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS": + elif mprops.subshape_type == "AXIS": bpy.ops.bim.edit_extrusion_axis() def hotkey_S_R(self): diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index 020aeba55e..c3d87e4759 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -300,7 +300,7 @@ class SelectIfcClashResults(bpy.types.Operator): def execute(self, context): # TODO refactor into new clash results system - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.filepath = bpy.path.ensure_ext(self.filepath, ".json") with open(self.filepath) as f: clash_sets = json.load(f) @@ -321,8 +321,9 @@ class SelectIfcClashResults(bpy.types.Operator): ifc_file = "" for scene in obj.users_scene: - if scene.BIMProperties.ifc_file: - ifc_file = scene.BIMProperties.ifc_file + bim_props = tool.Blender.get_bim_props(scene) + if bim_props.ifc_file: + ifc_file = bim_props.ifc_file if scene.library: break @@ -478,7 +479,7 @@ class SelectSmartGroup(bpy.types.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group + return tool.Ifc.get() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group def execute(self, context): selected_smart_group = context.scene.BIMClashProperties.active_smart_group diff --git a/src/bonsai/bonsai/bim/module/classification/operator.py b/src/bonsai/bonsai/bim/module/classification/operator.py index 199fa2d61b..fa5e914880 100644 --- a/src/bonsai/bonsai/bim/module/classification/operator.py +++ b/src/bonsai/bonsai/bim/module/classification/operator.py @@ -201,7 +201,8 @@ class EnableEditingClassification(bpy.types.Operator): def execute(self, context): def callback(name, prop, data): if name == "ReferenceTokens": - new = bpy.context.scene.BIMGeoreferenceProperties.projected_crs.add() + geo_props = tool.Georeference.get_georeference_props() + new = geo_props.projected_crs.add() new.name = name new.data_type = "string" new.is_null = data[name] is None @@ -239,7 +240,7 @@ class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator): classification: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "classification.remove_classification", tool.Ifc.get(), diff --git a/src/bonsai/bonsai/bim/module/classification/ui.py b/src/bonsai/bonsai/bim/module/classification/ui.py index 87257fbaaa..c171ff3cd6 100644 --- a/src/bonsai/bonsai/bim/module/classification/ui.py +++ b/src/bonsai/bonsai/bim/module/classification/ui.py @@ -21,7 +21,6 @@ import bonsai.bim.helper import bonsai.tool as tool import bonsai.bim.module.classification.prop as classification_prop from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.classification.data import ( ClassificationsData, ClassificationReferencesData, @@ -124,7 +123,7 @@ class ReferenceUI: self.sprops = context.scene.BIMClassificationProperties self.bprops = context.scene.BIMBSDDProperties self.props = context.scene.BIMClassificationReferenceProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.draw_add_ui(context) diff --git a/src/bonsai/bonsai/bim/module/constraint/operator.py b/src/bonsai/bonsai/bim/module/constraint/operator.py index deed4d6335..3a66b074c7 100644 --- a/src/bonsai/bonsai/bim/module/constraint/operator.py +++ b/src/bonsai/bonsai/bim/module/constraint/operator.py @@ -23,7 +23,6 @@ import ifcopenshell.api.constraint import ifcopenshell.util.attribute import bonsai.bim.helper import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore class LoadObjectives(bpy.types.Operator): @@ -84,7 +83,7 @@ class AddObjective(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - result = ifcopenshell.api.run("constraint.add_objective", IfcStore.get_file()) + result = ifcopenshell.api.run("constraint.add_objective", tool.Ifc.get()) bpy.ops.bim.load_objectives() bpy.ops.bim.enable_editing_constraint(constraint=result.id()) return {"FINISHED"} @@ -116,7 +115,7 @@ class RemoveConstraint(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = context.scene.BIMConstraintProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "constraint.remove_constraint", self.file, **{"constraint": self.file.by_id(self.constraint)} ) diff --git a/src/bonsai/bonsai/bim/module/constraint/ui.py b/src/bonsai/bonsai/bim/module/constraint/ui.py index f7d530a757..76f4c4a1bb 100644 --- a/src/bonsai/bonsai/bim/module/constraint/ui.py +++ b/src/bonsai/bonsai/bim/module/constraint/ui.py @@ -16,8 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import bonsai.tool as tool from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import draw_attributes from bonsai.bim.module.constraint.data import ConstraintsData, ObjectConstraintsData @@ -33,7 +33,7 @@ class BIM_PT_constraints(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not ConstraintsData.is_loaded: @@ -81,7 +81,7 @@ class BIM_PT_object_constraints(Panel): def poll(cls, context): if not context.active_object: return False - if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id): return False return bool(context.active_object.BIMObjectProperties.ifc_definition_id) @@ -93,7 +93,7 @@ class BIM_PT_object_constraints(Panel): self.oprops = obj.BIMObjectProperties self.sprops = context.scene.BIMConstraintProperties self.props = obj.BIMObjectConstraintProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.draw_add_ui() diff --git a/src/bonsai/bonsai/bim/module/cost/prop.py b/src/bonsai/bonsai/bim/module/cost/prop.py index 092f429121..abe035fdb6 100644 --- a/src/bonsai/bonsai/bim/module/cost/prop.py +++ b/src/bonsai/bonsai/bim/module/cost/prop.py @@ -19,7 +19,6 @@ import bpy import ifcopenshell.api import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.classification.data import CostClassificationsData from bonsai.bim.module.cost.data import CostSchedulesData, CostItemRatesData, CostItemQuantitiesData from bonsai.bim.prop import StrProperty, Attribute @@ -83,7 +82,7 @@ def update_cost_item_identification(self, context): props = context.scene.BIMCostProperties if not props.is_cost_update_enabled or self.identification == "XXX": return - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "cost.edit_cost_item", self.file, @@ -98,7 +97,7 @@ def update_cost_item_name(self, context): props = context.scene.BIMCostProperties if not props.is_cost_update_enabled or self.name == "Unnamed": return - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "cost.edit_cost_item", self.file, diff --git a/src/bonsai/bonsai/bim/module/cost/ui.py b/src/bonsai/bonsai/bim/module/cost/ui.py index 67d14437ed..387998e9ac 100644 --- a/src/bonsai/bonsai/bim/module/cost/ui.py +++ b/src/bonsai/bonsai/bim/module/cost/ui.py @@ -21,7 +21,6 @@ import bonsai.bim.helper import bonsai.bim.module.cost.prop as CostProp import bonsai.tool as tool from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.cost.data import CostSchedulesData from typing import Any @@ -37,7 +36,7 @@ class BIM_PT_cost_schedules(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): diff --git a/src/bonsai/bonsai/bim/module/covetool/operator.py b/src/bonsai/bonsai/bim/module/covetool/operator.py index 0bafcafa4f..1d64dd958e 100644 --- a/src/bonsai/bonsai/bim/module/covetool/operator.py +++ b/src/bonsai/bonsai/bim/module/covetool/operator.py @@ -20,8 +20,8 @@ import bpy import json import ifcopenshell import ifcopenshell.util.element +import bonsai.tool as tool from math import degrees, atan2 -from bonsai.bim.ifc import IfcStore from .api import Api @@ -92,7 +92,7 @@ class RunAnalysis(bpy.types.Operator): bl_label = "Run Analysis" def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.inputs = { "floors": [], "walls": [], diff --git a/src/bonsai/bonsai/bim/module/csv/operator.py b/src/bonsai/bonsai/bim/module/csv/operator.py index 7d59bc5a92..cd9e784bf2 100644 --- a/src/bonsai/bonsai/bim/module/csv/operator.py +++ b/src/bonsai/bonsai/bim/module/csv/operator.py @@ -27,7 +27,6 @@ import ifcopenshell import ifcopenshell.util.selector import bonsai.tool as tool import bonsai.bim.module.drawing.scheduler as scheduler -from bonsai.bim.ifc import IfcStore from bonsai.bim.handler import refresh_ui_data from typing import TYPE_CHECKING from collections import Counter @@ -212,7 +211,7 @@ class ExportIfcCsv(bpy.types.Operator): props = context.scene.CsvProperties self.filepath = bpy.path.ensure_ext(self.filepath, f".{props.format}") if props.should_load_from_memory: - ifc_file = IfcStore.get_file() + ifc_file = tool.Ifc.get() else: ifc_file = ifcopenshell.open(props.csv_ifc_file) results = ifcopenshell.util.selector.filter_elements( @@ -314,7 +313,7 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator): props = context.scene.CsvProperties if props.should_load_from_memory: - ifc_file = IfcStore.get_file() + ifc_file = tool.Ifc.get() else: ifc_file = ifcopenshell.open(props.csv_ifc_file) ifc_csv = ifccsv.IfcCsv() diff --git a/src/bonsai/bonsai/bim/module/csv/ui.py b/src/bonsai/bonsai/bim/module/csv/ui.py index 0d4d6ef11e..6aa41177b0 100644 --- a/src/bonsai/bonsai/bim/module/csv/ui.py +++ b/src/bonsai/bonsai/bim/module/csv/ui.py @@ -17,8 +17,8 @@ # along with Bonsai. If not, see . import bonsai.bim.helper +import bonsai.tool as tool from bpy.types import Panel -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.search.data import SearchData @@ -37,7 +37,7 @@ class BIM_PT_ifccsv(Panel): scene = context.scene props = scene.CsvProperties - if IfcStore.get_file(): + if tool.Ifc.get(): row = layout.row(align=True) row.prop(props, "should_load_from_memory") row.operator("bim.import_csv_attributes", icon="IMPORT", text="") @@ -49,7 +49,7 @@ class BIM_PT_ifccsv(Panel): row.operator("bim.export_csv_attributes", icon="EXPORT", text="") row.prop(props, "should_show_settings", icon="PREFERENCES", text="") - if not IfcStore.get_file() or not props.should_load_from_memory: + if not tool.Ifc.get() or not props.should_load_from_memory: row = layout.row(align=True) row.prop(props, "csv_ifc_file") row.operator("bim.select_csv_ifc_file", icon="FILE_FOLDER", text="") diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 33100956ef..4b16c3fb12 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -78,10 +78,10 @@ class PrintIfcFile(bpy.types.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def execute(self, context): - print(IfcStore.get_file().wrapped_data.to_string()) + print(tool.Ifc.get().wrapped_data.to_string()) return {"FINISHED"} @@ -101,13 +101,14 @@ class ConvertToBlender(bpy.types.Operator): if tool.Geometry.has_mesh_properties(data): if data.library: continue - data.BIMMeshProperties.ifc_definition_id = 0 + tool.Geometry.get_mesh_props(data).ifc_definition_id = 0 for material in bpy.data.materials: if material.library: continue tool.Ifc.unlink(obj=material) - context.scene.BIMProperties.ifc_file = "" - context.scene.BIMDebugProperties.attributes.clear() + bim_props = tool.Blender.get_bim_props() + bim_props.ifc_file = "" + tool.Debug.get_debug_props().attributes.clear() IfcStore.purge() bonsai.bim.handler.refresh_ui_data() return {"FINISHED"} @@ -160,7 +161,8 @@ class ProfileImportIFC(bpy.types.Operator): if not tool.Ifc.get(): cls.poll_message_set("No IFC file loaded.") return False - if not context.scene.BIMProperties.ifc_file: + bim_props = tool.Blender.get_bim_props() + if not bim_props.ifc_file: cls.poll_message_set("Current IFC file is not saved.") return False return True @@ -184,10 +186,10 @@ class CreateAllShapes(bpy.types.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() elements = self.file.by_type("IfcElement") + self.file.by_type("IfcSpace") total = len(elements) @@ -256,7 +258,7 @@ class CreateShapeFromStepId(bpy.types.Operator): logger = logging.getLogger("ImportIFC") self.ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) self.file = tool.Ifc.get() - element = self.file.by_id(self.step_id or int(context.scene.BIMDebugProperties.step_id)) + element = self.file.by_id(self.step_id or int(tool.Debug.get_debug_props().step_id)) settings = ifcopenshell.geom.settings() settings.set("keep-bounding-boxes", True) if self.should_include_curves: @@ -309,7 +311,7 @@ class RewindInspector(bpy.types.Operator): bl_description = "Rewind the Inspector to the previously inspected element" def execute(self, context): - props = context.scene.BIMDebugProperties + props = tool.Debug.get_debug_props() total_breadcrumbs = len(props.step_id_breadcrumb) if total_breadcrumbs < 2: return {"FINISHED"} @@ -328,13 +330,13 @@ class InspectFromStepId(bpy.types.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def execute(self, context): - self.file = IfcStore.get_file() - debug_props = context.scene.BIMDebugProperties + self.file = tool.Ifc.get() + debug_props = tool.Debug.get_debug_props() debug_props.active_step_id = self.step_id - crumb = context.scene.BIMDebugProperties.step_id_breadcrumb.add() + crumb = debug_props.step_id_breadcrumb.add() crumb.name = str(self.step_id) element = self.file.by_id(self.step_id) debug_props.attributes.clear() @@ -385,7 +387,7 @@ class InspectFromObject(bpy.types.Operator): if ( (data := obj.data) and tool.Geometry.has_mesh_properties(data) - and (ifc_id := data.BIMMeshProperties.ifc_definition_id) + and (ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id) ): return ifc_id @@ -422,7 +424,7 @@ class PrintObjectPlacement(bpy.types.Operator): return self.execute(context) def execute(self, context): - placement = ifcopenshell.util.placement.get_local_placement(IfcStore.get_file().by_id(self.step_id)) + placement = ifcopenshell.util.placement.get_local_placement(tool.Ifc.get().by_id(self.step_id)) if self.create_empty_object: bpy.ops.object.empty_add(type="ARROWS") si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) @@ -438,7 +440,8 @@ class ParseExpress(bpy.types.Operator): bl_label = "Parse Express" def execute(self, context): - core.parse_express(tool.Debug, context.scene.BIMDebugProperties.express_file) + props = tool.Debug.get_debug_props() + core.parse_express(tool.Debug, props.express_file) bonsai.bim.handler.refresh_ui_data() return {"FINISHED"} @@ -452,8 +455,9 @@ class SelectExpressFile(bpy.types.Operator): filter_glob: bpy.props.StringProperty(default="*.exp", options={"HIDDEN"}) def execute(self, context): + props = tool.Debug.get_debug_props() if os.path.exists(self.filepath) and "exp" in os.path.splitext(self.filepath)[1]: - context.scene.BIMDebugProperties.express_file = self.filepath + props.express_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -497,7 +501,7 @@ class PrintUnusedElementStats(bpy.types.Operator): ignore_styled_items: bpy.props.BoolProperty(name="Ignore Styled Items", default=True) def execute(self, context): - props = context.scene.BIMDebugProperties + props = tool.Debug.get_debug_props() # ignore some classes that could have zero 0 inverse references by their nature ignore_classes = [] if self.ignore_contexts: @@ -543,7 +547,7 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator): return True def _execute(self, context): - props = context.scene.BIMDebugProperties + props = tool.Debug.get_debug_props() if props.ifc_class_purge: purged_elements = core.purge_unused_elements(tool.Ifc, tool.Debug, props.ifc_class_purge) self.report({"INFO"}, f"{purged_elements} unused elements found and removed.") @@ -803,7 +807,7 @@ class DebugActiveDrawing(bpy.types.Operator): ) def execute(self, context: bpy.types.Context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() drawing_item = props.drawings[props.active_drawing_index] drawing = tool.Ifc.get().by_id(drawing_item.ifc_definition_id) diff --git a/src/bonsai/bonsai/bim/module/debug/prop.py b/src/bonsai/bonsai/bim/module/debug/prop.py index 01076ccde3..5a28b22f79 100644 --- a/src/bonsai/bonsai/bim/module/debug/prop.py +++ b/src/bonsai/bonsai/bim/module/debug/prop.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import bpy from bonsai.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -28,6 +29,9 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING, Literal, get_args + +DisplayType = Literal["BOUNDS", "WIRE", "SOLID", "TEXTURED"] class BIMDebugProperties(PropertyGroup): @@ -41,14 +45,23 @@ class BIMDebugProperties(PropertyGroup): inverse_references: CollectionProperty(name="Inverse References", type=Attribute) express_file: StringProperty(name="Express File") display_type: EnumProperty( - items=[ - ("BOUNDS", "Bounds", ""), - ("WIRE", "Wire", ""), - ("SOLID", "Solid", ""), - ("TEXTURED", "Textured", ""), - ], + items=[(display_type, display_type.capitalize(), "") for display_type in get_args(DisplayType)], name="Display Type", default="BOUNDS", ) ifc_class_purge: StringProperty(name="Unused Elements IFC Class", default="") package_name: StringProperty(name="Package Name", default="") + + if TYPE_CHECKING: + step_id: int + number_of_polygons: int + percentile_of_polygons: int + active_step_id: int + step_id_breadcrumb: bpy.types.bpy_prop_collection_idprop[StrProperty] + attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + inverse_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + inverse_references: bpy.types.bpy_prop_collection_idprop[Attribute] + express_file: str + display_type: str + ifc_class_purge: str + package_name: str diff --git a/src/bonsai/bonsai/bim/module/debug/ui.py b/src/bonsai/bonsai/bim/module/debug/ui.py index 3611920095..c2c43bd4bb 100644 --- a/src/bonsai/bonsai/bim/module/debug/ui.py +++ b/src/bonsai/bonsai/bim/module/debug/ui.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool from bpy.types import Panel @@ -32,10 +33,11 @@ class BIM_PT_debug(Panel): def draw(self, context): layout = self.layout - props = context.scene.BIMDebugProperties + props = tool.Debug.get_debug_props() + bim_props = tool.Blender.get_bim_props() row = self.layout.row(align=True) - row.prop(context.scene.BIMProperties, "ifc_file", text="") + row.prop(bim_props, "ifc_file", text="") row.operator("bim.validate_ifc_file", icon="CHECKMARK", text="") row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="") @@ -87,27 +89,25 @@ class BIM_PT_debug(Panel): row.prop(props, "step_id", text="") row = layout.split(factor=0.7, align=True) - row.operator("bim.select_high_polygon_meshes").threshold = context.scene.BIMDebugProperties.number_of_polygons + row.operator("bim.select_high_polygon_meshes").threshold = props.number_of_polygons row.prop(props, "number_of_polygons", text="") row = layout.split(factor=0.7, align=True) - row.operator("bim.select_highest_polygon_meshes").percentile = ( - context.scene.BIMDebugProperties.percentile_of_polygons - ) + row.operator("bim.select_highest_polygon_meshes").percentile = props.percentile_of_polygons row.prop(props, "percentile_of_polygons", text="") row = layout.split(factor=0.5, align=True) row.prop(props, "display_type", text="") - row.operator("bim.override_display_type").display = context.scene.BIMDebugProperties.display_type + row.operator("bim.override_display_type").display = props.display_type layout.operator("bim.purge_unused_representations") row = layout.row(align=True) - row.prop(context.scene.BIMDebugProperties, "ifc_class_purge", text="") + row.prop(props, "ifc_class_purge", text="") row.operator("bim.purge_unused_elements_by_class", text="Purge Orphaned", icon="TRASH") row.operator("bim.print_unused_elements_stats", text="", icon="INFO") - if context.active_object and context.active_object.data: - mprops = context.active_object.data.BIMMeshProperties + if context.active_object and (data := context.active_object.data): + mprops = tool.Geometry.get_mesh_props(data) row = layout.row() row.operator("bim.get_representation_ifc_parameters") for index, ifc_parameter in enumerate(mprops.ifc_parameters): @@ -123,7 +123,7 @@ class BIM_PT_debug(Panel): row.operator("bim.rewind_inspector", icon="FRAME_PREV", text="") row.prop(props, "active_step_id", text="") row = layout.row(align=True) - row.operator("bim.inspect_from_step_id").step_id = context.scene.BIMDebugProperties.active_step_id + row.operator("bim.inspect_from_step_id").step_id = props.active_step_id row.operator("bim.inspect_from_object") if props.attributes: diff --git a/src/bonsai/bonsai/bim/module/diff/operator.py b/src/bonsai/bonsai/bim/module/diff/operator.py index ead5dd6e04..dc9e981b19 100644 --- a/src/bonsai/bonsai/bim/module/diff/operator.py +++ b/src/bonsai/bonsai/bim/module/diff/operator.py @@ -70,8 +70,9 @@ class VisualiseDiff(bpy.types.Operator): ifc_file = "" for scene in obj.users_scene: - if scene.BIMProperties.ifc_file: - ifc_file = scene.BIMProperties.ifc_file + bim_props = tool.Blender.get_bim_props(scene) + if bim_props.ifc_file: + ifc_file = bim_props.ifc_file if scene.library: break @@ -257,8 +258,9 @@ class SelectDiffObjects(bpy.types.Operator): ifc_file = "" for scene in obj.users_scene: - if scene.BIMProperties.ifc_file: - ifc_file = scene.BIMProperties.ifc_file + bim_props = tool.Blender.get_bim_props(scene) + if bim_props.ifc_file: + ifc_file = bim_props.ifc_file if scene.library: break diff --git a/src/bonsai/bonsai/bim/module/diff/ui.py b/src/bonsai/bonsai/bim/module/diff/ui.py index b147f884d1..34e66df0f5 100644 --- a/src/bonsai/bonsai/bim/module/diff/ui.py +++ b/src/bonsai/bonsai/bim/module/diff/ui.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . from bpy.types import Panel -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.diff.data import DiffData import bonsai.bim.helper import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/document/data.py b/src/bonsai/bonsai/bim/module/document/data.py index 0a75605b5c..5cde82e499 100644 --- a/src/bonsai/bonsai/bim/module/document/data.py +++ b/src/bonsai/bonsai/bim/module/document/data.py @@ -52,7 +52,7 @@ class DocumentData: @classmethod def parent_document(cls): - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() if len(props.breadcrumbs): parent = tool.Ifc.get().by_id(int(props.breadcrumbs[-1].name)) if tool.Ifc.get_schema() == "IFC2X3": diff --git a/src/bonsai/bonsai/bim/module/document/operator.py b/src/bonsai/bonsai/bim/module/document/operator.py index 70c3401fb0..2ed1a0a8d5 100644 --- a/src/bonsai/bonsai/bim/module/document/operator.py +++ b/src/bonsai/bonsai/bim/module/document/operator.py @@ -24,7 +24,6 @@ import ifcopenshell.util.element import bonsai.bim.handler import bonsai.tool as tool import bonsai.core.document as core -from bonsai.bim.ifc import IfcStore class LoadProjectDocuments(bpy.types.Operator): @@ -116,7 +115,7 @@ class EditDocument(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() core.edit_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(props.active_document_id)) diff --git a/src/bonsai/bonsai/bim/module/document/prop.py b/src/bonsai/bonsai/bim/module/document/prop.py index 2f8d32408f..ef4fb29ab9 100644 --- a/src/bonsai/bonsai/bim/module/document/prop.py +++ b/src/bonsai/bonsai/bim/module/document/prop.py @@ -30,6 +30,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING def update_document_name(self: "Document", context: bpy.types.Context) -> None: @@ -57,6 +58,11 @@ class Document(PropertyGroup): ) ifc_definition_id: IntProperty(name="IFC Definition ID") + if TYPE_CHECKING: + identification: str + is_information: bool + ifc_definition_id: int + class BIMDocumentProperties(PropertyGroup): document_attributes: CollectionProperty(name="Document Attributes", type=Attribute) @@ -65,3 +71,11 @@ class BIMDocumentProperties(PropertyGroup): breadcrumbs: CollectionProperty(name="Breadcrumbs", type=StrProperty) active_document_index: IntProperty(name="Active Document Index") is_editing: BoolProperty(name="Is Editing", default=False) + + if TYPE_CHECKING: + document_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + active_document_id: int + documents: bpy.types.bpy_prop_collection_idprop[Document] + breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty] + active_document_index: int + is_editing: bool diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index d2cf2e7ab6..4bd935c8f6 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -16,8 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import bonsai.tool as tool from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import draw_attributes from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData @@ -33,13 +33,13 @@ class BIM_PT_documents(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not DocumentData.is_loaded: DocumentData.load() - self.props = context.scene.BIMDocumentProperties + self.props = tool.Document.get_document_props() row = self.layout.row(align=True) row.label(text="{} Documents Found".format(DocumentData.data["total_information"]), icon="FILE") @@ -92,7 +92,7 @@ class BIM_PT_object_documents(Panel): def poll(cls, context): if not context.active_object: return False - if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id): return False return bool(context.active_object.BIMObjectProperties.ifc_definition_id) @@ -102,8 +102,8 @@ class BIM_PT_object_documents(Panel): obj = context.active_object self.oprops = obj.BIMObjectProperties - self.props = context.scene.BIMDocumentProperties - self.file = IfcStore.get_file() + self.props = tool.Document.get_document_props() + self.file = tool.Ifc.get() self.draw_add_ui() diff --git a/src/bonsai/bonsai/bim/module/drawing/annotation.py b/src/bonsai/bonsai/bim/module/drawing/annotation.py index 8bc9ee46d5..9f02989384 100644 --- a/src/bonsai/bonsai/bim/module/drawing/annotation.py +++ b/src/bonsai/bonsai/bim/module/drawing/annotation.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import os import bpy import math @@ -125,7 +126,9 @@ class Annotator: return obj @staticmethod - def get_annotation_obj(drawing: ifcopenshell.entity_instance, object_type: str, data_type: str) -> bpy.types.Object: + def get_annotation_obj( + drawing: ifcopenshell.entity_instance, object_type: str, data_type: tool.Drawing.ANNOTATION_DATA_TYPE + ) -> bpy.types.Object: camera = tool.Ifc.get_object(drawing) co1, _, _, _ = Annotator.get_placeholder_coords(camera) matrix_world = tool.Drawing.get_camera_matrix(camera) @@ -147,11 +150,17 @@ class Annotator: collection.objects.link(obj) return obj - if object_type != "ANGLE": - for obj in collection.objects: - element = tool.Ifc.get_entity(obj) - if element and element.ObjectType == object_type and obj.type == object_type.upper(): - return obj + # TODO: remove as outdated? + # Is reusing the same objects preventing the creation of new annotations. + # if object_type != "ANGLE": + # for obj in collection.objects: + # element = tool.Ifc.get_entity(obj) + # if ( + # element + # and ifcopenshell.util.element.get_predefined_type(element) == object_type + # and obj.type == data_type.upper() + # ): + # return obj if data_type == "mesh": data = bpy.data.meshes.new(object_type) diff --git a/src/bonsai/bonsai/bim/module/drawing/data.py b/src/bonsai/bonsai/bim/module/drawing/data.py index be252a29c9..30fda54828 100644 --- a/src/bonsai/bonsai/bim/module/drawing/data.py +++ b/src/bonsai/bonsai/bim/module/drawing/data.py @@ -88,7 +88,8 @@ class SheetsData: project = tool.Ifc.get().by_type("IfcProject")[0] titleblocks_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir") if not titleblocks_dir: - titleblocks_dir = bpy.context.scene.DocProperties.titleblocks_dir + props = tool.Drawing.get_document_props() + titleblocks_dir = props.titleblocks_dir titleblocks_dir = tool.Ifc.resolve_uri(titleblocks_dir) if os.path.exists(titleblocks_dir): files.extend([str(f.stem) for f in Path(titleblocks_dir).glob("*.svg")]) @@ -120,23 +121,25 @@ class DrawingsData: @classmethod def location_hint(cls): - if bpy.context.scene.DocProperties.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]: + props = tool.Drawing.get_document_props() + if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]: results = [("0", "Origin", "")] results.extend( [(str(s.id()), s.Name or "Unnamed", "") for s in tool.Ifc.get().by_type("IfcBuildingStorey")] ) return results - elif bpy.context.scene.DocProperties.target_view in ["MODEL_VIEW"]: + elif props.target_view in ["MODEL_VIEW"]: return [(h.upper(), h, "") for h in ["Orthographic", "Perspective"]] return [(h.upper(), h, "") for h in ["North", "South", "East", "West"]] @classmethod def active_drawing_pset_data(cls): ifc_file = tool.Ifc.get() - drawing_id = bpy.context.scene.DocProperties.active_drawing_id + props = tool.Drawing.get_document_props() + drawing_id = props.active_drawing_id if drawing_id == 0: return {} - drawing = ifc_file.by_id(bpy.context.scene.DocProperties.active_drawing_id) + drawing = ifc_file.by_id(drawing_id) return ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing") @@ -361,7 +364,11 @@ class DecoratorData: element = tool.Ifc.get_entity(obj) supported_object_types = ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS") - if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in supported_object_types: + if ( + not element + or not element.is_a("IfcAnnotation") + or ifcopenshell.util.element.get_predefined_type(element) not in supported_object_types + ): return None dimension_style = "arrow" diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index e8fba1914d..9c26c2b1c5 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -423,7 +423,8 @@ class BaseDecorator: # font_size = 16 <-- this is a good default # TODO: need to synchronize it better with svg - magic_font_scale = bpy.context.scene.DocProperties.magic_font_scale + props = tool.Drawing.get_document_props() + magic_font_scale = props.magic_font_scale font_size_px = int(magic_font_scale * mm_to_px) * font_size_mm / 2.5 pos = pos - line_no * font_size_px * rotation_matrix[1] @@ -976,6 +977,7 @@ class FallDecorator(BaseDecorator): # same function as in svgwriter.py def get_label_text(): element = tool.Ifc.get_entity(obj) + assert element B, A = [v.co.xyz for v in spline_points[:2]] rise = abs(A.z - B.z) O = A.copy() @@ -988,13 +990,14 @@ class FallDecorator(BaseDecorator): angle = 90 # ues SLOPE_ANGLE as default - if element.ObjectType in ("FALL", "SLOPE_ANGLE"): + object_type = ifcopenshell.util.element.get_predefined_type(element) + if object_type in ("FALL", "SLOPE_ANGLE"): return f"{angle}°" - elif element.ObjectType == "SLOPE_FRACTION": + elif object_type == "SLOPE_FRACTION": if angle == 90: return "-" return f"{self.format_value(context, rise)} / {self.format_value(context, run)}" - elif element.ObjectType == "SLOPE_PERCENT": + elif object_type == "SLOPE_PERCENT": if angle == 90: return "-" return f"{round(angle_tg * 100)} %" @@ -2022,7 +2025,8 @@ class DecorationsHandler: for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"): self.decorators[object_type] = self.decorators["FALL"] self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"] - if drawing_font := bpy.context.scene.DocProperties.drawing_font: + props = tool.Drawing.get_document_props() + if drawing_font := props.drawing_font: drawing_font_path = tool.Blender.get_data_dir_path(Path("fonts") / drawing_font) if drawing_font_path.is_file(): font_id = blf.load(drawing_font_path.__str__()) @@ -2045,7 +2049,7 @@ class DecorationsHandler: if not element.is_a("IfcAnnotation"): continue - object_type: Union[str, None] = element.ObjectType + object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element) if object_type == "DRAWING": continue diff --git a/src/bonsai/bonsai/bim/module/drawing/gizmos.py b/src/bonsai/bonsai/bim/module/drawing/gizmos.py index 8e4f9bb97e..952ea0cb96 100644 --- a/src/bonsai/bonsai/bim/module/drawing/gizmos.py +++ b/src/bonsai/bonsai/bim/module/drawing/gizmos.py @@ -19,6 +19,7 @@ import bpy import blf import gpu +import bonsai.tool as tool from bpy import types from mathutils import Vector from mathutils import geometry @@ -478,21 +479,25 @@ class ExtrusionWidget(types.GizmoGroup): bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"} @classmethod - def poll(cls, ctx): - obj = ctx.object + def poll(cls, context): + obj = context.active_object return ( obj - and obj.type == "MESH" - and obj.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth") is not None + and (data := obj.data) + and isinstance(data, bpy.types.Mesh) + and tool.Geometry.get_mesh_props(data).ifc_parameters.get("IfcExtrudedAreaSolid/Depth") is not None ) - def setup(self, ctx): - target = ctx.object - prop = target.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth") + def setup(self, context: bpy.types.Context) -> None: + target = context.object + assert target + mesh = target.data + assert isinstance(mesh, bpy.types.Mesh) + prop = tool.Geometry.get_mesh_props(mesh).ifc_parameters.get("IfcExtrudedAreaSolid/Depth") basis = target.matrix_world.normalized() - theme = ctx.preferences.themes[0].user_interface - scale_value = self.get_scale_value(ctx.scene.unit_settings.system, ctx.scene.unit_settings.length_unit) + theme = context.preferences.themes[0].user_interface + scale_value = self.get_scale_value(context.scene.unit_settings.system, context.scene.unit_settings.length_unit) # setup handle gz = self.handle = self.gizmos.new("BIM_GT_uglydot_3d") @@ -521,23 +526,26 @@ class ExtrusionWidget(types.GizmoGroup): # gz.use_draw_modal = True # gz.target_set_prop('value', target.demo, 'depth') - def refresh(self, ctx): + def refresh(self, context: bpy.types.Context) -> None: """updating gizmos""" - target = ctx.object + target = context.active_object basis = target.matrix_world.normalized() self.handle.matrix_basis = basis self.guides.matrix_basis = basis - def update(self, ctx): + def update(self, context: bpy.types.Context) -> None: """updating object""" bpy.ops.bim.update_parametric_representation() - target = ctx.object - prop = target.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth") + target = context.active_object + assert target + mesh = target.data + assert isinstance(mesh, bpy.types.Mesh) + prop = tool.Geometry.get_mesh_props(mesh).ifc_parameters.get("IfcExtrudedAreaSolid/Depth") self.handle.target_set_prop("offset", prop, "value") self.guides.target_set_prop("depth", prop, "value") @staticmethod - def get_scale_value(system, length_unit): + def get_scale_value(system: str, length_unit: str) -> float: scale_value = 1 if system == "METRIC": if length_unit == "KILOMETERS": diff --git a/src/bonsai/bonsai/bim/module/drawing/handler.py b/src/bonsai/bonsai/bim/module/drawing/handler.py index abfc8bb261..d1622d1908 100644 --- a/src/bonsai/bonsai/bim/module/drawing/handler.py +++ b/src/bonsai/bonsai/bim/module/drawing/handler.py @@ -24,7 +24,8 @@ from bpy.app.handlers import persistent @persistent def load_post(*args): - if bpy.context.scene.DocProperties.should_draw_decorations: + props = tool.Drawing.get_document_props() + if props.should_draw_decorations: decoration.DecorationsHandler.install(bpy.context) else: decoration.DecorationsHandler.uninstall() @@ -35,9 +36,11 @@ def depsgraph_update_pre_handler(scene): set_active_camera_resolution(scene) -def set_active_camera_resolution(scene): - if not scene.camera or "/" not in scene.camera.name or not scene.DocProperties.drawings: +def set_active_camera_resolution(scene: bpy.types.Scene) -> None: + props = tool.Drawing.get_document_props() + if not scene.camera or "/" not in scene.camera.name or not props.drawings: return + assert isinstance(scene.camera.data, bpy.types.Camera) props = scene.camera.data.BIMCameraProperties ortho_scale = max((props.width, props.height)) aspect_ratio = props.width / props.height @@ -60,5 +63,3 @@ def set_active_camera_resolution(scene): scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x = int(raster_x) scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y = int(raster_y) - - current_drawing = scene.DocProperties.drawings[scene.DocProperties.current_drawing_index] diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index 5bd1661bfa..c63fd3d910 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -19,8 +19,10 @@ import bpy import math import mathutils.geometry +import ifcopenshell import bonsai.tool as tool from mathutils import Vector +from typing import Union # Code taken and updated from https://blenderartists.org/t/detecting-intersection-of-bounding-boxes/457520/2 @@ -135,6 +137,9 @@ def format_distance( scaleFactor = bpy.context.scene.unit_settings.scale_length unit_system = bpy.context.scene.unit_settings.system unit_length = bpy.context.scene.unit_settings.length_unit + area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol( + ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT") + ) value *= scaleFactor @@ -225,7 +230,24 @@ def format_distance( if add_inches or frac: tx_dist += '"' else: - tx_dist = str("%1.3f" % (value * toInches / inPerFoot)) + " sq. ft." + fmt = "%1.3f" + sq_feet = round(value * toInches / inPerFoot, 4) + tx_dist = "" + if area_unit_symbol == " ft2": + fmt += area_unit_symbol + tx_dist = fmt % sq_feet + if area_unit_symbol == " in2": + sq_inch = sq_feet * 144 + fmt += area_unit_symbol + tx_dist = fmt % sq_inch + if area_unit_symbol == " yd2": + sq_yard = sq_feet / 9 + fmt += area_unit_symbol + tx_dist = fmt % sq_yard + if area_unit_symbol == " mi2": + sq_mile = sq_feet / 27878400 + fmt += area_unit_symbol + tx_dist = fmt % sq_mile # METRIC FORMATTING elif unit_system == "METRIC": @@ -287,16 +309,38 @@ def format_distance( d_mm = value * (1000) tx_dist = fmt % d_mm if isArea: - tx_dist += s_code + if area_unit_symbol == " m2": + if decimal_places is None: + fmt = "%1.3f" + if hide_units is False: + fmt += area_unit_symbol + tx_dist = fmt % value + if area_unit_symbol == " cm2": + if decimal_places is None: + fmt = "%1.1f" + if hide_units is False: + fmt += area_unit_symbol + d_cm = value * (10000) + tx_dist = fmt % d_cm + if area_unit_symbol == " mm2": + if decimal_places is None: + fmt = "%1.0f" + if hide_units is False: + fmt += area_unit_symbol + d_cm = value * (1000000) + tx_dist = fmt % d_cm + else: tx_dist = fmt % value return tx_dist -def get_active_drawing(scene): +def get_active_drawing( + scene: bpy.types.Scene, +) -> Union[tuple[bpy.types.Collection, bpy.types.Camera], tuple[None, None]]: """Get active drawing collection and camera""" - props = scene.DocProperties + props = tool.Drawing.get_document_props() try: camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id)) return camera.BIMObjectProperties.collection, camera diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 950d701e4a..3528e4dc36 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -134,19 +134,19 @@ class AddDrawing(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Add a drawing view to the IFC project" def _execute(self, context): - self.props = context.scene.DocProperties - hint = self.props.location_hint - if self.props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]: + props = tool.Drawing.get_document_props() + hint = props.location_hint + if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]: hint = int(hint) core.add_drawing( tool.Ifc, tool.Collector, tool.Drawing, - target_view=self.props.target_view, + target_view=props.target_view, location_hint=hint, ) try: - drawing = tool.Ifc.get().by_id(self.props.active_drawing_id) + drawing = tool.Ifc.get().by_id(props.active_drawing_id) core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing) except: pass @@ -162,7 +162,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") return False @@ -176,7 +176,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator): row.prop(self, "should_duplicate_annotations") def _execute(self, context): - self.props = context.scene.DocProperties + props = tool.Drawing.get_document_props() core.duplicate_drawing( tool.Ifc, tool.Drawing, @@ -184,7 +184,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator): should_duplicate_annotations=self.should_duplicate_annotations, ) try: - drawing = tool.Ifc.get().by_id(self.props.active_drawing_id) + drawing = tool.Ifc.get().by_id(props.active_drawing_id) core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing) except: pass @@ -244,7 +244,7 @@ class CreateDrawing(bpy.types.Operator): return self.execute(context) def execute(self, context): - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() active_drawing_id = context.scene.camera.BIMObjectProperties.ifc_definition_id if self.print_all: @@ -261,7 +261,7 @@ class CreateDrawing(bpy.types.Operator): self.camera = context.scene.camera self.camera_element = tool.Ifc.get_entity(self.camera) self.camera_document = tool.Drawing.get_drawing_document(self.camera_element) - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() with profile("Drawing generation process"): with profile("Initialize drawing generation process"): @@ -385,7 +385,7 @@ class CreateDrawing(bpy.types.Operator): obj.hide_render = obj.name not in visible_object_names context.scene.render.filepath = str(Path(svg_path).with_suffix(".png")) - drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index] + drawing_style = self.props.drawing_styles[self.cprops.active_drawing_style_index] if drawing_style.render_type == "DEFAULT": bpy.ops.render.render(write_still=True) @@ -712,9 +712,11 @@ class CreateDrawing(bpy.types.Operator): } cached_linework -= edited_guids - files = {context.scene.BIMProperties.ifc_file: tool.Ifc.get()} + bim_props = tool.Blender.get_bim_props() + files = {bim_props.ifc_file: tool.Ifc.get()} - for link in context.scene.BIMProjectProperties.links: + props = tool.Project.get_project_props() + for link in props.links: if link.name not in IfcStore.session_files: IfcStore.session_files[link.name] = ifcopenshell.open(link.name) files[link.name] = IfcStore.session_files[link.name] @@ -729,7 +731,7 @@ class CreateDrawing(bpy.types.Operator): # Don't use draw.main() just whilst we're prototyping and experimenting # TODO: hash paths are never used ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest() - ifc_cache_path = os.path.join(context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5") + ifc_cache_path = os.path.join(bim_props.cache_dir, f"{ifc_hash}.h5") self.serialiser.setFile(ifc) drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc) @@ -1141,7 +1143,8 @@ class CreateDrawing(bpy.types.Operator): try: return tool.Ifc.get().by_guid(guid) except: - for link in bpy.context.scene.BIMProjectProperties.links: + props = tool.Project.get_project_props() + for link in props.links: if link.name not in IfcStore.session_files: IfcStore.session_files[link.name] = ifcopenshell.open(link.name) try: @@ -1367,7 +1370,11 @@ class CreateDrawing(bpy.types.Operator): elements = list(elements | filtered_drawing_annotations) annotations = sorted( - elements, key=lambda a: (tool.Drawing.get_annotation_z_index(a), 1 if a.ObjectType == "TEXT" else 0) + elements, + key=lambda a: ( + tool.Drawing.get_annotation_z_index(a), + 1 if ifcopenshell.util.element.get_predefined_type(a) == "TEXT" else 0, + ), ) precision = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "MetricPrecision") @@ -1458,7 +1465,8 @@ class AddSheet(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Add a sheet to the project" def _execute(self, context): - core.add_sheet(tool.Ifc, tool.Drawing, titleblock=context.scene.DocProperties.titleblock) + props = tool.Drawing.get_document_props() + core.add_sheet(tool.Ifc, tool.Drawing, titleblock=props.titleblock) class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator): @@ -1474,7 +1482,7 @@ class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator): cls.poll_message_set("Not implemented yet.") return False - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") return False @@ -1483,7 +1491,7 @@ class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): pass """ - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() core.duplicate_sheet( tool.Ifc, tool.Drawing, @@ -1508,7 +1516,7 @@ class OpenLayout(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id) sheet_builder = sheeter.SheetBuilder() sheet_builder.update_sheet_drawing_sizes(sheet) @@ -1530,7 +1538,8 @@ class SelectAllSheets(bpy.types.Operator): return self.execute(context) def execute(self, context): - for sheet in context.scene.DocProperties.sheets: + props = tool.Drawing.get_document_props() + for sheet in props.sheets: if sheet.is_selected != self.select_all: sheet.is_selected = self.select_all return {"FINISHED"} @@ -1550,7 +1559,7 @@ class OpenSheet(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_sheet_item(is_sheet=True): cls.poll_message_set("No sheet selected.") return False @@ -1564,7 +1573,7 @@ class OpenSheet(bpy.types.Operator, tool.Ifc.Operator): return self.execute(context) def execute(self, context): - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command if self.open_all: @@ -1612,9 +1621,10 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() # Won't be visible in UI anyway. - if not props.sheets or not context.scene.BIMProperties.data_dir: + bim_props = tool.Blender.get_bim_props() + if not props.sheets or not bim_props.data_dir: return False if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") @@ -1622,8 +1632,8 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator): return True def _execute(self, context): - props = context.scene.DocProperties - active_drawing = tool.Drawing.get_active_drawing_item() + props = tool.Drawing.get_document_props() + active_drawing = props.drawings[props.active_drawing_index] assert active_drawing active_sheet = tool.Drawing.get_active_sheet(context) @@ -1680,7 +1690,7 @@ class RemoveDrawingFromSheet(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() active_item = tool.Drawing.get_active_sheet_item() if active_item is None: return False @@ -1714,11 +1724,12 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_sheet_item(is_sheet=True): cls.poll_message_set("No sheet selected.") return False - return props.sheets and context.scene.BIMProperties.data_dir + bim_props = tool.Blender.get_bim_props() + return props.sheets and bim_props.data_dir def invoke(self, context, event): # opening all sheets on shift+click @@ -1731,7 +1742,7 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): scene = context.scene - props = scene.DocProperties + props = tool.Drawing.get_document_props() svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command svg2dxf_command = tool.Blender.get_addon_preferences().svg2dxf_command @@ -1838,7 +1849,8 @@ class SelectAllDrawings(bpy.types.Operator): return self.execute(context) def execute(self, context): - for drawing in context.scene.DocProperties.drawings: + props = tool.Drawing.get_document_props() + for drawing in props.drawings: if drawing.is_selected != self.select_all: drawing.is_selected = self.select_all return {"FINISHED"} @@ -1857,7 +1869,7 @@ class OpenDrawing(bpy.types.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") return False @@ -1871,7 +1883,7 @@ class OpenDrawing(bpy.types.Operator): return self.execute(context) def execute(self, context): - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() if self.open_all: drawings = [ tool.Ifc.get().by_id(d.ifc_definition_id) for d in self.props.drawings if d.is_drawing and d.is_selected @@ -1907,7 +1919,7 @@ class ActivateModel(bpy.types.Operator): bl_description = "Activate the model view, hide all annotations" def execute(self, context): - dprops = bpy.context.scene.DocProperties + dprops = tool.Drawing.get_document_props() dprops.active_drawing_id = 0 CutDecorator.uninstall() @@ -1971,11 +1983,12 @@ class ActivateDrawingBase: return self.execute(context) def execute(self, context): - if bpy.context.scene.DocProperties.is_editing_drawings == False: + props = tool.Drawing.get_document_props() + if props.is_editing_drawings == False: bpy.ops.bim.load_drawings() drawing = tool.Ifc.get().by_id(self.drawing) - dprops = bpy.context.scene.DocProperties + dprops = tool.Drawing.get_document_props() if self.use_quick_preview: tool.Blender.activate_camera(tool.Drawing.import_temporary_drawing_camera(drawing)) @@ -2027,7 +2040,7 @@ class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") return False @@ -2050,7 +2063,7 @@ class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_sheet_item(reference_type="DRAWING"): cls.poll_message_set("No drawing selected.") return False @@ -2066,7 +2079,8 @@ class SelectDocIfcFile(bpy.types.Operator): index: bpy.props.IntProperty() def execute(self, context): - context.scene.DocProperties.ifc_files[self.index].name = self.filepath + props = tool.Drawing.get_document_props() + props.ifc_files[self.index].name = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -2098,7 +2112,7 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") return False @@ -2112,12 +2126,10 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator): return self.execute(context) def _execute(self, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if self.remove_all: drawings = [ - tool.Ifc.get().by_id(d.ifc_definition_id) - for d in context.scene.DocProperties.drawings - if d.is_drawing and d.is_selected + tool.Ifc.get().by_id(d.ifc_definition_id) for d in props.drawings if d.is_drawing and d.is_selected ] else: if not self.drawing: @@ -2187,7 +2199,8 @@ class ReloadDrawingStyles(bpy.types.Operator): with open(json_path, "r") as fi: shading_styles_json = json.load(fi) - drawing_styles = context.scene.DocProperties.drawing_styles + props = tool.Drawing.get_document_props() + drawing_styles = props.drawing_styles drawing_styles.clear() styles = [style for style in shading_styles_json] for style_name in styles: @@ -2212,7 +2225,8 @@ class AddDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - drawing_styles = context.scene.DocProperties.drawing_styles + props = tool.Drawing.get_document_props() + drawing_styles = props.drawing_styles new = drawing_styles.add() # drawing style is saved to ifc on rename new.name = tool.Blender.ensure_unique_name("New Drawing Style", drawing_styles) @@ -2227,7 +2241,8 @@ class RemoveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): index: bpy.props.IntProperty() def execute(self, context): - context.scene.DocProperties.drawing_styles.remove(self.index) + props = tool.Drawing.get_document_props() + props.drawing_styles.remove(self.index) context.scene.camera.data.BIMCameraProperties.active_drawing_style_index = max(self.index - 1, 0) bpy.ops.bim.save_drawing_styles_data() return {"FINISHED"} @@ -2283,7 +2298,8 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): index = int(self.index) else: index = context.scene.camera.data.BIMCameraProperties.active_drawing_style_index - scene.DocProperties.drawing_styles[index].raster_style = json.dumps(style) + props = tool.Drawing.get_document_props() + props.drawing_styles[index].raster_style = json.dumps(style) bpy.ops.bim.save_drawing_styles_data() return {"FINISHED"} @@ -2309,7 +2325,8 @@ class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator): if not DrawingsData.is_loaded: DrawingsData.load() drawing_pset_data = DrawingsData.data["active_drawing_pset_data"] - drawing_styles = context.scene.DocProperties.drawing_styles + props = tool.Drawing.get_document_props() + drawing_styles = props.drawing_styles rel_path = drawing_pset_data["ShadingStyles"] current_style = drawing_pset_data.get("CurrentShadingStyle", None) @@ -2338,7 +2355,7 @@ class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator): new_style_name = None ifc_file = tool.Ifc.get() - drawing = ifc_file.by_id(context.scene.DocProperties.active_drawing_id) + drawing = ifc_file.by_id(props.active_drawing_id) pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing") ifcopenshell.api.run( "pset.edit_pset", ifc_file, pset=pset, properties={"CurrentShadingStyle": new_style_name} @@ -2357,17 +2374,18 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): scene = context.scene ifc_file = tool.Ifc.get() active_drawing_style_index = scene.camera.data.BIMCameraProperties.active_drawing_style_index + props = tool.Drawing.get_document_props() - if active_drawing_style_index >= len(scene.DocProperties.drawing_styles): + if active_drawing_style_index >= len(props.drawing_styles): self.report({"ERROR"}, "Could not find active drawing style") return {"CANCELLED"} - self.drawing_style = scene.DocProperties.drawing_styles[active_drawing_style_index] + self.drawing_style = props.drawing_styles[active_drawing_style_index] self.set_raster_style(context) self.set_query(context) - drawing = ifc_file.by_id(scene.DocProperties.active_drawing_id) + drawing = ifc_file.by_id(props.active_drawing_id) pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing") ifcopenshell.api.run( "pset.edit_pset", ifc_file, pset=pset, properties={"CurrentShadingStyle": self.drawing_style.name} @@ -2392,7 +2410,8 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): def set_query(self, context: bpy.types.Context) -> None: self.include_global_ids = [] self.exclude_global_ids = [] - for ifc_file in context.scene.DocProperties.ifc_files: + props = tool.Drawing.get_document_props() + for ifc_file in props.ifc_files: try: ifc = ifcopenshell.open(ifc_file.name) except: @@ -2507,14 +2526,15 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not props.schedules: cls.poll_message_set("No schedule selected.") return False - return props.schedules and props.sheets and context.scene.BIMProperties.data_dir + bim_props = tool.Blender.get_bim_props() + return props.schedules and props.sheets and bim_props.data_dir def _execute(self, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() active_schedule = props.schedules[props.active_schedule_index] active_sheet = tool.Drawing.get_active_sheet(context) schedule = tool.Ifc.get().by_id(active_schedule.ifc_definition_id) @@ -2573,14 +2593,15 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not props.references: cls.poll_message_set("No reference selected.") return False - return props.references and props.sheets and context.scene.BIMProperties.data_dir + bim_props = tool.Blender.get_bim_props() + return props.references and props.sheets and bim_props.data_dir def _execute(self, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() active_reference = props.references[props.active_reference_index] active_sheet = tool.Drawing.get_active_sheet(context) extref = tool.Ifc.get().by_id(active_reference.ifc_definition_id) @@ -2682,7 +2703,8 @@ class AddDrawingStyleAttribute(bpy.types.Operator): def execute(self, context): props = context.scene.camera.data.BIMCameraProperties - context.scene.DocProperties.drawing_styles[props.active_drawing_style_index].attributes.add() + dprops = tool.Drawing.get_document_props() + dprops.drawing_styles[props.active_drawing_style_index].attributes.add() return {"FINISHED"} @@ -2695,7 +2717,8 @@ class RemoveDrawingStyleAttribute(bpy.types.Operator): def execute(self, context): props = context.scene.camera.data.BIMCameraProperties - context.scene.DocProperties.drawing_styles[props.active_drawing_style_index].attributes.remove(self.index) + dprops = tool.Drawing.get_document_props() + dprops.drawing_styles[props.active_drawing_style_index].attributes.remove(self.index) return {"FINISHED"} @@ -2979,7 +3002,7 @@ class LoadSheets(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.load_sheets(tool.Drawing) - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() sheets_not_found = [] for sheet_prop in props.sheets: if not sheet_prop.is_sheet: @@ -3009,7 +3032,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator): document_type: Literal["SHEET", "TITLEBLOCK", "EMBEDDED"] def invoke(self, context, event): - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id) if sheet.is_a("IfcDocumentInformation"): self.document_type = "SHEET" @@ -3023,6 +3046,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator): return context.window_manager.invoke_props_dialog(self) def draw(self, context): + props = tool.Drawing.get_document_props() if self.document_type == "SHEET": row = self.layout.row() row.prop(self, "identification", text="Identification") @@ -3030,13 +3054,13 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator): row.prop(self, "name", text="Name") elif self.document_type == "TITLEBLOCK": row = self.layout.row() - row.prop(context.scene.DocProperties, "titleblock", text="Titleblock") + row.prop(props, "titleblock", text="Titleblock") elif self.document_type == "EMBEDDED": row = self.layout.row() row.prop(self, "identification", text="Identification") def _execute(self, context): - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id) if self.document_type == "SHEET": core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name) @@ -3134,7 +3158,7 @@ class ExpandTargetView(bpy.types.Operator): target_view: bpy.props.StringProperty() def execute(self, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() for drawing in [d for d in props.drawings if d.target_view == self.target_view]: drawing.is_expanded = True core.load_drawings(tool.Drawing) @@ -3150,7 +3174,7 @@ class ContractTargetView(bpy.types.Operator): target_view: bpy.props.StringProperty() def execute(self, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() for drawing in [d for d in props.drawings if d.target_view == self.target_view]: drawing.is_expanded = False core.load_drawings(tool.Drawing) @@ -3166,7 +3190,7 @@ class ExpandSheet(bpy.types.Operator): sheet: bpy.props.IntProperty() def execute(self, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]: sheet.is_expanded = True core.load_sheets(tool.Drawing) @@ -3182,7 +3206,7 @@ class ContractSheet(bpy.types.Operator): sheet: bpy.props.IntProperty() def execute(self, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]: sheet.is_expanded = False core.load_sheets(tool.Drawing) @@ -3373,7 +3397,7 @@ class ConvertSVGToDXF(bpy.types.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") return False @@ -3387,14 +3411,13 @@ class ConvertSVGToDXF(bpy.types.Operator): return self.execute(context) def execute(self, context): + props = tool.Drawing.get_document_props() if self.convert_all: drawings = [ - tool.Ifc.get().by_id(d.ifc_definition_id) - for d in context.scene.DocProperties.drawings - if d.is_drawing and d.is_selected + tool.Ifc.get().by_id(d.ifc_definition_id) for d in props.drawings if d.is_drawing and d.is_selected ] else: - drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)] + drawings = [tool.Ifc.get().by_id(props.drawings.get(self.view).ifc_definition_id)] drawing_uris: list[Path] = [] drawings_not_found: list[str] = [] diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index 02ce195100..6c875d1cfe 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -76,7 +76,7 @@ def update_diagram_scale(self, context): try: element = ( tool.Ifc.get() - .by_id(self.id_data.BIMMeshProperties.ifc_definition_id) + .by_id(tool.Geometry.get_mesh_props(self.id_data).ifc_definition_id) .OfProductRepresentation[0] .ShapeOfProduct[0] ) @@ -93,7 +93,7 @@ def update_diagram_scale(self, context): ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=diagram_scale) -def update_is_nts(self, context): +def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> None: if not self.update_props: return if not context.scene.camera or context.scene.camera.data != self.id_data: @@ -104,7 +104,7 @@ def update_is_nts(self, context): try: element = ( tool.Ifc.get() - .by_id(self.id_data.BIMMeshProperties.ifc_definition_id) + .by_id(tool.Geometry.get_mesh_props(self.id_data).ifc_definition_id) .OfProductRepresentation[0] .ShapeOfProduct[0] ) @@ -192,8 +192,8 @@ def get_drawing_style_name(self: "DrawingStyle"): def set_drawing_style_name(self: "DrawingStyle", new_value: str) -> None: """ensure the name is unique""" - scene = bpy.context.scene - drawing_styles = [s.name for s in scene.DocProperties.drawing_styles if s.name != self.name] + props = tool.Drawing.get_document_props() + drawing_styles = [s.name for s in props.drawing_styles if s.name != self.name] new_value = tool.Blender.ensure_unique_name(new_value, drawing_styles) old_value = self.name self["name"] = new_value diff --git a/src/bonsai/bonsai/bim/module/drawing/scheduler.py b/src/bonsai/bonsai/bim/module/drawing/scheduler.py index d00fd1d94f..00ce2c1612 100644 --- a/src/bonsai/bonsai/bim/module/drawing/scheduler.py +++ b/src/bonsai/bonsai/bim/module/drawing/scheduler.py @@ -56,7 +56,7 @@ def a1_to_rc(cell): class Scheduler: - def schedule(self, infile, outfile): + def schedule(self, infile: str, outfile: str) -> None: self.svg = svgwrite.Drawing( outfile, debug=False, @@ -71,11 +71,12 @@ class Scheduler: elif infile.endswith("xlsx"): self.schedule_xlsx(infile, outfile) - def parse_css(self, infile): + def parse_css(self, infile: str) -> None: + props = tool.Drawing.get_document_props() stylesheet_path = os.path.splitext(infile)[0] + ".css" if not os.path.exists(stylesheet_path): - stylesheet_rel_path = getattr(bpy.context.scene.DocProperties, "schedules_stylesheet_path") - ifc_file_path = os.path.dirname(IfcStore.path) + stylesheet_rel_path = props.schedules_stylesheet_path + ifc_file_path = os.path.dirname(tool.Ifc.get_path()) stylesheet_path = ifc_file_path + "\\" + stylesheet_rel_path if not os.path.exists(stylesheet_path): stylesheet_path = tool.Blender.get_data_dir_path(Path("assets") / "schedule.css") @@ -91,7 +92,7 @@ class Scheduler: self.svg.defs.add(self.svg.style(css)) - def schedule_xlsx(self, infile, outfile): + def schedule_xlsx(self, infile: str, outfile: str) -> None: workbook = openpyxl.open(infile, data_only=True) sheet = workbook.active @@ -236,7 +237,7 @@ class Scheduler: self.svg["viewBox"] = "0 0 {} {}".format(total_width, total_height) self.svg.save(pretty=True) - def schedule_ods(self, infile, outfile): + def schedule_ods(self, infile: str, outfile: str) -> None: doc = load_ods(infile) # useful for debugging ods @@ -495,11 +496,11 @@ class Scheduler: self.svg["viewBox"] = "0 0 {} {}".format(total_width, total_height) self.svg.save(pretty=True) - def get_style(self, style_name, styles): + def get_style(self, style_name: str, styles: dict) -> dict: style = styles[style_name] if style_name else {} return style - def get_box_alignment(self, style): + def get_box_alignment(self, style: dict) -> str: if style and "vertical-align" in style and style["vertical-align"] != "automatic": vertical_align = style["vertical-align"] else: diff --git a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py index e6dd66150a..9a249450f7 100644 --- a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py +++ b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py @@ -181,35 +181,35 @@ class SvgWriter: self.decimal_places = decimal_places for element in annotations: obj = tool.Ifc.get_object(element) - if not obj or element.ObjectType == "DRAWING": + if not obj or (object_type := ifcopenshell.util.element.get_predefined_type(element)) == "DRAWING": continue - elif element.ObjectType == "GRID": + elif object_type == "GRID": self.draw_grid_annotation(obj) - elif element.ObjectType == "TEXT_LEADER": + elif object_type == "TEXT_LEADER": self.draw_leader_annotation(obj) - elif element.ObjectType == "STAIR_ARROW": + elif object_type == "STAIR_ARROW": self.draw_stair_annotation(obj) - elif element.ObjectType == "DIMENSION": + elif object_type == "DIMENSION": self.draw_dimension_annotations(obj) - elif element.ObjectType == "ANGLE": + elif object_type == "ANGLE": self.draw_angle_annotations(obj) - elif element.ObjectType == "RADIUS": + elif object_type == "RADIUS": self.draw_radius_annotations(obj) - elif element.ObjectType == "DIAMETER": + elif object_type == "DIAMETER": self.draw_diameter_annotations(obj) - elif element.ObjectType == "ELEVATION": + elif object_type == "ELEVATION": self.draw_elevation_annotation(obj) - elif element.ObjectType == "SECTION": + elif object_type == "SECTION": self.draw_section_annotation(obj) - elif element.ObjectType == "BREAKLINE": + elif object_type == "BREAKLINE": self.draw_break_annotations(obj) - elif element.ObjectType == "PLAN_LEVEL": + elif object_type == "PLAN_LEVEL": self.draw_plan_level_annotation(obj) - elif element.ObjectType == "SECTION_LEVEL": + elif object_type == "SECTION_LEVEL": self.draw_section_level_annotation(obj) - elif element.ObjectType == "TEXT": + elif object_type == "TEXT": self.draw_text_annotation(obj, obj.location) - elif element.ObjectType in ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"): + elif object_type in ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"): self.draw_fall_annotations(obj) else: self.draw_misc_annotation(obj) @@ -1207,13 +1207,14 @@ class SvgWriter: angle = 90 # ues SLOPE_ANGLE as default - if element.ObjectType in ("FALL", "SLOPE_ANGLE"): + object_type = ifcopenshell.util.element.get_predefined_type(element) + if object_type in ("FALL", "SLOPE_ANGLE"): return f"{angle}°" - elif element.ObjectType == "SLOPE_FRACTION": + elif object_type == "SLOPE_FRACTION": if angle == 90: return "-" return f"{helper.format_distance(rise, precision=self.precision, decimal_places=self.decimal_places)} / {helper.format_distance(run, precision=self.precision, decimal_places=self.decimal_places)}" - elif element.ObjectType == "SLOPE_PERCENT": + elif object_type == "SLOPE_PERCENT": if angle == 90: return "-" return f"{round(angle_tg * 100)} %" diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index f35fc55325..6bf62ddec2 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -49,7 +49,7 @@ class BIM_PT_camera(Panel): return self.layout.use_property_split = True - dprops = context.scene.DocProperties + dprops = tool.Drawing.get_document_props() props = context.scene.camera.data.BIMCameraProperties col = self.layout.column(align=True) @@ -161,7 +161,7 @@ class BIM_PT_drawing_underlay(Panel): layout.use_property_split = True camera = context.scene.camera assert camera - dprops = context.scene.DocProperties + dprops = tool.Drawing.get_document_props() props = camera.data.BIMCameraProperties drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles) @@ -229,7 +229,7 @@ class BIM_PT_drawings(Panel): draw_project_not_saved_ui(self) return - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() if not self.props.is_editing_drawings: row = self.layout.row(align=True) @@ -302,7 +302,7 @@ class BIM_PT_schedules(Panel): draw_project_not_saved_ui(self) return - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() if not self.props.is_editing_schedules: row = self.layout.row(align=True) @@ -352,7 +352,7 @@ class BIM_PT_references(Panel): draw_project_not_saved_ui(self) return - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() if not self.props.is_editing_references: row = self.layout.row(align=True) @@ -394,7 +394,7 @@ class BIM_PT_sheets(Panel): draw_project_not_saved_ui(self) return - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() if not self.props.is_editing_sheets: row = self.layout.row(align=True) @@ -601,7 +601,7 @@ class BIM_UL_drawinglist(bpy.types.UIList): selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT" row.prop(item, "is_selected", text="", icon=selected_icon, emboss=False) row.prop(item, "name", text="", emboss=False) - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() if ( self.props.drawings and self.props.active_drawing_id diff --git a/src/bonsai/bonsai/bim/module/drawing/workspace.py b/src/bonsai/bonsai/bim/module/drawing/workspace.py index 30d57bf3db..16d26ea7e0 100644 --- a/src/bonsai/bonsai/bim/module/drawing/workspace.py +++ b/src/bonsai/bonsai/bim/module/drawing/workspace.py @@ -19,6 +19,7 @@ import os import bpy +import ifcopenshell.util.element import bonsai.core.type import bonsai.core.drawing as core import bonsai.tool as tool @@ -197,6 +198,8 @@ def create_annotation_occurrence(context): class AnnotationToolUI: + layout: bpy.types.UILayout + @classmethod def draw(cls, context, layout): cls.layout = layout @@ -224,7 +227,8 @@ class AnnotationToolUI: @classmethod def draw_create_object_interface(cls): row = cls.layout.row(align=True) - row.prop(bpy.context.scene.DocProperties, "should_draw_decorations", text="Viewport Annotations") + props = tool.Drawing.get_document_props() + row.prop(props, "should_draw_decorations", text="Viewport Annotations") @classmethod def draw_edit_object_interface(cls, context): @@ -330,7 +334,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): if not element or not element.is_a("IfcAnnotation"): continue - annotation_type = element.ObjectType + annotation_type = ifcopenshell.util.element.get_predefined_type(element) if annotation_type not in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP: self.report({"ERROR"}, f"Annotation type {annotation_type} is not supported for readjustment.") continue diff --git a/src/bonsai/bonsai/bim/module/fm/ui.py b/src/bonsai/bonsai/bim/module/fm/ui.py index 1e76ec61c3..89cd764adf 100644 --- a/src/bonsai/bonsai/bim/module/fm/ui.py +++ b/src/bonsai/bonsai/bim/module/fm/ui.py @@ -18,7 +18,6 @@ import bonsai.tool as tool from bpy.types import Panel -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.fm.data import FMData @@ -40,11 +39,11 @@ class BIM_PT_fm(Panel): scene = context.scene props = scene.BIMFMProperties - if IfcStore.get_file(): + if tool.Ifc.get(): row = layout.row() row.prop(props, "should_load_from_memory") - if not IfcStore.get_file() or not props.should_load_from_memory: + if not tool.Ifc.get() or not props.should_load_from_memory: row = layout.row() props.ifc_files.layout_file_select(row, "*.ifc;*.ifczip;*.ifcxml", "IFC File(s)") diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index 533cdcd6bf..4e9eb9cf6f 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -98,12 +98,14 @@ addon_keymaps = [] @persistent -def block_scale(scene): +def block_scale(scene: bpy.types.Scene) -> None: + import bonsai.tool as tool + if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active): if isinstance(obj, bpy.types.Object) and obj.BIMObjectProperties.ifc_definition_id: if obj.scale != (1, 1, 1): obj.scale = (1, 1, 1) - elif isinstance(obj, bpy.types.Mesh) and obj.BIMMeshProperties.ifc_definition_id: + elif isinstance(obj, bpy.types.Mesh) and tool.Geometry.get_mesh_props(obj).ifc_definition_id: if obj.scale != (1, 1, 1): obj.scale = (1, 1, 1) diff --git a/src/bonsai/bonsai/bim/module/geometry/data.py b/src/bonsai/bonsai/bim/module/geometry/data.py index a719d27893..2ba25df702 100644 --- a/src/bonsai/bonsai/bim/module/geometry/data.py +++ b/src/bonsai/bonsai/bim/module/geometry/data.py @@ -53,15 +53,16 @@ class ViewportData: obj = bpy.context.active_object element = tool.Ifc.get_entity(obj) - modes = [obj_mode] - - if bpy.context.scene.BIMGeometryProperties.representation_obj: + modes: list[tuple[str, str, str, str, int]] = [obj_mode] + gprops = tool.Geometry.get_geometry_props() + if gprops.representation_obj: modes.append(item_mode) if not obj: return modes - if obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs: + pprops = tool.Project.get_project_props() + if obj in pprops.clipping_planes_objs: pass elif element: if tool.Geometry.is_locked(element): @@ -107,8 +108,9 @@ class RepresentationsData: element = tool.Ifc.get_entity(obj) active_representation_id = None - if obj.data and hasattr(obj.data, "BIMMeshProperties"): - active_representation_id = obj.data.BIMMeshProperties.ifc_definition_id + active_representation = tool.Geometry.get_active_representation(obj) + if active_representation: + active_representation_id = active_representation.id() for representation in tool.Geometry.get_representations_iter(element): representation_type = representation.RepresentationType @@ -158,9 +160,9 @@ class RepresentationsData: if not obj.data: return [] element = tool.Ifc.get_entity(obj) - if not (active_representation_id := obj.data.BIMMeshProperties.ifc_definition_id): + base_representation = tool.Geometry.get_active_representation(obj) + if not base_representation: return [] # Maybe in profile editing mode - base_representation = tool.Ifc.get().by_id(active_representation_id) # shape aspects matching context of the active representation matching_shape_aspects = [] @@ -390,7 +392,7 @@ class PlacementData: def load(cls): cls.data = {"has_placement": cls.has_placement()} - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() obj = bpy.context.active_object if obj and props.has_blender_offset: xyz = cls.original_xyz(obj) @@ -413,7 +415,7 @@ class PlacementData: @classmethod def original_xyz(cls, obj): unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() xyz = ifcopenshell.util.geolocation.xyz2enh( obj.matrix_world[0][3], obj.matrix_world[1][3], diff --git a/src/bonsai/bonsai/bim/module/geometry/decorator.py b/src/bonsai/bonsai/bim/module/geometry/decorator.py index 2be10a27f0..5993cc4469 100644 --- a/src/bonsai/bonsai/bim/module/geometry/decorator.py +++ b/src/bonsai/bonsai/bim/module/geometry/decorator.py @@ -46,12 +46,14 @@ class ItemDecorator: obj_is_boolean: dict[str, list[ifcopenshell.entity_instance]] = {} objs: dict[str, dict[str, list]] = {} obj_matrix: dict[str, Matrix] = {} - for item_obj in context.scene.BIMGeometryProperties.item_objs: + props = tool.Geometry.get_geometry_props() + for item_obj in props.item_objs: if obj := item_obj.obj: obj: bpy.types.Object objs[obj.name] = cls.get_obj_data(obj) obj_is_selected[obj.name] = obj.select_get() - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + item = tool.Geometry.get_active_representation(obj) + assert item obj_is_boolean[obj.name] = [i for i in tool.Ifc.get().get_inverse(item) if i.is_a("IfcBooleanResult")] obj_matrix[obj.name] = obj.matrix_world.copy() @@ -143,7 +145,8 @@ class ItemDecorator: color = selected_elements_color blf.color(font_id, *color) - for item in context.scene.BIMGeometryProperties.item_objs: + props = tool.Geometry.get_geometry_props() + for item in props.item_objs: if (obj := item.obj) and obj.hide_get() == False: if obj.select_get(): centroid = obj.matrix_world @ Vector(obj.bound_box[0]).lerp(Vector(obj.bound_box[6]), 0.5) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index de59b7a61c..0763bd4963 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -23,12 +23,14 @@ import numpy as np import numpy.typing as npt import ifcopenshell import ifcopenshell.api.layer +import ifcopenshell.api.style import ifcopenshell.util.element import ifcopenshell.util.placement import ifcopenshell.util.representation import ifcopenshell.util.shape_builder import ifcopenshell.util.unit import ifcopenshell.api +import ifcopenshell.api.boundary import ifcopenshell.api.grid import bonsai.core.geometry import bonsai.core.geometry as core @@ -77,7 +79,8 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator): self.separate_element(element) def separate_item(self, context, obj): - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + item = tool.Geometry.get_active_representation(obj) + assert item if tool.Geometry.is_meshlike_item(item): previous_selected_objects = context.selected_objects bpy.ops.mesh.separate(type=self.type) @@ -85,7 +88,7 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator): if obj in previous_selected_objects: continue self.add_meshlike_item(obj) - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(tool.Geometry.get_geometry_props().representation_obj) else: self.report({"INFO"}, f"Separating an {item.is_a()} is not supported") @@ -120,7 +123,7 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator): representation.Items = list(representation.Items) + [item] obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}" - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Ifc.link(item, obj) props.add_item_object(obj, item) def separate_element(self, element): @@ -301,11 +304,12 @@ class AddRepresentation(bpy.types.Operator, tool.Ifc.Operator): return context.window_manager.invoke_props_dialog(self) def draw(self, context): + props = tool.Geometry.get_geometry_props() row = self.layout.row() row.prop(self, "representation_conversion_method", text="") if self.representation_conversion_method == "OBJECT": row = self.layout.row() - row.prop(context.scene.BIMGeometryProperties, "representation_from_object", text="") + row.prop(props, "representation_from_object", text="") class SelectConnection(bpy.types.Operator, tool.Ifc.Operator): @@ -456,13 +460,17 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} def update_obj_mesh_representation(self, context: bpy.types.Context, obj: bpy.types.Object) -> None: + data = obj.data + assert tool.Geometry.is_data_supported_for_adding_representation(data) + mprops = tool.Geometry.get_mesh_props(data) + product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) material = ifcopenshell.util.element.get_material(product, should_skip_usage=True) # NOTE: Currently iterator doesn't detect whether opening is actually affected the representation # or it's just present on the element. In theory, we can also allow editing representations # if we know that representation wasn't affected by existing openings. - has_openings = tool.Geometry.has_openings(product) and obj.data.BIMMeshProperties.has_openings_applied + has_openings = tool.Geometry.has_openings(product) and tool.Geometry.get_mesh_props(data).has_openings_applied if has_openings and not self.apply_openings: # Meshlike things with openings can only be updated without openings applied. if self.from_ui: @@ -479,13 +487,14 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator): elif product.is_a("IfcRelSpaceBoundary"): # TODO refactor settings = tool.Boundary.get_assign_connection_geometry_settings(obj) - ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings) + ifcopenshell.api.boundary.assign_connection_geometry(tool.Ifc.get(), **settings) return if tool.Ifc.is_moved(obj) or tool.Geometry.is_scaled(obj): core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) - old_representation = self.file.by_id(obj.data.BIMMeshProperties.ifc_definition_id) + old_representation = tool.Geometry.get_active_representation(obj) + assert old_representation if material and material.is_a() in ["IfcMaterialProfileSet", "IfcMaterialLayerSet"]: if self.ifc_representation_class == "IfcTessellatedFaceSet": # We are explicitly casting to a tessellation, so remove all parametric materials. @@ -544,12 +553,12 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator): tool.Geometry.run_style_add_style(obj=mat) for mat in tool.Geometry.get_object_materials_without_styles(obj) ] - ifcopenshell.api.run( - "style.assign_representation_styles", + props = tool.Geometry.get_geometry_props() + ifcopenshell.api.style.assign_representation_styles( self.file, shape_representation=new_representation, styles=tool.Geometry.get_styles(obj, only_assigned_to_faces=True), - should_use_presentation_style_assignment=context.scene.BIMGeometryProperties.should_use_presentation_style_assignment, + should_use_presentation_style_assignment=props.should_use_presentation_style_assignment, ) tool.Geometry.record_object_materials(obj) @@ -568,8 +577,8 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator): continue representation.RepresentationIdentifier = "Reference" - obj.data.BIMMeshProperties.ifc_definition_id = int(new_representation.id()) - obj.data.name = f"{old_representation.ContextOfItems.id()}/{new_representation.id()}" + tool.Ifc.link(new_representation, data) + data.name = tool.Loader.get_mesh_name(new_representation) # TODO: In simple scenarios, a type has a ShapeRepresentation of ID # 123. This is then mapped through mapped representations by @@ -585,7 +594,7 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator): # transformations. core.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_representation) - if obj.data.BIMMeshProperties.ifc_parameters: + if mprops.ifc_parameters: core.get_representation_ifc_parameters(tool.Geometry, obj=obj) @@ -597,12 +606,13 @@ class UpdateParametricRepresentation(bpy.types.Operator): @classmethod def poll(cls, context): - return context.active_object and context.active_object.mode == "OBJECT" + return (obj := context.active_object) and obj.mode == "OBJECT" and tool.Geometry.has_mesh_properties(obj.data) def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() obj = context.active_object - props = obj.data.BIMMeshProperties + assert obj and tool.Geometry.has_mesh_properties(obj.data) + props = tool.Geometry.get_mesh_props(obj.data) parameter = props.ifc_parameters[self.index] self.file.by_id(parameter.step_id)[parameter.index] = parameter.value show_representation_parameters = bool(props.ifc_parameters) @@ -626,8 +636,10 @@ class GetRepresentationIfcParameters(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - core.get_representation_ifc_parameters(tool.Geometry, obj=context.active_object) - parameters = context.active_object.data.BIMMeshProperties.ifc_parameters + obj = context.active_object + assert obj and tool.Geometry.has_mesh_properties((data := obj.data)) + core.get_representation_ifc_parameters(tool.Geometry, obj=obj) + parameters = tool.Geometry.get_mesh_props(data).ifc_parameters self.report({"INFO"}, f"{len(parameters)} parameters found.") @@ -671,7 +683,7 @@ class CopyRepresentation(bpy.types.Operator, tool.Ifc.Operator): ) -def lock_error_message(name): +def lock_error_message(name: str) -> str: return f"'{name}' is locked. Unlock it via the Spatial panel in the Project Overview tab." @@ -719,8 +731,10 @@ class OverrideDelete(bpy.types.Operator): if self.is_batch: row = self.layout.row() row.label(text="Warning: Faster deletion will use more memory.", icon="ERROR") + row = self.layout.row() + row.label(text="See system console for deletion progress.") - def _execute(self, context): + def _execute(self, context: bpy.types.Context): start_time = time() if self.is_batch: @@ -728,10 +742,19 @@ class OverrideDelete(bpy.types.Operator): self.process_arrays(context) clear_active_object = True - for obj in context.selected_objects: - try: - obj.name - except: + objects_to_remove = context.selected_objects + for i, obj in enumerate(objects_to_remove, 1): + # Log time. + time_since_start = time() - start_time + is_valid_data_block = tool.Blender.is_valid_data_block(obj) + if time_since_start > 10: + obj_name = f" ({obj.name})" if is_valid_data_block else "" + print( + f"Removing object {i}/{len(objects_to_remove)}{obj_name}. " + f"Time since start: {time_since_start:.2f} seconds." + ) + + if not is_valid_data_block: continue element = tool.Ifc.get_entity(obj) if element: @@ -781,7 +804,7 @@ class OverrideDelete(bpy.types.Operator): data["old_file"].redo() tool.Ifc.set(data["new_file"]) - def process_arrays(self, context): + def process_arrays(self, context: bpy.types.Context) -> None: selected_objects = set(context.selected_objects) array_parents = set() for obj in context.selected_objects: @@ -824,7 +847,7 @@ class OverrideOutlinerDelete(bpy.types.Operator): # unintended IFC spatial modifications. To make life less confusing for # the user, Delete means Delete. End of story. # Deep magick from the dawn of time - if IfcStore.get_file(): + if tool.Ifc.get(): return IfcStore.execute_ifc_operator(self, context) # https://blender.stackexchange.com/questions/203729/python-get-selected-objects-in-outliner objects_to_delete = set() @@ -948,7 +971,7 @@ class OverrideDuplicateMove(bpy.types.Operator): @staticmethod def execute_duplicate_operator(self, context, linked=False): # Deep magick from the dawn of time - if IfcStore.get_file(): + if tool.Ifc.get(): IfcStore.execute_ifc_operator(self, context) if self.new_active_obj: context.view_layer.objects.active = self.new_active_obj @@ -1029,7 +1052,8 @@ class OverrideDuplicateMove(bpy.types.Operator): # Unlink from previous boolean element # and keep object tracked for decorations. if is_tracked_opening: - new_obj.data.BIMMeshProperties.ifc_boolean_id = 0 + mprops = tool.Geometry.get_mesh_props(new_obj.data) + mprops.ifc_boolean_id = 0 tool.Root.add_tracked_opening(new_obj, tracked_opening_type) if obj == context.active_object: @@ -1053,7 +1077,7 @@ class OverrideDuplicateMove(bpy.types.Operator): if new.is_a("IfcRelSpaceBoundary"): surface = new.ConnectionGeometry.SurfaceOnRelatingElement temp_data.name = f"0/{surface.id()}" - temp_data.BIMMeshProperties.ifc_definition_id = surface.id() + tool.Ifc.link(surface, temp_data) else: tool.Blender.remove_data_block(temp_data) @@ -1089,12 +1113,14 @@ class OverrideDuplicateMove(bpy.types.Operator): @staticmethod def duplicate_item(obj: bpy.types.Object) -> None: props = tool.Geometry.get_geometry_props() - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + item = tool.Geometry.get_active_representation(obj) + assert item new_item = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), item) new_obj = obj.copy() + assert tool.Geometry.has_mesh_properties(obj.data) temp_data = obj.data.copy() new_obj.data = temp_data - new_obj.data.BIMMeshProperties.ifc_definition_id = new_item.id() + tool.Ifc.link(new_item, temp_data) new_obj.name = obj.data.name = f"Item/{new_item.is_a()}/{new_item.id()}" props.add_item_object(new_obj, new_item) @@ -1670,7 +1696,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator): def join_item(self) -> None: props = tool.Geometry.get_geometry_props() ifc_file = tool.Ifc.get() - item = tool.Ifc.get().by_id(self.target.data.BIMMeshProperties.ifc_definition_id) + item = tool.Geometry.get_active_representation(self.target) + assert item if tool.Geometry.is_meshlike_item(item): tool.Geometry.dissolve_triangulated_edges(self.target) item_objs = [i.obj for i in props.item_objs if i.obj] @@ -1693,7 +1720,7 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator): for item_data in items_data: props.add_item_object(item_data["obj"], ifc_file.by_id(item_data["ifc_definition_id"])) - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(props.representation_obj) bpy.context.view_layer.update() tool.Root.reload_item_decorator() @@ -1701,7 +1728,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator): ifc_file = tool.Ifc.get() builder = ShapeBuilder(ifc_file) si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) - representation = ifc_file.by_id(self.target.data.BIMMeshProperties.ifc_definition_id) + representation = tool.Geometry.get_active_representation(self.target) + assert representation representation_type = representation.RepresentationType if representation_type in ("Tessellation", "Brep"): for obj in bpy.context.selected_objects: @@ -1740,7 +1768,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator): continue # Only objects of the same representation type can be joined - obj_rep = ifc_file.by_id(obj.data.BIMMeshProperties.ifc_definition_id) + obj_rep = tool.Geometry.get_active_representation(obj) + assert obj_rep if obj_rep.RepresentationType != representation_type: obj.select_set(False) self.report( @@ -1891,9 +1920,10 @@ class OverrideEscape(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - if context.scene.BIMGeometryProperties.mode == "ITEM": + props = tool.Geometry.get_geometry_props() + if props.mode == "ITEM": tool.Geometry.disable_item_mode() - elif context.scene.BIMGeometryProperties.mode == "EDIT": + elif props.mode == "EDIT": bpy.ops.bim.override_mode_set_object("INVOKE_DEFAULT", should_save=False) tool.Geometry.disable_item_mode() elif tool.Model.get_model_props().openings: @@ -1949,6 +1979,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): def handle_single_object(self, context: bpy.types.Context, obj: bpy.types.Object) -> None: element = tool.Ifc.get_entity(obj) props = tool.Geometry.get_geometry_props() + pprops = tool.Project.get_project_props() if obj == props.representation_obj: self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly") elif obj in [o.obj for o in context.scene.BIMAggregateProperties.not_editing_objects]: @@ -1956,7 +1987,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): self.report( {"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly" ) - elif obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs: + elif obj in pprops.clipping_planes_objs: self.report({"ERROR"}, "Clipping planes cannot be edited") elif element: if not obj.data: @@ -2004,12 +2035,15 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): self.handle_single_object(context, obj) def enable_editing_representation_item(self, context: bpy.types.Context, obj: bpy.types.Object) -> None: - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) - element = tool.Ifc.get_entity(context.scene.BIMGeometryProperties.representation_obj) + item = tool.Geometry.get_active_representation(obj) + assert item + element = tool.Ifc.get_entity(tool.Geometry.get_geometry_props().representation_obj) if tool.Geometry.is_meshlike_item(item): tool.Geometry.dissolve_triangulated_edges(obj) tool.Blender.select_and_activate_single_object(context, obj) - obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data) + assert isinstance(mesh := obj.data, bpy.types.Mesh) + props = tool.Geometry.get_mesh_props(mesh) + props.mesh_checksum = tool.Geometry.get_mesh_checksum(mesh) self.enable_edit_mode(context) elif ( item.is_a("IfcSweptAreaSolid") @@ -2026,21 +2060,21 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): f"Couldn't import profile, editing it directly is not yet supported. Failing profile: {profile}.", ) return - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Ifc.link(item, obj.data) self.enable_edit_mode(context) ProfileDecorator.install(context) if not bpy.app.background: tool.Blender.set_viewport_tool("bim.cad_tool") elif item.is_a("IfcAnnotationFillArea"): tool.Model.import_annotation_fill_area(item, obj=obj) - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Ifc.link(item, obj.data) self.enable_edit_mode(context) ProfileDecorator.install(context) if not bpy.app.background: tool.Blender.set_viewport_tool("bim.cad_tool") elif tool.Geometry.is_curvelike_item(item): tool.Model.import_curve(item, obj=obj) - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Ifc.link(item, obj.data) self.enable_edit_mode(context) ProfileDecorator.install(context) if not bpy.app.background: @@ -2051,10 +2085,11 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): def enable_edit_mode(self, context: bpy.types.Context) -> Union[None, set[str]]: if tool.Blender.toggle_edit_mode(context) == {"CANCELLED"}: return {"CANCELLED"} - context.scene.BIMGeometryProperties.is_changing_mode = True - if context.scene.BIMGeometryProperties.mode != "EDIT": - context.scene.BIMGeometryProperties.mode = "EDIT" - context.scene.BIMGeometryProperties.is_changing_mode = False + props = tool.Geometry.get_geometry_props() + props.is_changing_mode = True + if props.mode != "EDIT": + props.mode = "EDIT" + props.is_changing_mode = False def has_aggregates(self, objs): for obj in objs: @@ -2101,14 +2136,15 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): tool.Blender.toggle_edit_mode(context) - context.scene.BIMGeometryProperties.is_changing_mode = True - if context.scene.BIMGeometryProperties.representation_obj: - if context.scene.BIMGeometryProperties.mode != "ITEM": - context.scene.BIMGeometryProperties.mode = "ITEM" + props = tool.Geometry.get_geometry_props() + props.is_changing_mode = True + if props.representation_obj: + if props.mode != "ITEM": + props.mode = "ITEM" else: - if context.scene.BIMGeometryProperties.mode != "OBJECT": - context.scene.BIMGeometryProperties.mode = "OBJECT" - context.scene.BIMGeometryProperties.is_changing_mode = False + if props.mode != "OBJECT": + props.mode = "OBJECT" + props.is_changing_mode = False if context.active_object and self.should_save: element = tool.Ifc.get_entity(context.active_object) @@ -2154,24 +2190,27 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): else: bpy.ops.bim.edit_extrusion_profile() return self.execute(context) - elif obj.data.BIMMeshProperties.ifc_definition_id: - if not tool.Geometry.has_geometric_data(obj): + elif representation := tool.Geometry.get_active_representation(obj): + if not tool.Geometry.is_geometric_data(obj.data): self.is_valid = False self.should_save = False - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + assert tool.Geometry.has_mesh_properties(obj.data) + mesh_props = tool.Geometry.get_mesh_props(obj.data) if tool.Geometry.is_meshlike( representation - ) and obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data): + ) and mesh_props.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data): self.edited_objs.append(obj) elif getattr(element, "HasOpenings", None): self.unchanged_objs_with_openings.append(obj) else: tool.Ifc.finish_edit(obj) elif element.is_a("IfcGridAxis"): - if not tool.Geometry.has_geometric_data(obj): + if not tool.Geometry.is_geometric_data(obj.data): self.is_valid = False self.should_save = False - if obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data): + assert tool.Geometry.has_mesh_properties(obj.data) + mesh_props = tool.Geometry.get_mesh_props(obj.data) + if mesh_props.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data): self.edited_objs.append(obj) else: tool.Ifc.finish_edit(obj) @@ -2196,9 +2235,11 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} def edit_representation_item(self, obj: bpy.types.Object) -> None: - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + props = tool.Geometry.get_geometry_props() + item = tool.Geometry.get_active_representation(obj) + assert item if tool.Geometry.is_meshlike_item(item): - if tool.Geometry.has_geometric_data(obj) and obj.data.polygons: + if tool.Geometry.is_geometric_data(obj.data) and obj.data.polygons: tool.Geometry.edit_meshlike_item(obj) else: tool.Geometry.import_item(obj) @@ -2219,11 +2260,11 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile) ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile) - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(props.representation_obj) tool.Geometry.import_item(obj) tool.Geometry.import_item_attributes(obj) - element = tool.Ifc.get_entity(bpy.context.scene.BIMGeometryProperties.representation_obj) + element = tool.Ifc.get_entity(props.representation_obj) # Only certain classes should have a footprint if element.is_a() in ("IfcSlab", "IfcRamp"): footprint_context = ifcopenshell.util.representation.get_context( @@ -2276,9 +2317,9 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): for inverse in tool.Ifc.get().get_inverse(item): ifcopenshell.util.element.replace_attribute(inverse, item, profile) ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item) - obj.data.BIMMeshProperties.ifc_definition_id = profile.id() + tool.Ifc.link(profile, obj.data) - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(props.representation_obj) tool.Geometry.import_item(obj) tool.Geometry.import_item_attributes(obj) elif tool.Geometry.is_curvelike_item(item): @@ -2304,10 +2345,9 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): ifcopenshell.util.element.replace_attribute(inverse, item, new) ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item) - obj.data.BIMMeshProperties.ifc_definition_id = new.id() + tool.Ifc.link(new, obj.data) tool.Geometry.import_item(obj) - props = tool.Geometry.get_geometry_props() for item in additional_curves: representation = tool.Geometry.get_active_representation(props.representation_obj) representation = ifcopenshell.util.representation.resolve_representation(representation) @@ -2316,7 +2356,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): name = f"Item/{item.is_a()}/{item.id()}" mesh = bpy.data.meshes.new(name) new_obj = bpy.data.objects.new(name, mesh) - new_obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Ifc.link(item, new_obj.data) bpy.context.collection.objects.link(new_obj) props.add_item_object(new_obj, item) new_obj.matrix_world = obj.matrix_world @@ -2329,10 +2369,11 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): def enable_edit_mode(self, context): if tool.Blender.toggle_edit_mode(context) == {"CANCELLED"}: return {"CANCELLED"} - context.scene.BIMGeometryProperties.is_changing_mode = True - if context.scene.BIMGeometryProperties.mode != "EDIT": - context.scene.BIMGeometryProperties.mode = "EDIT" - context.scene.BIMGeometryProperties.is_changing_mode = False + props = tool.Geometry.get_geometry_props() + props.is_changing_mode = True + if props.mode != "EDIT": + props.mode = "EDIT" + props.is_changing_mode = False class FlipObject(bpy.types.Operator): @@ -2369,12 +2410,13 @@ class EnableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): item.tags += "," item.tags += tag - if obj.data and hasattr(obj.data, "BIMMeshProperties"): - active_representation_id = obj.data.BIMMeshProperties.ifc_definition_id - representation = tool.Ifc.get().by_id(active_representation_id) + if tool.Geometry.has_mesh_properties((data := obj.data)): + representation = tool.Geometry.get_data_representation(data) + assert representation # Shape aspects must be considered from the PartOfProductDefinitionShape level element = tool.Ifc.get_entity(obj) + assert element product_reps = [] if element.is_a("IfcProduct"): product_reps = [element.Representation] @@ -2439,7 +2481,8 @@ class DisableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() - obj.BIMGeometryProperties.is_editing = False + assert obj + tool.Geometry.get_object_geometry_props(obj).is_editing = False class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator): @@ -2450,9 +2493,12 @@ class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - if context.scene.BIMGeometryProperties.representation_obj: + if tool.Geometry.get_geometry_props().representation_obj: return False # Artificial restriction for now to prevent removing when in item mode - if not (obj := tool.Geometry.get_active_or_representation_obj()) or len(obj.BIMGeometryProperties.items) <= 1: + if ( + not (obj := tool.Geometry.get_active_or_representation_obj()) + or len(tool.Geometry.get_object_geometry_props(obj).items) <= 1 + ): cls.poll_message_set( "Active object need to have more than 1 representation items to keep representation valid" ) @@ -2487,13 +2533,17 @@ class SelectRepresentationItem(bpy.types.Operator): def execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() - item = tool.Ifc.get().by_id(obj.BIMGeometryProperties.active_item.ifc_definition_id) + obj_props = tool.Geometry.get_object_geometry_props(obj) + assert obj_props.active_item + item = tool.Ifc.get().by_id(obj_props.active_item.ifc_definition_id) item_ids = self.get_nested_item_ids(item) props = tool.Geometry.get_geometry_props() for item_obj in props.item_objs: - if item_obj.obj.data.BIMMeshProperties.ifc_definition_id in item_ids: - tool.Blender.select_object(item_obj.obj) + obj_ = item_obj.obj + props = tool.Geometry.get_mesh_props(obj_.data) + if props.ifc_definition_id in item_ids: + tool.Blender.select_object(obj_) return {"FINISHED"} def get_nested_item_ids(self, item): @@ -2511,7 +2561,7 @@ class SelectRepresentationItem(bpy.types.Operator): def poll_editing_representation_item_style(cls, context): if not (obj := tool.Geometry.get_active_or_representation_obj()): return False - props = obj.BIMGeometryProperties + props = tool.Geometry.get_object_geometry_props(obj) if not props.is_editing: return False if not (item := props.active_item): @@ -2546,7 +2596,8 @@ class EnableEditingRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() - props = obj.BIMGeometryProperties + assert obj + props = tool.Geometry.get_object_geometry_props(obj) props.is_editing_item_style = True ifc_file = tool.Ifc.get() @@ -2565,7 +2616,8 @@ class EditRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() - props = obj.BIMGeometryProperties + assert obj + props = tool.Geometry.get_object_geometry_props(obj) props.is_editing_item_style = False ifc_file = tool.Ifc.get() @@ -2587,7 +2639,7 @@ class DisableEditingRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operato def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() - props = obj.BIMGeometryProperties + props = tool.Geometry.get_object_geometry_props(obj) props.is_editing_item_style = False @@ -2603,7 +2655,8 @@ class UnassignRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): active_obj = tool.Geometry.get_active_or_representation_obj() - active_props = active_obj.BIMGeometryProperties + assert active_obj + active_props = tool.Geometry.get_object_geometry_props(active_obj) active_props.is_editing_item_style = False # Get active representation item @@ -2670,7 +2723,8 @@ class EnableEditingRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Op def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() - props = obj.BIMGeometryProperties + assert obj + props = tool.Geometry.get_object_geometry_props(obj) props.is_editing_item_shape_aspect = True # set dropdown to currently active shape aspect @@ -2686,11 +2740,13 @@ class EditRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMGeometryProperties + props = tool.Geometry.get_object_geometry_props(obj) props.is_editing_item_shape_aspect = False ifc_file = tool.Ifc.get() + assert props.active_item representation_item_id = props.active_item.ifc_definition_id representation_item = ifc_file.by_id(representation_item_id) @@ -2743,7 +2799,8 @@ class DisableEditingRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.O def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() - props = obj.BIMGeometryProperties + assert obj + props = tool.Geometry.get_object_geometry_props(obj) props.is_editing_item_shape_aspect = False @@ -2754,10 +2811,12 @@ class RemoveRepresentationItemFromShapeAspect(bpy.types.Operator, tool.Ifc.Opera def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMGeometryProperties + props = tool.Geometry.get_object_geometry_props(obj) ifc_file = tool.Ifc.get() + assert props.active_item representation_item_id = props.active_item.ifc_definition_id representation_item = ifc_file.by_id(representation_item_id) shape_aspect = ifc_file.by_id(props.active_item.shape_aspect_id) @@ -2840,7 +2899,7 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): boolean_ids.add(item.SecondOperand.id()) continue item_mesh = bpy.data.meshes.new(f"Item/{item.is_a()}/{item_id}") - item_mesh.BIMMeshProperties.ifc_definition_id = item_id + tool.Ifc.link(item, item_mesh) item_obj = bpy.data.objects.new(f"Item/{item.is_a()}/{item_id}", item_mesh) item_obj.matrix_world = obj.matrix_world @@ -2870,7 +2929,7 @@ class UpdateItemAttributes(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object tool.Geometry.sync_item_positions() tool.Geometry.update_item_attributes(obj) - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(tool.Geometry.get_geometry_props().representation_obj) tool.Geometry.import_item(obj) tool.Root.reload_item_decorator() @@ -2887,6 +2946,10 @@ class NameProfile(bpy.types.Operator, tool.Ifc.Operator): options={"SKIP_SAVE"}, ) + if TYPE_CHECKING: + extrusion_item_obj: str + profile_name: str + def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) @@ -2901,7 +2964,7 @@ class NameProfile(bpy.types.Operator, tool.Ifc.Operator): ifc_file = tool.Ifc.get() extrusion_item_obj = bpy.data.objects[self.extrusion_item_obj] - mesh_props = extrusion_item_obj.data.BIMMeshProperties + mesh_props = tool.Geometry.get_mesh_props(extrusion_item_obj.data) extrusion = ifc_file.by_id(mesh_props.ifc_definition_id) assert extrusion.is_a("IfcSweptAreaSolid") profile = extrusion.SweptArea @@ -2971,9 +3034,9 @@ class AddMeshlikeItem(bpy.types.Operator, tool.Ifc.Operator): props.add_item_object(obj, item) representation.Items = list(representation.Items) + [item] - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(props.representation_obj) obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}" - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id() tool.Root.reload_item_decorator() @@ -3019,10 +3082,10 @@ class AddSweptAreaSolidItem(bpy.types.Operator, tool.Ifc.Operator): props.add_item_object(obj, item) representation.Items = list(representation.Items) + [item] - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(props.representation_obj) obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}" - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id() tool.Geometry.import_item(obj) tool.Geometry.import_item_attributes(obj) tool.Root.reload_item_decorator() @@ -3090,10 +3153,10 @@ class AddCurvelikeItem(bpy.types.Operator, tool.Ifc.Operator): props.add_item_object(obj, item) representation.Items = list(representation.Items) + [item] - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(props.representation_obj) obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}" - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id() tool.Geometry.import_item(obj) tool.Geometry.import_item_attributes(obj) @@ -3139,10 +3202,10 @@ class AddHalfSpaceSolidItem(bpy.types.Operator, tool.Ifc.Operator): representation = ifcopenshell.util.representation.resolve_representation(representation) representation.Items = list(representation.Items) + [item] - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(props.representation_obj) obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}" - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id() tool.Geometry.import_item(obj) # TODO refactor to core and not rely on selection @@ -3168,7 +3231,7 @@ class OverrideMove(bpy.types.Operator): def execute(self, context): # Deep magick from the dawn of time - if IfcStore.get_file(): + if tool.Ifc.get(): IfcStore.execute_ifc_operator(self, context) if self.new_active_obj: context.view_layer.objects.active = self.new_active_obj diff --git a/src/bonsai/bonsai/bim/module/geometry/prop.py b/src/bonsai/bonsai/bim/module/geometry/prop.py index 2ab4ec5efe..7b80e9b040 100644 --- a/src/bonsai/bonsai/bim/module/geometry/prop.py +++ b/src/bonsai/bonsai/bim/module/geometry/prop.py @@ -32,7 +32,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) -from typing import Optional, TYPE_CHECKING, Union +from typing import Optional, TYPE_CHECKING, Union, Literal def get_contexts(self, context): @@ -270,6 +270,9 @@ class BIMObjectGeometryProperties(PropertyGroup): representation_item_layer: str +GeometryMode = Literal["OBJECT", "ITEM", "EDIT"] + + class BIMGeometryProperties(PropertyGroup): # Revit workaround should_use_presentation_style_assignment: BoolProperty(name="Force Presentation Style Assignment", default=False) @@ -308,7 +311,7 @@ class BIMGeometryProperties(PropertyGroup): should_force_faceted_brep: bool should_force_triangulation: bool is_changing_mode: bool - mode: str + mode: GeometryMode representation_obj: Union[bpy.types.Object, None] item_objs: bpy.types.bpy_prop_collection_idprop[RepresentationItemObject] representation_from_object: Union[bpy.types.Object, None] diff --git a/src/bonsai/bonsai/bim/module/geometry/ui.py b/src/bonsai/bonsai/bim/module/geometry/ui.py index 01d0a8ae0e..8ba3958222 100644 --- a/src/bonsai/bonsai/bim/module/geometry/ui.py +++ b/src/bonsai/bonsai/bim/module/geometry/ui.py @@ -20,7 +20,6 @@ import bpy import bonsai.bim import bonsai.tool as tool from bpy.types import Panel, Menu, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import prop_with_search from bonsai.bim.module.geometry.data import ( RepresentationsData, @@ -53,9 +52,10 @@ def mode_menu(self, context): UIData.load() ifc_icon = f"{UIData.data['menu_icon_color_mode']}_ifc" row = self.layout.row(align=True) - if context.scene.BIMGeometryProperties.mode == "EDIT": + props = tool.Geometry.get_geometry_props() + if props.mode == "EDIT": row.operator("bim.override_mode_set_object", icon="CANCEL", text="Discard Changes").should_save = False - row.prop(context.scene.BIMGeometryProperties, "mode", text="", icon_value=bonsai.bim.icons[ifc_icon].icon_id) + row.prop(props, "mode", text="", icon_value=bonsai.bim.icons[ifc_icon].icon_id) def object_menu(self, context): @@ -336,9 +336,9 @@ class BIM_PT_connections(Panel): def poll(cls, context): if not context.active_object: return False - if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id): return False - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not ConnectionsData.is_loaded: @@ -411,15 +411,18 @@ class BIM_PT_mesh(Panel): @classmethod def poll(cls, context): return ( - context.active_object is not None - and context.active_object.type == "MESH" - and hasattr(context.active_object.data, "BIMMeshProperties") - and context.active_object.data.BIMMeshProperties.ifc_definition_id + (obj := context.active_object) is not None + and (mesh := obj.data) + and isinstance(mesh, bpy.types.Mesh) + and tool.Geometry.get_mesh_props(mesh).ifc_definition_id ) def draw(self, context): - if not context.active_object.data: - return + obj = context.active_object + assert obj + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) + row = self.layout.row() row.label(text="Advanced Users Only", icon="ERROR") @@ -427,7 +430,7 @@ class BIM_PT_mesh(Panel): row = layout.row() text = "Manually Save Representation" - if tool.Ifc.is_edited(context.active_object): + if tool.Ifc.is_edited(obj): text += "*" row.operator("bim.update_representation", text=text) @@ -454,8 +457,8 @@ class BIM_PT_mesh(Panel): op = row.operator("bim.update_representation", text="Convert To Arbitrary Extrusion With Voids") op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids" - if context.active_object and context.active_object.data: - mprops = context.active_object.data.BIMMeshProperties + if True: + mprops = tool.Geometry.get_mesh_props(mesh) row = layout.row() row.operator("bim.get_representation_ifc_parameters") for index, ifc_parameter in enumerate(mprops.ifc_parameters): @@ -477,7 +480,7 @@ class BIM_PT_placement(Panel): @classmethod def poll(cls, context): - return context.active_object and context.active_object.BIMObjectProperties.ifc_definition_id + return (obj := context.active_object) and obj.BIMObjectProperties.ifc_definition_id def draw(self, context): if not PlacementData.is_loaded: @@ -571,10 +574,10 @@ class BIM_PT_workarounds(Panel): @classmethod def poll(cls, context): return ( - context.active_object is not None - and context.active_object.type == "MESH" - and hasattr(context.active_object.data, "BIMMeshProperties") - and context.active_object.data.BIMMeshProperties.ifc_definition_id + (obj := context.active_object) is not None + and (mesh := obj.data) + and isinstance(mesh, bpy.types.Mesh) + and tool.Geometry.get_mesh_props(mesh).ifc_definition_id ) def draw(self, context): @@ -588,7 +591,7 @@ class BIM_PT_workarounds(Panel): class BIM_UL_representation_items(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item(self, context, layout: bpy.types.UILayout, data, item, icon, active_data, active_propname): if item: icon = "MATERIAL" if item.surface_style else "MESH_UVSPHERE" row = layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/georeference/data.py b/src/bonsai/bonsai/bim/module/georeference/data.py index 314de226ff..b64ef55c39 100644 --- a/src/bonsai/bonsai/bim/module/georeference/data.py +++ b/src/bonsai/bonsai/bim/module/georeference/data.py @@ -167,7 +167,7 @@ class GeoreferenceData: result["rotation"] = str(round(ifcopenshell.util.geolocation.yaxis2angle(*wcs[:, 1][:2]), 3)) result["x"], result["y"], result["z"] = wcs[:, 3][:3] - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: blender_xyz = ifcopenshell.util.geolocation.enh2xyz( result["x"], @@ -193,7 +193,7 @@ class GeoreferenceData: @classmethod def local_origin(cls): - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if not props.has_blender_offset: return unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) diff --git a/src/bonsai/bonsai/bim/module/georeference/decorator.py b/src/bonsai/bonsai/bim/module/georeference/decorator.py index bd5a9b97e8..afd9dfba27 100644 --- a/src/bonsai/bonsai/bim/module/georeference/decorator.py +++ b/src/bonsai/bonsai/bim/module/georeference/decorator.py @@ -21,6 +21,7 @@ import blf import gpu import bmesh import ifcopenshell +import ifcopenshell.util.geolocation import bonsai.tool as tool from math import radians from bpy.types import SpaceView3D @@ -53,7 +54,8 @@ class GeoreferenceDecorator: cls.is_installed = False def draw_batch(self, shader_type, content_pos, color, indices=None): - self.scale = bpy.context.scene.BIMGeoreferenceProperties.visualization_scale + props = tool.Georeference.get_georeference_props() + self.scale = props.visualization_scale content_pos = [v * self.scale for v in content_pos] shader = self.line_shader if shader_type == "LINES" else self.shader batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices) @@ -64,7 +66,7 @@ class GeoreferenceDecorator: if not GeoreferenceData.is_loaded: GeoreferenceData.load() - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if not props.model_origin: # If this is empty, no georeferencing data has been loaded. return @@ -177,7 +179,7 @@ class GeoreferenceDecorator: if not GeoreferenceData.is_loaded: GeoreferenceData.load() - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if not props.model_origin: # If this is empty, no georeferencing data has been loaded. return @@ -350,7 +352,7 @@ class GeoreferenceDecorator: self.gn_angle = float(GeoreferenceData.data["map_derived_angle"] or 0) self.tn_angle = float(GeoreferenceData.data["true_derived_angle"] or 0) - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: blender_angle = ifcopenshell.util.geolocation.xaxis2angle( float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate) diff --git a/src/bonsai/bonsai/bim/module/georeference/prop.py b/src/bonsai/bonsai/bim/module/georeference/prop.py index f0458b2045..58a3088b86 100644 --- a/src/bonsai/bonsai/bim/module/georeference/prop.py +++ b/src/bonsai/bonsai/bim/module/georeference/prop.py @@ -33,15 +33,18 @@ from bpy.props import ( ) from bonsai.bim.module.georeference.data import GeoreferenceData from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator +from typing import TYPE_CHECKING -def get_coordinate_operation_class(self, context): +def get_coordinate_operation_class( + self: "BIMGeoreferenceProperties", context: bpy.types.Context +) -> list[tuple[str, str, str]]: if not GeoreferenceData.is_loaded: GeoreferenceData.load() return GeoreferenceData.data["coordinate_operation_class"] -def update_true_north_angle(self, context): +def update_true_north_angle(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: if self.is_changing_angle: return self.is_changing_angle = True @@ -54,7 +57,7 @@ def update_true_north_angle(self, context): self.is_changing_angle = False -def update_true_north_vector(self, context): +def update_true_north_vector(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: if self.is_changing_angle: return self.is_changing_angle = True @@ -67,7 +70,7 @@ def update_true_north_vector(self, context): self.is_changing_angle = False -def update_grid_north_angle(self, context): +def update_grid_north_angle(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: if self.is_changing_angle: return self.is_changing_angle = True @@ -81,7 +84,7 @@ def update_grid_north_angle(self, context): self.is_changing_angle = False -def update_grid_north_vector(self, context): +def update_grid_north_vector(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: if self.is_changing_angle: return self.is_changing_angle = True @@ -95,15 +98,15 @@ def update_grid_north_vector(self, context): self.is_changing_angle = False -def update_should_visualise(self, context): +def update_should_visualise(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: if self.should_visualise: GeoreferenceDecorator.install(bpy.context) else: GeoreferenceDecorator.uninstall() -def update_blender_coordinates(self, context): - props = bpy.context.scene.BIMGeoreferenceProperties +def update_blender_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: + props = self if props.is_updating_coordinates: return props.is_updating_coordinates = True @@ -123,8 +126,8 @@ def update_blender_coordinates(self, context): props.is_updating_coordinates = False -def update_local_coordinates(self, context): - props = bpy.context.scene.BIMGeoreferenceProperties +def update_local_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: + props = self if props.is_updating_coordinates: return props.is_updating_coordinates = True @@ -147,8 +150,8 @@ def update_local_coordinates(self, context): props.is_updating_coordinates = False -def update_map_coordinates(self, context): - props = bpy.context.scene.BIMGeoreferenceProperties +def update_map_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: + props = self if props.is_updating_coordinates: return props.is_updating_coordinates = True @@ -248,3 +251,45 @@ class BIMGeoreferenceProperties(PropertyGroup): wcs_y: StringProperty(name="WCS Y", default="0") wcs_z: StringProperty(name="WCS Z", default="0") wcs_rotation: StringProperty(name="WCS Rotation", default="0") + + if TYPE_CHECKING: + coordinate_operation_class: str + is_changing_angle: bool + is_editing: bool + is_editing_wcs: bool + is_editing_true_north: bool + coordinate_operation: bpy.types.bpy_prop_collection_idprop[Attribute] + projected_crs: bpy.types.bpy_prop_collection_idprop[Attribute] + is_updating_coordinates: bool + blender_coordinates: str + local_coordinates: str + map_coordinates: str + should_visualise: bool + visualization_scale: float + grid_north_angle: str + x_axis_abscissa: str + x_axis_ordinate: str + x_axis_is_null: bool + + host_model_origin: str + host_model_origin_si: str + host_model_project_north: str + + model_origin: str + model_origin_si: str + model_project_north: str + + has_blender_offset: bool + blender_offset_x: str + blender_offset_y: str + blender_offset_z: str + blender_x_axis_abscissa: str + blender_x_axis_ordinate: str + + true_north_angle: str + true_north_abscissa: str + true_north_ordinate: str + wcs_x: str + wcs_y: str + wcs_z: str + wcs_rotation: str diff --git a/src/bonsai/bonsai/bim/module/georeference/ui.py b/src/bonsai/bonsai/bim/module/georeference/ui.py index 8b2dfd43e0..3dfec3a9f5 100644 --- a/src/bonsai/bonsai/bim/module/georeference/ui.py +++ b/src/bonsai/bonsai/bim/module/georeference/ui.py @@ -32,7 +32,7 @@ class BIM_PT_gis(Panel): bl_parent_id = "BIM_PT_tab_geometry" def draw_header(self, context): - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() row = self.layout.row(align=True) icon = "HIDE_OFF" if props.should_visualise else "HIDE_ON" row.label(text="") # empty text occupies the left of the row @@ -43,7 +43,7 @@ class BIM_PT_gis(Panel): def draw(self, context): self.layout.use_property_split = True self.layout.use_property_decorate = False - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if not GeoreferenceData.is_loaded: GeoreferenceData.load() @@ -54,7 +54,7 @@ class BIM_PT_gis(Panel): self.draw_ui(context) def draw_editable_ui(self, context): - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() row = self.layout.row(align=True) row.label(text="Projected CRS", icon="WORLD") row.operator("bim.edit_georeferencing", icon="CHECKMARK", text="") @@ -87,7 +87,7 @@ class BIM_PT_gis(Panel): draw_attribute(attribute, self.layout.row()) def draw_ui(self, context): - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if tool.Ifc.get_schema() == "IFC2X3": row = self.layout.row() @@ -149,7 +149,7 @@ class BIM_PT_gis_true_north(Panel): if not GeoreferenceData.is_loaded: GeoreferenceData.load() - self.props = context.scene.BIMGeoreferenceProperties + self.props = tool.Georeference.get_georeference_props() if self.props.is_editing_true_north: self.draw_editable_ui(context) @@ -200,7 +200,7 @@ class BIM_PT_gis_blender(Panel): if not GeoreferenceData.is_loaded: GeoreferenceData.load() - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: row = self.layout.row() @@ -233,7 +233,7 @@ class BIM_PT_gis_wcs(Panel): if not GeoreferenceData.is_loaded: GeoreferenceData.load() - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.is_editing_wcs: self.draw_editable_ui(context) @@ -263,7 +263,7 @@ class BIM_PT_gis_wcs(Panel): row.operator("bim.enable_editing_wcs", icon="GREASEPENCIL", text="") def draw_editable_ui(self, context): - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() row = self.layout.row(align=True) row.label(text="World Coordinate System", icon="EMPTY_ARROWS") @@ -293,7 +293,7 @@ class BIM_PT_gis_calculator(Panel): if not GeoreferenceData.is_loaded: GeoreferenceData.load() - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: row = self.layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/group/operator.py b/src/bonsai/bonsai/bim/module/group/operator.py index e991dcf4f1..9a9124ebfa 100644 --- a/src/bonsai/bonsai/bim/module/group/operator.py +++ b/src/bonsai/bonsai/bim/module/group/operator.py @@ -22,7 +22,6 @@ import ifcopenshell.api.group import ifcopenshell.util.attribute import bonsai.bim.helper import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore import json @@ -129,8 +128,7 @@ class RemoveGroup(bpy.types.Operator, tool.Ifc.Operator): group: bpy.props.IntProperty() def _execute(self, context): - props = context.scene.BIMGroupProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run("group.remove_group", self.file, **{"group": self.file.by_id(self.group)}) bpy.ops.bim.load_groups() return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/group/ui.py b/src/bonsai/bonsai/bim/module/group/ui.py index 88077f99eb..8cd7d1fe2a 100644 --- a/src/bonsai/bonsai/bim/module/group/ui.py +++ b/src/bonsai/bonsai/bim/module/group/ui.py @@ -17,8 +17,8 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import draw_attributes from bonsai.bim.module.group.data import GroupsData, ObjectGroupsData @@ -34,7 +34,7 @@ class BIM_PT_groups(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not GroupsData.is_loaded: @@ -93,7 +93,7 @@ class BIM_PT_object_groups(Panel): def poll(cls, context): if not context.active_object: return False - return IfcStore.get_file() and context.active_object.BIMObjectProperties.ifc_definition_id + return tool.Ifc.get() and context.active_object.BIMObjectProperties.ifc_definition_id def draw(self, context): if not ObjectGroupsData.is_loaded: diff --git a/src/bonsai/bonsai/bim/module/layer/operator.py b/src/bonsai/bonsai/bim/module/layer/operator.py index 9c2f4a2249..5480c86034 100644 --- a/src/bonsai/bonsai/bim/module/layer/operator.py +++ b/src/bonsai/bonsai/bim/module/layer/operator.py @@ -24,7 +24,6 @@ import ifcopenshell.util.element import ifcopenshell.util.attribute import bonsai.bim.helper import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore class LoadLayers(bpy.types.Operator): @@ -33,7 +32,7 @@ class LoadLayers(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = context.scene.BIMLayerProperties props.layers.clear() for layer in tool.Ifc.get().by_type("IfcPresentationLayerAssignment"): @@ -126,7 +125,7 @@ class RemovePresentationLayer(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = context.scene.BIMLayerProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run("layer.remove_layer", self.file, **{"layer": self.file.by_id(self.layer)}) bpy.ops.bim.load_layers() return {"FINISHED"} @@ -142,12 +141,12 @@ class AssignPresentationLayer(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): item = bpy.data.meshes.get(self.item) if self.item else context.active_object.data - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "layer.assign_layer", self.file, **{ - "items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)], + "items": [self.file.by_id(tool.Geometry.get_mesh_props(item).ifc_definition_id)], "layer": self.file.by_id(self.layer), }, ) @@ -164,15 +163,10 @@ class UnassignPresentationLayer(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): item = bpy.data.meshes.get(self.item) if self.item else context.active_object.data - self.file = IfcStore.get_file() - ifcopenshell.api.run( - "layer.unassign_layer", - self.file, - **{ - "items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)], - "layer": self.file.by_id(self.layer), - }, - ) + ifc_file = tool.Ifc.get() + representation = tool.Geometry.get_data_representation(item) + assert representation + ifcopenshell.api.layer.unassign_layer(ifc_file, items=[representation], layer=ifc_file.by_id(self.layer)) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/layer/ui.py b/src/bonsai/bonsai/bim/module/layer/ui.py index 8c3b77d30a..609cb62815 100644 --- a/src/bonsai/bonsai/bim/module/layer/ui.py +++ b/src/bonsai/bonsai/bim/module/layer/ui.py @@ -17,8 +17,8 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool from bpy.types import Panel, UIList, Mesh -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import draw_attributes from bonsai.bim.module.layer.data import LayersData @@ -34,7 +34,7 @@ class BIM_PT_layers(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not LayersData.is_loaded: @@ -77,7 +77,6 @@ class BIM_UL_layers(UIList): row.label(text=item.name) if context.active_object and isinstance(context.active_object.data, Mesh): - mprops = context.active_object.data.BIMMeshProperties if item.ifc_definition_id in LayersData.data["active_layers"]: op = row.operator("bim.unassign_presentation_layer", text="", icon="KEYFRAME_HLT", emboss=False) op.layer = item.ifc_definition_id diff --git a/src/bonsai/bonsai/bim/module/material/data.py b/src/bonsai/bonsai/bim/module/material/data.py index 50a03603df..a332a89804 100644 --- a/src/bonsai/bonsai/bim/module/material/data.py +++ b/src/bonsai/bonsai/bim/module/material/data.py @@ -282,8 +282,9 @@ class ObjectMaterialData: if item.is_a("IfcMaterialLayer"): total_thickness = item.LayerThickness unit_system = bpy.context.scene.unit_settings.system + props = tool.Drawing.get_document_props() if unit_system == "IMPERIAL": - precision = bpy.context.scene.DocProperties.imperial_precision + precision = props.imperial_precision else: precision = None formatted_thickness = format_distance( diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index 820ac5359e..3e9ff20924 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -29,7 +29,6 @@ import bonsai.tool as tool import bonsai.core.style import bonsai.core.material as core import bonsai.bim.module.model.profile as model_profile -from bonsai.bim.ifc import IfcStore from typing import Any, Union, TYPE_CHECKING from bonsai.bim.module.model import wall, slab @@ -118,7 +117,7 @@ class AssignParameterizedProfile(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() profile = ifcopenshell.api.run( "profile.add_parameterized_profile", self.file, @@ -292,7 +291,7 @@ class AddConstituent(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "material.add_constituent", self.file, @@ -328,7 +327,7 @@ class AddProfile(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = tool.Material.get_material_props() ifcopenshell.api.run( "material.add_profile", @@ -387,7 +386,7 @@ class ReorderMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() material_set = self.file.by_id(self.material_set) ifcopenshell.api.run( "material.reorder_set_item", @@ -443,7 +442,7 @@ class AddListItem(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "material.add_list_item", self.file, @@ -463,7 +462,7 @@ class RemoveListItem(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "material.remove_list_item", self.file, @@ -557,7 +556,7 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator): material_set_usage: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() active_obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object props = active_obj.BIMObjectMaterialProperties element = tool.Ifc.get_entity(active_obj) @@ -663,7 +662,7 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): material_set_item: bpy.props.IntProperty() def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.mprops = tool.Material.get_material_props() self.props = obj.BIMObjectMaterialProperties @@ -707,7 +706,7 @@ class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = obj.BIMObjectMaterialProperties mprops = tool.Material.get_material_props() element = tool.Ifc.get_entity(obj) diff --git a/src/bonsai/bonsai/bim/module/material/prop.py b/src/bonsai/bonsai/bim/module/material/prop.py index f21a34a702..04a596e73f 100644 --- a/src/bonsai/bonsai/bim/module/material/prop.py +++ b/src/bonsai/bonsai/bim/module/material/prop.py @@ -22,7 +22,6 @@ from ifcopenshell.util.doc import get_entity_doc import bonsai.tool as tool from bonsai.bim.module.material.data import MaterialsData, ObjectMaterialData from bonsai.bim.module.profile.data import ProfileData -from bonsai.bim.ifc import IfcStore from bonsai.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index 19865be4e8..fe0fce2413 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -21,7 +21,6 @@ import bonsai.bim.helper import bonsai.tool as tool import bpy from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import draw_attributes from bonsai.bim.helper import prop_with_search from bonsai.bim.module.material.data import MaterialsData, ObjectMaterialData @@ -43,7 +42,7 @@ class BIM_PT_materials(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not MaterialsData.is_loaded: @@ -143,9 +142,9 @@ class BIM_PT_object_material(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - if not hasattr(IfcStore.get_file().by_id(props.ifc_definition_id), "HasAssociations"): + if not hasattr(tool.Ifc.get().by_id(props.ifc_definition_id), "HasAssociations"): return False return True @@ -153,7 +152,7 @@ class BIM_PT_object_material(Panel): if not ObjectMaterialData.is_loaded: ObjectMaterialData.load() - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.oprops = context.active_object.BIMObjectProperties self.props = context.active_object.BIMObjectMaterialProperties self.mprops = tool.Material.get_material_props() @@ -351,9 +350,10 @@ class BIM_PT_object_material(Panel): if ObjectMaterialData.data["total_thickness"]: total_thickness = ObjectMaterialData.data["total_thickness"] unit_system = bpy.context.scene.unit_settings.system + props = tool.Drawing.get_document_props() if unit_system == "IMPERIAL": - precision = bpy.context.scene.DocProperties.imperial_precision + precision = props.imperial_precision else: precision = None formatted_thickness = format_distance( diff --git a/src/bonsai/bonsai/bim/module/misc/operator.py b/src/bonsai/bonsai/bim/module/misc/operator.py index 198408eeb5..20bd454d35 100644 --- a/src/bonsai/bonsai/bim/module/misc/operator.py +++ b/src/bonsai/bonsai/bim/module/misc/operator.py @@ -299,7 +299,7 @@ class DrawSystemArrows(bpy.types.Operator, tool.Ifc.Operator): tool.Blender.select_and_activate_single_object(context, curve) def get_absolute_matrix(self, matrix): - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: matrix = np.array( ifcopenshell.util.geolocation.global2local( diff --git a/src/bonsai/bonsai/bim/module/model/data.py b/src/bonsai/bonsai/bim/module/model/data.py index cc713e5bd7..5cc0f64699 100644 --- a/src/bonsai/bonsai/bim/module/model/data.py +++ b/src/bonsai/bonsai/bim/module/model/data.py @@ -26,7 +26,7 @@ from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc import bonsai.tool as tool from math import degrees from natsort import natsorted -from typing import Union +from typing import Union, Optional def refresh(): @@ -45,17 +45,14 @@ class AuthoringData: data = {} type_thumbnails = {} types_per_page = 9 - ifc_element_type = None is_loaded = False @classmethod - def load(cls, ifc_element_type=None): + def load(cls, ifc_element_type: Optional[str] = None): cls.is_loaded = True cls.props = tool.Model.get_model_props() - if ifc_element_type: - cls.ifc_element_type = None if ifc_element_type == "all" else ifc_element_type cls.data["default_container"] = cls.default_container() - cls.data["ifc_element_type"] = cls.ifc_element_type + cls.data["ifc_element_type"] = ifc_element_type cls.data["ifc_classes"] = cls.ifc_classes() cls.data["ifc_class_current"] = cls.ifc_class_current() # Make sure .ifc_classes() was run before next lines @@ -97,7 +94,7 @@ class AuthoringData: @classmethod def default_container(cls) -> str | None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = tool.Spatial.get_spatial_props() if props.default_container: try: return tool.Ifc.get().by_id(props.default_container).Name diff --git a/src/bonsai/bonsai/bim/module/model/handler.py b/src/bonsai/bonsai/bim/module/model/handler.py index 2b30a1238c..b69b595c34 100644 --- a/src/bonsai/bonsai/bim/module/model/handler.py +++ b/src/bonsai/bonsai/bim/module/model/handler.py @@ -20,7 +20,6 @@ import bpy import ifcopenshell import ifcopenshell.api from bonsai.bim.module.model import product, wall, slab, profile, opening, task -from bonsai.bim.ifc import IfcStore from bpy.app.handlers import persistent diff --git a/src/bonsai/bonsai/bim/module/model/opening.py b/src/bonsai/bonsai/bim/module/model/opening.py index 0e73e71a91..319ffdd345 100644 --- a/src/bonsai/bonsai/bim/module/model/opening.py +++ b/src/bonsai/bonsai/bim/module/model/opening.py @@ -38,7 +38,6 @@ import bonsai.tool as tool import bonsai.core.geometry import bonsai.bim.import_ifc as import_ifc from collections import defaultdict -from bonsai.bim.ifc import IfcStore from math import pi, radians from mathutils import Vector, Matrix from bpy.types import Operator @@ -236,9 +235,9 @@ class FilledOpeningGenerator: voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element] for voided_element in voided_elements: voided_obj = tool.Ifc.get_object(voided_element) - if not voided_obj.data: + representation = tool.Geometry.get_active_representation(voided_obj) + if not representation: continue - representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id) bonsai.core.geometry.switch_representation( tool.Ifc, tool.Geometry, @@ -465,10 +464,13 @@ class AddBoolean(Operator, tool.Ifc.Operator): self.report({"INFO"}, "At least two representation items must be selected to add a boolean.") return {"CANCELLED"} - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() - first_item = tool.Ifc.get().by_id(first_obj.data.BIMMeshProperties.ifc_definition_id) - second_items = [tool.Ifc.get().by_id(o.data.BIMMeshProperties.ifc_definition_id) for o in second_objs] + first_item = tool.Geometry.get_active_representation(first_obj) + assert first_item + second_items = [ + representation for o in second_objs if (representation := tool.Geometry.get_active_representation(o)) + ] booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator) rep_obj = tool.Geometry.get_geometry_props().representation_obj @@ -664,6 +666,7 @@ class EditOpenings(Operator, tool.Ifc.Operator): def edit_openings( self, building_objs: set[bpy.types.Object], opening_elements: set[ifcopenshell.entity_instance] ) -> None: + props = tool.Geometry.get_geometry_props() objects_to_remove: set[bpy.types.Object] = set() for opening_element in opening_elements: opening_obj = tool.Ifc.get_object(opening_element) @@ -690,8 +693,8 @@ class EditOpenings(Operator, tool.Ifc.Operator): self.get_all_building_objects_of_similar_openings(opening_element) ) # NB this has nothing to do with clone similar_opening tool.Ifc.unlink(element=opening_element) - if bpy.context.scene.BIMGeometryProperties.representation_obj == opening_obj: - bpy.context.scene.BIMGeometryProperties.representation_obj = None + if props.representation_obj == opening_obj: + props.representation_obj = None objects_to_remove.add(opening_obj) tool.Blender.remove_data_blocks(objects_to_remove, remove_unused_data=True) @@ -817,11 +820,11 @@ class RemoveBoolean(Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() return props.active_boolean def _execute(self, context): - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() ifcopenshell.api.geometry.remove_boolean( tool.Ifc.get(), tool.Ifc.get().by_id(props.active_boolean.ifc_definition_id) ) @@ -841,7 +844,7 @@ class SelectBoolean(Operator): @classmethod def poll(cls, context): - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() return props.active_boolean def invoke(self, context, event): @@ -850,9 +853,9 @@ class SelectBoolean(Operator): return self.execute(context) def execute(self, context): - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() queue = [tool.Ifc.get().by_id(props.active_boolean.ifc_definition_id)] - items = {i.ifc_definition_id: i.obj for i in context.scene.BIMGeometryProperties.item_objs} + items = {i.ifc_definition_id: i.obj for i in tool.Geometry.get_geometry_props().item_objs} while queue: item = queue.pop() if item.is_a("IfcBooleanResult"): @@ -911,11 +914,12 @@ class DecorationsHandler: gpu.state.point_size_set(6) gpu.state.blend_set("ALPHA") + gprops = tool.Geometry.get_geometry_props() for opening in props.openings: obj = opening.obj - if context.scene.BIMGeometryProperties.representation_obj == obj: + if gprops.representation_obj == obj: # We are editing the representation of the opening : - for item in context.scene.BIMGeometryProperties.item_objs: + for item in gprops.item_objs: if item.obj.mode == "EDIT": obj = item.obj break diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 2f780015a1..d879a5d474 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -35,7 +35,6 @@ import bonsai.core.root import bonsai.core.geometry import bonsai.core.model as core import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore from math import pi, sin, cos, degrees, tan, radians from mathutils import Vector, Matrix, Quaternion from bonsai.bim.module.model.opening import FilledOpeningGenerator @@ -430,71 +429,90 @@ def get_horizontal_profile_preview_data(context, relating_type): case "9": grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts] - # Create profile curve - scale_mat = Matrix.Scale(-1, 4, (1.0, 0.0, 0.0)) - grouped_verts = [scale_mat @ Vector(v) for v in grouped_verts] - profile_curve = bpy.data.curves.new("Profile", type="CURVE") - profile_curve.dimensions = "2D" - profile_curve.splines.new("POLY") - profile_curve.splines[0].points.add(len(grouped_verts)) - - for i, point in enumerate(profile_curve.splines[0].points): - if i == len(grouped_verts): # Close curve - point.co = Vector((*grouped_verts[0], 0)) - continue - point.co = Vector((*grouped_verts[i], 0)) - profile_obj = bpy.data.objects.new("Profile", profile_curve) - - # Create path curve with profile object as bevel - path_curve = bpy.data.curves.new("Polyline", type="CURVE") - path_curve.dimensions = "2D" - path_curve.splines.new("POLY") - path_curve.splines[0].points.add(len(polyline_verts) - 1) - for i, point in enumerate(path_curve.splines[0].points): - point.co = Vector((*polyline_verts[i], 0)) - path_curve.splines[0].use_smooth = False - path_curve.bevel_mode = "OBJECT" - path_curve.bevel_object = profile_obj - - # Convert path curve to mesh - # This operation throws a warning when done during gpu drawing, so it was removed from the decorator file to be handled here - path_obj = bpy.data.objects.new("Preview", path_curve) - context.scene.collection.objects.link(path_obj) - bpy.context.view_layer.objects.active = path_obj - dg = context.evaluated_depsgraph_get() - path_obj = path_obj.evaluated_get(dg) - me = path_obj.to_mesh() - - # Create bmesh from path mesh - bm = bmesh.new() - new_verts = [bm.verts.new(v.co) for v in me.vertices] - index = [[v for v in edge.vertices] for edge in me.edges] - new_edges = [bm.edges.new((new_verts[i[0]], new_verts[i[1]])) for i in index] - for face in me.polygons: - verts = [new_verts[i] for i in face.vertices] - bm.faces.new(verts) - bm.verts.index_update() - bm.edges.index_update() - tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()] - - bpy.data.objects.remove(bpy.data.objects[path_obj.name], do_unlink=True) - bpy.data.objects.remove(bpy.data.objects[profile_obj.name], do_unlink=True) - try: - bpy.data.curves.remove(profile_obj.data, do_unlink=True) - except: - pass - try: - bpy.data.curves.remove(path_obj.data, do_unlink=True) - except: - pass - data = {} - data["verts"] = [tuple(v.co) for v in bm.verts] - data["edges"] = [(edge.verts[0].index, edge.verts[1].index) for edge in bm.edges] + data["verts"] = [] + data["edges"] = [] + data["tris"] = [] + + grouped_verts = [(v) for v in grouped_verts] + + all_bm = bmesh.new() + for i in range(len(polyline_verts) - 1): + mesh = bpy.data.meshes.new("TempMesh") + # Create the initial mesh from the profile verts + bm = create_bmesh_from_vertices(grouped_verts, is_closed=True) + bm.verts.ensure_lookup_table() + # Creates the clipping plane formed by two segments. + # The first one is for the profile start, based on the current and previous segment of the polyline. + # The second is for the profile end, based on the current and the next segment. + if i == 0: + d = (polyline_verts[i + 1] - polyline_verts[i]).normalized() + clip_start = d + else: + d1 = (polyline_verts[i] - polyline_verts[i - 1]).normalized() + d2 = (polyline_verts[i] - polyline_verts[i + 1]).normalized() + clip_start = (d1 - d2).normalized() + + if i == len(polyline_verts) - 2: + d = (polyline_verts[i + 1] - polyline_verts[i]).normalized() + clip_end = d + else: + d1 = (polyline_verts[i + 1] - polyline_verts[i]).normalized() + d2 = (polyline_verts[i + 1] - polyline_verts[i + 2]).normalized() + clip_end = (d1 - d2).normalized() + + # Rotates the profile face to the right direction + direction = polyline_verts[i + 1] - polyline_verts[i] + position = polyline_verts[i] + rotation_matrix = direction.to_track_quat("Z", "Y").to_matrix().to_4x4() + bmesh.ops.transform(bm, verts=bm.verts, matrix=rotation_matrix) + bmesh.ops.translate(bm, verts=bm.verts, vec=position) + bmesh.ops.translate(bm, verts=bm.verts, vec=-direction) + + # Extrude and move the new face + last_face = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:]) + new_verts = [e for e in last_face["geom"] if isinstance(e, bmesh.types.BMVert)] + bmesh.ops.translate(bm, verts=new_verts, vec=direction * 3) + # Apply the cutting planes + cut = bmesh.ops.bisect_plane( + bm, + geom=bm.verts[:] + bm.edges[:] + bm.faces[:], + plane_co=polyline_verts[i], + plane_no=clip_start, + clear_inner=True, + ) + bm.verts.index_update() + bm.edges.index_update() + cut = bmesh.ops.bisect_plane( + bm, + geom=bm.verts[:] + bm.edges[:] + bm.faces[:], + plane_co=polyline_verts[i + 1], + plane_no=clip_end, + clear_outer=True, + ) + + bm.to_mesh(mesh) + bm.free() + mesh.update() + all_bm.from_mesh(mesh) + bpy.data.meshes.remove(bpy.data.meshes["TempMesh"]) + + # It's necessary to add the mesh to an object to get the expected result. + mesh = bpy.data.meshes.new("TempMesh2") + all_bm.to_mesh(mesh) + all_bm.free() + obj = bpy.data.objects.new("TempObj", mesh) + bm = bmesh.new() + bm.from_mesh(obj.data) + bpy.data.meshes.remove(bpy.data.meshes["TempMesh2"]) + + verts = [tuple(v.co) for v in bm.verts] + edges = [[v.index for v in e.verts] for e in bm.edges] + tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()] + data["verts"] = verts + data["edges"] = edges data["tris"] = tris - bm.free() - return data @@ -598,7 +616,7 @@ class PolylineOperator: self.report({"WARNING"}, "The number typed is not valid.") return is_valid else: - if self.input_type in {"X", "Y"}: + if self.input_type in {"X", "Y", "Z"}: tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) elif self.input_type in {"D", "A"}: tool.Polyline.calculate_x_y_and_z(context, self.input_ui, self.tool_state) @@ -633,21 +651,21 @@ class PolylineOperator: if x: if event.shift and event.value == "PRESS" and event.type == "X": self.tool_state.use_default_container = False - self.tool_state.plane_method = "YZ" + self.tool_state.plane_method = "YZ" if self.tool_state.plane_method != "YZ" else None self.tool_state.axis_method = None tool.Blender.update_viewport() if y: if event.shift and event.value == "PRESS" and event.type == "Y": self.tool_state.use_default_container = False - self.tool_state.plane_method = "XZ" + self.tool_state.plane_method = "XZ" if self.tool_state.plane_method != "XZ" else None self.tool_state.axis_method = None tool.Blender.update_viewport() if z: if event.shift and event.value == "PRESS" and event.type == "Z": self.tool_state.use_default_container = False - self.tool_state.plane_method = "XY" + self.tool_state.plane_method = "XY" if self.tool_state.plane_method != "XY" else None self.tool_state.axis_method = None tool.Blender.update_viewport() diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index ddc9eb5fea..4e4ebd731d 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -58,7 +58,8 @@ class AddEmptyType(bpy.types.Operator, AddObjectHelper): def execute(self, context): obj = bpy.data.objects.new("TYPEX", None) context.scene.collection.objects.link(obj) - context.scene.BIMRootProperties.ifc_product = "IfcElementType" + rprops = tool.Root.get_root_props() + rprops.ifc_product = "IfcElementType" tool.Blender.select_and_activate_single_object(context, obj) return {"FINISHED"} @@ -71,7 +72,7 @@ class AddDefaultType(bpy.types.Operator, tool.Ifc.Operator): ifc_element_type: bpy.props.StringProperty() def _execute(self, context): - props = context.scene.BIMRootProperties + props = tool.Root.get_root_props() props.ifc_product = "IfcElementType" props.ifc_class = self.ifc_element_type if self.ifc_element_type == "IfcWallType": @@ -363,7 +364,7 @@ class AddConstrTypeInstance(bpy.types.Operator, tool.Ifc.Operator): ) bonsai.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type) - rprops = context.scene.BIMRootProperties + rprops = tool.Root.get_root_props() ifc_context = None if get_enum_items(rprops, "contexts", context): ifc_context = int(rprops.contexts or "0") or None @@ -757,7 +758,7 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings): elements.append(element) for element in elements: - obj = IfcStore.get_element(element.id()) + obj = tool.Ifc.get_object_by_identifier(element.id()) if not obj: continue representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index 6286422f99..c094da200c 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -22,6 +22,7 @@ import bmesh import mathutils.geometry import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.geometry import ifcopenshell.util.type import ifcopenshell.util.unit import ifcopenshell.util.element @@ -47,6 +48,7 @@ class DumbProfileGenerator: self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) def generate(self, insertion_type="CURSOR"): + self.insertion_type = insertion_type self.file = tool.Ifc.get() self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) material = ifcopenshell.util.element.get_material(self.relating_type) @@ -69,9 +71,9 @@ class DumbProfileGenerator: self.rotation = 0 self.location = Vector((0, 0, 0)) self.cardinal_point = int(props.cardinal_point) - if insertion_type == "POLYLINE": + if self.insertion_type == "POLYLINE": return self.derive_from_polyline() - elif insertion_type == "CURSOR": + elif self.insertion_type == "CURSOR": return self.derive_from_cursor() def derive_from_polyline(self) -> tuple[list[Union[dict[str, Any], None]], bool]: @@ -106,13 +108,16 @@ class DumbProfileGenerator: matrix_world = Matrix() if self.relating_type.is_a() not in ("IfcColumnType", "IfcPileType"): - matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world - matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world + if self.insertion_type not in {"POLYLINE"}: + matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world + matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world + else: + rotation_matrix = self.direction.to_track_quat("Z", "Y") + matrix_world = rotation_matrix.to_matrix().to_4x4() @ matrix_world matrix_world.translation = self.location - if self.container_obj: + if self.insertion_type not in {"POLYLINE"} and self.container_obj: matrix_world.translation.z = self.container_obj.location.z - element = bonsai.core.root.assign_class( tool.Ifc, tool.Collector, @@ -170,19 +175,19 @@ class DumbProfileGenerator: return obj def create_profile_from_2_points(self, coords, should_round=False) -> Union[dict[str, Any], None]: - direction = coords[1] - coords[0] - length = direction.length + self.direction = coords[1] - coords[0] + length = self.direction.length if round(length, 4) < 0.1: return data = {"coords": coords} self.depth = length - self.rotation = atan2(direction[1], direction[0]) + self.rotation = atan2(self.direction[1], self.direction[0]) if should_round: # Round to nearest 50mm (yes, metric for now) self.length = 0.05 * round(length / 0.05) # Round to nearest 5 degrees - nearest_degree = (math.pi / 180) * 5 + nearest_degree = (pi / 180) * 5 self.rotation = nearest_degree * round(self.rotation / nearest_degree) self.location = coords[0] data["obj"] = self.create_profile() @@ -265,7 +270,7 @@ class DumbProfileRegenerator: def _regenerate_from_type(self, related_object: ifcopenshell.entity_instance) -> None: obj = tool.Ifc.get_object(related_object) - if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id: + if not obj or not tool.Geometry.get_active_representation(obj): return DumbProfileRecalculator().recalculate([obj]) @@ -501,7 +506,9 @@ class DumbProfileJoiner: "geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_axis ) - def get_placement_axes(body_representation): + def get_placement_axes( + body_representation: Union[ifcopenshell.entity_instance, None], + ) -> Union[tuple[tuple[float, float, float], tuple[float, float, float]], tuple[None, None]]: if not body_representation: return None, None extrusion = tool.Model.get_extrusion(body_representation) @@ -513,8 +520,7 @@ class DumbProfileJoiner: return ((0.0, 0.0, 1.0), (1.0, 0.0, 0.0)) old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") - new_body = ifcopenshell.api.run( - "geometry.add_profile_representation", + new_body = ifcopenshell.api.geometry.add_profile_representation( tool.Ifc.get(), context=self.body_context, profile=self.profile, @@ -527,8 +533,9 @@ class DumbProfileJoiner: if old_body: for inverse in tool.Ifc.get().get_inverse(old_body): ifcopenshell.util.element.replace_attribute(inverse, old_body, new_body) - obj.data.BIMMeshProperties.ifc_definition_id = int(new_body.id()) - obj.data.name = f"{self.body_context.id()}/{new_body.id()}" + assert isinstance(mesh := obj.data, bpy.types.Mesh) + tool.Ifc.link(new_body, mesh) + mesh.name = tool.Loader.get_mesh_name(new_body) bonsai.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_body) else: ifcopenshell.api.run( @@ -1119,6 +1126,8 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato def __init__(self): super().__init__() + self.input_ui = tool.Polyline.create_input_ui(init_z=True) + self.input_options = ["D", "A", "X", "Y", "Z"] self.relating_type = None props = tool.Model.get_model_props() relating_type_id = props.relating_type_id @@ -1163,7 +1172,9 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato self.handle_mouse_move(context, event, should_round=True) - self.choose_axis(event) + self.choose_axis(event, z=True) + + self.choose_plane(event) self.handle_snap_selection(context, event) @@ -1199,6 +1210,6 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato def _invoke(self, context, event): super().invoke(context, event) ProductDecorator.install(context) - self.tool_state.use_default_container = True - self.tool_state.plane_method = "XY" + self.tool_state.use_default_container = False + self.tool_state.plane_method = None return {"RUNNING_MODAL"} diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 7466a4f70b..4864da1348 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -60,6 +60,8 @@ def get_materials(self, context): def update_ifc_class(self, context): bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class) + AuthoringData.data["ifc_class_current"] = self.ifc_class + AuthoringData.data["type_elements"] = AuthoringData.type_elements() AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id() AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail() if tool.Blender.get_enum_safe(self, "relating_type_id") is None: diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index de3078819d..1010217cae 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -259,7 +259,7 @@ class DumbSlabPlaner: self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str], new_thickness: float ) -> None: obj = tool.Ifc.get_object(related_object) - if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id: + if not obj or not tool.Geometry.get_active_representation(obj): return material = ifcopenshell.util.element.get_material(related_object) @@ -885,11 +885,13 @@ class DrawPolylineSlab(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): return {"FINISHED"} slab = DumbSlabGenerator(self.relating_type).generate("POLYLINE") + if not slab: + return model_props = tool.Model.get_model_props() direction_sense = model_props.direction_sense offset = model_props.offset - model = IfcStore.get_file() + model = tool.Ifc.get() element = tool.Ifc.get_entity(slab) material = ifcopenshell.util.element.get_material(element) material_set_usage = model.by_id(material.id()) diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index b0a59a3916..297ce5a2d7 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -117,9 +117,8 @@ class LaunchTypeManager(bpy.types.Operator): op.ifc_product = "IfcElementType" op.ifc_class = AuthoringData.data["ifc_element_type"] or props.ifc_class or "" - if AuthoringData.data["total_types"]: - row = self.layout.row(align=True) - row.prop(props, "search_name", icon="FILTER", text="") + row = self.layout.row(align=True) + row.prop(props, "search_name", icon="FILTER", text="") columns = self.layout.column_flow(columns=3) if AuthoringData.data["total_pages"] > 0: diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 5a94be7528..daf9d9a43b 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -377,7 +377,7 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): walls, is_polyline_closed = DumbWallGenerator(self.relating_type).generate("POLYLINE") for wall in walls: - model = IfcStore.get_file() + model = tool.Ifc.get() element = tool.Ifc.get_entity(wall["obj"]) material = ifcopenshell.util.element.get_material(element) material_set_usage = model.by_id(material.id()) @@ -598,7 +598,7 @@ class DumbWallGenerator: self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) def generate(self, insertion_type="CURSOR"): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.layers = tool.Model.get_material_layer_parameters(self.relating_type) if not self.layers["thickness"]: return @@ -908,7 +908,7 @@ class DumbWallPlaner: self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str] ) -> None: obj = tool.Ifc.get_object(related_object) - if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id: + if not obj or not tool.Geometry.get_active_representation(obj): return material = ifcopenshell.util.element.get_material(related_object) @@ -1331,7 +1331,9 @@ class DumbWallJoiner: axis = body = tool.Model.get_wall_axis(obj)["reference"] self.axis = copy.deepcopy(axis) self.body = copy.deepcopy(body) - extrusion_data = self.get_extrusion_data(tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)) + representation = tool.Geometry.get_active_representation(obj) + assert representation + extrusion_data = self.get_extrusion_data(representation) height = extrusion_data["height"] x_angle = extrusion_data["x_angle"] self.clippings = [] @@ -1410,8 +1412,9 @@ class DumbWallJoiner: if old_body: for inverse in tool.Ifc.get().get_inverse(old_body): ifcopenshell.util.element.replace_attribute(inverse, old_body, new_body) - obj.data.BIMMeshProperties.ifc_definition_id = int(new_body.id()) - obj.data.name = f"{self.body_context.id()}/{new_body.id()}" + assert isinstance(mesh := obj.data, bpy.types.Mesh) + tool.Ifc.link(new_body, mesh) + mesh.name = tool.Loader.get_mesh_name(new_body) bonsai.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_body) else: ifcopenshell.api.run( @@ -1708,10 +1711,11 @@ class DumbWallJoiner: return True - def clip(self, wall1, slab2): + def clip(self, wall1: bpy.types.Object, slab2: bpy.types.Object) -> float: """returns height of the clipped wall, adds clipping plane to `clippings`""" element1 = tool.Ifc.get_entity(wall1) element2 = tool.Ifc.get_entity(slab2) + assert element1 and element2 layers1 = tool.Model.get_material_layer_parameters(element1) axis1 = tool.Model.get_wall_axis(wall1, layers1) @@ -1719,7 +1723,9 @@ class DumbWallJoiner: bases = [axis1["base"][0].to_3d(), axis1["base"][1].to_3d(), axis1["side"][0].to_3d(), axis1["side"][1].to_3d()] bases = [Vector((v[0], v[1], wall1.matrix_world.translation.z)) for v in bases] # add wall Z location - extrusion = self.get_extrusion_data(tool.Ifc.get().by_id(wall1.data.BIMMeshProperties.ifc_definition_id)) + representation = tool.Geometry.get_active_representation(wall1) + assert representation + extrusion = self.get_extrusion_data(representation) wall_dir = wall1.matrix_world.to_quaternion() @ extrusion["direction"] slab_pt = slab2.matrix_world @ Vector((0, 0, 0)) diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index ad4446b9b6..014ebd013b 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -96,14 +96,16 @@ class BimTool(WorkSpaceTool): def draw_settings( cls, context: bpy.types.Context, layout: bpy.types.UILayout, ws_tool: bpy.types.WorkSpaceTool ) -> None: - if context.scene.BIMGeometryProperties.mode == "ITEM": + props = tool.Geometry.get_geometry_props() + ifc_element_type = None if cls.ifc_element_type == "all" else cls.ifc_element_type + if props.mode == "ITEM": EditItemUI.draw(context, layout) elif ( active_ifc_object := (context.active_object and tool.Ifc.get_entity(context.active_object)) ) and context.selected_objects: - EditObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) + EditObjectUI.draw(context, layout, ifc_element_type=ifc_element_type) else: - CreateObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) + CreateObjectUI.draw(context, layout, ifc_element_type=ifc_element_type) class WallTool(BimTool): @@ -430,7 +432,7 @@ class EditItemUI: obj = context.active_object assert obj - mesh_props = obj.data.BIMMeshProperties + mesh_props = tool.Geometry.get_mesh_props(obj.data) if AuthoringData.data["is_representation_item_swept_solid"]: # TODO: support EndSweptArea for IfcRevolvedAreaSolidTapered, # will need to add second attribute for this. @@ -440,10 +442,10 @@ class EditItemUI: op = row.operator("bim.name_profile", text="", icon="TAG") op.extrusion_item_obj = obj.name - for item_attribute in obj.data.BIMMeshProperties.item_attributes: + for item_attribute in mesh_props.item_attributes: row = cls.layout.row() draw_attribute(item_attribute, cls.layout) - if len(obj.data.BIMMeshProperties.item_attributes) or AuthoringData.data["is_representation_item_swept_solid"]: + if len(mesh_props.item_attributes) or AuthoringData.data["is_representation_item_swept_solid"]: row = cls.layout.row() row.operator("bim.update_item_attributes", icon="FILE_REFRESH", text="") @@ -499,9 +501,7 @@ class CreateObjectUI: layout: bpy.types.UILayout @classmethod - def draw( - cls, context: bpy.types.Context, layout: bpy.types.UILayout, ifc_element_type: Optional[str] = None - ) -> None: + def draw(cls, context: bpy.types.Context, layout: bpy.types.UILayout, ifc_element_type: Union[str, None]) -> None: cls.layout = layout cls.props = tool.Model.get_model_props() @@ -515,15 +515,13 @@ class CreateObjectUI: if not AuthoringData.is_loaded: AuthoringData.load(ifc_element_type) - elif ifc_element_type == "all" and AuthoringData.data["ifc_element_type"] is not None: - AuthoringData.load("all") elif AuthoringData.data["ifc_element_type"] != ifc_element_type: AuthoringData.load(ifc_element_type) - if ifc_element_type and context.region.type == "TOOL_HEADER": + if context.region.type == "TOOL_HEADER": tool_name = ( "Multi Object Tool" - if ifc_element_type == "all" + if ifc_element_type is None else format_ifc_camel_case(ifc_element_type.removesuffix("Type")) + " Tool" ) cls.layout.label(text=tool_name, icon="TOOL_SETTINGS") @@ -746,8 +744,6 @@ class EditObjectUI: if not AuthoringData.is_loaded: AuthoringData.load(ifc_element_type) - elif ifc_element_type == "all" and AuthoringData.data["ifc_element_type"] is not None: - AuthoringData.load("all") elif AuthoringData.data["ifc_element_type"] != ifc_element_type: AuthoringData.load(ifc_element_type) @@ -1136,7 +1132,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): row.prop(self, "z") def hotkey_S_A(self): - if bpy.context.scene.BIMGeometryProperties.mode == "ITEM": + gprops = tool.Geometry.get_geometry_props() + if gprops.mode == "ITEM": bpy.ops.wm.call_menu(name="BIM_MT_add_representation_item") return diff --git a/src/bonsai/bonsai/bim/module/nest/operator.py b/src/bonsai/bonsai/bim/module/nest/operator.py index 00b1f15f33..f3247817ec 100644 --- a/src/bonsai/bonsai/bim/module/nest/operator.py +++ b/src/bonsai/bonsai/bim/module/nest/operator.py @@ -21,7 +21,6 @@ import ifcopenshell import ifcopenshell.util.element import bonsai.tool as tool import bonsai.core.nest as core -from bonsai.bim.ifc import IfcStore class BIM_OT_nest_assign_object(bpy.types.Operator, tool.Ifc.Operator): @@ -113,7 +112,7 @@ class BIM_OT_select_components(bpy.types.Operator): obj: bpy.props.StringProperty() def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() obj = bpy.data.objects.get(self.obj) or context.active_object components = ifcopenshell.util.element.get_components(tool.Ifc.get_entity(obj)) component_objs = set(tool.Ifc.get_object(c) for c in components) @@ -132,7 +131,7 @@ class BIM_OT_select_nest(bpy.types.Operator): obj: bpy.props.StringProperty() def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() obj = bpy.data.objects.get(self.obj) or context.active_object nest = ifcopenshell.util.element.get_nest(tool.Ifc.get_entity(obj)) nest_obj = tool.Ifc.get_object(nest) diff --git a/src/bonsai/bonsai/bim/module/nest/ui.py b/src/bonsai/bonsai/bim/module/nest/ui.py index b1983a2411..1fd8f5ca4a 100644 --- a/src/bonsai/bonsai/bim/module/nest/ui.py +++ b/src/bonsai/bonsai/bim/module/nest/ui.py @@ -16,9 +16,9 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import bonsai.tool as tool from bpy.types import Panel from bonsai.bim.module.nest.data import NestData -from bonsai.bim.ifc import IfcStore class BIM_PT_nest(Panel): @@ -37,9 +37,9 @@ class BIM_PT_nest(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"): + if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"): return False return True diff --git a/src/bonsai/bonsai/bim/module/profile/data.py b/src/bonsai/bonsai/bim/module/profile/data.py index 88bd85197c..38a32d34f7 100644 --- a/src/bonsai/bonsai/bim/module/profile/data.py +++ b/src/bonsai/bonsai/bim/module/profile/data.py @@ -94,7 +94,7 @@ class ProfileData: obj = bpy.context.active_object return ( obj - and obj.data - and hasattr(obj.data, "BIMMeshProperties") - and obj.data.BIMMeshProperties.subshape_type == "PROFILE" + and (data := obj.data) + and isinstance(data, bpy.types.Mesh) + and tool.Geometry.get_mesh_props(data).subshape_type == "PROFILE" ) diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index b0d5073429..eab0db8ef2 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -18,6 +18,7 @@ import bpy import ifcopenshell.api +import ifcopenshell.api.profile import ifcopenshell.util.element import bonsai.bim.helper import bonsai.tool as tool @@ -174,12 +175,12 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator): props.object_to_profile = None if not indices: points = [(0, 0), (0.1, 0), (0.1, 0.1), (0, 0.1), (0, 0)] - profile = ifcopenshell.api.run("profile.add_arbitrary_profile", tool.Ifc.get(), profile=points) + profile = ifcopenshell.api.profile.add_arbitrary_profile(tool.Ifc.get(), profile=points) else: if "inner_curves" not in indices: points = [(obj.data.vertices[i].co.x, obj.data.vertices[i].co.y) for i in indices["profile"]] points.append(points[0]) - profile = ifcopenshell.api.run("profile.add_arbitrary_profile", tool.Ifc.get(), profile=points) + profile = ifcopenshell.api.profile.add_arbitrary_profile(tool.Ifc.get(), profile=points) else: outer_points = [(obj.data.vertices[i].co.x, obj.data.vertices[i].co.y) for i in indices["profile"]] outer_points.append(outer_points[0]) @@ -189,8 +190,7 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator): ] for curve in inner_points: curve.append(curve[0]) - profile = ifcopenshell.api.run( - "profile.add_arbitrary_profile_with_voids", + profile = ifcopenshell.api.profile.add_arbitrary_profile_with_voids( tool.Ifc.get(), outer_profile=outer_points, inner_profiles=inner_points, @@ -248,7 +248,12 @@ class EnableEditingArbitraryProfile(bpy.types.Operator): def disable_editing_arbitrary_profile(context): obj = context.active_object - if obj and obj.type == "MESH" and obj.data and obj.data.BIMMeshProperties.subshape_type == "PROFILE": + if ( + obj + and (mesh := obj.data) + and isinstance(mesh, bpy.types.Mesh) + and tool.Geometry.get_mesh_props(mesh).subshape_type == "PROFILE" + ): ProfileDecorator.uninstall() bpy.ops.object.mode_set(mode="OBJECT") profile_mesh = obj.data diff --git a/src/bonsai/bonsai/bim/module/profile/prop.py b/src/bonsai/bonsai/bim/module/profile/prop.py index 9a2da938e6..3f25c5f4e4 100644 --- a/src/bonsai/bonsai/bim/module/profile/prop.py +++ b/src/bonsai/bonsai/bim/module/profile/prop.py @@ -21,7 +21,6 @@ import ifcopenshell import ifcopenshell.util.schema import ifcopenshell.util.attribute import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore from bonsai.bim.prop import StrProperty, Attribute from bonsai.bim.module.profile.data import ProfileData from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/project/decorator.py b/src/bonsai/bonsai/bim/module/project/decorator.py index cd00264224..1f738dbc90 100644 --- a/src/bonsai/bonsai/bim/module/project/decorator.py +++ b/src/bonsai/bonsai/bim/module/project/decorator.py @@ -31,7 +31,8 @@ from typing import Union @persistent def toggle_decorations_on_load(*args): - if bpy.context.scene.BIMProjectProperties.clipping_planes: + props = tool.Project.get_project_props() + if props.clipping_planes: ClippingPlaneDecorator.install(bpy.context) else: ClippingPlaneDecorator.uninstall() @@ -99,7 +100,7 @@ class ProjectDecorator: selected_edges = [] selected_tris = [] - props = context.scene.BIMProjectProperties + props = tool.Project.get_project_props() try: obj = props.queried_obj selected_vertices = obj["selected_vertices"] @@ -171,7 +172,8 @@ class ClippingPlaneDecorator: unselected_edges = [] unselected_tris = [] - for clipping_plane in context.scene.BIMProjectProperties.clipping_planes: + props = tool.Project.get_project_props() + for clipping_plane in props.clipping_planes: obj = clipping_plane.obj if not obj or not obj.data: continue diff --git a/src/bonsai/bonsai/bim/module/project/gizmo.py b/src/bonsai/bonsai/bim/module/project/gizmo.py index e5da7e3d52..bb372b9cb7 100644 --- a/src/bonsai/bonsai/bim/module/project/gizmo.py +++ b/src/bonsai/bonsai/bim/module/project/gizmo.py @@ -18,6 +18,7 @@ import bpy +import bonsai.tool as tool from bpy.types import GizmoGroup from mathutils import Matrix @@ -32,11 +33,12 @@ class ClippingPlane(GizmoGroup): @classmethod def poll(cls, context): obj = context.object + props = tool.Project.get_project_props() return ( context.selected_objects and obj and obj.name.startswith("ClippingPlane") - and obj in [sp.obj for sp in context.scene.BIMProjectProperties.clipping_planes] + and obj in [sp.obj for sp in props.clipping_planes] ) def setup(self, context): diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index aa1c8b5403..b91e0d8158 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -75,35 +75,37 @@ class NewProject(bpy.types.Operator): def execute(self, context): bpy.ops.wm.read_homefile() + pprops = tool.Project.get_project_props() + bim_props = tool.Blender.get_bim_props() if self.preset == "metric_m": - bpy.context.scene.BIMProjectProperties.export_schema = "IFC4" + pprops.export_schema = "IFC4" bpy.context.scene.unit_settings.system = "METRIC" bpy.context.scene.unit_settings.length_unit = "METERS" - bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE" - bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE" - bpy.context.scene.BIMProjectProperties.template_file = "0" + bim_props.area_unit = "SQUARE_METRE" + bim_props.volume_unit = "CUBIC_METRE" + pprops.template_file = "0" elif self.preset == "metric_mm": - bpy.context.scene.BIMProjectProperties.export_schema = "IFC4" + pprops.export_schema = "IFC4" bpy.context.scene.unit_settings.system = "METRIC" bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" - bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE" - bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE" - bpy.context.scene.BIMProjectProperties.template_file = "0" + bim_props.area_unit = "SQUARE_METRE" + bim_props.volume_unit = "CUBIC_METRE" + pprops.template_file = "0" elif self.preset == "imperial_ft": - bpy.context.scene.BIMProjectProperties.export_schema = "IFC4" + pprops.export_schema = "IFC4" bpy.context.scene.unit_settings.system = "IMPERIAL" bpy.context.scene.unit_settings.length_unit = "FEET" - bpy.context.scene.BIMProperties.area_unit = "square foot" - bpy.context.scene.BIMProperties.volume_unit = "cubic foot" - bpy.context.scene.BIMProjectProperties.template_file = "0" + bim_props.area_unit = "square foot" + bim_props.volume_unit = "cubic foot" + pprops.template_file = "0" elif self.preset == "demo": - bpy.context.scene.BIMProjectProperties.export_schema = "IFC4" + pprops.export_schema = "IFC4" bpy.context.scene.unit_settings.system = "METRIC" bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" - bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE" - bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE" - bpy.context.scene.BIMProjectProperties.template_file = "IFC4 Demo Template.ifc" + bim_props.area_unit = "SQUARE_METRE" + bim_props.volume_unit = "CUBIC_METRE" + pprops.template_file = "IFC4 Demo Template.ifc" if self.preset != "wizard": bpy.ops.bim.create_project() @@ -126,7 +128,7 @@ class CreateProject(bpy.types.Operator): return {"FINISHED"} def _execute(self, context): - props = context.scene.BIMProjectProperties + props = tool.Project.get_project_props() template = None if props.template_file == "0" else props.template_file if tool.Blender.is_default_scene(): for obj in bpy.data.objects: @@ -236,7 +238,7 @@ class RefreshLibrary(bpy.types.Operator): elements.update(library_file.by_type(importable_type)) rels = tool.Project.get_project_library_rels(library_file) elements = {e for e in elements if not tool.Project.is_element_assigned_to_project_library(e, rels)} - self.props.add_library_project_library("Unassigned", len(elements), 0) + self.props.add_library_project_library("Unassigned", len(elements), 0, False) ifc_project = library_file.by_type("IfcProject")[0] hierarchy = tool.Project.get_project_hierarchy(library_file) @@ -304,13 +306,14 @@ class ChangeLibraryElement(bpy.types.Operator): if self.breadcrumb_type == "LIBRARY": hierarchy = tool.Project.get_project_hierarchy(library_file) assert active_project_library is not None - if active_project_library == "NO_LIBRARY" or not hierarchy[active_project_library]: - for appendable_type in sorted(tool.Project.get_appendable_asset_types()): - elements = library_file.by_type(appendable_type) - if elements := filter_elements(elements): - self.props.add_library_asset_class(appendable_type, len(elements)) - else: + if active_project_library != "NO_LIBRARY" and hierarchy[active_project_library]: tool.Project.load_project_libraries_to_ui(active_project_library, hierarchy) + + for appendable_type in sorted(tool.Project.get_appendable_asset_types()): + elements = library_file.by_type(appendable_type) + if elements := filter_elements(elements): + self.props.add_library_asset_class(appendable_type, len(elements)) + else: # breadcrumb_type CLASS. elements = self.library_file.by_type(self.element_name) elements = list(filter_elements(elements)) @@ -501,10 +504,10 @@ class AppendEntireLibrary(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.library = IfcStore.library_file query = ", ".join(tool.Project.get_appendable_asset_types()) @@ -521,10 +524,10 @@ class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.library = IfcStore.library_file for element in ifcopenshell.util.selector.filter_elements(self.library, self.query): @@ -554,7 +557,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - poll = bool(IfcStore.get_file()) + poll = bool(tool.Ifc.get()) if not poll: cls.poll_message_set("Please create or load a project first.") return poll @@ -591,7 +594,8 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): self.import_presentation_style_from_ifc(element, context) try: - context.scene.BIMProjectProperties.library_elements[self.prop_index].is_appended = True + props = tool.Project.get_project_props() + props.library_elements[self.prop_index].is_appended = True except: # TODO Remove this terrible code when I refactor this into the core pass @@ -599,7 +603,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} def import_material_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None: - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() logger = logging.getLogger("ImportIFC") ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) ifc_importer = import_ifc.IfcImporter(ifc_import_settings) @@ -617,7 +621,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): ifc_importer.create_style(style) def import_product_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None: - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() logger = logging.getLogger("ImportIFC") ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) ifc_importer = import_ifc.IfcImporter(ifc_import_settings) @@ -630,7 +634,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): ifc_importer.place_objects_in_collections() def import_type_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None: - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() logger = logging.getLogger("ImportIFC") ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) @@ -645,7 +649,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): def import_materials(self, element: ifcopenshell.entity_instance, ifc_importer: import_ifc.IfcImporter) -> None: for material in ifcopenshell.util.element.get_materials(element): - if IfcStore.get_element(material.id()): + if tool.Ifc.get_object_by_identifier(material.id()): continue self.import_material_styles(material, ifc_importer) @@ -659,7 +663,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): if not element.is_a("IfcRepresentationItem") or not element.StyledByItem: continue for element2 in self.file.traverse(element.StyledByItem[0]): - if element2.is_a("IfcSurfaceStyle") and not IfcStore.get_element(element2.id()): + if element2.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element2.id()): ifc_importer.create_style(element2) def import_material_styles( @@ -670,7 +674,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): if not material.HasRepresentation: return for element in self.file.traverse(material.HasRepresentation[0]): - if element.is_a("IfcSurfaceStyle") and not IfcStore.get_element(element.id()): + if element.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element.id()): ifc_importer.create_style(element) @@ -767,14 +771,14 @@ class EnableEditingHeader(bpy.types.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def execute(self, context): - self.file = IfcStore.get_file() - props = context.scene.BIMProjectProperties + self.file = tool.Ifc.get() + props = tool.Project.get_project_props() props.is_editing = True - mvd = "".join(IfcStore.get_file().wrapped_data.header.file_description.description) + mvd = "".join(tool.Ifc.get().wrapped_data.header.file_description.description) if "[" in mvd: props.mvd = mvd.split("[")[1][0:-1] else: @@ -804,7 +808,7 @@ class EditHeader(bpy.types.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def execute(self, context): IfcStore.begin_transaction(self) @@ -817,8 +821,8 @@ class EditHeader(bpy.types.Operator): return result def _execute(self, context): - self.file = IfcStore.get_file() - props = context.scene.BIMProjectProperties + self.file = tool.Ifc.get() + props = tool.Project.get_project_props() props.is_editing = True self.file.wrapped_data.header.file_description.description = (f"ViewDefinition[{props.mvd}]",) @@ -829,7 +833,7 @@ class EditHeader(bpy.types.Operator): return {"FINISHED"} def record_state(self): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() return { "description": self.file.wrapped_data.header.file_description.description, "author": self.file.wrapped_data.header.file_name.author, @@ -838,14 +842,14 @@ class EditHeader(bpy.types.Operator): } def rollback(self, data): - file = IfcStore.get_file() + file = tool.Ifc.get() file.wrapped_data.header.file_description.description = data["old"]["description"] file.wrapped_data.header.file_name.author = data["old"]["author"] file.wrapped_data.header.file_name.organization = data["old"]["organisation"] file.wrapped_data.header.file_name.authorization = data["old"]["authorisation"] def commit(self, data): - file = IfcStore.get_file() + file = tool.Ifc.get() file.wrapped_data.header.file_description.description = data["new"]["description"] file.wrapped_data.header.file_name.author = data["new"]["author"] file.wrapped_data.header.file_name.organization = data["new"]["organisation"] @@ -859,7 +863,8 @@ class DisableEditingHeader(bpy.types.Operator): bl_description = "Cancel unsaved header information" def execute(self, context): - context.scene.BIMProjectProperties.is_editing = False + props = tool.Project.get_project_props() + props.is_editing = False return {"FINISHED"} @@ -975,16 +980,18 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): if not self.is_advanced and not self.should_start_fresh_session: bpy.ops.bim.convert_to_blender() - context.scene.BIMProperties.ifc_file = filepath + bim_props = tool.Blender.get_bim_props() + bim_props.ifc_file = filepath if not tool.Ifc.get(): self.report( {"ERROR"}, f"Error loading IFC file from filepath '{filepath}'. See logs above in the system console for the details.", ) return {"CANCELLED"} - context.scene.BIMProjectProperties.is_loading = True - context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement")) - context.scene.BIMProjectProperties.use_relative_project_path = self.use_relative_path + props = tool.Project.get_project_props() + props.is_loading = True + props.total_elements = len(tool.Ifc.get().by_type("IfcElement")) + props.use_relative_project_path = self.use_relative_path tool.Blender.register_toolbar() tool.Project.add_recent_ifc_project(self.get_filepath_abs()) @@ -1034,13 +1041,15 @@ class RevertProject(bpy.types.Operator, IFCFileSelector): @classmethod def poll(cls, context): - if not context.scene.BIMProperties.ifc_file: + props = tool.Blender.get_bim_props() + if not props.ifc_file: cls.poll_message_set("IFC project need to be loaded and saved on the disk.") return False return True def execute(self, context): - bpy.ops.bim.load_project(should_start_fresh_session=True, filepath=context.scene.BIMProperties.ifc_file) + props = tool.Blender.get_bim_props() + bpy.ops.bim.load_project(should_start_fresh_session=True, filepath=props.ifc_file) return {"FINISHED"} @@ -1050,20 +1059,23 @@ class LoadProjectElements(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - self.props = context.scene.BIMProjectProperties - self.file = IfcStore.get_file() + self.props = tool.Project.get_project_props() + self.file = tool.Ifc.get() bonsai.bim.schema.reload(self.file.schema_identifier) start = time.time() logger = logging.getLogger("ImportIFC") path_log = tool.Blender.get_data_dir_path("process.log") if not os.access(path_log.parent, os.W_OK): - path_log = os.path.join(tempfile.mkdtemp(), "process.log") + path_log = os.path.join( + tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None), "process.log" + ) logging.basicConfig( filename=path_log, filemode="a", level=logging.DEBUG, ) - settings = import_ifc.IfcImportSettings.factory(context, context.scene.BIMProperties.ifc_file, logger) + props = tool.Blender.get_bim_props() + settings = import_ifc.IfcImportSettings.factory(context, props.ifc_file, logger) settings.has_filter = self.props.filter_mode != "NONE" settings.should_filter_spatial_elements = self.props.should_filter_spatial_elements if self.props.filter_mode == "DECOMPOSITION": @@ -1081,7 +1093,8 @@ class LoadProjectElements(bpy.types.Operator): ifc_importer.execute() settings.logger.info("Import finished in {:.2f} seconds".format(time.time() - start)) print("Import finished in {:.2f} seconds".format(time.time() - start)) - context.scene.BIMProjectProperties.is_loading = False + props = tool.Project.get_project_props() + props.is_loading = False tool.Project.load_pset_templates() tool.Project.load_default_thumbnails() @@ -1153,7 +1166,8 @@ class ToggleFilterCategories(bpy.types.Operator): should_select: bpy.props.BoolProperty(name="Should Select", default=True) def execute(self, context): - for filter_category in context.scene.BIMProjectProperties.filter_categories: + props = tool.Project.get_project_props() + for filter_category in props.filter_categories: filter_category.is_selected = self.should_select return {"FINISHED"} @@ -1180,7 +1194,7 @@ class LinkIfc(bpy.types.Operator): directory: str def draw(self, context): - pprops = context.scene.BIMProjectProperties + pprops = tool.Project.get_project_props() row = self.layout.row() row.prop(self, "use_relative_path") row = self.layout.row() @@ -1201,7 +1215,8 @@ class LinkIfc(bpy.types.Operator): if bpy.data.filepath and filepath.samefile(bpy.data.filepath): self.report({"INFO"}, "Can't link the current .blend file") continue - new = context.scene.BIMProjectProperties.links.add() + props = tool.Project.get_project_props() + new = props.links.add() filepath = tool.Ifc.get_uri(filepath, use_relative_path=self.use_relative_path) new.name = filepath status = bpy.ops.bim.load_link(filepath=filepath, use_cache=self.use_cache) @@ -1232,9 +1247,10 @@ class UnlinkIfc(bpy.types.Operator): def execute(self, context): filepath = Path(self.filepath).as_posix() bpy.ops.bim.unload_link(filepath=filepath) - index = context.scene.BIMProjectProperties.links.find(filepath) + props = tool.Project.get_project_props() + index = props.links.find(filepath) if index != -1: - context.scene.BIMProjectProperties.links.remove(index) + props.links.remove(index) return {"FINISHED"} @@ -1254,8 +1270,9 @@ class UnloadLink(bpy.types.Operator): if tool.Blender.ensure_blender_path_is_abs(Path(library.filepath)) == filepath: bpy.data.libraries.remove(library) - links = context.scene.BIMProjectProperties.links - link = links.get(self.filepath) + props = tool.Project.get_project_props() + links = props.links + link = links[self.filepath] # Let's assume that user might delete it. if empty_handle := link.empty_handle: bpy.data.objects.remove(empty_handle) @@ -1264,7 +1281,7 @@ class UnloadLink(bpy.types.Operator): if not any([l.is_loaded for l in links]): ProjectDecorator.uninstall() # we make sure we don't draw queried object from the file that was just unlinked - elif queried_obj := context.scene.BIMProjectProperties.queried_obj: + elif queried_obj := props.queried_obj: queried_filepath = Path(queried_obj["ifc_filepath"]) if queried_filepath == filepath: ProjectDecorator.uninstall() @@ -1296,7 +1313,7 @@ class LoadLink(bpy.types.Operator): def link_blend(self, filepath: Path) -> None: with bpy.data.libraries.load(str(filepath), link=True) as (data_from, data_to): data_to.scenes = data_from.scenes - link = bpy.context.scene.BIMProjectProperties.links[self.filepath] + link = tool.Project.get_project_props().links[self.filepath] for scene in bpy.data.scenes: if not scene.library or Path(scene.library.filepath) != filepath: continue @@ -1322,13 +1339,14 @@ class LoadLink(bpy.types.Operator): if not blend_filepath.exists(): pprops = tool.Project.get_project_props() - gprops = bpy.context.scene.BIMGeoreferenceProperties + gprops = tool.Georeference.get_georeference_props() code = f""" import bpy def run(): - gprops = bpy.context.scene.BIMGeoreferenceProperties + import bonsai.tool as tool + gprops = tool.Georeference.get_georeference_props() # Our model origin becomes their host model origin gprops.host_model_origin = "{gprops.model_origin}" gprops.host_model_origin_si = "{gprops.model_origin_si}" @@ -1339,7 +1357,7 @@ def run(): gprops.blender_offset_z = "{gprops.blender_offset_z}" gprops.blender_x_axis_abscissa = "{gprops.blender_x_axis_abscissa}" gprops.blender_x_axis_ordinate = "{gprops.blender_x_axis_ordinate}" - pprops = bpy.context.scene.BIMProjectProperties + pprops = tool.Project.get_project_props() pprops.distance_limit = {pprops.distance_limit} pprops.false_origin_mode = "{pprops.false_origin_mode}" pprops.false_origin = "{pprops.false_origin}" @@ -1394,7 +1412,7 @@ except Exception as e: with open(json_filepath, "r") as f: data = json.load(f) - gprops = bpy.context.scene.BIMGeoreferenceProperties + gprops = tool.Georeference.get_georeference_props() for prop in ("model_origin", "model_origin_si", "model_project_north"): if (value := data.get(prop, None)) is not None: setattr(gprops, prop, value) @@ -1430,8 +1448,8 @@ class ToggleLinkSelectability(bpy.types.Operator): link: bpy.props.StringProperty(name="Linked IFC Filepath") def execute(self, context): - props = context.scene.BIMProjectProperties - link = props.links.get(self.link) + props = tool.Project.get_project_props() + link = props.links[self.link] self.library_filepath = tool.Blender.ensure_blender_path_is_abs(Path(self.link).with_suffix(".ifc.cache.blend")) link.is_selectable = (is_selectable := not link.is_selectable) for collection in self.get_linked_collections(): @@ -1457,8 +1475,8 @@ class ToggleLinkVisibility(bpy.types.Operator): mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE"))) def execute(self, context): - props = context.scene.BIMProjectProperties - link = props.links.get(self.link) + props = tool.Project.get_project_props() + link = props.links[self.link] self.library_filepath = tool.Blender.ensure_blender_path_is_abs(Path(self.link).with_suffix(".ifc.cache.blend")) if self.mode == "WIREFRAME": self.toggle_wireframe(link) @@ -1504,7 +1522,7 @@ class SelectLinkHandle(bpy.types.Operator): index: bpy.props.IntProperty(name="Link Index") def execute(self, context): - props = context.scene.BIMProjectProperties + props = tool.Project.get_project_props() link = props.links[self.index] handle = link.empty_handle if not handle: @@ -1546,8 +1564,9 @@ class ExportIFC(bpy.types.Operator): bpy.ops.wm.save_mainfile("INVOKE_DEFAULT") return {"FINISHED"} - self.use_relative_path = context.scene.BIMProjectProperties.use_relative_project_path - if (filepath := context.scene.BIMProperties.ifc_file) and not self.should_save_as: + self.use_relative_path = tool.Project.get_project_props().use_relative_project_path + props = tool.Blender.get_bim_props() + if (filepath := props.ifc_file) and not self.should_save_as: self.filepath = str(tool.Blender.ensure_blender_path_is_abs(Path(filepath))) return self.execute(context) if not self.filepath: @@ -1561,7 +1580,7 @@ class ExportIFC(bpy.types.Operator): return {"RUNNING_MODAL"} def execute(self, context): - project_props = context.scene.BIMProjectProperties + project_props = tool.Project.get_project_props() project_props.use_relative_project_path = self.use_relative_path if project_props.should_disable_undo_on_save: old_history_size = tool.Ifc.get().history_size @@ -1579,7 +1598,9 @@ class ExportIFC(bpy.types.Operator): logger = logging.getLogger("ExportIFC") path_log = tool.Blender.get_data_dir_path("process.log") if not os.access(path_log.parent, os.W_OK): - path_log = os.path.join(tempfile.mkdtemp(), "process.log") + path_log = os.path.join( + tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None), "process.log" + ) logging.basicConfig( filename=path_log, filemode="a", @@ -1606,18 +1627,20 @@ class ExportIFC(bpy.types.Operator): print("Export finished in {:.2f} seconds".format(time.time() - start)) # New project created in Bonsai should be in recent projects too. tool.Project.add_recent_ifc_project(Path(output_file)) - scene = context.scene - if not scene.DocProperties.ifc_files: - new = scene.DocProperties.ifc_files.add() + props = tool.Drawing.get_document_props() + if not props.ifc_files: + new = props.ifc_files.add() new.name = output_file - if context.scene.BIMProjectProperties.use_relative_project_path and bpy.data.is_saved: + props = tool.Project.get_project_props() + if props.use_relative_project_path and bpy.data.is_saved: output_file = os.path.relpath(output_file, bpy.path.abspath("//")) - if scene.BIMProperties.ifc_file != output_file and extension not in ("ifczip", "ifcjson"): - scene.BIMProperties.ifc_file = output_file + bim_props = tool.Blender.get_bim_props() + if bim_props.ifc_file != output_file and extension not in ("ifczip", "ifcjson"): + bim_props.ifc_file = output_file save_blend_file = bool(bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath) if save_blend_file: bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath) - bpy.context.scene.BIMProperties.is_dirty = False + bim_props.is_dirty = False bonsai.bim.handler.refresh_ui_data() self.report( {"INFO"}, @@ -1655,8 +1678,8 @@ class LoadLinkedProject(bpy.types.Operator): start = time.time() - pprops = bpy.context.scene.BIMProjectProperties - gprops = bpy.context.scene.BIMGeoreferenceProperties + pprops = tool.Project.get_project_props() + gprops = tool.Georeference.get_georeference_props() self.filepath = Path(self.filepath).as_posix() print("Processing", self.filepath) @@ -1870,7 +1893,7 @@ class LoadLinkedProject(bpy.types.Operator): mesh = bpy.data.meshes.new("Mesh") geometry = shape.geometry - gprops = bpy.context.scene.BIMGeoreferenceProperties + gprops = tool.Georeference.get_georeference_props() if ( gprops.has_blender_offset and geometry.verts @@ -1976,9 +1999,8 @@ class QueryLinkedElement(bpy.types.Operator): from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d LinksData.linked_data = {} - props = context.scene.BIMProjectProperties + props = tool.Project.get_project_props() props.queried_obj = None - props.quried_obj_root = None for area in bpy.context.screen.areas: if area.type == "PROPERTIES": @@ -2114,6 +2136,7 @@ class AppendInspectedLinkedElement(AppendLibraryElement): def _execute(self, context): from bonsai.bim.module.project.data import LinksData + props = tool.Project.get_project_props() if not LinksData.linked_data: self.report({"INFO"}, "No linked element found.") return {"CANCELLED"} @@ -2123,7 +2146,7 @@ class AppendInspectedLinkedElement(AppendLibraryElement): self.report({"INFO"}, "Cannot find Global Id for element.") return {"CANCELLED"} - queried_obj = context.scene.BIMProjectProperties.queried_obj + queried_obj = props.queried_obj ifc_file = tool.Ifc.get() linked_ifc_file: ifcopenshell.file @@ -2270,34 +2293,36 @@ class RefreshClippingPlanes(bpy.types.Operator): def modal(self, context, event): should_refresh = False + props = tool.Project.get_project_props() self.clean_deleted_planes(context) - for clipping_plane in context.scene.BIMProjectProperties.clipping_planes: + for clipping_plane in props.clipping_planes: if clipping_plane.obj and self.is_moved(clipping_plane.obj): should_refresh = True break - total_planes = len(context.scene.BIMProjectProperties.clipping_planes) + total_planes = len(props.clipping_planes) if should_refresh or total_planes != self.total_planes: self.refresh_clipping_planes(context) - for clipping_plane in context.scene.BIMProjectProperties.clipping_planes: + for clipping_plane in props.clipping_planes: if clipping_plane.obj: tool.Geometry.record_object_position(clipping_plane.obj) self.total_planes = total_planes return {"PASS_THROUGH"} - def clean_deleted_planes(self, context): + def clean_deleted_planes(self, context: bpy.types.Context) -> None: + props = tool.Project.get_project_props() while True: - for i, clipping_plane in enumerate(context.scene.BIMProjectProperties.clipping_planes): + for i, clipping_plane in enumerate(props.clipping_planes): if clipping_plane.obj: try: clipping_plane.obj.name except: - context.scene.BIMProjectProperties.clipping_planes.remove(i) + props.clipping_planes.remove(i) break else: - context.scene.BIMProjectProperties.clipping_planes.remove(i) + props.clipping_planes.remove(i) break else: break @@ -2321,14 +2346,15 @@ class RefreshClippingPlanes(bpy.types.Operator): region = next(r for r in area.regions if r.type == "WINDOW") data = region.data - if not len(context.scene.BIMProjectProperties.clipping_planes): + props = tool.Project.get_project_props() + if not len(props.clipping_planes): data.use_clip_planes = False else: with bpy.context.temp_override(area=area, region=region): bpy.ops.view3d.clip_border() clip_planes = [] - for clipping_plane in bpy.context.scene.BIMProjectProperties.clipping_planes: + for clipping_plane in tool.Project.get_project_props().clipping_planes: obj = clipping_plane.obj if not obj: continue @@ -2367,8 +2393,8 @@ class CreateClippingPlane(bpy.types.Operator): from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d # Clean up deleted planes - - if len(context.scene.BIMProjectProperties.clipping_planes) > 5: + props = tool.Project.get_project_props() + if len(props.clipping_planes) > 5: self.report({"INFO"}, "Maximum of six clipping planes allowed.") return {"FINISHED"} @@ -2408,7 +2434,7 @@ class CreateClippingPlane(bpy.types.Operator): context.scene.cursor.location = location - new = context.scene.BIMProjectProperties.clipping_planes.add() + new = tool.Project.get_project_props().clipping_planes.add() new.obj = plane_obj tool.Blender.set_active_object(plane_obj) @@ -2439,7 +2465,7 @@ class FlipClippingPlane(bpy.types.Operator): def execute(self, context): obj = context.active_object - if obj in context.scene.BIMProjectProperties.clipping_planes_objs: + if obj in tool.Project.get_project_props().clipping_planes_objs: obj.rotation_euler[0] += radians(180) context.view_layer.update() return {"FINISHED"} @@ -2457,14 +2483,15 @@ class BIM_OT_save_clipping_planes(bpy.types.Operator): @classmethod def poll(cls, context): if IfcStore.path: - return context.scene.BIMProjectProperties.clipping_planes + return tool.Project.get_project_props().clipping_planes cls.poll_message_set("Please Save The IFC File") def execute(self, context): clipping_planes_to_serialize = defaultdict(dict) - clipping_planes = context.scene.BIMProjectProperties.clipping_planes + clipping_planes = tool.Project.get_project_props().clipping_planes for clipping_plane in clipping_planes: obj = clipping_plane.obj + assert obj name = obj.name clipping_planes_to_serialize[name]["location"] = obj.location[0:3] clipping_planes_to_serialize[name]["rotation"] = obj.rotation_euler[0:3] @@ -2490,13 +2517,14 @@ class BIM_OT_load_clipping_planes(bpy.types.Operator): cls.poll_message_set("Please Save The IFC File") def execute(self, context): - bpy.data.batch_remove(context.scene.BIMProjectProperties.clipping_planes_objs) - context.scene.BIMProjectProperties.clipping_planes.clear() + props = tool.Project.get_project_props() + bpy.data.batch_remove(props.clipping_planes_objs) + props.clipping_planes.clear() with open(Path(IfcStore.path).with_name(CLIPPING_PLANES_FILE_NAME), "r") as file: clipping_planes_dict = json.load(file) for name, values in clipping_planes_dict.items(): bpy.ops.bim.create_clipping_plane() - obj = context.scene.BIMProjectProperties.clipping_planes_objs[-1] + obj = props.clipping_planes_objs[-1] obj.name = name obj.location = values["location"] obj.rotation_euler = values["rotation"] diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index e56826be57..a570724fee 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -121,7 +121,7 @@ def update_filter_mode(self: "BIMProjectProperties", context: bpy.types.Context) self.filter_categories.clear() if self.filter_mode == "NONE": return - file = IfcStore.get_file() + file = tool.Ifc.get() if self.filter_mode == "DECOMPOSITION": if file.schema == "IFC2X3": elements = file.by_type("IfcSpatialStructureElement") @@ -180,6 +180,8 @@ class LibraryElement(PropertyGroup): element_type: EnumProperty(items=[(i, i, "") for i in get_args(LibraryElementType)], name="Element Type") # Asset group. asset_count: IntProperty(name="Asset Count") + # Asset library. + has_sublibraries: BoolProperty(name="Has Sublibraries", default=False) # Asset. ifc_definition_id: IntProperty(name="IFC Definition ID") is_declared: BoolProperty(name="Is Declared", default=False) @@ -194,6 +196,7 @@ class LibraryElement(PropertyGroup): name: str element_type: LibraryElementType asset_count: int + has_sublibraries: bool ifc_definition_id: int is_declared: bool is_appended: bool @@ -299,7 +302,14 @@ class BIMProjectProperties(PropertyGroup): should_merge_materials_by_colour: BoolProperty(name="Merge Materials by Colour", default=False) should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False) should_load_geometry: BoolProperty(name="Load Geometry", default=True) - should_clean_mesh: BoolProperty(name="Clean Meshes", default=False) + should_clean_mesh: BoolProperty( + name="Clean Meshes", + description=( + "Convert all triangles to quads for meshes. " + "By default Bonsai is importing meshes triangulated (even if they are not stored as triangulated in IFC)." + ), + default=False, + ) should_cache: BoolProperty(name="Cache", default=False) deflection_tolerance: FloatProperty(name="Deflection Tolerance", default=0.001) angular_tolerance: FloatProperty(name="Angular Tolerance", default=0.5) @@ -395,12 +405,15 @@ class BIMProjectProperties(PropertyGroup): def clipping_planes_objs(self) -> list[bpy.types.Object]: return list({cp.obj for cp in self.clipping_planes if cp.obj}) - def add_library_project_library(self, name: str, asset_count: int, ifc_definition_id: int) -> LibraryElement: + def add_library_project_library( + self, name: str, asset_count: int, ifc_definition_id: int, has_sublibraries: bool + ) -> LibraryElement: new = self.library_elements.add() new["name"] = name new.asset_count = asset_count new.element_type = "LIBRARY" new.ifc_definition_id = ifc_definition_id + new.has_sublibraries = has_sublibraries return new def add_library_asset_class(self, name: str, asset_count: int) -> LibraryElement: diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index cd0c69512f..c5d390c780 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -28,7 +28,7 @@ from bonsai.bim.module.project.data import ProjectData, LinksData from typing import TYPE_CHECKING if TYPE_CHECKING: - from bonsai.bim.module.project.prop import LibraryElement, BIMProjectProperties + from bonsai.bim.module.project.prop import LibraryElement, BIMProjectProperties, FilterCategory, Link def file_import_menu(self, context): @@ -151,9 +151,9 @@ class BIM_PT_project(Panel): self.layout.use_property_decorate = False self.layout.use_property_split = True - props = context.scene.BIMProperties + props = tool.Blender.get_bim_props() pprops = self.props = tool.Project.get_project_props() - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() if pprops.is_loading: self.draw_advanced_loading_ui(context) elif self.file or props.ifc_file: @@ -247,7 +247,7 @@ class BIM_PT_project(Panel): def draw_editing_buttons(self, context, row): pprops = self.props - if IfcStore.get_file(): + if tool.Ifc.get(): if pprops.is_editing: row.operator("bim.edit_header", icon="CHECKMARK", text="") row.operator("bim.disable_editing_header", icon="CANCEL", text="") @@ -257,10 +257,10 @@ class BIM_PT_project(Panel): def draw_editable_file_info(self, context): pprops = self.props - if IfcStore.get_file(): + if tool.Ifc.get(): row = self.layout.row(align=True) row.label(text="IFC Schema", icon="FILE_CACHE") - row.label(text=IfcStore.get_file().schema) + row.label(text=tool.Ifc.get().schema) if pprops.is_editing: row = self.layout.row(align=True) @@ -281,7 +281,7 @@ class BIM_PT_project(Panel): else: row = self.layout.row(align=True) row.label(text="IFC MVD", icon="FILE_HIDDEN") - mvd = "".join(IfcStore.get_file().wrapped_data.header.file_description.description) + mvd = "".join(tool.Ifc.get().wrapped_data.header.file_description.description) if "[" in mvd: mvd = mvd.split("[")[1][0:-1] row.label(text=mvd) @@ -305,7 +305,7 @@ class BIM_PT_project(Panel): def draw_loaded_project_ui(self, context): # file name row - props = context.scene.BIMProperties + props = tool.Blender.get_bim_props() file_name_row = self.layout.row(align=True) file_name_row.label(text=os.path.basename(props.ifc_file), icon="FILE") self.draw_editing_buttons(context, file_name_row) @@ -315,7 +315,7 @@ class BIM_PT_project(Panel): # file path row and actions section row = self.layout.row(align=True) - if context.scene.BIMProperties.is_dirty: + if props.is_dirty: row.label(text="Saved*", icon="EXPORT") else: row.label(text="Saved", icon="EXPORT") @@ -339,7 +339,7 @@ class BIM_PT_new_project_wizard(Panel): self.layout.use_property_decorate = False self.layout.use_property_split = True - props = context.scene.BIMProperties + props = tool.Blender.get_bim_props() pprops = tool.Project.get_project_props() prop_with_search(self.layout, pprops, "export_schema") row = self.layout.row() @@ -512,7 +512,7 @@ class BIM_UL_library(UIList): ): if item: row = layout.row(align=True) - if item.element_type != "ASSET" and item.asset_count > 0: + if item.element_type != "ASSET" and (item.asset_count > 0 or item.has_sublibraries): op = row.operator("bim.change_library_element", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False) op.element_name = item.name op.breadcrumb_type = item.element_type @@ -542,7 +542,16 @@ class BIM_UL_library(UIList): class BIM_UL_filter_categories(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMProjectProperties, + item: FilterCategory, + icon, + active_data, + active_propname, + ): if item: row = layout.row(align=True) row.label(text=f"{item.name} ({item.total_elements})") @@ -556,7 +565,17 @@ class BIM_UL_filter_categories(UIList): class BIM_UL_links(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMProjectProperties, + item: Link, + icon, + active_data, + active_propname, + index, + ): if item: row = layout.row(align=True) if item.is_loaded: diff --git a/src/bonsai/bonsai/bim/module/pset/operator.py b/src/bonsai/bonsai/bim/module/pset/operator.py index 4165951a53..691d2e6930 100644 --- a/src/bonsai/bonsai/bim/module/pset/operator.py +++ b/src/bonsai/bonsai/bim/module/pset/operator.py @@ -94,7 +94,7 @@ class EditPset(bpy.types.Operator, tool.Ifc.Operator): properties: bpy.props.StringProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = tool.Pset.get_pset_props(self.obj, self.obj_type) ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context) element = tool.Ifc.get().by_id(ifc_definition_id) @@ -226,7 +226,7 @@ class AddQto(bpy.types.Operator, tool.Ifc.Operator): obj_type: bpy.props.StringProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() qto_name = tool.Pset.get_pset_name(self.obj, self.obj_type, pset_type="QTO") bpy.ops.bim.enable_pset_editing( pset_id=0, pset_name=qto_name, pset_type="QTO", obj=self.obj, obj_type=self.obj_type @@ -314,7 +314,7 @@ class BIM_OT_rename_parameters(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props_to_map = context.scene.RenameProperties - ifc_file = IfcStore.get_file() + ifc_file = tool.Ifc.get() all_ifc_elements = ifc_file.by_type("IfcElement") for ifc_element in all_ifc_elements: @@ -347,7 +347,7 @@ class BIM_OT_add_edit_custom_property(bpy.types.Operator, tool.Ifc.Operator): index: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = context.scene.AddEditProperties for obj in tool.Blender.get_selected_objects(): @@ -402,7 +402,7 @@ class BIM_OT_bulk_remove_psets(bpy.types.Operator, tool.Ifc.Operator): index: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = context.scene.DeletePsets for obj in tool.Blender.get_selected_objects(): diff --git a/src/bonsai/bonsai/bim/module/pset/prop.py b/src/bonsai/bonsai/bim/module/pset/prop.py index 86a3363557..6a1b68841f 100644 --- a/src/bonsai/bonsai/bim/module/pset/prop.py +++ b/src/bonsai/bonsai/bim/module/pset/prop.py @@ -26,7 +26,6 @@ import bonsai.tool as tool from bonsai.bim.prop import Attribute, StrProperty from bonsai.bim.module.pset.data import AddEditCustomPropertiesData, ObjectPsetsData, MaterialPsetsData from bonsai.bim.module.material.data import ObjectMaterialData -from bonsai.bim.ifc import IfcStore from bpy.types import PropertyGroup from bpy.props import ( PointerProperty, @@ -135,7 +134,7 @@ def get_resource_pset_names(self, context): global psetnames rprops = context.scene.BIMResourceProperties rtprops = context.scene.BIMResourceTreeProperties - ifc_class = IfcStore.get_file().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a() + ifc_class = tool.Ifc.get().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a() if ifc_class not in psetnames: psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True, schema=tool.Ifc.get_schema()) psetnames[ifc_class] = blender_formatted_enum_from_psets(psets) @@ -146,7 +145,7 @@ def get_resource_qto_names(self, context): global qtonames rprops = context.scene.BIMResourceProperties rtprops = context.scene.BIMResourceTreeProperties - ifc_class = IfcStore.get_file().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a() + ifc_class = tool.Ifc.get().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a() if ifc_class not in qtonames: psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, qto_only=True, schema=tool.Ifc.get_schema()) qtonames[ifc_class] = blender_formatted_enum_from_psets(psets) @@ -174,7 +173,7 @@ def get_group_qto_names(self, context): def get_profile_pset_names(self, context): global psetnames pprops = tool.Profile.get_profile_props() - ifc_class = IfcStore.get_file().by_id(pprops.profiles[pprops.active_profile_index].ifc_definition_id).is_a() + ifc_class = tool.Ifc.get().by_id(pprops.profiles[pprops.active_profile_index].ifc_definition_id).is_a() if ifc_class not in psetnames: psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True, schema=tool.Ifc.get_schema()) psetnames[ifc_class] = blender_formatted_enum_from_psets(psets) @@ -290,7 +289,8 @@ class AddEditProperties(PropertyGroup): enum_values: CollectionProperty(name="Enum Values", type=Attribute) def get_value_name(self) -> Union[Literal["string_value", "bool_value", "int_value", "float_value"], None]: - ifc_data_type = IfcStore.get_schema().declaration_by_name(self.primary_measure_type) + schema = tool.Ifc.schema() + ifc_data_type = schema.declaration_by_name(self.primary_measure_type) data_type = ifcopenshell.util.attribute.get_primitive_type(ifc_data_type) if data_type == "string": return "string_value" diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py index 21f3c1b083..4adda725dc 100644 --- a/src/bonsai/bonsai/bim/module/pset/ui.py +++ b/src/bonsai/bonsai/bim/module/pset/ui.py @@ -20,7 +20,6 @@ from __future__ import annotations import bpy import bonsai.tool as tool from bpy.types import Panel -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import prop_with_search, get_display_value from bonsai.bim.module.pset.data import ( ObjectPsetsData, @@ -247,7 +246,7 @@ class BIM_PT_object_psets(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False return True @@ -325,7 +324,7 @@ class BIM_PT_object_qtos(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False return True diff --git a/src/bonsai/bonsai/bim/module/pset_template/prop.py b/src/bonsai/bonsai/bim/module/pset_template/prop.py index 66cd7e3fea..fdd6188a76 100644 --- a/src/bonsai/bonsai/bim/module/pset_template/prop.py +++ b/src/bonsai/bonsai/bim/module/pset_template/prop.py @@ -21,6 +21,7 @@ import bpy import ifcopenshell import ifcopenshell.util.attribute from ifcopenshell.util.doc import get_attribute_doc +import bonsai.tool as tool from bonsai.bim.module.pset_template.data import PsetTemplatesData from bonsai.bim.prop import StrProperty, Attribute from bonsai.bim.ifc import IfcStore @@ -183,7 +184,8 @@ class PropTemplate(PropertyGroup): def get_value_name(self) -> str: if self.primary_measure_type == "-": return "string_value" - ifc_data_type = IfcStore.get_schema().declaration_by_name(self.primary_measure_type) + schema = tool.Ifc.schema() + ifc_data_type = schema.declaration_by_name(self.primary_measure_type) data_type = ifcopenshell.util.attribute.get_primitive_type(ifc_data_type) if data_type == "string": return "string_value" diff --git a/src/bonsai/bonsai/bim/module/qto/calculator.py b/src/bonsai/bonsai/bim/module/qto/calculator.py index e4ba0580a7..871b128db6 100644 --- a/src/bonsai/bonsai/bim/module/qto/calculator.py +++ b/src/bonsai/bonsai/bim/module/qto/calculator.py @@ -451,7 +451,8 @@ def get_side_area(o: bpy.types.Object) -> float: def get_cross_section_area(obj: bpy.types.Object) -> float: - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + representation = tool.Geometry.get_active_representation(obj) + assert representation item = representation.Items[0] while True: if item.is_a("IfcExtrudedAreaSolid"): diff --git a/src/bonsai/bonsai/bim/module/qto/operator.py b/src/bonsai/bonsai/bim/module/qto/operator.py index fe1574d1fa..63d16335a1 100644 --- a/src/bonsai/bonsai/bim/module/qto/operator.py +++ b/src/bonsai/bonsai/bim/module/qto/operator.py @@ -21,7 +21,6 @@ import ifcopenshell import ifcopenshell.api import bonsai.tool as tool import bonsai.core.qto as core -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.qto import helper diff --git a/src/bonsai/bonsai/bim/module/resource/prop.py b/src/bonsai/bonsai/bim/module/resource/prop.py index 8613f44d1a..b5f1360c4e 100644 --- a/src/bonsai/bonsai/bim/module/resource/prop.py +++ b/src/bonsai/bonsai/bim/module/resource/prop.py @@ -19,7 +19,6 @@ import bpy import ifcopenshell.api import ifcopenshell.util.resource -from bonsai.bim.ifc import IfcStore import bonsai.tool as tool import bonsai.bim.module.pset.data import bonsai.bim.module.resource.data diff --git a/src/bonsai/bonsai/bim/module/resource/ui.py b/src/bonsai/bonsai/bim/module/resource/ui.py index 5ffdac8881..898749ff67 100644 --- a/src/bonsai/bonsai/bim/module/resource/ui.py +++ b/src/bonsai/bonsai/bim/module/resource/ui.py @@ -17,9 +17,9 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool import bonsai.bim.helper from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.resource.data import ResourceData from typing import Any @@ -35,7 +35,7 @@ class BIM_PT_resources(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): diff --git a/src/bonsai/bonsai/bim/module/root/data.py b/src/bonsai/bonsai/bim/module/root/data.py index 10c50839ec..64edaa2a9d 100644 --- a/src/bonsai/bonsai/bim/module/root/data.py +++ b/src/bonsai/bonsai/bim/module/root/data.py @@ -54,32 +54,14 @@ class IfcClassData: @classmethod def ifc_products(cls): - products = [ - "IfcElementType", - "IfcElement", - "IfcFeatureElement", - "IfcSpatialElement", - "IfcSpatialElementType", - "IfcStructuralItem", - "IfcAnnotation", - "IfcRelSpaceBoundary", - ] + products = tool.Root.get_ifc_products() version = tool.Ifc.get_schema() - if version == "IFC2X3": - products = [ - "IfcElementType", - "IfcElement", - "IfcFeatureElement", - "IfcSpatialStructureElement", - "IfcStructuralItem", - "IfcAnnotation", - "IfcRelSpaceBoundary", - ] return [(e, e, (get_entity_doc(version, e) or {}).get("description", "")) for e in products] @classmethod def ifc_classes(cls): - ifc_product = bpy.context.scene.BIMRootProperties.ifc_product + rprops = tool.Root.get_root_props() + ifc_product = rprops.ifc_product declaration = tool.Ifc.schema().declaration_by_name(ifc_product) declarations = ifcopenshell.util.schema.get_subtypes(declaration) names = [d.name() for d in declarations] @@ -99,7 +81,8 @@ class IfcClassData: @classmethod def ifc_predefined_types(cls): types_enum = [] - ifc_class = bpy.context.scene.BIMRootProperties.ifc_class + rprops = tool.Root.get_root_props() + ifc_class = rprops.ifc_class declaration = tool.Ifc.schema().declaration_by_name(ifc_class) version = tool.Ifc.get_schema() for attribute in declaration.attributes(): @@ -133,7 +116,8 @@ class IfcClassData: @classmethod def representation_template(cls): - ifc_class = bpy.context.scene.BIMRootProperties.ifc_class + rprops = tool.Root.get_root_props() + ifc_class = rprops.ifc_class templates = [ ("EMPTY", "No Geometry", "Start with an empty object"), None, diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index 4801d3f999..a699287365 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -42,32 +42,33 @@ class EnableReassignClass(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): + rprops = tool.Root.get_root_props() obj = context.active_object self.file = tool.Ifc.get() - ifc_class = obj.name.split("/")[0] + element = tool.Ifc.get_entity(obj) + assert element + ifc_class = element.is_a() context.active_object.BIMObjectProperties.is_reassigning_class = True - ifc_products = [ - "IfcElement", - "IfcElementType", - "IfcSpatialElement", - "IfcGroup", - "IfcStructural", - "IfcPositioningElement", - "IfcContext", - "IfcAnnotation", - "IfcRelSpaceBoundary", - ] + ifc_products = tool.Root.get_ifc_products() + schema = tool.Ifc.schema() + declaration = schema.declaration_by_name(ifc_class) for ifc_product in ifc_products: - if ifcopenshell.util.schema.is_a(IfcStore.get_schema().declaration_by_name(ifc_class), ifc_product): - context.scene.BIMRootProperties.ifc_product = ifc_product + if ifcopenshell.util.schema.is_a(declaration, ifc_product): + rprops.ifc_product = ifc_product + break + else: + self.report({"ERROR"}, f"Couldn't find matching IFC product for the selected object: '{element}'.") + obj.BIMObjectProperties.is_reassigning_class = False + return {"CANCELLED"} + element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) - context.scene.BIMRootProperties.ifc_class = element.is_a() - context.scene.BIMRootProperties.relating_class_object = None + rprops.ifc_class = element.is_a() + rprops.relating_class_object = None if hasattr(element, "PredefinedType"): if element.PredefinedType: - context.scene.BIMRootProperties.ifc_predefined_type = element.PredefinedType + rprops.ifc_predefined_type = element.PredefinedType userdefined_type = ifcopenshell.util.element.get_predefined_type(element) - context.scene.BIMRootProperties.ifc_userdefined_type = userdefined_type or "" + rprops.ifc_userdefined_type = userdefined_type or "" return {"FINISHED"} @@ -94,9 +95,9 @@ class ReassignClass(bpy.types.Operator, tool.Ifc.Operator): else: objects = set(context.selected_objects + [context.active_object]) self.file = tool.Ifc.get() - root_props = context.scene.BIMRootProperties - ifc_product: str = root_props.ifc_product - ifc_class: str = root_props.ifc_class + root_props = tool.Root.get_root_props() + ifc_product = root_props.ifc_product + ifc_class = root_props.ifc_class type_ifc_class = next(iter(ifcopenshell.util.type.get_applicable_types(ifc_class, self.file.schema)), None) predefined_type = root_props.ifc_predefined_type @@ -176,7 +177,7 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator): ifc_representation_class: bpy.props.StringProperty() def _execute(self, context): - props = context.scene.BIMRootProperties + props = tool.Root.get_root_props() objects: list[bpy.types.Object] = [] if self.obj: objects = [bpy.data.objects[self.obj]] @@ -358,7 +359,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE") def _invoke(self, context, event): - props = context.scene.BIMRootProperties + props = tool.Root.get_root_props() # For convenience, preselect OBJs if applicable if props.ifc_product == "IfcFeatureElement": if (obj := tool.Blender.get_active_object(is_selected=True)) and obj.type == "MESH": @@ -376,7 +377,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): return context.window_manager.invoke_props_dialog(self) def _execute(self, context): - props = context.scene.BIMRootProperties + props = tool.Root.get_root_props() predefined_type = ( props.ifc_userdefined_type if props.ifc_predefined_type == "USERDEFINED" else props.ifc_predefined_type ) @@ -500,7 +501,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): else: material = ifcopenshell.api.run("material.add_material", tool.Ifc.get(), name="Unknown") if representation_template == "PROFILESET": - profile_id = tool.Blender.get_enum_safe(context.scene.BIMRootProperties, "profile") + profile_id = tool.Blender.get_enum_safe(props, "profile") if profile_id in ("-", None): profile = next((p for p in ifc_file.by_type("IfcProfileDef") if p.ProfileName), None) if profile is None: @@ -589,7 +590,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): tool.Blender.set_active_object(obj) def draw(self, context): - props = context.scene.BIMRootProperties + props = tool.Root.get_root_props() self.layout.use_property_split = True self.layout.use_property_decorate = False row = self.layout.row() @@ -600,7 +601,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): if not self.ifc_product: prop_with_search(self.layout, props, "ifc_product", text="Definition", should_click_ok=True) prop_with_search(self.layout, props, "ifc_class", should_click_ok=True) - ifc_predefined_types = root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context) + ifc_predefined_types = root_prop.get_ifc_predefined_types(props, context) if ifc_predefined_types: prop_with_search(self.layout, props, "ifc_predefined_type", should_click_ok=True) if props.ifc_predefined_type == "USERDEFINED": diff --git a/src/bonsai/bonsai/bim/module/root/prop.py b/src/bonsai/bonsai/bim/module/root/prop.py index 5243d95c03..4c358dfde1 100644 --- a/src/bonsai/bonsai/bim/module/root/prop.py +++ b/src/bonsai/bonsai/bim/module/root/prop.py @@ -34,28 +34,27 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING, Union -def get_ifc_predefined_types(self, context): +def get_ifc_predefined_types(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]: if not IfcClassData.is_loaded: IfcClassData.load() return IfcClassData.data["ifc_predefined_types"] -def get_representation_template(self, context): +def get_representation_template(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]: if not IfcClassData.is_loaded: IfcClassData.load() return IfcClassData.data["representation_template"] -def refresh_classes(self, context): - old_class = context.scene.BIMRootProperties.ifc_class - old_predefined_type = ( - context.scene.BIMRootProperties.ifc_predefined_type if get_ifc_predefined_types(self, context) else "" - ) +def refresh_classes(self: "BIMRootProperties", context: bpy.types.Context) -> None: + old_class = self.ifc_class + old_predefined_type = self.ifc_predefined_type if get_ifc_predefined_types(self, context) else "" enum = get_ifc_classes(self, context) - context.scene.BIMRootProperties.ifc_class = enum[0][0] + self.ifc_class = enum[0][0] IfcClassData.load() if self.ifc_product == "IfcFeatureElement": @@ -81,48 +80,44 @@ def refresh_classes(self, context): self.ifc_predefined_type = old_predefined_type -def refresh_predefined_types(self, context): +def refresh_predefined_types(self: "BIMRootProperties", context: bpy.types.Context) -> None: IfcClassData.load() enum = get_ifc_predefined_types(self, context) if enum: - context.scene.BIMRootProperties.ifc_predefined_type = enum[0][0] + self.ifc_predefined_type = enum[0][0] -def update_class_enum(self, context): - self.ifc_class = self.ifc_class_filter_enum - - -def get_ifc_products(self, context): +def get_ifc_products(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]: if not IfcClassData.is_loaded: IfcClassData.load() return IfcClassData.data["ifc_products"] -def get_ifc_classes(self, context): +def get_ifc_classes(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]: if not IfcClassData.is_loaded: IfcClassData.load() return IfcClassData.data["ifc_classes"] -def get_ifc_classes_suggestions(): +def get_ifc_classes_suggestions(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]: if not IfcClassData.is_loaded: IfcClassData.load() return IfcClassData.data["ifc_classes_suggestions"] -def get_contexts(self, context): +def get_contexts(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]: if not IfcClassData.is_loaded: IfcClassData.load() return IfcClassData.data["contexts"] -def get_profile(self, context): +def get_profile(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]: if not IfcClassData.is_loaded: IfcClassData.load() return IfcClassData.data["profile"] -def update_relating_class_from_object(self, context): +def update_relating_class_from_object(self: "BIMRootProperties", context: bpy.types.Context) -> None: if self.relating_class_object is None: return element = tool.Ifc.get_entity(self.relating_class_object) @@ -139,7 +134,7 @@ def update_relating_class_from_object(self, context): bpy.ops.bim.reassign_class() -def is_object_class_applicable(self, obj): +def is_object_class_applicable(self: "BIMRootProperties", obj: bpy.types.Object) -> bool: element = tool.Ifc.get_entity(obj) if not element: return False @@ -149,11 +144,11 @@ def is_object_class_applicable(self, obj): return element.is_a("IfcTypeObject") == active_element.is_a("IfcTypeObject") -def poll_representation_obj(self, obj): +def poll_representation_obj(self: "BIMRootProperties", obj: bpy.types.Object) -> bool: return obj.type == "MESH" and obj.data.polygons -def poll_featured_obj(self, obj): +def poll_featured_obj(self: "BIMRootProperties", obj: bpy.types.Object) -> bool: return tool.Ifc.get_entity(obj) @@ -192,3 +187,16 @@ class BIMRootProperties(PropertyGroup): getter_enum_suggestions = { "ifc_class": get_ifc_classes_suggestions, } + + if TYPE_CHECKING: + contexts: str + description: str + ifc_product: str + ifc_class: str + ifc_predefined_type: str + ifc_userdefined_type: str + featured_obj: Union[bpy.types.Object, None] + representation_template: str + representation_obj: Union[bpy.types.Object, None] + profile: str + relating_class_object: Union[bpy.types.Object, None] diff --git a/src/bonsai/bonsai/bim/module/root/ui.py b/src/bonsai/bonsai/bim/module/root/ui.py index 809230656f..6985bf832b 100644 --- a/src/bonsai/bonsai/bim/module/root/ui.py +++ b/src/bonsai/bonsai/bim/module/root/ui.py @@ -17,9 +17,9 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool import bonsai.bim.module.root.prop as root_prop from bpy.types import Panel -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import prop_with_search from bonsai.bim.module.root.data import IfcClassData from bonsai.bim.module.model.data import AuthoringData @@ -38,12 +38,13 @@ class BIM_PT_class(Panel): def poll(cls, context): if not context.active_object: return False - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not IfcClassData.is_loaded: IfcClassData.load() props = context.active_object.BIMObjectProperties + rprops = tool.Root.get_root_props() if props.ifc_definition_id: if not IfcClassData.data["has_entity"]: row = self.layout.row(align=True) @@ -58,10 +59,10 @@ class BIM_PT_class(Panel): row.operator("bim.disable_reassign_class", icon="CANCEL", text="") self.draw_class_dropdowns( context, - root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context), + root_prop.get_ifc_predefined_types(rprops, context), is_reassigning_class=True, ) - self.layout.prop(context.scene.BIMRootProperties, "relating_class_object", icon="COPYDOWN") + self.layout.prop(rprops, "relating_class_object", icon="COPYDOWN") else: row = self.layout.row(align=True) row.label( @@ -78,16 +79,16 @@ class BIM_PT_class(Panel): if AuthoringData.data["is_representation_item_active"]: return - ifc_predefined_types = root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context) + ifc_predefined_types = root_prop.get_ifc_predefined_types(rprops, context) self.draw_class_dropdowns(context, ifc_predefined_types) row = self.layout.row(align=True) op = row.operator("bim.assign_class") - op.ifc_class = context.scene.BIMRootProperties.ifc_class - op.predefined_type = context.scene.BIMRootProperties.ifc_predefined_type if ifc_predefined_types else "" - op.userdefined_type = context.scene.BIMRootProperties.ifc_userdefined_type + op.ifc_class = rprops.ifc_class + op.predefined_type = rprops.ifc_predefined_type if ifc_predefined_types else "" + op.userdefined_type = rprops.ifc_userdefined_type def draw_class_dropdowns(self, context, ifc_predefined_types, is_reassigning_class=False): - props = context.scene.BIMRootProperties + props = tool.Root.get_root_props() layout = self.layout prop_with_search(layout, props, "ifc_product") prop_with_search(layout, props, "ifc_class") diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 5083fb2445..32d3729d58 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -39,6 +39,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING, Literal, get_args class AddFilterGroup(Operator): @@ -144,13 +145,19 @@ class Search(Operator): bl_idname = "bim.search" bl_label = "Search" - property_group: bpy.props.StringProperty(name="Property Group", default="") + PropertyGroupType = Literal["CsvProperties", "BIMSearchProperties"] + property_group: bpy.props.EnumProperty( + name="Property Group", items=[(i, i, "") for i in get_args(PropertyGroupType)] + ) + + if TYPE_CHECKING: + property_group: PropertyGroupType def execute(self, context): if self.property_group == "CsvProperties": props = context.scene.CsvProperties elif self.property_group == "BIMSearchProperties": - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() else: raise Exception(f"bim.search - unexpected property group name '{self.property_group}'.") @@ -213,11 +220,12 @@ class LoadSearch(Operator, tool.Ifc.Operator): def _execute(self, context): filter_groups = tool.Search.get_filter_groups(self.module) - group = tool.Ifc.get().by_id(int(context.scene.BIMSearchProperties.saved_searches)) + props = tool.Search.get_search_props() + group = tool.Ifc.get().by_id(int(props.saved_searches)) tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups) def draw(self, context): - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() row = self.layout.row() row.prop(props, "saved_searches", text="") @@ -239,7 +247,7 @@ class ColourByProperty(Operator): return result def _execute(self, context): - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() query = props.colourscheme_query if props.colourscheme_key == "QUERY" else props.colourscheme_key if not query: @@ -358,11 +366,11 @@ class SelectByProperty(Operator): @classmethod def poll(cls, context): - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() return props.active_colourscheme_index < len(props.colourscheme) def execute(self, context): - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() query = props.colourscheme_query if props.colourscheme_key == "QUERY" else props.colourscheme_key if not query: @@ -420,7 +428,7 @@ class SaveColourscheme(Operator, tool.Ifc.Operator): if not self.name: return - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() query = props.colourscheme_query group = [g for g in tool.Ifc.get().by_type("IfcGroup") if g.Name == self.name] @@ -446,7 +454,7 @@ class LoadColourscheme(Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() group = tool.Ifc.get().by_id(int(props.saved_colourschemes)) description = json.loads(group.Description) props.colourscheme_query = description.get("colourscheme_query") @@ -458,7 +466,7 @@ class LoadColourscheme(Operator, tool.Ifc.Operator): new.colour = data["colour"] def draw(self, context): - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() row = self.layout.row() row.prop(props, "saved_colourschemes", text="") @@ -549,7 +557,7 @@ class ResetObjectColours(Operator): def execute(self, context): for obj in context.visible_objects: obj.color = (1, 1, 1, 1) - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() props.colourscheme.clear() return {"FINISHED"} @@ -562,7 +570,7 @@ class ToggleFilterSelection(Operator): action: EnumProperty(items=(("SELECT", "Select", ""), ("DESELECT", "Deselect", ""))) def execute(self, context): - props = bpy.context.scene.BIMSearchProperties + props = tool.Search.get_search_props() self.selecting_actionbool = self.action == "SELECT" if props.filter_type == "CLASSES": for ifc_class in props.filter_classes: @@ -587,7 +595,7 @@ class ActivateIfcClassFilter(Operator): return True def invoke(self, context, event): - props = bpy.context.scene.BIMSearchProperties + props = tool.Search.get_search_props() props.filter_classes.clear() ifc_types = {} for obj in context.selected_objects: @@ -606,18 +614,20 @@ class ActivateIfcClassFilter(Operator): return context.window_manager.invoke_props_dialog(self, width=250) def execute(self, context): - bpy.context.scene.BIMSearchProperties.filter_classes.clear() + props = tool.Search.get_search_props() + props.filter_classes.clear() return {"FINISHED"} def draw(self, context): + props = tool.Search.get_search_props() self.layout.template_list( "BIM_UL_ifc_class_filter", "", - context.scene.BIMSearchProperties, + props, "filter_classes", - context.scene.BIMSearchProperties, + props, "filter_classes_index", - rows=min(len(bpy.context.scene.BIMSearchProperties.filter_classes), 20), + rows=min(len(props.filter_classes), 20), ) row = self.layout.row(align=True) row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT" @@ -638,7 +648,7 @@ class ActivateContainerFilter(Operator): return True def invoke(self, context, event): - props = bpy.context.scene.BIMSearchProperties + props = tool.Search.get_search_props() props.filter_container.clear() containers = {} @@ -661,18 +671,20 @@ class ActivateContainerFilter(Operator): return context.window_manager.invoke_props_dialog(self, width=250) def execute(self, context): - bpy.context.scene.BIMSearchProperties.filter_container.clear() + props = tool.Search.get_search_props() + props.filter_container.clear() return {"FINISHED"} def draw(self, context): + props = tool.Search.get_search_props() self.layout.template_list( "BIM_UL_ifc_building_storey_filter", "", - context.scene.BIMSearchProperties, + props, "filter_container", - context.scene.BIMSearchProperties, + props, "filter_container_index", - rows=min(len(bpy.context.scene.BIMSearchProperties.filter_container), 20), + rows=min(len(props.filter_container), 20), ) row = self.layout.row(align=True) row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT" @@ -711,14 +723,14 @@ class SelectSimilar(Operator, tool.Ifc.Operator): return self.execute(context) def _execute(self, context): - props = context.scene.BIMSearchProperties obj = context.active_object element = tool.Ifc.get_entity(obj) key = self.key if key == "PredefinedType": key = "predefined_type" value = ifcopenshell.util.selector.get_element_value(element, key) - tolerance = bpy.context.scene.DocProperties.tolerance + dprops = tool.Drawing.get_document_props() + tolerance = dprops.tolerance # Determine the number of decimal places based on the magnitude of the rounding value if tolerance < 1: diff --git a/src/bonsai/bonsai/bim/module/search/prop.py b/src/bonsai/bonsai/bim/module/search/prop.py index 08cfb46d8f..61cff222d3 100644 --- a/src/bonsai/bonsai/bim/module/search/prop.py +++ b/src/bonsai/bonsai/bim/module/search/prop.py @@ -33,33 +33,34 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING -def get_element_key(self, context): +def get_element_key(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not SelectSimilarData.is_loaded: SelectSimilarData.load() return SelectSimilarData.data["element_key"] -def get_colourscheme_key(self, context): +def get_colourscheme_key(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not ColourByPropertyData.is_loaded: ColourByPropertyData.load() return ColourByPropertyData.data["colourscheme_key"] -def get_saved_searches(self, context): +def get_saved_searches(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not SearchData.is_loaded: SearchData.load() return SearchData.data["saved_searches"] -def get_saved_colourschemes(self, context): +def get_saved_colourschemes(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not ColourByPropertyData.is_loaded: ColourByPropertyData.load() return ColourByPropertyData.data["saved_colourschemes"] -def update_is_class_selected(self, context): +def update_is_class_selected(self: "BIMFilterClasses", context: bpy.types.Context) -> None: if self.is_selected: for obj in self.unselected_objects: obj.obj.select_set(True) @@ -73,7 +74,7 @@ def update_is_class_selected(self, context): new.obj = obj -def update_is_container_selected(self, context): +def update_is_container_selected(self: "BIMFilterBuildingStoreys", context: bpy.types.Context) -> None: if self.is_selected: for obj in self.unselected_objects: obj.obj.select_set(True) @@ -87,15 +88,15 @@ def update_is_container_selected(self, context): new.obj = obj -def update_show_flat_colours(self, context): +def update_show_flat_colours(self: "BIMSearchProperties", context: bpy.types.Context) -> None: + space = tool.Blender.get_view3d_space() + assert space if self.show_flat_colours: - space = tool.Blender.get_view3d_space() space.shading.light = "FLAT" space.shading.color_type = "OBJECT" space.shading.show_object_outline = True space.shading.show_cavity = True else: - space = tool.Blender.get_view3d_space() space.shading.type = "SOLID" space.shading.light = "STUDIO" space.shading.show_object_outline = True @@ -108,6 +109,11 @@ class BIMFilterClasses(PropertyGroup): total: IntProperty(name="Total") unselected_objects: CollectionProperty(type=ObjProperty, name="Unfiltered Objects") + if TYPE_CHECKING: + is_selected: bool + total: int + unselected_objects: bpy.types.bpy_prop_collection_idprop[ObjProperty] + class BIMFilterBuildingStoreys(PropertyGroup): name: StringProperty(name="Name") @@ -115,12 +121,21 @@ class BIMFilterBuildingStoreys(PropertyGroup): total: IntProperty(name="Total") unselected_objects: CollectionProperty(type=ObjProperty, name="Unfiltered Objects") + if TYPE_CHECKING: + is_selected: bool + total: int + unselected_objects: bpy.types.bpy_prop_collection_idprop[ObjProperty] + class BIMColour(PropertyGroup): name: StringProperty(name="Name") total: IntProperty(name="Total") colour: FloatVectorProperty(name="Colour", subtype="COLOR", default=(1, 0, 0), min=0.0, max=1.0) + if TYPE_CHECKING: + total: int + colour: tuple[float, float, float] + class BIMSearchProperties(PropertyGroup): element_key: EnumProperty(items=get_element_key, name="Element Key") @@ -189,6 +204,29 @@ class BIMSearchProperties(PropertyGroup): filter_container_index: IntProperty(name="Filter Level Index") show_flat_colours: BoolProperty(name="Flat Colours", default=False, update=update_show_flat_colours) + if TYPE_CHECKING: + element_key: str + filter_query: str + filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup] + facet: str + saved_searches: str + saved_colourschemes: str + colourscheme_key: str + colourscheme_query: str + palette: str + min_mode: Literal["AUTO", "MANUAL"] + max_mode: Literal["AUTO", "MANUAL"] + min_value: float + max_value: float + colourscheme: bpy.types.bpy_prop_collection_idprop[BIMColour] + active_colourscheme_index: int + filter_type: str + filter_classes: bpy.types.bpy_prop_collection_idprop[BIMFilterClasses] + filter_classes_index: int + filter_container: bpy.types.bpy_prop_collection_idprop[BIMFilterBuildingStoreys] + filter_container_index: int + show_flat_colours: bool + def get_classes(self, ifc_product): declaration = tool.Ifc.schema().declaration_by_name(ifc_product) diff --git a/src/bonsai/bonsai/bim/module/search/ui.py b/src/bonsai/bonsai/bim/module/search/ui.py index ce0b346027..a9254415e7 100644 --- a/src/bonsai/bonsai/bim/module/search/ui.py +++ b/src/bonsai/bonsai/bim/module/search/ui.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool import bonsai.bim.helper from bpy.types import Panel from bonsai.bim.module.search.data import SearchData, ColourByPropertyData, SelectSimilarData @@ -34,7 +35,7 @@ class BIM_PT_search(Panel): if not SearchData.is_loaded: SearchData.load() - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() bonsai.bim.helper.draw_filter(self.layout, props.filter_groups, SearchData, "search") @@ -73,7 +74,7 @@ class BIM_PT_colour_by_property(Panel): if not ColourByPropertyData.is_loaded: ColourByPropertyData.load() - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() row = self.layout.row(align=True) row.label(text=f"{len(ColourByPropertyData.data['saved_colourschemes'])} Saved Colourschemes") @@ -127,7 +128,7 @@ class BIM_PT_select_similar(Panel): if not SelectSimilarData.is_loaded: SelectSimilarData.load() - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() if SelectSimilarData.data["element_key"]: row = self.layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py index 6730353b00..5f5854edd2 100644 --- a/src/bonsai/bonsai/bim/module/sequence/operator.py +++ b/src/bonsai/bonsai/bim/module/sequence/operator.py @@ -31,7 +31,6 @@ import ifcopenshell.util.sequence import ifcopenshell.util.selector from datetime import datetime from dateutil import parser, relativedelta -from bonsai.bim.ifc import IfcStore from bpy_extras.io_utils import ImportHelper from typing import get_args, TYPE_CHECKING from typing_extensions import assert_never @@ -700,7 +699,7 @@ class ImportP6(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): def _execute(self, context): from ifc4d.p62ifc import P62Ifc - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() start = time.time() p62ifc = P62Ifc() p62ifc.xml = self.filepath @@ -728,7 +727,7 @@ class ImportP6XER(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): def _execute(self, context): from ifc4d.p6xer2ifc import P6XER2Ifc - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() start = time.time() p6xer2ifc = P6XER2Ifc() p6xer2ifc.xer = self.filepath @@ -756,7 +755,7 @@ class ImportPP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): def _execute(self, context): from ifc4d.pp2ifc import PP2Ifc - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() start = time.time() pp2ifc = PP2Ifc() pp2ifc.pp = self.filepath @@ -784,7 +783,7 @@ class ImportMSP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): def _execute(self, context): from ifc4d.msp2ifc import MSP2Ifc - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() start = time.time() msp2ifc = MSP2Ifc() msp2ifc.xml = self.filepath @@ -814,7 +813,7 @@ class ExportMSP(bpy.types.Operator, ImportHelper): def execute(self, context): from ifc4d.ifc2msp import Ifc2Msp - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() start = time.time() ifc2msp = Ifc2Msp() ifc2msp.work_schedule = self.file.by_type("IfcWorkSchedule")[0] @@ -847,7 +846,7 @@ class ExportP6(bpy.types.Operator, ImportHelper): def execute(self, context): from ifc4d.ifc2p6 import Ifc2P6 - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() start = time.time() ifc2p6 = Ifc2P6() ifc2p6.xml = bpy.path.ensure_ext(self.filepath, ".xml") diff --git a/src/bonsai/bonsai/bim/module/sequence/prop.py b/src/bonsai/bonsai/bim/module/sequence/prop.py index d2e550138d..ce0e307076 100644 --- a/src/bonsai/bonsai/bim/module/sequence/prop.py +++ b/src/bonsai/bonsai/bim/module/sequence/prop.py @@ -23,7 +23,6 @@ import ifcopenshell.util.attribute import ifcopenshell.util.date import bonsai.tool as tool import bonsai.core.sequence as core -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.sequence.data import SequenceData, AnimationColorSchemeData, refresh as refresh_sequence_data import bonsai.bim.module.resource.data import bonsai.bim.module.pset.data diff --git a/src/bonsai/bonsai/bim/module/sequence/ui.py b/src/bonsai/bonsai/bim/module/sequence/ui.py index 2320bb80b4..21ed962852 100644 --- a/src/bonsai/bonsai/bim/module/sequence/ui.py +++ b/src/bonsai/bonsai/bim/module/sequence/ui.py @@ -22,7 +22,6 @@ import isodate import bonsai.tool as tool import bonsai.bim.helper from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import draw_attributes from bonsai.bim.module.sequence.data import ( WorkPlansData, @@ -45,7 +44,7 @@ class BIM_PT_status(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): self.props = context.scene.BIMStatusProperties @@ -77,7 +76,7 @@ class BIM_PT_work_plans(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and file.schema != "IFC2X3" def draw(self, context): @@ -147,7 +146,7 @@ class BIM_PT_work_schedules(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): @@ -592,7 +591,7 @@ class BIM_PT_animation_Color_Scheme(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): @@ -980,7 +979,7 @@ class BIM_PT_work_calendars(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): diff --git a/src/bonsai/bonsai/bim/module/spatial/__init__.py b/src/bonsai/bonsai/bim/module/spatial/__init__.py index 3194d18f3a..ee40768f08 100644 --- a/src/bonsai/bonsai/bim/module/spatial/__init__.py +++ b/src/bonsai/bonsai/bim/module/spatial/__init__.py @@ -26,7 +26,6 @@ classes = ( operator.DeleteContainer, operator.DereferenceStructure, operator.DisableEditingContainer, - operator.EditContainerAttributes, operator.EnableEditingContainer, operator.ExpandContainer, operator.ImportSpatialDecomposition, diff --git a/src/bonsai/bonsai/bim/module/spatial/data.py b/src/bonsai/bonsai/bim/module/spatial/data.py index eb8e6585b4..b3e863a329 100644 --- a/src/bonsai/bonsai/bim/module/spatial/data.py +++ b/src/bonsai/bonsai/bim/module/spatial/data.py @@ -58,7 +58,7 @@ class SpatialData: @classmethod def default_container(cls) -> str | None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = tool.Spatial.get_spatial_props() if props.default_container: try: return tool.Ifc.get().by_id(props.default_container).Name @@ -102,7 +102,7 @@ class SpatialDecompositionData: @classmethod def default_container(cls) -> str | None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = tool.Spatial.get_spatial_props() if props.default_container: try: return tool.Ifc.get().by_id(props.default_container).Name @@ -112,7 +112,7 @@ class SpatialDecompositionData: @classmethod def subelement_class(cls) -> list[tuple[str, str, str]]: results = [] - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = tool.Spatial.get_spatial_props() if not (container := props.active_container): return results container_class = tool.Ifc.get().by_id(container.ifc_definition_id).is_a() diff --git a/src/bonsai/bonsai/bim/module/spatial/decorator.py b/src/bonsai/bonsai/bim/module/spatial/decorator.py index b977210805..a7c1059fc7 100644 --- a/src/bonsai/bonsai/bim/module/spatial/decorator.py +++ b/src/bonsai/bonsai/bim/module/spatial/decorator.py @@ -66,7 +66,8 @@ class GridDecorator: blf.size(font_id, 12) blf.enable(font_id, blf.SHADOW) - for axis in context.scene.BIMGridProperties.grid_axes: + grid_props = tool.Spatial.get_grid_props() + for axis in grid_props.grid_axes: if not (obj := axis.obj) or obj.hide_get() == True: continue if obj.select_get() and context.mode != "OBJECT": @@ -120,7 +121,8 @@ class GridDecorator: selected_edges = [] unselected_verts = [] unselected_edges = [] - for axis in context.scene.BIMGridProperties.grid_axes: + grid_props = tool.Spatial.get_grid_props() + for axis in grid_props.grid_axes: if (obj := axis.obj) and obj.hide_get() == False: if obj.select_get(): if context.mode != "OBJECT": diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index 009e1739ed..1e6ed86ee6 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -96,7 +96,7 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator): container = tool.Ifc.get().by_id(self.container) elif ( (obj := tool.Blender.get_active_object()) - and (props := obj.BIMObjectSpatialProperties) + and (props := tool.Spatial.get_object_spatial_props(obj)) and (container_obj := props.container_obj) and (container := tool.Ifc.get_entity(container_obj)) ): @@ -243,17 +243,6 @@ class ImportSpatialDecomposition(bpy.types.Operator): return {"FINISHED"} -class EditContainerAttributes(bpy.types.Operator): - bl_idname = "bim.edit_container_attributes" - bl_label = "Edit container attributes" - bl_options = {"REGISTER", "UNDO"} - container: bpy.props.IntProperty() - - def execute(self, context): - core.edit_container_attributes(tool.Spatial, entity=tool.Ifc.get().by_id(self.container)) - return {"FINISHED"} - - class ContractContainer(bpy.types.Operator): bl_idname = "bim.contract_container" bl_label = "Contract Container" diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py index 1017d8f069..910bb2e28c 100644 --- a/src/bonsai/bonsai/bim/module/spatial/prop.py +++ b/src/bonsai/bonsai/bim/module/spatial/prop.py @@ -36,15 +36,18 @@ import bonsai.core.geometry import ifcopenshell import ifcopenshell.util.element import ifcopenshell.util.unit +from typing import TYPE_CHECKING, Union, Literal -def get_subelement_class(self, context): +def get_subelement_class( + self: "BIMSpatialDecompositionProperties", context: bpy.types.Context +) -> list[tuple[str, str, str]]: if not SpatialDecompositionData.is_loaded: SpatialDecompositionData.load() return SpatialDecompositionData.data["subelement_class"] -def update_elevation(self, context): +def update_elevation(self: "BIMContainer", context: bpy.types.Context) -> None: try: elevation = float(self.elevation) if self.elevation != str(elevation): @@ -91,7 +94,7 @@ def update_element_mode(self: "BIMSpatialDecompositionProperties", context: bpy. tool.Spatial.load_contained_elements() -def update_grid_is_locked(self, context): +def update_grid_is_locked(self: "BIMGridProperties", context: bpy.types.Context) -> None: if not tool.Ifc.get(): return if tool.Ifc.get().schema in ("IFC2X3", "IFC4"): @@ -108,7 +111,7 @@ def update_grid_is_locked(self, context): bonsai.bim.handler.refresh_ui_data() -def update_spatial_is_locked(self, context): +def update_spatial_is_locked(self: "BIMSpatialDecompositionProperties", context: bpy.types.Context) -> None: if not tool.Ifc.get(): return if tool.Ifc.get().schema == "IFC2X3": @@ -150,6 +153,10 @@ class BIMObjectSpatialProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing") container_obj: PointerProperty(type=bpy.types.Object, name="Container", poll=poll_container_obj) + if TYPE_CHECKING: + is_editing: bool + container_obj: Union[bpy.types.Object, None] + class BIMContainer(PropertyGroup): name: StringProperty(name="Name", update=update_name) @@ -162,6 +169,16 @@ class BIMContainer(PropertyGroup): is_expanded: BoolProperty(name="Is Expanded") ifc_definition_id: IntProperty(name="IFC Definition ID") + if TYPE_CHECKING: + ifc_class: str + description: str + long_name: str + elevation: str + level_index: int + has_children: bool + is_expanded: bool + ifc_definition_id: int + class Element(PropertyGroup): name: StringProperty(name="Name") @@ -185,6 +202,16 @@ class Element(PropertyGroup): ), ) + if TYPE_CHECKING: + ifc_class: str + identification: str + ifc_definition_id: int + level: int + has_children: bool + total: int + is_expanded: bool + type: Literal["CLASS", "TYPE", "CLASSIFICATION", "OCCURRENCE"] + class BIMSpatialDecompositionProperties(PropertyGroup): is_locked: BoolProperty( @@ -223,6 +250,23 @@ class BIMSpatialDecompositionProperties(PropertyGroup): name="Should Include Children", default=True, update=update_should_include_children ) + if TYPE_CHECKING: + is_locked: bool + is_visible: bool + container_filter: str + containers: bpy.types.bpy_prop_collection_idprop[BIMContainer] + contracted_containers: str + active_container_index: int + element_filter: str + elements: bpy.types.bpy_prop_collection_idprop[Element] + expanded_elements: str + active_element_index: int + total_elements: int + element_mode: Literal["TYPE", "DECOMPOSITION", "CLASSIFICATION"] + subelement_class: str + default_container: int + should_include_children: bool + @property def active_container(self): if self.containers and self.active_container_index < len(self.containers): @@ -248,3 +292,8 @@ class BIMGridProperties(PropertyGroup): update=update_grid_is_visible, ) grid_axes: CollectionProperty(name="Grid Axes", type=ObjProperty) + + if TYPE_CHECKING: + is_locked: bool + is_visible: bool + grid_axes: bpy.types.bpy_prop_collection_idprop[ObjProperty] diff --git a/src/bonsai/bonsai/bim/module/spatial/ui.py b/src/bonsai/bonsai/bim/module/spatial/ui.py index 0569504e25..da20a63d7c 100644 --- a/src/bonsai/bonsai/bim/module/spatial/ui.py +++ b/src/bonsai/bonsai/bim/module/spatial/ui.py @@ -16,10 +16,15 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy from bpy.types import Panel, UIList from bonsai.bim.module.spatial.data import SpatialData, SpatialDecompositionData import bonsai.tool as tool +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.spatial.prop import BIMSpatialDecompositionProperties, BIMContainer, Element class BIM_PT_spatial(Panel): @@ -40,7 +45,9 @@ class BIM_PT_spatial(Panel): if not SpatialData.is_loaded: SpatialData.load() - osprops = context.active_object.BIMObjectSpatialProperties + obj = context.active_object + assert obj + osprops = tool.Spatial.get_object_spatial_props(obj) if osprops.is_editing: if SpatialData.data["default_container"]: @@ -103,7 +110,7 @@ class BIM_PT_spatial_decomposition(Panel): return tool.Ifc.get() def draw_header(self, context): - props = context.scene.BIMSpatialDecompositionProperties + props = tool.Spatial.get_spatial_props() row = self.layout.row(align=True) row.label(text="") # empty text occupies the left of the row icon = "HIDE_OFF" if props.is_visible else "HIDE_ON" @@ -114,7 +121,7 @@ class BIM_PT_spatial_decomposition(Panel): def draw(self, context): if not SpatialDecompositionData.is_loaded: SpatialDecompositionData.load() - self.props = context.scene.BIMSpatialDecompositionProperties + self.props = tool.Spatial.get_spatial_props() if SpatialDecompositionData.data["default_container"]: row = self.layout.row(align=True) @@ -233,7 +240,7 @@ class BIM_PT_grids(Panel): self.layout.row().operator("mesh.add_grid", icon="ADD", text="Add Grids") def draw_header(self, context): - props = context.scene.BIMGridProperties + props = tool.Spatial.get_grid_props() row = self.layout.row(align=True) row.label(text="") # empty text occupies the left of the row icon = "HIDE_OFF" if props.is_visible else "HIDE_ON" @@ -243,25 +250,36 @@ class BIM_PT_grids(Panel): class BIM_UL_containers_manager(UIList): + icon_by_class = { + "IfcProject": "FILE", + "IfcSite": "WORLD", + "IfcBuilding": "HOME", + "IfcBuildingStorey": "LINENUMBERS_OFF", + "IfcSpace": "ANTIALIASED", + "IfcFacilityPart": "MOD_FLUID", + "IfcBridgePart": "MOD_FLUID", + "IfcFacilityPartCommon": "MOD_FLUID", + "IfcMarinePart": "MOD_FLUID", + "IfcRailwayPart": "MOD_FLUID", + "IfcRoadPart": "MOD_FLUID", + } + def __init__(self): self.use_filter_show = True - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMSpatialDecompositionProperties, + item: BIMContainer, + icon, + active_data, + active_propname, + ): if item: row = layout.row(align=True) - icon = { - "IfcProject": "FILE", - "IfcSite": "WORLD", - "IfcBuilding": "HOME", - "IfcBuildingStorey": "LINENUMBERS_OFF", - "IfcSpace": "ANTIALIASED", - "IfcFacilityPart": "MOD_FLUID", - "IfcBridgePart": "MOD_FLUID", - "IfcFacilityPartCommon": "MOD_FLUID", - "IfcMarinePart": "MOD_FLUID", - "IfcRailwayPart": "MOD_FLUID", - "IfcRoadPart": "MOD_FLUID", - }.get(item.ifc_class, "META_PLANE") + icon = self.icon_by_class.get(item.ifc_class, "META_PLANE") split = row.split(factor=0.85) if item.long_name: split2 = split.split(factor=0.7) @@ -275,7 +293,7 @@ class BIM_UL_containers_manager(UIList): row.prop(item, "name", emboss=False, text="", icon=icon) split.prop(item, "elevation", emboss=False, text="") - def draw_hierarchy(self, row, item): + def draw_hierarchy(self, row: bpy.types.UILayout, item: BIMContainer) -> None: if item.level_index: for i in range(0, item.level_index - 1): row.label(text="", icon="BLANK1") @@ -293,11 +311,12 @@ class BIM_UL_containers_manager(UIList): def draw_filter(self, context, layout): row = layout.row() - row.prop(context.scene.BIMSpatialDecompositionProperties, "container_filter", text="", icon="VIEWZOOM") + props = tool.Spatial.get_spatial_props() + row.prop(props, "container_filter", text="", icon="VIEWZOOM") - def filter_items(self, context, data, propname): + def filter_items(self, context: bpy.types.Context, data: BIMSpatialDecompositionProperties, propname: str): items = getattr(data, propname) - filter_name = context.scene.BIMSpatialDecompositionProperties.container_filter.lower() + filter_name = data.container_filter.lower() filter_flags = [self.bitflag_filter_item] * len(items) for idx, item in enumerate(items): @@ -313,7 +332,7 @@ class BIM_UL_containers_manager(UIList): return filter_flags, [] items = getattr(data, propname) - filter_name = context.scene.BIMSpatialDecompositionProperties.container_filter + filter_name = data.container_filter filtered = bpy.types.UI_UL_list.filter_items_by_name(filter_name, self.bitflag_filter_item, items, "name") return filtered, [] @@ -326,7 +345,18 @@ class BIM_UL_elements(UIList): icon_id = "DISCLOSURE_TRI_DOWN" if is_expanded else "DISCLOSURE_TRI_RIGHT" row.operator("bim.toggle_container_element", text="", emboss=False, icon=icon_id).element_index = index - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, fit_flag): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMSpatialDecompositionProperties, + item: Element, + icon, + active_data, + active_propname, + index, + fit_flag, + ): if item: row = layout.row(align=True) for _ in range(item.level): @@ -341,11 +371,12 @@ class BIM_UL_elements(UIList): def draw_filter(self, context, layout): row = layout.row() - row.prop(context.scene.BIMSpatialDecompositionProperties, "element_filter", text="", icon="VIEWZOOM") + props = tool.Spatial.get_spatial_props() + row.prop(props, "element_filter", text="", icon="VIEWZOOM") - def filter_items(self, context, data, propname): + def filter_items(self, context: bpy.types.Context, data: BIMSpatialDecompositionProperties, propname: str): items = getattr(data, propname) - filter_name = context.scene.BIMSpatialDecompositionProperties.element_filter + filter_name = data.element_filter filtered = bpy.types.UI_UL_list.filter_items_by_name(filter_name, self.bitflag_filter_item, items, "name") return filtered, [] diff --git a/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py b/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py index 8230c55c66..a91a7947c5 100644 --- a/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py +++ b/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py @@ -26,7 +26,6 @@ import ifcopenshell.api import ifcopenshell.util.attribute import ifcopenshell.util.unit as ifcunit import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.structural.shader import DecorationShader from typing import Literal, TypedDict, Iterable @@ -307,7 +306,7 @@ class ShaderInfo: props = bpy.context.scene.BIMStructuralProperties group_definition_id = int(props.load_group_to_show) - file = IfcStore.get_file() + file = tool.Ifc.get() groups = [file.by_id(group_definition_id)] recursive_subgroups(groups, 10, props.activity_type) @@ -333,7 +332,7 @@ class ShaderInfo: orientation = np.eye(3) if reference_frame == "LOCAL_COORDS": orientation = rotation - blender_object: bpy.types.Object = IfcStore.get_element(getattr(surf, "GlobalId", None)) + blender_object: bpy.types.Object = tool.Ifc.get_object_by_identifier(getattr(surf, "GlobalId", None)) mat = blender_object.matrix_world mesh: bpy.types.Mesh = blender_object.data @@ -454,7 +453,7 @@ class ShaderInfo: activity_list = value["activities"] if len(activity_list) == 0: continue - blender_object = IfcStore.get_element(getattr(conn, "GlobalId", None)) + blender_object = tool.Ifc.get_object_by_identifier(getattr(conn, "GlobalId", None)) if blender_object.type == "MESH": conn_location = blender_object.matrix_world @ blender_object.data.vertices[0].co rotation = self.get_point_connection_rotation(conn) @@ -580,7 +579,7 @@ class ShaderInfo: if len(activity_list) == 0: continue - blender_object = IfcStore.get_element(getattr(member, "GlobalId", None)) + blender_object = tool.Ifc.get_object_by_identifier(getattr(member, "GlobalId", None)) start_co = blender_object.matrix_world @ blender_object.data.vertices[0].co end_co = blender_object.matrix_world @ blender_object.data.vertices[1].co diff --git a/src/bonsai/bonsai/bim/module/structural/operator.py b/src/bonsai/bonsai/bim/module/structural/operator.py index 646e5ad294..ba08227179 100644 --- a/src/bonsai/bonsai/bim/module/structural/operator.py +++ b/src/bonsai/bonsai/bim/module/structural/operator.py @@ -20,13 +20,13 @@ import bpy import json import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.structural import ifcopenshell.util.attribute import bonsai.bim.helper import bonsai.core.structural as core import bonsai.tool as tool from math import degrees from mathutils import Vector, Matrix -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.structural.decorator import LoadsDecorator @@ -77,13 +77,12 @@ class AddStructuralMemberConnection(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object oprops = obj.BIMObjectProperties props = obj.BIMStructuralProperties - file = IfcStore.get_file() + file = tool.Ifc.get() related_structural_connection = file.by_id(oprops.ifc_definition_id) relating_structural_member = file.by_id(props.relating_structural_member.BIMObjectProperties.ifc_definition_id) if not relating_structural_member.is_a("IfcStructuralMember"): return {"FINISHED"} - ifcopenshell.api.run( - "structural.add_structural_member_connection", + ifcopenshell.api.structural.add_structural_member_connection( file, relating_structural_member=relating_structural_member, related_structural_connection=related_structural_connection, @@ -125,7 +124,7 @@ class RemoveStructuralConnectionCondition(bpy.types.Operator, tool.Ifc.Operator) connects_structural_member: bpy.props.IntProperty() def _execute(self, context): - file = IfcStore.get_file() + file = tool.Ifc.get() relation = file.by_id(self.connects_structural_member) connection = relation.RelatedStructuralConnection ifcopenshell.api.run("structural.remove_structural_connection_condition", file, **{"relation": relation}) @@ -139,7 +138,7 @@ class AddStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): connection: bpy.props.IntProperty() def _execute(self, context): - file = IfcStore.get_file() + file = tool.Ifc.get() connection = file.by_id(self.connection) ifcopenshell.api.run("structural.add_structural_boundary_condition", file, **{"connection": connection}) return {"FINISHED"} @@ -152,7 +151,7 @@ class RemoveStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): connection: bpy.props.IntProperty() def _execute(self, context): - file = IfcStore.get_file() + file = tool.Ifc.get() connection = file.by_id(self.connection) ifcopenshell.api.run("structural.remove_structural_boundary_condition", file, **{"connection": connection}) return {"FINISHED"} @@ -170,8 +169,9 @@ class EnableEditingStructuralBoundaryCondition(bpy.types.Operator): props.boundary_condition_attributes.clear() condition = tool.Ifc.get().by_id(self.boundary_condition) + schema = tool.Ifc.schema() - for attribute in IfcStore.get_schema().declaration_by_name(condition.is_a()).all_attributes(): + for attribute in schema.declaration_by_name(condition.is_a()).all_attributes(): value = getattr(condition, attribute.name(), None) data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) new = props.boundary_condition_attributes.add() @@ -207,7 +207,7 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object props = obj.BIMStructuralProperties - file = IfcStore.get_file() + file = tool.Ifc.get() connection = file.by_id(self.connection) condition = connection.AppliedCondition @@ -353,7 +353,7 @@ class EnableEditingStructuralItemAxis(bpy.types.Operator): oprops = obj.BIMObjectProperties props = obj.BIMStructuralProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() item = self.file.by_id(oprops.ifc_definition_id) z_axis = Vector(item.Axis.DirectionRatios).normalized() @ obj.matrix_world if item.Axis else None x_axis = (obj.data.vertices[1].co - obj.data.vertices[0].co).normalized() @@ -407,7 +407,7 @@ class EditStructuralItemAxis(bpy.types.Operator, tool.Ifc.Operator): props = obj.BIMStructuralProperties relative_matrix = props.axis_empty.matrix_world @ obj.matrix_world.inverted() z_axis = relative_matrix.col[2][0:3] - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.edit_structural_item_axis", self.file, @@ -428,7 +428,7 @@ class EnableEditingStructuralConnectionCS(bpy.types.Operator): oprops = obj.BIMObjectProperties props = obj.BIMStructuralProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() item = self.file.by_id(oprops.ifc_definition_id) location = obj.data.vertices[0].co @@ -496,7 +496,7 @@ class EditStructuralConnectionCS(bpy.types.Operator, tool.Ifc.Operator): relative_matrix = props.ccs_empty.matrix_world @ obj.matrix_world.inverted() x_axis = relative_matrix.col[0][0:3] z_axis = relative_matrix.col[2][0:3] - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.edit_structural_connection_cs", self.file, @@ -515,7 +515,7 @@ class AssignStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator): load_case: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "aggregate.assign_object", self.file, @@ -534,7 +534,7 @@ class UnassignStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator): load_case: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "aggregate.unassign_object", self.file, @@ -552,7 +552,7 @@ class AddStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - ifcopenshell.api.run("structural.add_structural_load_case", IfcStore.get_file()) + ifcopenshell.api.run("structural.add_structural_load_case", tool.Ifc.get()) return {"FINISHED"} @@ -564,7 +564,7 @@ class EditStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = context.scene.BIMStructuralProperties attributes = bonsai.bim.helper.export_attributes(props.load_case_attributes) - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.edit_structural_load_case", self.file, @@ -581,7 +581,7 @@ class RemoveStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator): load_case: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.remove_structural_load_case", self.file, load_case=self.file.by_id(self.load_case) ) @@ -639,7 +639,7 @@ class AddStructuralLoadGroup(bpy.types.Operator, tool.Ifc.Operator): load_case: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() load_group = ifcopenshell.api.run("structural.add_structural_load_group", self.file) ifcopenshell.api.run( "group.assign_group", self.file, products=[load_group], group=self.file.by_id(self.load_case) @@ -654,7 +654,7 @@ class RemoveStructuralLoadGroup(bpy.types.Operator, tool.Ifc.Operator): load_group: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.remove_structural_load_group", self.file, load_group=self.file.by_id(self.load_group) ) @@ -668,7 +668,7 @@ class EnableEditingStructuralLoadGroupActivities(bpy.types.Operator): load_group: bpy.props.IntProperty() def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.props = context.scene.BIMStructuralProperties self.props.active_load_group_id = self.load_group self.props.load_group_editing_type = "ACTIVITY" @@ -693,7 +693,7 @@ class AddStructuralActivity(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): self.props = context.scene.BIMStructuralProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() for obj in context.selected_objects: if not obj.BIMObjectProperties.ifc_definition_id: continue @@ -741,7 +741,7 @@ class LoadStructuralLoads(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = context.scene.BIMStructuralProperties props.structural_loads.clear() loads = tool.Ifc.get().by_type("IfcStructuralLoad") @@ -750,7 +750,7 @@ class LoadStructuralLoads(bpy.types.Operator): for structural_load in loads: if ( names.count(structural_load.Name or "Unnamed") > 1 - and len(self.file.get_inverse(structural_load)) < 2 + and self.file.get_total_inverses(structural_load) < 2 ): continue new = props.structural_loads.add() @@ -787,7 +787,7 @@ class AddStructuralLoad(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): result = ifcopenshell.api.run( - "structural.add_structural_load", IfcStore.get_file(), name="New Load", ifc_class=self.ifc_class + "structural.add_structural_load", tool.Ifc.get(), name="New Load", ifc_class=self.ifc_class ) bpy.ops.bim.load_structural_loads() bpy.ops.bim.enable_editing_structural_load(structural_load=result.id()) @@ -828,7 +828,7 @@ class RemoveStructuralLoad(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = context.scene.BIMStructuralProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.remove_structural_load", self.file, @@ -846,7 +846,7 @@ class EditStructuralLoad(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = context.scene.BIMStructuralProperties attributes = bonsai.bim.helper.export_attributes(props.structural_load_attributes) - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.edit_structural_load", self.file, @@ -877,7 +877,7 @@ class LoadBoundaryConditions(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = context.scene.BIMStructuralProperties props.boundary_conditions.clear() conditions = tool.Ifc.get().by_type("IfcBoundaryCondition") @@ -936,7 +936,7 @@ class AddBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): result = ifcopenshell.api.run( "structural.add_structural_boundary_condition", - IfcStore.get_file(), + tool.Ifc.get(), name="New Load", ifc_class=self.ifc_class, ) @@ -957,7 +957,8 @@ class EnableEditingBoundaryCondition(bpy.types.Operator): boundary_condition = tool.Ifc.get().by_id(self.boundary_condition) # bonsai.bim.helper.import_attributes(data["type"], props.boundary_condition_attributes, data) - for attribute in IfcStore.get_schema().declaration_by_name(boundary_condition.is_a()).all_attributes(): + schema = tool.Ifc.schema() + for attribute in schema.declaration_by_name(boundary_condition.is_a()).all_attributes(): value = getattr(boundary_condition, attribute.name(), None) data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) new = props.boundary_condition_attributes.add() @@ -1000,7 +1001,7 @@ class RemoveBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = context.scene.BIMStructuralProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.remove_structural_boundary_condition", self.file, @@ -1017,7 +1018,7 @@ class EditBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = context.scene.BIMStructuralProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() # attributes = bonsai.bim.helper.export_attributes(props.boundary_condition_attributes) attributes = {} for attribute in props.boundary_condition_attributes: diff --git a/src/bonsai/bonsai/bim/module/structural/prop.py b/src/bonsai/bonsai/bim/module/structural/prop.py index 740c97f1e6..2ecc29834a 100644 --- a/src/bonsai/bonsai/bim/module/structural/prop.py +++ b/src/bonsai/bonsai/bim/module/structural/prop.py @@ -19,7 +19,6 @@ from math import radians import bpy import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore from bonsai.bim.prop import StrProperty, Attribute from bonsai.bim.module.structural.data import ( StructuralLoadCasesData, diff --git a/src/bonsai/bonsai/bim/module/structural/ui.py b/src/bonsai/bonsai/bim/module/structural/ui.py index 767724bdd5..d8f69e232e 100644 --- a/src/bonsai/bonsai/bim/module/structural/ui.py +++ b/src/bonsai/bonsai/bim/module/structural/ui.py @@ -17,9 +17,9 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool import bonsai.bim.helper from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import draw_attributes, prop_with_search from bonsai.bim.module.structural.data import ( StructuralBoundaryConditionsData, @@ -90,9 +90,9 @@ class BIM_PT_structural_boundary_conditions(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): + if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): return False return True @@ -125,9 +125,9 @@ class BIM_PT_connected_structural_members(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): + if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): return False return True @@ -178,9 +178,9 @@ class BIM_PT_structural_member(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcStructuralMember"): + if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralMember"): return False return True @@ -221,9 +221,9 @@ class BIM_PT_structural_connection(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): + if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): return False return True @@ -274,7 +274,7 @@ class BIM_PT_structural_analysis_models(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): @@ -348,7 +348,7 @@ class BIM_PT_structural_load_cases(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): @@ -443,7 +443,7 @@ class BIM_PT_show_structural_activities(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): @@ -474,7 +474,7 @@ class BIM_PT_structural_loads(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): @@ -543,7 +543,7 @@ class BIM_PT_boundary_conditions(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py index ef7914c69e..5f2d6d2a52 100644 --- a/src/bonsai/bonsai/bim/module/style/operator.py +++ b/src/bonsai/bonsai/bim/module/style/operator.py @@ -155,10 +155,10 @@ class UnlinkStyle(bpy.types.Operator, tool.Ifc.Operator): # for unlinked blender material. updated_meshes = set() for obj in bpy.data.objects: - mesh = obj.data - if not isinstance(mesh, bpy.types.Mesh): + if not (mesh := obj.data) or not isinstance(mesh, bpy.types.Mesh): continue - if not mesh.BIMMeshProperties.ifc_definition_id: + representation = tool.Geometry.get_data_representation(mesh) + if not representation: continue if mesh in updated_meshes: continue @@ -1137,13 +1137,16 @@ class AssignStyleToSelected(bpy.types.Operator, tool.Ifc.Operator): ifc_file = tool.Ifc.get() style = ifc_file.by_id(self.style_id) material = tool.Ifc.get_object(style) + assert isinstance(material, bpy.types.Material) has_items = False representations: dict[ifcopenshell.entity_instance, bpy.types.Object] = {} for obj in context.selected_objects: if tool.Geometry.is_representation_item(obj): has_items = True - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + assert isinstance(obj.data, bpy.types.Mesh) + item = tool.Geometry.get_active_representation(obj) + assert item tool.Style.assign_style_to_representation_item(item, style) obj.data.materials.clear() obj.data.materials.append(material) @@ -1156,7 +1159,8 @@ class AssignStyleToSelected(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} if has_items: - tool.Geometry.reload_representation(context.scene.BIMGeometryProperties.representation_obj) + gprops = tool.Geometry.get_geometry_props() + tool.Geometry.reload_representation(gprops.representation_obj) bpy.ops.bim.disable_editing_representation_items() bpy.ops.bim.enable_editing_representation_items() diff --git a/src/bonsai/bonsai/bim/module/style/ui.py b/src/bonsai/bonsai/bim/module/style/ui.py index 2486ee91e8..2b7eaad760 100644 --- a/src/bonsai/bonsai/bim/module/style/ui.py +++ b/src/bonsai/bonsai/bim/module/style/ui.py @@ -20,9 +20,7 @@ import bpy import bonsai.bim.helper import bonsai.tool as tool from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.style.data import StylesData, BlenderMaterialStyleData -from typing import Union class BIM_PT_styles(Panel): @@ -36,7 +34,7 @@ class BIM_PT_styles(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not StylesData.is_loaded: diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index 4ef3c255f7..5c9750dc23 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -21,9 +21,7 @@ import ifcopenshell.api import bonsai.tool as tool import bonsai.core.system as core import bonsai.bim.helper -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.system.data import PortData -from mathutils import Matrix class LoadSystems(bpy.types.Operator): diff --git a/src/bonsai/bonsai/bim/module/tester/operator.py b/src/bonsai/bonsai/bim/module/tester/operator.py index 652d0ff61a..b16abd3fcd 100644 --- a/src/bonsai/bonsai/bim/module/tester/operator.py +++ b/src/bonsai/bonsai/bim/module/tester/operator.py @@ -72,7 +72,7 @@ class ExecuteIfcTester(bpy.types.Operator): # No need for if-statement, just postponing lots of diffs. if True: - dirpath = tempfile.mkdtemp() + dirpath = tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None) start = time.time() output = Path(os.path.join(dirpath, "{}_{}.html".format(Path(ifc_path).name, Path(specs_path).name))) diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py index 866407be3e..ee370e5ae5 100644 --- a/src/bonsai/bonsai/bim/module/type/operator.py +++ b/src/bonsai/bonsai/bim/module/type/operator.py @@ -29,7 +29,6 @@ import bonsai.tool as tool import bonsai.core.geometry import bonsai.core.type as core import bonsai.core.root -from bonsai.bim.ifc import IfcStore class AssignType(bpy.types.Operator, tool.Ifc.Operator): @@ -66,7 +65,7 @@ class UnassignType(bpy.types.Operator, tool.Ifc.Operator): def exclude_callback(attribute): return attribute.is_a("IfcProfileDef") and attribute.ProfileName - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() objs = [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects for obj in objs: element = tool.Ifc.get_entity(obj) @@ -187,7 +186,7 @@ class SelectSimilarType(bpy.types.Operator): related_object: bpy.props.StringProperty() def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() objects = bpy.context.selected_objects # store relating types to avoid selecting same elements multiple times @@ -229,7 +228,7 @@ class SelectTypeObjects(bpy.types.Operator): relating_type: bpy.props.StringProperty() def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() relating_type = bpy.data.objects.get(self.relating_type) if self.relating_type else context.active_object at_least_one_selectable_typed_object = False for element in ifcopenshell.util.element.get_types(tool.Ifc.get_entity(relating_type)): diff --git a/src/bonsai/bonsai/bim/module/type/prop.py b/src/bonsai/bonsai/bim/module/type/prop.py index ac7986fe6b..aedb5c1993 100644 --- a/src/bonsai/bonsai/bim/module/type/prop.py +++ b/src/bonsai/bonsai/bim/module/type/prop.py @@ -20,8 +20,6 @@ import bpy import ifcopenshell.util.element import ifcopenshell.util.type from bonsai.bim.module.type.data import TypeData -from bonsai.bim.prop import StrProperty, Attribute -from bonsai.bim.ifc import IfcStore import bonsai.tool as tool from bpy.types import PropertyGroup from bpy.props import ( diff --git a/src/bonsai/bonsai/bim/module/type/ui.py b/src/bonsai/bonsai/bim/module/type/ui.py index 4bfca516e3..405507f168 100644 --- a/src/bonsai/bonsai/bim/module/type/ui.py +++ b/src/bonsai/bonsai/bim/module/type/ui.py @@ -19,7 +19,6 @@ import bonsai.tool as tool import bonsai.bim.module.type.prop as type_prop from bpy.types import Panel -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import prop_with_search from bonsai.bim.module.type.data import TypeData diff --git a/src/bonsai/bonsai/bim/module/unit/prop.py b/src/bonsai/bonsai/bim/module/unit/prop.py index edd23f2ce4..f0f04e16ae 100644 --- a/src/bonsai/bonsai/bim/module/unit/prop.py +++ b/src/bonsai/bonsai/bim/module/unit/prop.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import bpy -from bonsai.bim.ifc import IfcStore from bonsai.bim.prop import StrProperty, Attribute from bonsai.bim.module.unit.data import UnitsData from bpy.types import PropertyGroup @@ -31,21 +30,22 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING -def get_unit_classes(self, context): +def get_unit_classes(self: "BIMUnitProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not UnitsData.is_loaded: UnitsData.load() return UnitsData.data["unit_classes"] -def get_conversion_unit_types(self, context): +def get_conversion_unit_types(self: "BIMUnitProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not UnitsData.is_loaded: UnitsData.load() return UnitsData.data["conversion_unit_types"] -def get_named_unit_types(self, context): +def get_named_unit_types(self: "BIMUnitProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not UnitsData.is_loaded: UnitsData.load() return UnitsData.data["named_unit_types"] @@ -58,6 +58,12 @@ class Unit(PropertyGroup): ifc_class: StringProperty(name="IFC Class") ifc_definition_id: IntProperty(name="IFC Definition ID") + if TYPE_CHECKING: + unit_type: str + is_assigned: bool + ifc_class: str + ifc_definition_id: int + class BIMUnitProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing") @@ -68,3 +74,13 @@ class BIMUnitProperties(PropertyGroup): conversion_unit_types: EnumProperty(items=get_conversion_unit_types, name="Conversion Unit Types") named_unit_types: EnumProperty(items=get_named_unit_types, name="Named Unit Types") unit_attributes: CollectionProperty(name="Unit Attributes", type=Attribute) + + if TYPE_CHECKING: + is_editing: bool + units: bpy.types.bpy_prop_collection_idprop[Unit] + active_unit_index: int + active_unit_id: int + unit_classes: str + conversion_unit_types: str + named_unit_types: str + unit_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] diff --git a/src/bonsai/bonsai/bim/module/unit/ui.py b/src/bonsai/bonsai/bim/module/unit/ui.py index 77e0117699..f7b55a96e3 100644 --- a/src/bonsai/bonsai/bim/module/unit/ui.py +++ b/src/bonsai/bonsai/bim/module/unit/ui.py @@ -17,8 +17,8 @@ # along with Bonsai. If not, see . import bonsai.bim.helper +import bonsai.tool as tool from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import prop_with_search from bonsai.bim.module.unit.data import UnitsData @@ -34,14 +34,14 @@ class BIM_PT_units(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file def draw(self, context): if not UnitsData.is_loaded: UnitsData.load() - self.props = context.scene.BIMUnitProperties + self.props = tool.Unit.get_unit_props() row = self.layout.row(align=True) row.label(text="{} Units Found".format(UnitsData.data["total_units"]), icon="SNAP_GRID") @@ -103,7 +103,7 @@ class BIM_PT_units(Panel): class BIM_UL_units(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - props = context.scene.BIMUnitProperties + props = tool.Unit.get_unit_props() if item: icon = "MOD_MESHDEFORM" if item.ifc_class == "IfcSIUnit": diff --git a/src/bonsai/bonsai/bim/module/void/data.py b/src/bonsai/bonsai/bim/module/void/data.py index fe387b6f81..133d3e15f5 100644 --- a/src/bonsai/bonsai/bim/module/void/data.py +++ b/src/bonsai/bonsai/bim/module/void/data.py @@ -127,26 +127,14 @@ class BooleansData: def booleans(cls): props = tool.Geometry.get_geometry_props() obj = props.representation_obj or bpy.context.active_object - if ( - not obj.data - or not hasattr(obj.data, "BIMMeshProperties") - or not obj.data.BIMMeshProperties.ifc_definition_id - ): + if not (representation := tool.Geometry.get_active_representation(obj)): return [] - - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) return tool.Model.get_booleans(representation=representation) @classmethod def manual_booleans(cls): props = tool.Geometry.get_geometry_props() obj = props.representation_obj or bpy.context.active_object - if ( - not obj.data - or not hasattr(obj.data, "BIMMeshProperties") - or not obj.data.BIMMeshProperties.ifc_definition_id - ): + if not (representation := tool.Geometry.get_active_representation(obj)): return [] - - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) return tool.Model.get_manual_booleans(tool.Ifc.get_entity(obj), representation=representation) diff --git a/src/bonsai/bonsai/bim/module/void/operator.py b/src/bonsai/bonsai/bim/module/void/operator.py index 5867ed9950..91537ed528 100644 --- a/src/bonsai/bonsai/bim/module/void/operator.py +++ b/src/bonsai/bonsai/bim/module/void/operator.py @@ -24,7 +24,6 @@ import bonsai.tool as tool import bonsai.core.geometry import bonsai.core.root import bonsai.bim.handler -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.model.opening import FilledOpeningGenerator @@ -98,7 +97,7 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator): break element_had_openings = tool.Geometry.has_openings(voided_element) - body_context = ifcopenshell.util.representation.get_context(IfcStore.get_file(), "Model", "Body") + body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body") if not element2: element2 = bonsai.core.root.assign_class( tool.Ifc, @@ -139,7 +138,8 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator): tool.Ifc, tool.Geometry, tool.Surveyor, obj=voided_obj ) - representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id) + representation = tool.Geometry.get_active_representation(voided_obj) + assert representation bonsai.core.geometry.switch_representation( tool.Ifc, tool.Geometry, @@ -214,7 +214,7 @@ class AddFilling(bpy.types.Operator, tool.Ifc.Operator): opening = context.scene.objects.get(self.opening, context.scene.VoidProperties.desired_opening) if opening is None: return {"FINISHED"} - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() element_id = obj.BIMObjectProperties.ifc_definition_id opening_id = opening.BIMObjectProperties.ifc_definition_id if not element_id or not opening_id or element_id == opening_id: @@ -269,20 +269,18 @@ class BooleansMarkAsManual(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): obj = context.active_object - if ( - obj - and tool.Ifc.get_entity(obj) - and hasattr(obj.data, "BIMMeshProperties") - and obj.data.BIMMeshProperties.ifc_definition_id - ): + if obj and tool.Ifc.get_entity(obj) and tool.Geometry.get_active_representation(obj): return True cls.poll_message_set("Need to select IFC element with representation") return False def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + assert element + representation = tool.Geometry.get_active_representation(obj) + assert representation booleans = tool.Model.get_booleans(representation=representation) if self.mark_as_manual: @@ -304,13 +302,13 @@ class EnableEditingBooleans(bpy.types.Operator): @classmethod def poll(cls, context): - if not bpy.context.scene.BIMGeometryProperties.representation_obj: + if not tool.Geometry.get_geometry_props().representation_obj: cls.poll_message_set("To enable editing booleans object should be in item mode.") return False return True def execute(self, context): - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() gprops = tool.Geometry.get_geometry_props() rep_obj = gprops.representation_obj assert rep_obj @@ -344,6 +342,6 @@ class DisableEditingBooleans(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() props.is_editing = False return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/void/prop.py b/src/bonsai/bonsai/bim/module/void/prop.py index 8c6bbaf4e6..0170dbd606 100644 --- a/src/bonsai/bonsai/bim/module/void/prop.py +++ b/src/bonsai/bonsai/bim/module/void/prop.py @@ -19,34 +19,51 @@ import bpy from bpy.types import PropertyGroup from bpy.props import PointerProperty, StringProperty, IntProperty, BoolProperty, CollectionProperty, EnumProperty -from typing import Union +from typing import Union, TYPE_CHECKING, Literal, get_args + +OperatorType = Literal["DIFFERENCE", "INTERSECTION", "UNION"] class Boolean(PropertyGroup): name: StringProperty(name="Name") - operator: StringProperty(name="Operator") + operator: EnumProperty( + items=[(i, i, "") for i in get_args(OperatorType)], + name="Operator", + default="DIFFERENCE", + ) ifc_definition_id: IntProperty(name="IFC Definition ID") level: IntProperty(name="Level") + if TYPE_CHECKING: + operator: OperatorType + name: str + ifc_definition_id: int + level: int + class VoidProperties(PropertyGroup): desired_opening: PointerProperty(name="Desired Opening To Fill", type=bpy.types.Object) + if TYPE_CHECKING: + desired_opening: Union[bpy.types.Object, None] + class BIMBooleanProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing", default=False) booleans: CollectionProperty(name="Booleans", type=Boolean) active_boolean_index: IntProperty(name="Active Boolean Index") operator: EnumProperty( - items=[ - ("DIFFERENCE", "DIFFERENCE", ""), - ("INTERSECTION", "INTERSECTION", ""), - ("UNION", "UNION", ""), - ], + items=[(i, i, "") for i in get_args(OperatorType)], name="Operator", default="DIFFERENCE", ) + if TYPE_CHECKING: + is_editing: bool + booleans: bpy.types.bpy_prop_collection_idprop[Boolean] + active_boolean_index: int + operator: OperatorType + @property def active_boolean(self) -> Union[Boolean, None]: if self.booleans and 0 <= self.active_boolean_index < len(self.booleans): diff --git a/src/bonsai/bonsai/bim/module/void/ui.py b/src/bonsai/bonsai/bim/module/void/ui.py index d07266a56f..69b34a40e1 100644 --- a/src/bonsai/bonsai/bim/module/void/ui.py +++ b/src/bonsai/bonsai/bim/module/void/ui.py @@ -126,13 +126,10 @@ class BIM_PT_booleans(Panel): @classmethod def poll(cls, context): return ( - context.active_object is not None - and context.active_object.type == "MESH" - and hasattr(context.active_object.data, "BIMMeshProperties") - and ( - context.active_object.data.BIMMeshProperties.ifc_definition_id - or context.active_object.data.BIMMeshProperties.ifc_boolean_id - ) + (obj := context.active_object) is not None + and isinstance(data := obj.data, bpy.types.Mesh) + and (mesh_props := tool.Geometry.get_mesh_props(data)) + and (mesh_props.ifc_definition_id or mesh_props.ifc_boolean_id) ) def draw(self, context): @@ -141,13 +138,13 @@ class BIM_PT_booleans(Panel): obj = context.active_object assert obj + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) - if not context.active_object.data: - return layout = self.layout - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() - if context.active_object.data.BIMMeshProperties.ifc_definition_id: + if tool.Geometry.get_mesh_props(mesh).ifc_definition_id: row = layout.row(align=True) total_booleans = BooleansData.data["total_booleans"] manual_booleans = BooleansData.data["manual_booleans"] diff --git a/src/bonsai/bonsai/bim/module/web/data.py b/src/bonsai/bonsai/bim/module/web/data.py index fa94ad26d8..db939cadfa 100644 --- a/src/bonsai/bonsai/bim/module/web/data.py +++ b/src/bonsai/bonsai/bim/module/web/data.py @@ -39,9 +39,11 @@ class WebData: @classmethod def get_ifc_file_name(cls): - filename = os.path.basename(bpy.context.scene.BIMProperties.ifc_file) + props = tool.Blender.get_bim_props() + filename = os.path.basename(props.ifc_file) return filename @classmethod def get_is_dirty(cls): - return bpy.context.scene.BIMProperties.is_dirty + props = tool.Blender.get_bim_props() + return props.is_dirty diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 5ba1596203..f32cd2ef65 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -130,7 +130,8 @@ class CloseBlendWarning(bpy.types.Operator): bl_label = "Close Blend Warning" def execute(self, context): - bpy.context.scene.BIMProperties.has_blend_warning = False + props = tool.Blender.get_bim_props() + props.has_blend_warning = False return {"FINISHED"} def draw(self, context): @@ -213,11 +214,13 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector): def execute(self, context): if self.is_existing_ifc_file(): - context.scene.BIMProperties.ifc_file = self.get_filepath() + props = tool.Blender.get_bim_props() + props.ifc_file = self.get_filepath() return {"FINISHED"} def invoke(self, context, event): - filepath = Path(context.scene.BIMProperties.ifc_file) + props = tool.Blender.get_bim_props() + filepath = Path(props.ifc_file) res = tool.Blender.operator_invoke_filepath_hotkeys(self, context, event, filepath) if res is not None: return res @@ -226,47 +229,27 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector): return {"RUNNING_MODAL"} -class SelectDataDir(bpy.types.Operator): - bl_idname = "bim.select_data_dir" - bl_label = "Select Data Directory" +class SelectDir(bpy.types.Operator): + bl_idname = "bim.select_dir" + bl_label = "Select Directory" bl_options = {"REGISTER", "UNDO"} - bl_description = "Select the directory that contains all IFC data es. PSet, styles, etc..." + bl_description = "Open a file browser to choose the directory" filepath: bpy.props.StringProperty(subtype="FILE_PATH") + data_path: bpy.props.StringProperty(name="Data Path") def execute(self, context): - context.scene.BIMProperties.data_dir = os.path.dirname(self.filepath) - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - - -class SelectCacheDir(bpy.types.Operator): - bl_idname = "bim.select_cache_dir" - bl_label = "Select Cache Directory" - bl_options = {"REGISTER", "UNDO"} - bl_description = "Select the directory that contains HDF5 cache files" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def execute(self, context): - context.scene.BIMProperties.cache_dir = os.path.dirname(self.filepath) - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - - -class SelectSchemaDir(bpy.types.Operator): - bl_idname = "bim.select_schema_dir" - bl_label = "Select Schema Directory" - bl_options = {"REGISTER", "UNDO"} - bl_description = "Select the directory containing the IFC schema specification" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def execute(self, context): - context.scene.BIMProperties.schema_dir = os.path.dirname(self.filepath) + crumbs = self.data_path.split(".") + if crumbs[0] == "preferences": + crumbs.pop(0) + data = tool.Blender.get_addon_preferences() + else: + data = context + while crumbs: + crumb = crumbs.pop(0) + if crumbs: + data = getattr(data, crumb) + else: + setattr(data, crumb, os.path.dirname(self.filepath)) return {"FINISHED"} def invoke(self, context, event): @@ -595,7 +578,8 @@ class BIM_OT_add_section_plane(bpy.types.Operator): backfacing.location = mix_backfacing.location + Vector((-200, 200)) emission = nodes.new(type="ShaderNodeEmission") - emission.inputs[0].default_value = list(context.scene.BIMProperties.section_plane_colour) + [1] + props = tool.Blender.get_bim_props() + emission.inputs[0].default_value = list(props.section_plane_colour) + [1] emission.location = mix_backfacing.location - Vector((200, 150)) cut_obj = nodes.new(type="ShaderNodeTexCoord") @@ -659,7 +643,8 @@ class BIM_OT_add_section_plane(bpy.types.Operator): material = bpy.data.materials.new("Section Override") material.use_nodes = True - if context.scene.BIMProperties.should_section_selected_objects: + props = tool.Blender.get_bim_props() + if props.should_section_selected_objects: objects = list(context.selected_objects) else: objects = list(context.visible_objects) @@ -816,7 +801,9 @@ class ReloadIfcFile(bpy.types.Operator, tool.Ifc.Operator): logger = logging.getLogger("ImportIFC") path_log = tool.Blender.get_data_dir_path("process.log") if not os.access(path_log.parent, os.W_OK): - path_log = os.path.join(tempfile.mkdtemp(), "process.log") + path_log = os.path.join( + tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None), "process.log" + ) logging.basicConfig( filename=path_log, filemode="a", @@ -832,7 +819,8 @@ class ReloadIfcFile(bpy.types.Operator, tool.Ifc.Operator): settings.logger.info("Import finished in {:.2f} seconds".format(time.time() - start)) print("Import finished in {:.2f} seconds".format(time.time() - start)) - context.scene.BIMProperties.ifc_file = self.filepath + bim_props = tool.Blender.get_bim_props() + bim_props.ifc_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -846,7 +834,8 @@ class AddIfcFile(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.scene.DocProperties.ifc_files.add() + props = tool.Drawing.get_document_props() + props.ifc_files.add() return {"FINISHED"} @@ -857,7 +846,8 @@ class RemoveIfcFile(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.scene.DocProperties.ifc_files.remove(self.index) + props = tool.Drawing.get_document_props() + props.ifc_files.remove(self.index) return {"FINISHED"} @@ -868,7 +858,8 @@ class FetchObjectPassport(bpy.types.Operator): def execute(self, context): # TODO: this is dead code, awaiting reimplementation. See #1222. for reference in context.active_object.BIMObjectProperties.document_references: - reference = context.scene.BIMProperties.document_references[reference.name] + bim_props = tool.Blender.get_bim_props() + reference = bim_props.document_references[reference.name] if reference.location[-6:] == ".blend": self.fetch_blender(reference, context) return {"FINISHED"} @@ -1078,7 +1069,8 @@ class ClippingPlaneCutWithCappings(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - cutting_planes = [p.obj for p in context.scene.BIMProjectProperties.clipping_planes] + props = tool.Project.get_project_props() + cutting_planes = [obj for p in props.clipping_planes if (obj := p.obj)] if not cutting_planes: self.report({"INFO"}, "No cutting planes found.") return {"FINISHED"} @@ -1088,7 +1080,7 @@ class ClippingPlaneCutWithCappings(bpy.types.Operator): objects_processed, t0 = 0, time.time() wm.progress_begin(0, len(context.selected_objects)) for obj_i, obj in enumerate(context.selected_objects): - if obj.type != "MESH": + if not isinstance((mesh := obj.data), bpy.types.Mesh): continue if obj in cutting_planes: @@ -1100,7 +1092,6 @@ class ClippingPlaneCutWithCappings(bpy.types.Operator): ws_to_ls = obj.matrix_world.inverted() rotation = ws_to_ls.to_quaternion() - mesh = obj.data bm = tool.Blender.get_bmesh_for_mesh(mesh) object_changed = False @@ -1124,7 +1115,7 @@ class ClippingPlaneCutWithCappings(bpy.types.Operator): # don't swap mesh if it wasn't affected by any of the cutting planes if object_changed: temp_mesh = bpy.data.meshes.new("temp_cut") - temp_mesh.BIMMeshProperties.replaced_mesh = mesh + tool.Geometry.get_mesh_props(temp_mesh).replaced_mesh = mesh for material in mesh.materials: temp_mesh.materials.append(material) obj.data = temp_mesh @@ -1181,9 +1172,10 @@ class RevertClippingPlaneCut(bpy.types.Operator): self.report({"INFO"}, f"{objects_processed} processed - {time.time()-t0:.3f} sec") return {"FINISHED"} - def revert_object_mesh(self, obj): + def revert_object_mesh(self, obj: bpy.types.Object) -> None: mesh = obj.data - replaced_mesh = mesh.BIMMeshProperties.replaced_mesh + assert isinstance(mesh, bpy.types.Mesh) + replaced_mesh = tool.Geometry.get_mesh_props(mesh).replaced_mesh if replaced_mesh: obj.data = replaced_mesh tool.Blender.remove_data_block(mesh, do_unlink=False) diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index 8c790444f1..9a69807c23 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -119,23 +119,27 @@ def get_attribute_enum_values(prop: "Attribute", context: bpy.types.Context) -> def update_schema_dir(self: "BIMProperties", context: bpy.types.Context) -> None: import bonsai.bim.schema - bonsai.bim.schema.ifc.schema_dir = context.scene.BIMProperties.schema_dir + bim_props = tool.Blender.get_bim_props() + bonsai.bim.schema.ifc.schema_dir = bim_props.schema_dir def update_data_dir(self: "BIMProperties", context: bpy.types.Context) -> None: import bonsai.bim.schema - bonsai.bim.schema.ifc.data_dir = context.scene.BIMProperties.data_dir + bim_props = tool.Blender.get_bim_props() + bonsai.bim.schema.ifc.data_dir = bim_props.data_dir def update_cache_dir(self: "BIMProperties", context: bpy.types.Context) -> None: import bonsai.bim.schema - bonsai.bim.schema.ifc.cache_dir = context.scene.BIMProperties.cache_dir + bim_props = tool.Blender.get_bim_props() + bonsai.bim.schema.ifc.cache_dir = bim_props.cache_dir def update_ifc_file(self: "BIMProperties", context: bpy.types.Context) -> None: - if context.scene.BIMProperties.ifc_file: + bim_props = tool.Blender.get_bim_props() + if bim_props.ifc_file: bonsai.bim.handler.loadIfcStore(context.scene) @@ -230,15 +234,16 @@ def update_attribute_value(self: "Attribute", context: bpy.types.Context) -> Non def update_is_null(self: "Attribute", context: bpy.types.Context) -> None: - if not self.is_null: - return - self.string_value = "" - self.int_value = 0 - self.float_value = 0 - self.length_value = 0 - self.bool_value = False - if self.is_null is not True: - self.is_null = True + if self.is_null: + if self.data_type != "enum" and self.get_value() != (default := self.get_value_default()): + self.set_value(default) + if self.is_null is not True: + self.is_null = True + if self.update: + update = globals() + for name in self.update.split("."): + update = update[name] if isinstance(update, dict) else getattr(update, name) + update(self, context) def set_int_value(self: "Attribute", new_value: int) -> None: @@ -268,12 +273,16 @@ def set_length_value(self: "Attribute", value: float) -> None: def get_display_name(self: "Attribute") -> str: + DISPLAY_UNIT_TYPES = ("AREA", "VOLUME", "FORCE") name = self.name - if not self.special_type or self.special_type == "LENGTH": + if not self.special_type or self.special_type not in DISPLAY_UNIT_TYPES: return name unit_type = f"{self.special_type}UNIT" project_unit = ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), unit_type) + if not project_unit: + return name + unit_symbol = ifcopenshell.util.unit.get_unit_symbol(project_unit) return f"{name}, {unit_symbol}" @@ -326,13 +335,35 @@ class Attribute(PropertyGroup): value_max_constraint: BoolProperty(default=False, description="True if the numerical value has an upper bound") special_type: StringProperty(name="Special Value Type", default="") metadata: StringProperty(name="Metadata", description="For storing some additional information about the attribute") + update: StringProperty(name="Update", description="Custom update function to be executed") if TYPE_CHECKING: name: str + display_name: str description: str ifc_class: str data_type: AttributeDataType special_type: AttributeSpecialType + string_value: str + bool_value: bool + int_value: int + float_value: float + length_value: float + enum_items: str + enum_descriptions: bpy.types.bpy_prop_collection_idprop[StrProperty] + enum_value: str + filepath_value: MultipleFileSelect + filter_glob: str + is_null: bool + is_optional: bool + is_uri: bool + is_selected: bool + value_min: float + value_min_constraint: bool + value_max: float + value_max_constraint: bool + metadata: str + update: str def get_value(self) -> Union[str, float, int, bool, None]: if self.is_optional and self.is_null: @@ -437,6 +468,13 @@ class BIMAreaProperties(PropertyGroup): active_tab: BoolProperty(default=True, name="Active Tab") inactive_tab: BoolProperty(default=False, name="Inactive Tab") + if TYPE_CHECKING: + tab: str + previous_tab: str + alt_tab: str + active_tab: bool + inactive_tab: bool + # BIMAreaProperties exists per area and is setup on load post. However, for new # or temporary screens, they may not be setup yet, so this global tab @@ -447,6 +485,11 @@ class BIMTabProperties(PropertyGroup): active_tab: BoolProperty(default=True, name="Active Tab") inactive_tab: BoolProperty(default=False, name="Inactive Tab") + if TYPE_CHECKING: + tab: str + active_tab: bool + inactive_tab: bool + class BIMProperties(PropertyGroup): is_dirty: BoolProperty(name="Is Dirty", default=False) @@ -515,6 +558,21 @@ class BIMProperties(PropertyGroup): name="IFC Volume Unit", ) + if TYPE_CHECKING: + is_dirty: bool + schema_dir: str + data_dir: str + cache_dir: str + has_blend_warning: bool + pset_dir: str + ifc_file: str + last_transaction: str + should_section_selected_objects: bool + section_plane_colour: tuple[float, float, float] + section_line_decorator_width: float + area_unit: str + volume_unit: str + class IfcParameter(PropertyGroup): name: StringProperty(name="Name") @@ -523,6 +581,13 @@ class IfcParameter(PropertyGroup): value: FloatProperty(name="Value") # For now, only floats type: StringProperty(name="Type") + if TYPE_CHECKING: + name: str + step_id: int + index: int + value: float + type: str + class PsetQto(PropertyGroup): name: StringProperty(name="Name") @@ -530,20 +595,32 @@ class PsetQto(PropertyGroup): is_expanded: BoolProperty(name="Is Expanded", default=True) is_editable: BoolProperty(name="Is Editable") + if TYPE_CHECKING: + properties: bpy.types.bpy_prop_collection_idprop[Attribute] + is_expanded: bool + is_editable: bool + class GlobalId(PropertyGroup): name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") class BIMCollectionProperties(PropertyGroup): obj: PointerProperty(type=bpy.types.Object) + if TYPE_CHECKING: + obj: Union[bpy.types.Object, None] + + +BlenderOffsetType = Literal["NONE", "OBJECT_PLACEMENT", "CARTESIAN_POINT", "NOT_APPLICABLE"] + class BIMObjectProperties(PropertyGroup): collection: PointerProperty(type=bpy.types.Collection) ifc_definition_id: IntProperty(name="IFC Definition ID") blender_offset_type: EnumProperty( - items=[(o, o, "") for o in ["NONE", "OBJECT_PLACEMENT", "CARTESIAN_POINT", "NOT_APPLICABLE"]], + items=[(o, o, "") for o in get_args(BlenderOffsetType)], name="Blender Offset", default="NONE", ) @@ -553,6 +630,16 @@ class BIMObjectProperties(PropertyGroup): location_checksum: StringProperty(name="Location Checksum") rotation_checksum: StringProperty(name="Rotation Checksum") + if TYPE_CHECKING: + collection: Union[bpy.types.Collection, None] + ifc_definition_id: int + blender_offset_type: BlenderOffsetType + cartesian_point_offset: str + is_reassigning_class: bool + is_renaming: bool + location_checksum: str + rotation_checksum: str + def get_profiles(self: "BIMMeshProperties", context: bpy.types.Context): from bonsai.bim.module.model.data import ItemData @@ -562,6 +649,9 @@ def get_profiles(self: "BIMMeshProperties", context: bpy.types.Context): return ItemData.data["profiles_enum"] +SubshapeType = Literal["-", "PROFILE", "AXIS"] + + class BIMMeshProperties(PropertyGroup): ifc_definition_id: IntProperty(name="IFC Definition ID") ifc_boolean_id: IntProperty(name="IFC Boolean ID") @@ -570,7 +660,7 @@ class BIMMeshProperties(PropertyGroup): is_native: BoolProperty(name="Is Native", default=False) is_swept_solid: BoolProperty(name="Is Swept Solid") is_parametric: BoolProperty(name="Is Parametric", default=False) - subshape_type: EnumProperty(name="Subshape Type", items=[(i, i, "") for i in ("-", "PROFILE", "AXIS")]) + subshape_type: EnumProperty(name="Subshape Type", items=[(i, i, "") for i in get_args(SubshapeType)]) ifc_parameters: CollectionProperty(name="IFC Parameters", type=IfcParameter) item_attributes: CollectionProperty(name="Item Attributes", type=Attribute) item_profile: EnumProperty(name="Item Profile", items=get_profiles) @@ -578,6 +668,22 @@ class BIMMeshProperties(PropertyGroup): mesh_checksum: StringProperty(name="Mesh Checksum", default="") replaced_mesh: PointerProperty(type=bpy.types.Mesh, description="Original mesh to revert section cutaway") + if TYPE_CHECKING: + ifc_definition_id: int + ifc_boolean_id: int + obj: Union[bpy.types.Object, None] + has_openings_applied: bool + is_native: bool + is_swept_solid: bool + is_parametric: bool + subshape_type: SubshapeType + ifc_parameters: bpy.types.bpy_prop_collection_idprop[IfcParameter] + item_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + item_profile: str + material_checksum: str + mesh_checksum: str + replaced_mesh: Union[bpy.types.Mesh, None] + class BIMFacet(PropertyGroup): name: StringProperty(name="Name") @@ -597,16 +703,30 @@ class BIMFacet(PropertyGroup): ], ) + if TYPE_CHECKING: + pset: str + value: str + type: str + comparison: Literal["=", "!=", ">=", "<=", ">", "<", "*=", "!*="] + class BIMFilterGroup(PropertyGroup): filters: CollectionProperty(type=BIMFacet, name="filters") + if TYPE_CHECKING: + filters: bpy.types.bpy_prop_collection_idprop[BIMFacet] + class BIMSnapGroups(PropertyGroup): object: BoolProperty(name="Object", default=True) polyline: BoolProperty(name="Polyline", default=True) measure: BoolProperty(name="Measure", default=True) + if TYPE_CHECKING: + object: bool + polyline: bool + measure: bool + class BIMSnapProperties(PropertyGroup): vertex: BoolProperty(name="Vertex", default=True) @@ -614,3 +734,10 @@ class BIMSnapProperties(PropertyGroup): edge_center: BoolProperty(name="Edge Center", default=True) edge_intersection: BoolProperty(name="Edge Intersection", default=True) face: BoolProperty(name="Face", default=True) + + if TYPE_CHECKING: + vertex: bool + edge: bool + edge_center: bool + edge_intersection: bool + face: bool diff --git a/src/bonsai/bonsai/bim/schema.py b/src/bonsai/bonsai/bim/schema.py index 6d67d2e20a..cae1e32146 100644 --- a/src/bonsai/bonsai/bim/schema.py +++ b/src/bonsai/bonsai/bim/schema.py @@ -19,6 +19,8 @@ import ifcopenshell import ifcopenshell.util.pset import bonsai.tool as tool +import bpy +import bpy_restrict_state class IfcSchema: @@ -47,10 +49,16 @@ class IfcSchema: self.psetqto.get_applicable.cache_clear() self.psetqto.get_applicable_names.cache_clear() self.psetqto.get_by_name.cache_clear() + + # During register we cannot access the context either way. + if isinstance(bpy.context, bpy_restrict_state._RestrictContext): + return for path in tool.Blender.get_data_dir_paths("pset", "*.ifc"): self.psetqto.templates.append(ifcopenshell.open(path)) +# TODO: do we really need to load it on module import? +# Loading it on IFC load should be enough. ifc = IfcSchema() diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index a666d3fd94..4223706b8a 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -19,6 +19,7 @@ import os import bpy import platform +import bonsai.bim.helper from pathlib import Path from bpy.types import Panel from bpy.props import StringProperty, IntProperty, BoolProperty @@ -34,7 +35,7 @@ import bonsai.bim import bonsai.tool as tool from ifcopenshell.util.file import IfcHeaderExtractor from bonsai.bim.prop import Attribute -from typing import Optional +from typing import Optional, TYPE_CHECKING class IFCFileSelector: @@ -120,7 +121,7 @@ class BIM_PT_section_plane(Panel): def draw(self, context): layout = self.layout layout.use_property_split = True - props = context.scene.BIMProperties + props = tool.Blender.get_bim_props() layout.prop(props, "should_section_selected_objects") layout.prop(props, "section_plane_colour") @@ -147,7 +148,7 @@ class BIM_PT_section_with_cappings(Panel): row.operator("bim.clipping_plane_cut_with_cappings", icon="XRAY", text="Cut") row.operator("bim.revert_clipping_plane_cut", icon="FILE_REFRESH", text="Revert Cut") - props = context.scene.BIMProjectProperties + props = tool.Project.get_project_props() box = layout.box() header = box.row(align=True) header.label(text="Clipping Planes") @@ -245,6 +246,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): description="If disabled, the toolbar will only load when an IFC model is active", ) should_play_chaching_sound: BoolProperty(name="Play A Cha-Ching Sound When Project Costs Updates", default=False) + tmp_dir: StringProperty( + name="Temporary Directory", + description="Path to create and store temporary files. If left blank, a system default will be used.", + ) spatial_elements_unselectable: BoolProperty( name="Make Spatial Elements Unselectable By Default", default=True, @@ -298,7 +303,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): size=4, description="Color of background overlays", ) - opening_focus_opacity: bpy.props.IntProperty( default=100, min=0, @@ -308,7 +312,29 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): description="When modifying openings, other elements of the model will display with some transparency.\n0 is fully transparent and 100 is fully opaque", ) - def draw(self, context): + if TYPE_CHECKING: + svg2pdf_command: str + svg2dxf_command: str + svg_command: str + layout_svg_command: str + pdf_command: str + spreadsheet_command: str + should_hide_empty_props: bool + should_setup_workspace: bool + activate_workspace: bool + should_setup_toolbar: bool + should_play_chaching_sound: bool + spatial_elements_unselectable: bool + tmp_dir: str + decorations_colour: tuple[float, float, float, float] + decorator_color_selected: tuple[float, float, float, float] + decorator_color_unselected: tuple[float, float, float, float] + decorator_color_special: tuple[float, float, float, float] + decorator_color_error: tuple[float, float, float, float] + decorator_color_background: tuple[float, float, float, float] + opening_focus_opacity: int + + def draw(self, context: bpy.types.Context) -> None: layout = self.layout row = layout.row() @@ -329,7 +355,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Drawing", self.draw_drawing_settings) bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Openings", self.draw_openings_settings) - def draw_commands(self, layout, context): + def draw_commands(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: layout.prop(self, "svg2pdf_command") layout.prop(self, "svg2dxf_command") layout.prop(self, "svg_command") @@ -337,15 +363,16 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): layout.prop(self, "pdf_command") layout.prop(self, "spreadsheet_command") - def draw_misc_settings(self, layout, context): + def draw_misc_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: layout.prop(self, "should_hide_empty_props") layout.prop(self, "should_setup_workspace") layout.prop(self, "activate_workspace") layout.prop(self, "should_setup_toolbar") layout.prop(self, "should_play_chaching_sound") layout.prop(self, "spatial_elements_unselectable") - layout.prop(context.scene.BIMProjectProperties, "should_disable_undo_on_save") - layout.prop(context.scene.BIMProjectProperties, "should_stream") + props = tool.Project.get_project_props() + layout.prop(props, "should_disable_undo_on_save") + layout.prop(props, "should_stream") def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: props = tool.Model.get_model_props() @@ -353,34 +380,41 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): if props.occurrence_name_style == "CUSTOM": layout.prop(props, "occurrence_name_function") - def draw_directories(self, layout, context): + def draw_directories(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: + props = tool.Blender.get_bim_props() row = layout.row(align=True) - row.prop(context.scene.BIMProperties, "data_dir") - row.operator("bim.select_data_dir", icon="FILE_FOLDER", text="") + row.prop(props, "data_dir") + row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.data_dir" row = layout.row(align=True) - row.prop(context.scene.BIMProperties, "cache_dir") - row.operator("bim.select_cache_dir", icon="FILE_FOLDER", text="") + row.prop(props, "cache_dir") + row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.cache_dir" - def draw_drawing_settings(self, layout, context): - layout.prop(context.scene.BIMProperties, "pset_dir") - layout.prop(context.scene.DocProperties, "sheets_dir") - layout.prop(context.scene.DocProperties, "layouts_dir") - layout.prop(context.scene.DocProperties, "titleblocks_dir") - layout.prop(context.scene.DocProperties, "drawings_dir") - layout.prop(context.scene.DocProperties, "stylesheet_path") - layout.prop(context.scene.DocProperties, "schedules_stylesheet_path") - layout.prop(context.scene.DocProperties, "markers_path") - layout.prop(context.scene.DocProperties, "symbols_path") - layout.prop(context.scene.DocProperties, "patterns_path") - layout.prop(context.scene.DocProperties, "shadingstyles_path") - layout.prop(context.scene.DocProperties, "shadingstyle_default") + row = layout.row(align=True) + row.prop(self, "tmp_dir") + row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "preferences.tmp_dir" + + def draw_drawing_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: + props = tool.Blender.get_bim_props() + layout.prop(props, "pset_dir") + dprops = tool.Drawing.get_document_props() + layout.prop(dprops, "sheets_dir") + layout.prop(dprops, "layouts_dir") + layout.prop(dprops, "titleblocks_dir") + layout.prop(dprops, "drawings_dir") + layout.prop(dprops, "stylesheet_path") + layout.prop(dprops, "schedules_stylesheet_path") + layout.prop(dprops, "markers_path") + layout.prop(dprops, "symbols_path") + layout.prop(dprops, "patterns_path") + layout.prop(dprops, "shadingstyles_path") + layout.prop(dprops, "shadingstyle_default") row = layout.row() - row.prop(context.scene.DocProperties, "drawing_font") - row.prop(context.scene.DocProperties, "magic_font_scale") - layout.prop(context.scene.DocProperties, "imperial_precision") - layout.prop(context.scene.DocProperties, "tolerance") - layout.prop(context.scene.DocProperties, "classes_to_wireframe") + row.prop(dprops, "drawing_font") + row.prop(dprops, "magic_font_scale") + layout.prop(dprops, "imperial_precision") + layout.prop(dprops, "tolerance") + layout.prop(dprops, "classes_to_wireframe") def draw_decorator_colors(self, layout, context): layout.row().prop(self, "decorations_colour") @@ -478,8 +512,8 @@ class BIM_PT_tabs(Panel): if not tool.Ifc.get(): return - props = context.scene.BIMProperties - if props.has_blend_warning: + bim_props = tool.Blender.get_bim_props() + if bim_props.has_blend_warning: box = self.layout.box() box.alert = True row = box.row(align=True) @@ -488,9 +522,10 @@ class BIM_PT_tabs(Panel): op.uri = "https://docs.bonsaibim.org/guides/troubleshooting.html#saving-and-loading-blend-files" row.operator("bim.close_blend_warning", text="", icon="CANCEL") - if context.mode == "OBJECT" and context.scene.BIMGeometryProperties.mode in ("OBJECT", "ITEM"): + gprops = tool.Geometry.get_geometry_props() + if context.mode == "OBJECT" and gprops.mode in ("OBJECT", "ITEM"): pass - elif context.mode.startswith("EDIT") and context.scene.BIMGeometryProperties.mode == "EDIT": + elif context.mode.startswith("EDIT") and gprops.mode == "EDIT": pass else: box = self.layout.box() @@ -524,11 +559,11 @@ class BIM_PT_tab_new_project_wizard(Panel): def poll(cls, context): if not tool.Blender.is_tab(context, "PROJECT"): return False - props = context.scene.BIMProperties - pprops = context.scene.BIMProjectProperties + bim_props = tool.Blender.get_bim_props() + pprops = tool.Project.get_project_props() if pprops.is_loading: return False - elif tool.Ifc.get() or props.ifc_file: + elif tool.Ifc.get() or bim_props.ifc_file: return False return True @@ -546,11 +581,11 @@ class BIM_PT_tab_project_info(Panel): def poll(cls, context): if not tool.Blender.is_tab(context, "PROJECT"): return False - props = context.scene.BIMProperties - pprops = context.scene.BIMProjectProperties + bim_props = tool.Blender.get_bim_props() + pprops = tool.Project.get_project_props() if pprops.is_loading: return True - elif tool.Ifc.get() or props.ifc_file: + elif tool.Ifc.get() or bim_props.ifc_file: return True return False @@ -846,6 +881,7 @@ class BIM_PT_tab_object_metadata(Panel): @classmethod def poll(cls, context): + props = tool.Project.get_project_props() return ( tool.Blender.is_tab(context, "OBJECT") and tool.Ifc.get() @@ -854,7 +890,7 @@ class BIM_PT_tab_object_metadata(Panel): and ( obj.type != "EMPTY" or not obj.instance_collection - or not any(l.empty_handle == obj for l in context.scene.BIMProjectProperties.links) + or not any(l.empty_handle == obj for l in props.links) ) ) @@ -1223,7 +1259,7 @@ class BIM_PT_decorators_overlay(Panel): view = context.space_data overlay = view.overlay - georeference_props = bpy.context.scene.BIMGeoreferenceProperties + georeference_props = tool.Georeference.get_georeference_props() aggregate_props = bpy.context.scene.BIMAggregateProperties nest_props = bpy.context.scene.BIMNestProperties model_props = tool.Model.get_model_props() diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py index e5ae3cd493..c01b70bd55 100644 --- a/src/bonsai/bonsai/core/drawing.py +++ b/src/bonsai/bonsai/core/drawing.py @@ -417,6 +417,7 @@ def add_annotation( drawing_tool.show_decorations() obj = drawing_tool.create_annotation_object(drawing, object_type) element = ifc.get_entity(obj) + # TODO: element is never None? if not element: relating_type_rep = drawing_tool.get_annotation_representation(relating_type) if relating_type else None element = drawing_tool.run_root_assign_class( diff --git a/src/bonsai/bonsai/core/spatial.py b/src/bonsai/bonsai/core/spatial.py index bdfb410a43..50e80bbed8 100644 --- a/src/bonsai/bonsai/core/spatial.py +++ b/src/bonsai/bonsai/core/spatial.py @@ -130,11 +130,6 @@ def set_orientation_slot(spatial: tool.Spatial, product: ifcopenshell.entity_ins spatial.create_orientation_slots([product], use=True) -def edit_container_attributes(spatial: tool.Spatial, entity: ifcopenshell.entity_instance) -> None: - spatial.edit_container_attributes(entity) - spatial.import_spatial_decomposition() - - def contract_container(spatial: tool.Spatial, container: ifcopenshell.entity_instance) -> None: spatial.contract_container(container) spatial.import_spatial_decomposition() diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index b84e931871..d96b019df2 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -901,7 +901,6 @@ class Spatial: def deselect_objects(cls): pass def disable_editing(cls, obj): pass def duplicate_object_and_data(cls, obj): pass - def edit_container_attributes(cls, entity): pass def edit_container_name(cls, container, name): pass def enable_editing(cls, obj): pass def expand_container(cls, container): pass diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index ad62b1fabc..a4a2ed4558 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -37,9 +37,12 @@ from mathutils import Vector from pathlib import Path from functools import lru_cache from bonsai.bim.ifc import IFC_CONNECTED_TYPE -from typing import Any, Optional, Union, Literal, Iterable, Callable, TypeVar, Generator +from typing import Any, Optional, Union, Literal, Iterable, Callable, TypeVar, Generator, TYPE_CHECKING from typing_extensions import assert_never +if TYPE_CHECKING: + from bonsai.bim.prop import BIMProperties + VIEWPORT_ATTRIBUTES = [ "view_matrix", @@ -913,7 +916,7 @@ class Blender(bonsai.core.tool.Blender): return False if not (element := tool.Ifc.get_entity(obj)): return True - if obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs: + if obj in tool.Project.get_project_props().clipping_planes_objs: return False usage_type = tool.Model.get_usage_type(element) if usage_type in ("LAYER1", "LAYER2"): @@ -1477,10 +1480,8 @@ class Blender(bonsai.core.tool.Blender): @classmethod def get_user_data_dir(cls) -> Path: - try: - return Path(bpy.context.scene.BIMProperties.data_dir) - except AttributeError: - return Path() + props = tool.Blender.get_bim_props() + return Path(props.data_dir) @classmethod def get_data_dir_path(cls, relative_path: Union[str, Path]) -> Path: @@ -1538,3 +1539,9 @@ class Blender(bonsai.core.tool.Blender): dct = {cls.bl_idname: cls.ifc_element_type for cls in (BimTool.__subclasses__())} return types.MappingProxyType(dct) + + @classmethod + def get_bim_props(cls, scene: Optional[bpy.types.Scene] = None) -> BIMProperties: + if scene is None: + scene = bpy.context.scene + return scene.BIMProperties diff --git a/src/bonsai/bonsai/tool/brick.py b/src/bonsai/bonsai/tool/brick.py index 4b034f2160..acd8d50b01 100644 --- a/src/bonsai/bonsai/tool/brick.py +++ b/src/bonsai/bonsai/tool/brick.py @@ -86,10 +86,11 @@ class Brick(bonsai.core.tool.Brick): project = tool.Ifc.get().by_type("IfcProject")[0] ns = Namespace(namespace) brick_project = ns[project.GlobalId] + props = tool.Blender.get_bim_props() with BrickStore.new_changeset() as cs: cs.add((brick_project, A, REF.ifcProject)) cs.add((brick_project, REF.ifcProjectID, Literal(project.GlobalId))) - cs.add((brick_project, REF.ifcFileLocation, Literal(bpy.context.scene.BIMProperties.ifc_file))) + cs.add((brick_project, REF.ifcFileLocation, Literal(props.ifc_file))) if project.Name: cs.add((brick_project, URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal(project.Name))) return str(brick_project) diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py index 0dd9b6c855..7752423ce4 100644 --- a/src/bonsai/bonsai/tool/debug.py +++ b/src/bonsai/bonsai/tool/debug.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import os import json import bpy @@ -30,10 +31,17 @@ import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from mathutils import Vector from collections import defaultdict -from typing import Iterable, Literal +from typing import Iterable, Literal, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.debug.prop import BIMDebugProperties class Debug(bonsai.core.tool.Debug): + @classmethod + def get_debug_props(cls) -> BIMDebugProperties: + return bpy.context.scene.BIMDebugProperties + @classmethod def add_schema_identifier(cls, schema: W.schema_definition) -> None: IfcStore.schema_identifiers.append(schema.name()) @@ -46,7 +54,8 @@ class Debug(bonsai.core.tool.Debug): @classmethod def purge_hdf5_cache(cls) -> None: - cache_dir = bpy.context.scene.BIMProperties.cache_dir + props = tool.Blender.get_bim_props() + cache_dir = props.cache_dir filelist = [f for f in os.listdir(cache_dir) if f.endswith(".h5")] for f in filelist: try: diff --git a/src/bonsai/bonsai/tool/document.py b/src/bonsai/bonsai/tool/document.py index 25799948fc..5d1e5eee54 100644 --- a/src/bonsai/bonsai/tool/document.py +++ b/src/bonsai/bonsai/tool/document.py @@ -16,56 +16,68 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import ifcopenshell.util.system import bonsai.bim.helper import bonsai.core.tool import bonsai.tool as tool -from typing import Any, Union, Sequence +from typing import Any, Union, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.document.prop import BIMDocumentProperties class Document(bonsai.core.tool.Document): + @classmethod + def get_document_props(cls) -> BIMDocumentProperties: + return bpy.context.scene.BIMDocumentProperties + @classmethod def add_breadcrumb(cls, document: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() new = props.breadcrumbs.add() new.name = str(document.id()) @classmethod def clear_breadcrumbs(cls) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() props.breadcrumbs.clear() @classmethod def clear_document_tree(cls) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() props.documents.clear() @classmethod def disable_editing_document(cls) -> None: - bpy.context.scene.BIMDocumentProperties.active_document_id = 0 + props = cls.get_document_props() + props.active_document_id = 0 @classmethod def disable_editing_ui(cls) -> None: - bpy.context.scene.BIMDocumentProperties.is_editing = False + props = cls.get_document_props() + props.is_editing = False @classmethod def enable_editing_ui(cls) -> None: - bpy.context.scene.BIMDocumentProperties.is_editing = True + props = cls.get_document_props() + props.is_editing = True @classmethod def export_document_attributes(cls) -> dict[str, Any]: - return bonsai.bim.helper.export_attributes(bpy.context.scene.BIMDocumentProperties.document_attributes) + props = cls.get_document_props() + return bonsai.bim.helper.export_attributes(props.document_attributes) @classmethod def get_active_breadcrumb(cls) -> Union[ifcopenshell.entity_instance, None]: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() if len(props.breadcrumbs): return tool.Ifc.get().by_id(int(props.breadcrumbs[-1].name)) @classmethod def import_document_attributes(cls, document: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() props.document_attributes.clear() def callback(attr_name: str, _, data: dict[str, Any]) -> Union[bool, None]: @@ -86,7 +98,7 @@ class Document(bonsai.core.tool.Document): @classmethod def import_project_documents(cls) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() props.documents.clear() project = tool.Ifc.get().by_type("IfcProject")[0] for rel in project.HasAssociations or []: @@ -100,7 +112,7 @@ class Document(bonsai.core.tool.Document): @classmethod def import_references(cls, document: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() is_ifc2x3 = tool.Ifc.get_schema() == "IFC2X3" references = cls.get_document_references(document) for element in references: @@ -115,7 +127,7 @@ class Document(bonsai.core.tool.Document): @classmethod def import_subdocuments(cls, document: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() if document.IsPointer: for element in document.IsPointer[0].RelatedDocuments or []: new = props.documents.add() @@ -130,13 +142,14 @@ class Document(bonsai.core.tool.Document): @classmethod def remove_latest_breadcrumb(cls) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() if len(props.breadcrumbs): props.breadcrumbs.remove(len(props.breadcrumbs) - 1) @classmethod def set_active_document(cls, document: ifcopenshell.entity_instance) -> None: - bpy.context.scene.BIMDocumentProperties.active_document_id = document.id() + props = cls.get_document_props() + props.active_document_id = document.id() @classmethod def get_document_information_id(cls, document: ifcopenshell.entity_instance) -> Union[str, None]: diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 134f3bcf5f..9f72fd51ee 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -51,7 +51,7 @@ from shapely.ops import unary_union from lxml import etree from mathutils import Vector, Matrix from fractions import Fraction -from typing import Optional, Union, Iterable, Any, Literal, Sequence, TYPE_CHECKING +from typing import Optional, Union, Iterable, Any, Literal, Sequence, TYPE_CHECKING, NamedTuple from pathlib import Path if TYPE_CHECKING: @@ -65,25 +65,32 @@ class Drawing(bonsai.core.tool.Drawing): # ObjectType: annotation_name, description, icon, data_type # fmt: off - ANNOTATION_TYPES_DATA = { - "DIMENSION": ("Dimension", "Add dimensions annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "FIXED_SIZE", "curve"), - "ANGLE": ("Angle", "", "DRIVER_ROTATIONAL_DIFFERENCE", "curve"), - "RADIUS": ("Radius", "", "FORWARD", "curve"), - "DIAMETER": ("Diameter", "Add diameter annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "ARROW_LEFTRIGHT", "curve"), - "TEXT": ("Text", "", "SMALL_CAPS", "empty"), - "TEXT_LEADER": ("Leader", "", "TRACKING_BACKWARDS", "curve"), - "STAIR_ARROW": ("Stair Arrow", "Add stair arrow annotation.\nIf you have IfcStairFlight object selected, it will be used as a reference for the annotation", "SCREEN_BACK", "curve"), - "PLAN_LEVEL": ("Level (Plan)", "", "SORTBYEXT", "curve"), - "SECTION_LEVEL": ("Level (Section)", "", "TRIA_DOWN", "curve"), - "BREAKLINE": ("Breakline", "", "FCURVE", "mesh"), - "SYMBOL": ("Symbol", "", "KEYFRAME", "empty"), - "MULTI_SYMBOL": ("Multi-Symbol", "", "OUTLINER_DATA_POINTCLOUD", "mesh"), - "LINEWORK": ("Line", "", "SNAP_MIDPOINT", "mesh"), - "BATTING": ("Batting", "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set", "FORCE_FORCE", "mesh"), - "REVISION_CLOUD":("Revision Cloud", "Add revision cloud", "VOLUME_DATA", "mesh"), - "FILL_AREA": ("Fill Area", "", "NODE_TEXTURE", "mesh"), - "FALL": ("Fall", "", "SORT_ASC", "curve"), - "IMAGE": ("Image", "Add reference image attached to the drawing", "TEXTURE", "mesh"), + + class AnnotationObjectType(NamedTuple): + annotation_name: str + description: str + icon: str + data_type: Drawing.ANNOTATION_DATA_TYPE + + ANNOTATION_TYPES_DATA: dict[str, AnnotationObjectType] = { + "DIMENSION": AnnotationObjectType("Dimension", "Add dimensions annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "FIXED_SIZE", "curve"), + "ANGLE": AnnotationObjectType("Angle", "", "DRIVER_ROTATIONAL_DIFFERENCE", "curve"), + "RADIUS": AnnotationObjectType("Radius", "", "FORWARD", "curve"), + "DIAMETER": AnnotationObjectType("Diameter", "Add diameter annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "ARROW_LEFTRIGHT", "curve"), + "TEXT": AnnotationObjectType("Text", "", "SMALL_CAPS", "empty"), + "TEXT_LEADER": AnnotationObjectType("Leader", "", "TRACKING_BACKWARDS", "curve"), + "STAIR_ARROW": AnnotationObjectType("Stair Arrow", "Add stair arrow annotation.\nIf you have IfcStairFlight object selected, it will be used as a reference for the annotation", "SCREEN_BACK", "curve"), + "PLAN_LEVEL": AnnotationObjectType("Level (Plan)", "", "SORTBYEXT", "curve"), + "SECTION_LEVEL": AnnotationObjectType("Level (Section)", "", "TRIA_DOWN", "curve"), + "BREAKLINE": AnnotationObjectType("Breakline", "", "FCURVE", "mesh"), + "SYMBOL": AnnotationObjectType("Symbol", "", "KEYFRAME", "empty"), + "MULTI_SYMBOL": AnnotationObjectType("Multi-Symbol", "", "OUTLINER_DATA_POINTCLOUD", "mesh"), + "LINEWORK": AnnotationObjectType("Line", "", "SNAP_MIDPOINT", "mesh"), + "BATTING": AnnotationObjectType("Batting", "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set", "FORCE_FORCE", "mesh"), + "REVISION_CLOUD":AnnotationObjectType("Revision Cloud", "Add revision cloud", "VOLUME_DATA", "mesh"), + "FILL_AREA": AnnotationObjectType("Fill Area", "", "NODE_TEXTURE", "mesh"), + "FALL": AnnotationObjectType("Fall", "", "SORT_ASC", "curve"), + "IMAGE": AnnotationObjectType("Image", "Add reference image attached to the drawing", "TEXTURE", "mesh"), } # fmt: on @@ -112,7 +119,7 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def get_annotation_data_type(cls, object_type: str) -> ANNOTATION_DATA_TYPE: - return cls.ANNOTATION_TYPES_DATA[object_type][3] + return cls.ANNOTATION_TYPES_DATA[object_type].data_type @classmethod def create_annotation_object(cls, drawing: ifcopenshell.entity_instance, object_type: str) -> bpy.types.Object: @@ -238,7 +245,7 @@ class Drawing(bonsai.core.tool.Drawing): element_type = element.is_a() - if element_type == "IfcAnnotation" and element.ObjectType in object_types: + if element_type == "IfcAnnotation" and ifcopenshell.util.element.get_predefined_type(element) in object_types: return True if element_type == "IfcTypeProduct" and ( @@ -346,19 +353,23 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def disable_editing_drawings(cls) -> None: - bpy.context.scene.DocProperties.is_editing_drawings = False + props = tool.Drawing.get_document_props() + props.is_editing_drawings = False @classmethod def disable_editing_schedules(cls) -> None: - bpy.context.scene.DocProperties.is_editing_schedules = False + props = tool.Drawing.get_document_props() + props.is_editing_schedules = False @classmethod def disable_editing_references(cls) -> None: - bpy.context.scene.DocProperties.is_editing_references = False + props = tool.Drawing.get_document_props() + props.is_editing_references = False @classmethod def disable_editing_sheets(cls) -> None: - bpy.context.scene.DocProperties.is_editing_sheets = False + props = tool.Drawing.get_document_props() + props.is_editing_sheets = False @classmethod def disable_editing_text(cls, obj: bpy.types.Object) -> None: @@ -383,19 +394,23 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def enable_editing_drawings(cls) -> None: - bpy.context.scene.DocProperties.is_editing_drawings = True + props = tool.Drawing.get_document_props() + props.is_editing_drawings = True @classmethod def enable_editing_schedules(cls) -> None: - bpy.context.scene.DocProperties.is_editing_schedules = True + props = tool.Drawing.get_document_props() + props.is_editing_schedules = True @classmethod def enable_editing_references(cls) -> None: - bpy.context.scene.DocProperties.is_editing_references = True + props = tool.Drawing.get_document_props() + props.is_editing_references = True @classmethod def enable_editing_sheets(cls) -> None: - bpy.context.scene.DocProperties.is_editing_sheets = True + props = tool.Drawing.get_document_props() + props.is_editing_sheets = True @classmethod def enable_editing_text(cls, obj: bpy.types.Object) -> None: @@ -616,7 +631,8 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def is_editing_sheets(cls) -> bool: - return bpy.context.scene.DocProperties.is_editing_sheets + props = tool.Drawing.get_document_props() + return props.is_editing_sheets @classmethod def remove_literal_from_annotation(cls, obj: bpy.types.Object, literal: ifcopenshell.entity_instance) -> None: @@ -810,7 +826,7 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def import_drawings(cls) -> None: - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() expanded_target_views = {d.target_view for d in props.drawings if d.is_expanded} if not hasattr(cls, "drawing_selected_states"): cls.drawing_selected_states = {} @@ -1048,7 +1064,8 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def show_decorations(cls) -> None: - bpy.context.scene.DocProperties.should_draw_decorations = True + props = tool.Drawing.get_document_props() + props.should_draw_decorations = True @classmethod def update_text_value(cls, obj: bpy.types.Object) -> None: @@ -1147,36 +1164,34 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def get_default_layout_path(cls, identification: str, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] + props = tool.Drawing.get_document_props() layouts_dir = ( - ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "LayoutsDir") - or bpy.context.scene.DocProperties.layouts_dir + ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "LayoutsDir") or props.layouts_dir ) return os.path.join(layouts_dir, cls.sanitise_filename(f"{identification} - {name}.svg")).replace("\\", "/") @classmethod def get_default_sheet_path(cls, identification: str, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] - sheets_dir = ( - ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "SheetsDir") - or bpy.context.scene.DocProperties.sheets_dir - ) + props = tool.Drawing.get_document_props() + sheets_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "SheetsDir") or props.sheets_dir return os.path.join(sheets_dir, cls.sanitise_filename(f"{identification} - {name}.svg")).replace("\\", "/") @classmethod def get_default_titleblock_path(cls, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] + props = tool.Drawing.get_document_props() titleblocks_dir = ( - ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir") - or bpy.context.scene.DocProperties.titleblocks_dir + ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir") or props.titleblocks_dir ) return os.path.join(titleblocks_dir, cls.sanitise_filename(f"{name}.svg")).replace("\\", "/") @classmethod def get_default_drawing_path(cls, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] + props = tool.Drawing.get_document_props() drawings_dir = ( - ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "DrawingsDir") - or bpy.context.scene.DocProperties.drawings_dir + ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "DrawingsDir") or props.drawings_dir ) return os.path.join(drawings_dir, cls.sanitise_filename(f"{name}.svg")).replace("\\", "/") @@ -1187,15 +1202,16 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def get_default_drawing_resource_path(cls, resource: str) -> Union[str, None]: project = tool.Ifc.get().by_type("IfcProject")[0] + props = tool.Drawing.get_document_props() resource_path = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", f"{resource}Path") or getattr( - bpy.context.scene.DocProperties, f"{resource.lower()}_path" + props, f"{resource.lower()}_path" ) if resource_path: return resource_path.replace("\\", "/") @classmethod def get_default_shading_style(cls) -> str: - dprops = bpy.context.scene.DocProperties + dprops = tool.Drawing.get_document_props() return dprops.shadingstyle_default @classmethod @@ -1501,7 +1517,7 @@ class Drawing(bonsai.core.tool.Drawing): dst.data = dst.data.copy() dst.name = dst.name.replace("IfcGridAxis/", "") dst.BIMObjectProperties.ifc_definition_id = 0 - dst.data.BIMMeshProperties.ifc_definition_id = 0 + tool.Geometry.get_geometry_props(dst).ifc_definition_id = 0 return dst def disassemble(obj: bpy.types.Object) -> tuple[bpy.types.Object, bmesh.types.BMesh]: @@ -1883,7 +1899,8 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def is_active_drawing(cls, drawing: ifcopenshell.entity_instance) -> bool: - return drawing.id() == bpy.context.scene.DocProperties.active_drawing_id + props = tool.Drawing.get_document_props() + return drawing.id() == props.active_drawing_id @classmethod def run_drawing_activate_model(cls) -> None: diff --git a/src/bonsai/bonsai/tool/feature.py b/src/bonsai/bonsai/tool/feature.py index d8fd8921eb..0990aef008 100644 --- a/src/bonsai/bonsai/tool/feature.py +++ b/src/bonsai/bonsai/tool/feature.py @@ -16,17 +16,25 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import bonsai.core.tool import bonsai.tool as tool import bonsai.bim.helper import ifcopenshell -from typing import Iterable +from typing import Iterable, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.void.prop import BIMBooleanProperties class Feature(bonsai.core.tool.Feature): # TODO: consolidate module/model/opening and module/void into new module/feature + @classmethod + def get_boolean_props(cls) -> BIMBooleanProperties: + return bpy.context.scene.BIMBooleanProperties + @classmethod def add_feature(cls, featured_obj: bpy.types.Object, feature_objs: Iterable[bpy.types.Object]) -> None: featured_element = tool.Ifc.get_entity(featured_obj) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index fca6541088..4947666bd6 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -27,6 +27,7 @@ import numpy.typing as npt import multiprocessing import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.geometry import ifcopenshell.api.grid import ifcopenshell.api.profile import ifcopenshell.api.style @@ -53,11 +54,23 @@ from math import radians, pi from mathutils import Vector, Matrix from mathutils.bvhtree import BVHTree from bonsai.bim.ifc import IfcStore -from typing import Union, Iterable, Optional, Literal, Iterator, List, TYPE_CHECKING, get_args, Generator, cast +from typing import ( + Union, + Iterable, + Optional, + Literal, + Iterator, + List, + TYPE_CHECKING, + get_args, + Generator, + cast, + TypeGuard, +) from typing_extensions import TypeIs if TYPE_CHECKING: - from bonsai.bim.prop import Attribute + from bonsai.bim.prop import Attribute, BIMMeshProperties from bonsai.bim.module.geometry.prop import BIMObjectGeometryProperties, BIMGeometryProperties @@ -70,6 +83,10 @@ class Geometry(bonsai.core.tool.Geometry): def get_object_geometry_props(cls, object: bpy.types.Object) -> BIMObjectGeometryProperties: return object.BIMGeometryProperties + @classmethod + def get_mesh_props(cls, mesh: TYPES_WITH_MESH_PROPERTIES) -> BIMMeshProperties: + return mesh.BIMMeshProperties + @classmethod def change_object_data(cls, obj: bpy.types.Object, data: bpy.types.ID, is_global: bool = False) -> None: if is_global: @@ -130,11 +147,11 @@ class Geometry(bonsai.core.tool.Geometry): def is_locked(cls, element: ifcopenshell.entity_instance) -> bool: if element.is_a("IfcProject"): return True - elif tool.Root.is_spatial_element(element) and bpy.context.scene.BIMSpatialDecompositionProperties.is_locked: + elif tool.Root.is_spatial_element(element) and tool.Spatial.get_spatial_props().is_locked: return True elif ( element.is_a("IfcPositioningElement") or element.is_a("IfcGrid") or element.is_a("IfcGridAxis") - ) and bpy.context.scene.BIMGridProperties.is_locked: + ) and tool.Spatial.get_grid_props().is_locked: return True return False @@ -182,7 +199,9 @@ class Geometry(bonsai.core.tool.Geometry): if item_obj.obj == obj: props.item_objs.remove(i) break - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) + item = tool.Ifc.get().by_id(tool.Geometry.get_mesh_props(mesh).ifc_definition_id) cls.remove_representation_item(item) cls.reload_representation(props.representation_obj) bpy.data.objects.remove(obj) @@ -249,17 +268,20 @@ class Geometry(bonsai.core.tool.Geometry): bonsai.core.system.remove_port(tool.Ifc, tool.System, port=port) ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element) - if isinstance(obj.data, bpy.types.Mesh) and not tool.Ifc.get_entity_by_id( - obj.data.BIMMeshProperties.ifc_definition_id + data = obj.data + if ( + tool.Geometry.has_mesh_properties(data) + and tool.Ifc.get_entity_by_id(tool.Geometry.get_mesh_props(data).ifc_definition_id) is None ): - tool.Blender.remove_data_block(obj.data) + tool.Blender.remove_data_block(data) if is_spatial: bonsai.core.spatial.import_spatial_decomposition(tool.Spatial) try: obj.name - if bpy.context.scene.BIMGeometryProperties.representation_obj == obj: - bpy.context.scene.BIMGeometryProperties.representation_obj = None + props = tool.Geometry.get_geometry_props() + if props.representation_obj == obj: + props.representation_obj = None bpy.data.objects.remove(obj) except: pass @@ -268,7 +290,9 @@ class Geometry(bonsai.core.tool.Geometry): def dissolve_triangulated_edges(cls, obj: bpy.types.Object) -> None: # AdvancedBreps may contain non-faceted, curved faces (e.g. as part of # a cylinder) so dissolving edges should not be allowed. - mesh_element = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + mesh = obj.data + assert isinstance(mesh, Geometry.TYPES_WITH_MESH_PROPERTIES) + mesh_element = tool.Ifc.get().by_id(tool.Geometry.get_mesh_props(mesh).ifc_definition_id) if ( ( mesh_element.is_a("IfcShapeRepresentation") @@ -279,26 +303,30 @@ class Geometry(bonsai.core.tool.Geometry): or not obj.data ): return - if hasattr(obj.data, "attributes") and (ios_edges_attribute := obj.data.attributes.get("ios_edges")): + + if not isinstance(mesh, bpy.types.Mesh): + return + + if hasattr(mesh, "attributes") and (ios_edges_attribute := mesh.attributes.get("ios_edges")): # Edges from a forced triangulation are stored as True in a boolean attribute on the mesh bm = bmesh.new() - bm.from_mesh(obj.data) + bm.from_mesh(mesh) edges_to_dissolve = [e for i, e in enumerate(bm.edges) if not ios_edges_attribute.data[i].value] bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve) - bm.to_mesh(obj.data) + bm.to_mesh(mesh) bm.free() - elif "ios_edges" in obj.data: + elif "ios_edges" in mesh: bm = bmesh.new() - bm.from_mesh(obj.data) - edges_to_keep = set(map(frozenset, obj.data["ios_edges"])) + bm.from_mesh(mesh) + edges_to_keep = set(map(frozenset, mesh["ios_edges"])) edges_to_dissolve = [] for edge in bm.edges: if frozenset([vert.index for vert in edge.verts]) not in edges_to_keep: edges_to_dissolve.append(edge) bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve) - bm.to_mesh(obj.data) + bm.to_mesh(mesh) bm.free() - del obj.data["ios_edges"] + del mesh["ios_edges"] @classmethod def apply_item_ids_as_vertex_groups(cls, obj: bpy.types.Object) -> None: @@ -486,13 +514,19 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def get_active_representation(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]: """:return: IfcRepresentation/IfcRepresentationItem or None""" - if obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.ifc_definition_id: - return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + if ( + (data := obj.data) + and isinstance(data, Geometry.TYPES_WITH_MESH_PROPERTIES) + and (ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id) + ): + return tool.Ifc.get().by_id(ifc_id) @classmethod - def get_data_representation(cls, data: bpy.types.Mesh) -> ifcopenshell.entity_instance | None: - if hasattr(data, "BIMMeshProperties") and data.BIMMeshProperties.ifc_definition_id: - return tool.Ifc.get().by_id(data.BIMMeshProperties.ifc_definition_id) + def get_data_representation(cls, data: bpy.types.ID) -> ifcopenshell.entity_instance | None: + if isinstance(data, Geometry.TYPES_WITH_MESH_PROPERTIES) and ( + ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id + ): + return tool.Ifc.get().by_id(ifc_id) @classmethod def get_active_representation_context(cls, obj: bpy.types.Object) -> ifcopenshell.entity_instance: @@ -670,13 +704,13 @@ class Geometry(bonsai.core.tool.Geometry): return data.users != 0 @classmethod - def has_geometric_data(cls, obj: bpy.types.Object) -> bool: - if not obj.data: + def is_geometric_data(cls, data: Union[bpy.types.ID, None]) -> TypeGuard[Union[bpy.types.Mesh, bpy.types.Curve]]: + if not data: return False - if isinstance(obj.data, bpy.types.Mesh): - return bool(obj.data.vertices) - elif isinstance(obj.data, bpy.types.Curve): - return bool(obj.data.splines) + if isinstance(data, bpy.types.Mesh): + return bool(data.vertices) + elif isinstance(data, bpy.types.Curve): + return bool(data.splines) return False @classmethod @@ -826,7 +860,8 @@ class Geometry(bonsai.core.tool.Geometry): ifc_importer.material_creator.load_existing_materials() shape_has_openings = cls.does_shape_has_openings(shape) ifc_importer.material_creator.create(element, obj, mesh, shape_has_openings) - mesh.BIMMeshProperties.has_openings_applied = apply_openings + mprops = tool.Geometry.get_mesh_props(mesh) + mprops.has_openings_applied = apply_openings if not shape_has_openings: tool.Loader.load_indexed_colour_map(representation, mesh) tool.Loader.link_mesh(shape, mesh) @@ -852,7 +887,8 @@ class Geometry(bonsai.core.tool.Geometry): ifc_importer.material_creator.load_existing_materials() shape_has_openings = False ifc_importer.material_creator.create(element, obj, mesh, shape_has_openings) - mesh.BIMMeshProperties.has_openings_applied = apply_openings + mprops = tool.Geometry.get_mesh_props(mesh) + mprops.has_openings_applied = apply_openings if not shape_has_openings: tool.Loader.load_indexed_colour_map(representation, mesh) meshes[mesh_name] = mesh @@ -867,7 +903,7 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def import_representation_parameters(cls, data: bpy.types.Mesh) -> None: - props = data.BIMMeshProperties + props = tool.Geometry.get_mesh_props(data) elements = tool.Ifc.get().traverse(tool.Ifc.get().by_id(props.ifc_definition_id)) props.ifc_parameters.clear() for element in elements: @@ -974,7 +1010,8 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def is_profile_based(cls, data: bpy.types.Mesh) -> bool: - return data.BIMMeshProperties.subshape_type == "PROFILE" + props = tool.Geometry.get_mesh_props(data) + return props.subshape_type == "PROFILE" @classmethod def is_profile_object_active(cls) -> bool: @@ -992,7 +1029,7 @@ class Geometry(bonsai.core.tool.Geometry): data = obj.data if ( isinstance(data, Geometry.TYPES_WITH_MESH_PROPERTIES) - and (ifc_id := data.BIMMeshProperties.ifc_definition_id) + and (ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id) and ((item := tool.Ifc.get().by_id(ifc_id)).is_a("IfcRepresentationItem")) ): return item @@ -1008,14 +1045,14 @@ class Geometry(bonsai.core.tool.Geometry): if tool.Ifc.get_entity(obj): return obj elif tool.Geometry.is_representation_item(obj): - return bpy.context.scene.BIMGeometryProperties.representation_obj + return tool.Geometry.get_geometry_props().representation_obj @classmethod def is_boolean_operand(cls, obj: bpy.types.Object) -> bool: return bool( (data := obj.data) and isinstance(data, Geometry.TYPES_WITH_MESH_PROPERTIES) - and (ifc_id := data.BIMMeshProperties.ifc_definition_id) + and (ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id) and (item := tool.Ifc.get().by_id(ifc_id)) and ( item.is_a("IfcBooleanResult") @@ -1041,7 +1078,8 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def record_object_materials(cls, obj: bpy.types.Object) -> None: - obj.data.BIMMeshProperties.material_checksum = cls.get_material_checksum(obj) + props = tool.Geometry.get_mesh_props(obj.data) + props.material_checksum = cls.get_material_checksum(obj) @classmethod def record_object_position(cls, obj: bpy.types.Object) -> None: @@ -1146,11 +1184,13 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def should_force_faceted_brep(cls) -> bool: - return bpy.context.scene.BIMGeometryProperties.should_force_faceted_brep + props = tool.Geometry.get_geometry_props() + return props.should_force_faceted_brep @classmethod def should_force_triangulation(cls) -> bool: - return bpy.context.scene.BIMGeometryProperties.should_force_triangulation + props = tool.Geometry.get_geometry_props() + return props.should_force_triangulation @classmethod def should_generate_uvs(cls, obj: bpy.types.Object) -> bool: @@ -1167,7 +1207,8 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def should_use_presentation_style_assignment(cls) -> bool: - return bpy.context.scene.BIMGeometryProperties.should_use_presentation_style_assignment + props = tool.Geometry.get_geometry_props() + return props.should_use_presentation_style_assignment @classmethod def get_model_representations(cls) -> list[ifcopenshell.entity_instance]: @@ -1238,7 +1279,8 @@ class Geometry(bonsai.core.tool.Geometry): In the most cases just use reload_representation as it will handle those complications by itself. """ - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + representation = cls.get_active_representation(obj) + assert representation bonsai.core.geometry.switch_representation( tool.Ifc, tool.Geometry, @@ -1526,7 +1568,7 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def get_blender_offset_type(cls, obj: bpy.types.Object) -> Optional[str]: - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: if (result := obj.BIMObjectProperties.blender_offset_type) == "NONE": result = obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT" @@ -1641,7 +1683,8 @@ class Geometry(bonsai.core.tool.Geometry): for item_obj in props.item_objs: if not (obj := item_obj.obj) or not tool.Ifc.is_moved(obj): continue - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + item = cls.get_active_representation(obj) + assert item if item.is_a("IfcSweptAreaSolid"): has_changed = True old_position = item.Position @@ -1683,7 +1726,7 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def import_item_attributes(cls, obj: bpy.types.Object) -> None: - props = obj.data.BIMMeshProperties + props = tool.Geometry.get_mesh_props(obj.data) props.item_attributes.clear() item = tool.Ifc.get().by_id(props.ifc_definition_id) allowed_attributes = [ @@ -1710,10 +1753,10 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def update_item_attributes(cls, obj: bpy.types.Object) -> None: - props = obj.data.BIMMeshProperties + props = tool.Geometry.get_mesh_props(obj.data) ifc_file = tool.Ifc.get() - item = tool.Ifc.get().by_id(props.ifc_definition_id) + item = ifc_file.by_id(props.ifc_definition_id) for attribute in props.item_attributes: setattr(item, attribute.name, attribute.get_value()) @@ -1738,7 +1781,9 @@ class Geometry(bonsai.core.tool.Geometry): tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(tool.Ifc.get()) tool.Loader.settings.context_settings = tool.Loader.create_settings() tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True) - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + assert isinstance(obj.data, bpy.types.Mesh) + item = tool.Geometry.get_active_representation(obj) + assert item obj.data.clear_geometry() if item.is_a("IfcHalfSpaceSolid"): @@ -1802,12 +1847,14 @@ class Geometry(bonsai.core.tool.Geometry): props.mode = "OBJECT" props.is_changing_mode = False props.representation_obj = None - bpy.context.scene.BIMBooleanProperties.is_editing = False + tool.Feature.get_boolean_props().is_editing = False @classmethod def edit_meshlike_item(cls, obj: bpy.types.Object) -> None: - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) - if obj.data.BIMMeshProperties.mesh_checksum == cls.get_mesh_checksum(obj.data): + item = tool.Geometry.get_active_representation(obj) + assert item + mprops = tool.Geometry.get_mesh_props(obj.data) + if mprops.mesh_checksum == cls.get_mesh_checksum(obj.data): return builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) @@ -1830,8 +1877,8 @@ class Geometry(bonsai.core.tool.Geometry): for inverse in tool.Ifc.get().get_inverse(item): ifcopenshell.util.element.replace_attribute(inverse, item, new_item) ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item) - obj.data.BIMMeshProperties.ifc_definition_id = new_item.id() - cls.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Ifc.link(new_item, obj.data) + cls.reload_representation(props.representation_obj) @classmethod def split_by_loose_parts(cls, obj: bpy.types.Object) -> List[bpy.types.Mesh]: diff --git a/src/bonsai/bonsai/tool/georeference.py b/src/bonsai/bonsai/tool/georeference.py index 9cd80ce993..7b8ee46d24 100644 --- a/src/bonsai/bonsai/tool/georeference.py +++ b/src/bonsai/bonsai/tool/georeference.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import json import numpy as np @@ -27,24 +28,34 @@ import ifcopenshell.util.unit import bonsai.core.tool import bonsai.tool as tool import bonsai.bim.helper -from typing import Any, Union, Literal +from typing import Any, Union, Literal, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.georeference.prop import BIMGeoreferenceProperties class Georeference(bonsai.core.tool.Georeference): COORDINATE_TYPE = Literal["blender", "local", "map"] + @classmethod + def get_georeference_props(cls) -> BIMGeoreferenceProperties: + return bpy.context.scene.BIMGeoreferenceProperties + @classmethod def add_georeferencing(cls) -> None: + props = cls.get_georeference_props() tool.Ifc.run( "georeference.add_georeferencing", - ifc_class=bpy.context.scene.BIMGeoreferenceProperties.coordinate_operation_class, + ifc_class=props.coordinate_operation_class, ) @classmethod def import_projected_crs(cls) -> None: + props = tool.Georeference.get_georeference_props() + def callback(name, prop, data): if name == "MapUnit": - new = bpy.context.scene.BIMGeoreferenceProperties.projected_crs.add() + new = props.projected_crs.add() new.name = name new.data_type = "enum" new.is_null = data[name] is None @@ -58,9 +69,9 @@ class Georeference(bonsai.core.tool.Georeference): ) if data["MapUnit"]: new.enum_value = str(data["MapUnit"].id()) + new.update = "tool.Georeference.update_map_unit" return True - props = bpy.context.scene.BIMGeoreferenceProperties props.projected_crs.clear() if tool.Ifc.get_schema() == "IFC2X3": @@ -72,11 +83,27 @@ class Georeference(bonsai.core.tool.Georeference): bonsai.bim.helper.import_attributes2(projected_crs, props.projected_crs, callback=callback) return + @classmethod + def update_map_unit(cls, self, context) -> None: + if unit_id := self.get_value(): + map_unit = tool.Ifc.get().by_id(int(unit_id)) + project_unit = ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT") + if map_unit and project_unit: + result = ifcopenshell.util.unit.convert_unit(1, project_unit, map_unit) + else: + result = 1.0 + else: + result = 1.0 + props = cls.get_georeference_props() + for attribute in props.coordinate_operation: + if attribute.name == "Scale": + attribute.set_value(str(result)) + @classmethod def import_coordinate_operation(cls) -> None: def callback(name, prop, data): if name in ("FirstCoordinate", "SecondCoordinate"): - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() if name == "FirstCoordinate": new = props.coordinate_operation.add() new.name = "Measure Type" @@ -94,7 +121,7 @@ class Georeference(bonsai.core.tool.Georeference): prop.string_value = "" if prop.is_null else str(data[name].wrappedValue) return True elif name == "XAxisAbscissa": - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() props.is_changing_angle = True if data["XAxisAbscissa"] is None or data["XAxisOrdinate"] is None: props.x_axis_is_null = True @@ -116,7 +143,7 @@ class Georeference(bonsai.core.tool.Georeference): prop.string_value = "" if prop.is_null else str(data[name]) return True - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() props.coordinate_operation.clear() if tool.Ifc.get_schema() == "IFC2X3": @@ -135,7 +162,7 @@ class Georeference(bonsai.core.tool.Georeference): if tool.Ifc.get_schema() == "IFC2X3": return - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() props.is_changing_angle = True props.true_north_abscissa = "0" props.true_north_ordinate = "1" @@ -159,7 +186,7 @@ class Georeference(bonsai.core.tool.Georeference): attributes[prop.name] = tool.Ifc.get().by_id(int(prop.enum_value)) return True - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() return bonsai.bim.helper.export_attributes(props.projected_crs, callback=callback) @classmethod @@ -175,7 +202,7 @@ class Georeference(bonsai.core.tool.Georeference): attributes[prop.name] = tool.Ifc.get().create_entity(measure_type, float(prop.string_value)) return True elif prop.name == "XAxisAbscissa": - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() if props.x_axis_is_null: attributes["XAxisAbscissa"] = None attributes["XAxisOrdinate"] = None @@ -190,12 +217,12 @@ class Georeference(bonsai.core.tool.Georeference): attributes[prop.name] = float(prop.string_value) return True - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() return bonsai.bim.helper.export_attributes(props.coordinate_operation, callback=callback) @classmethod def get_true_north_attributes(cls) -> Union[list[float], None]: - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() try: return [float(props.true_north_abscissa), float(props.true_north_ordinate)] except ValueError: @@ -203,36 +230,42 @@ class Georeference(bonsai.core.tool.Georeference): @classmethod def enable_editing(cls) -> None: - bpy.context.scene.BIMGeoreferenceProperties.is_editing = True + props = cls.get_georeference_props() + props.is_editing = True @classmethod def disable_editing(cls) -> None: - bpy.context.scene.BIMGeoreferenceProperties.is_editing = False + props = cls.get_georeference_props() + props.is_editing = False @classmethod def enable_editing_wcs(cls) -> None: - bpy.context.scene.BIMGeoreferenceProperties.is_editing_wcs = True + props = cls.get_georeference_props() + props.is_editing_wcs = True @classmethod def disable_editing_wcs(cls) -> None: - bpy.context.scene.BIMGeoreferenceProperties.is_editing_wcs = False + props = cls.get_georeference_props() + props.is_editing_wcs = False @classmethod def enable_editing_true_north(cls) -> None: - bpy.context.scene.BIMGeoreferenceProperties.is_editing_true_north = True + props = cls.get_georeference_props() + props.is_editing_true_north = True @classmethod def disable_editing_true_north(cls) -> None: - bpy.context.scene.BIMGeoreferenceProperties.is_editing_true_north = False + props = cls.get_georeference_props() + props.is_editing_true_north = False @classmethod def set_coordinates(cls, io: COORDINATE_TYPE, coordinates: list[float]) -> None: - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() setattr(props, f"{io}_coordinates", ",".join([str(o) for o in coordinates])) @classmethod def get_coordinates(cls, io: COORDINATE_TYPE) -> list[float]: - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() return [float(co) for co in getattr(props, f"{io}_coordinates").split(",")] @classmethod @@ -244,7 +277,7 @@ class Georeference(bonsai.core.tool.Georeference): def xyz2enh( cls, coordinates: tuple[float, float, float], should_return_in_map_units: bool = True ) -> tuple[float, float, float]: - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() if props.has_blender_offset: coordinates = ifcopenshell.util.geolocation.xyz2enh( coordinates[0], @@ -263,7 +296,7 @@ class Georeference(bonsai.core.tool.Georeference): @classmethod def enh2xyz(cls, coordinates: tuple[float, float, float]) -> tuple[float, float, float]: coordinates = ifcopenshell.util.geolocation.auto_enh2xyz(tool.Ifc.get(), *coordinates) - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() if props.has_blender_offset: coordinates = ifcopenshell.util.geolocation.enh2xyz( coordinates[0], @@ -313,7 +346,7 @@ class Georeference(bonsai.core.tool.Georeference): @classmethod def import_wcs(cls) -> None: - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() wcs = None for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): wcs = context.WorldCoordinateSystem @@ -330,7 +363,7 @@ class Georeference(bonsai.core.tool.Georeference): @classmethod def export_wcs(cls) -> dict[str, float]: - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() return { "x": float(props.wcs_x), "y": float(props.wcs_y), @@ -345,7 +378,7 @@ class Georeference(bonsai.core.tool.Georeference): @classmethod def set_model_origin(cls) -> None: unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - gprops = bpy.context.scene.BIMGeoreferenceProperties + gprops = tool.Georeference.get_georeference_props() e, n, h = cls.xyz2enh((0, 0, 0), should_return_in_map_units=False) gprops.model_origin = f"{e},{n},{h}" gprops.model_origin_si = f"{e * unit_scale},{n * unit_scale},{h * unit_scale}" @@ -359,4 +392,4 @@ class Georeference(bonsai.core.tool.Georeference): @classmethod def has_blender_offset(cls) -> bool: - return bpy.context.scene.BIMGeoreferenceProperties.has_blender_offset + return tool.Georeference.get_georeference_props().has_blender_offset diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py index 5b3f8bb48a..ce9f856f80 100644 --- a/src/bonsai/bonsai/tool/ifc.py +++ b/src/bonsai/bonsai/tool/ifc.py @@ -111,7 +111,7 @@ class Ifc(bonsai.core.tool.Ifc): elif isinstance(obj, bpy.types.Material): props = obj.BIMStyleProperties else: - props = obj.BIMMeshProperties + props = tool.Geometry.get_mesh_props(obj) if props and (ifc_definition_id := props.ifc_definition_id): try: @@ -129,9 +129,13 @@ class Ifc(bonsai.core.tool.Ifc): return None @classmethod - def get_object(cls, element: ifcopenshell.entity_instance) -> IFC_CONNECTED_TYPE: + def get_object(cls, element: ifcopenshell.entity_instance) -> Union[IFC_CONNECTED_TYPE, None]: return IfcStore.get_element(element.id()) + @classmethod + def get_object_by_identifier(cls, id_or_guid: Union[int, str]) -> Union[IFC_CONNECTED_TYPE, None]: + return IfcStore.get_element(id_or_guid) + @classmethod def rebuild_element_maps(cls) -> None: """Rebuilds the id_map and guid_map @@ -180,7 +184,7 @@ class Ifc(bonsai.core.tool.Ifc): cls.setup_listeners(obj) IfcStore.edited_objs = set() - edited_objs = bpy.context.scene.BIMProjectProperties.edited_objs + edited_objs = tool.Project.get_project_props().edited_objs for i in range(len(edited_objs))[::-1]: obj = edited_objs[i].obj if obj: @@ -220,7 +224,7 @@ class Ifc(bonsai.core.tool.Ifc): """ if obj in IfcStore.edited_objs: return - edited_objs = bpy.context.scene.BIMProjectProperties.edited_objs + edited_objs = tool.Project.get_project_props().edited_objs edited_objs.add().obj = obj IfcStore.edited_objs.add(obj) IfcStore.history_edit_object(obj, finish_editing=False) @@ -233,7 +237,7 @@ class Ifc(bonsai.core.tool.Ifc): """ if obj not in IfcStore.edited_objs: return - edited_objs = bpy.context.scene.BIMProjectProperties.edited_objs + edited_objs = tool.Project.get_project_props().edited_objs edited_objs.remove(next(i for i, o in enumerate(edited_objs) if o.obj == obj)) IfcStore.edited_objs.discard(obj) IfcStore.history_edit_object(obj, finish_editing=True) diff --git a/src/bonsai/bonsai/tool/ifcgit.py b/src/bonsai/bonsai/tool/ifcgit.py index 5b0f9b88e5..93475c090c 100644 --- a/src/bonsai/bonsai/tool/ifcgit.py +++ b/src/bonsai/bonsai/tool/ifcgit.py @@ -340,8 +340,8 @@ class IfcGit: @classmethod def get_revisions_step_ids(cls) -> Union[STEP_IDS, None]: - - path_ifc = bpy.data.scenes["Scene"].BIMProperties.ifc_file + props = tool.Blender.get_bim_props() + path_ifc = tool.Blender.get_bim_props().ifc_file props = bpy.context.scene.IfcGitProperties repo = IfcGitRepo.repo item = props.ifcgit_commits[props.commit_index] diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index f79f4b4509..7fb5fc4788 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -89,7 +89,7 @@ class Loader(bonsai.core.tool.Loader): @classmethod def get_mesh_name(cls, representation: ifcopenshell.entity_instance) -> str: - context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0 + context_id = context.id() if (context := getattr(representation, "ContextOfItems", None)) else 0 return "{}/{}".format(context_id, representation.id()) @classmethod @@ -105,7 +105,7 @@ class Loader(bonsai.core.tool.Loader): mesh: tool.Geometry.TYPES_WITH_MESH_PROPERTIES, ) -> None: geometry = shape.geometry if hasattr(shape, "geometry") else shape - mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id.split("-")[0]) + tool.Geometry.get_mesh_props(mesh).ifc_definition_id = int(geometry.id.split("-")[0]) @classmethod def create_surface_style_shading( @@ -698,7 +698,7 @@ class Loader(bonsai.core.tool.Loader): project_north = 0 if has_offset or has_rotation: - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.blender_offset_x = str(model_offset[0]) props.blender_offset_y = str(model_offset[1]) props.blender_offset_z = str(model_offset[2]) @@ -747,7 +747,7 @@ class Loader(bonsai.core.tool.Loader): cls, element: ifcopenshell.entity_instance, is_gross: bool = False ) -> Union[ifcopenshell.geom.ShapeElementType, None]: context_settings = cls.settings.gross_context_settings if is_gross else cls.settings.context_settings - geometry_library = bpy.context.scene.BIMProjectProperties.geometry_library + geometry_library = tool.Project.get_project_props().geometry_library for settings in context_settings: try: result = ifcopenshell.geom.create_shape(settings, element, geometry_library=geometry_library) @@ -952,7 +952,7 @@ class Loader(bonsai.core.tool.Loader): matrix[1][3] = offset_xyz[1] matrix[2][3] = offset_xyz[2] - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: if obj.BIMObjectProperties.blender_offset_type == "NONE": obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT" @@ -1018,6 +1018,74 @@ class Loader(bonsai.core.tool.Loader): mesh["ios_material_ids"] = ifcopenshell.util.shape.get_faces_material_style_ids(geometry).tolist() return mesh + @classmethod + def slice_layerset_mesh(cls, element: ifcopenshell.entity_instance, mesh: bpy.types.Mesh) -> bpy.types.Mesh: + if True: # This feature is still experimental + return mesh + if not (material := ifcopenshell.util.element.get_material(element)): + return mesh + elif material.is_a("IfcMaterialLayerSetUsage"): + usage = material + layer_set = material.ForLayerSet + offset = usage.OffsetFromReferenceLine * cls.unit_scale + sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1 + elif material.is_a("IfcMaterialLayerSet"): + usage = None + layer_set = material + offset = 0 + sense_factor = 1 + else: + return mesh + if len(layer_set.MaterialLayers) == 1: + return mesh + bm = bmesh.new() + bm.from_mesh(mesh) + prev_co = None + co = Vector((0.0, offset, 0.0)) + no = Vector((0.0, 1.0, 0.0)) + # Cache this + body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") + styles = {} + has_layer_styles = False + for i, material in mesh.materials: + if style := tool.Ifc.get_entity(material): + styles[style] = i + for layer in layer_set.MaterialLayers[:-1]: + prev_co = co.copy() + co.y = layer.LayerThickness * cls.unit_scale * sense_factor + bisect_geom = bmesh.ops.bisect_plane( + bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no + ) + bmesh.ops.duplicate(bm, geom=bisect_geom["geom_cut"]) + if style := ifcopenshell.util.representation.get_material_style(layer.Material, body): + if (material_index := styles.get(style, None)) is None: + material_index = len(mesh.materials) + mesh.materials.append(tool.Ifc.get_object(style)) + for face in bisect_geom["geom"]: + if isinstance(face, bmesh.types.BMFace): + center = face.calc_center_bounds() * sense_factor + if center.y < co.y and center.y > prev_co.y: + face.material_index = material_index + has_layer_styles = True + + # Last layer + layer = layer_set.MaterialLayers[-1] + if style := ifcopenshell.util.representation.get_material_style(layer.Material, body): + if (material_index := styles.get(style, None)) is None: + material_index = len(mesh.materials) + mesh.materials.append(tool.Ifc.get_object(style)) + for face in bisect_geom["geom"]: + if isinstance(face, bmesh.types.BMFace): + center = face.calc_center_bounds() * sense_factor + if center.y > co.y: + face.material_index = material_index + has_layer_styles = True + + bm.to_mesh(mesh) + bm.free() + mesh["has_layer_styles"] = has_layer_styles + return mesh + @classmethod def create_mesh_from_shape( cls, diff --git a/src/bonsai/bonsai/tool/misc.py b/src/bonsai/bonsai/tool/misc.py index 87d367895b..db43a14c5d 100644 --- a/src/bonsai/bonsai/tool/misc.py +++ b/src/bonsai/bonsai/tool/misc.py @@ -113,10 +113,11 @@ class Misc(bonsai.core.tool.Misc): new_objs = [] for obj in objs: - if obj.type != "MESH" or obj == cutter: + mesh = obj.data + if not isinstance(mesh, bpy.types.Mesh) or obj == cutter: continue new_obj = obj.copy() - new_obj.data = obj.data.copy() + new_obj.data = mesh.copy() for collection in obj.users_collection: collection.objects.link(new_obj) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 6db9718d4d..e55798602f 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -277,7 +277,7 @@ class Model(bonsai.core.tool.Model): mesh = bpy.data.meshes.new("Axis") mesh.from_pydata(cls.vertices, cls.edges, []) - mesh.BIMMeshProperties.subshape_type = "AXIS" + tool.Geometry.get_mesh_props(mesh).subshape_type = "AXIS" if obj is None: obj = bpy.data.objects.new("Axis", mesh) @@ -334,7 +334,7 @@ class Model(bonsai.core.tool.Model): mesh = bpy.data.meshes.new("Profile") mesh.from_pydata(cls.vertices, cls.edges, []) - mesh.BIMMeshProperties.subshape_type = "PROFILE" + tool.Geometry.get_mesh_props(mesh).subshape_type = "PROFILE" if obj is None: obj = bpy.data.objects.new("Profile", mesh) @@ -376,7 +376,7 @@ class Model(bonsai.core.tool.Model): mesh = bpy.data.meshes.new("Curve") mesh.from_pydata(cls.vertices, cls.edges, []) - mesh.BIMMeshProperties.subshape_type = "PROFILE" + tool.Geometry.get_mesh_props(mesh).subshape_type = "PROFILE" if obj is None: obj = bpy.data.objects.new("Curve", mesh) @@ -417,7 +417,7 @@ class Model(bonsai.core.tool.Model): mesh = bpy.data.meshes.new("Surface") mesh.from_pydata(cls.vertices, cls.edges, []) - mesh.BIMMeshProperties.subshape_type = "PROFILE" + tool.Geometry.get_mesh_props(mesh).subshape_type = "PROFILE" if obj is None: obj = bpy.data.objects.new("Surface", mesh) @@ -569,7 +569,10 @@ class Model(bonsai.core.tool.Model): element: Optional[ifcopenshell.entity_instance] = None, representation: Optional[ifcopenshell.entity_instance] = None, ) -> list[ifcopenshell.entity_instance]: + """Either element or representation must be provided.""" + assert element or representation, "Either element or representation must be provided." if representation is None: + assert element representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if not representation: return [] @@ -1461,9 +1464,9 @@ class Model(bonsai.core.tool.Model): after material assignment or material unassignment. """ for element in elements: - if not (obj := tool.Ifc.get_object(element)) or not obj.data: + if not (obj := tool.Ifc.get_object(element)) or not (data := obj.data): continue - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + representation = tool.Ifc.get().by_id(tool.Geometry.get_mesh_props(data).ifc_definition_id) bonsai.core.geometry.switch_representation( tool.Ifc, tool.Geometry, @@ -1931,7 +1934,9 @@ class Model(bonsai.core.tool.Model): or it's not referring to an object (e.g. potential boolean object).""" if obj.type != "MESH": return - return obj.data.BIMMeshProperties.obj + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) + return tool.Geometry.get_mesh_props(mesh).obj @classmethod def get_tracked_opening_type(cls, obj: bpy.types.Object) -> Union[Literal["OPENING", "BOOLEAN"], None]: diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 00fbad7483..b5c9ad4232 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -20,6 +20,7 @@ import bpy import bmesh import math import ifcopenshell +import ifcopenshell.util.unit import bonsai.core.tool import bonsai.tool as tool from bonsai.bim.module.drawing.helper import format_distance @@ -266,10 +267,20 @@ class Polyline(bonsai.core.tool.Polyline): snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0] snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) - if tool_state.use_default_container: - snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation)) + if tool_state.is_input_on: + if tool_state.use_default_container: + mouse_vector = Vector( + (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), default_container_elevation) + ) + else: + mouse_vector = Vector( + (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), input_ui.get_number_value("Z")) + ) else: - snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) + if tool_state.use_default_container: + snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation)) + else: + snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) if len(polyline_points) > 1: second_to_last_point_data = polyline_points[len(polyline_points) - 2] @@ -452,13 +463,17 @@ class Polyline(bonsai.core.tool.Polyline): def format_input_ui_units(cls, value: float, is_area: bool = False) -> str: unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) if bpy.context.scene.unit_settings.system == "IMPERIAL": - precision = bpy.context.scene.DocProperties.imperial_precision + dprops = tool.Drawing.get_document_props() + precision = dprops.imperial_precision + if is_area: + props = tool.Blender.get_bim_props() + area_unit = props.area_unit + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get(), unit_type=area_unit) else: precision = None - value = value if is_area else value / unit_scale return format_distance( - value, + value / unit_scale, precision=precision, hide_units=False, isArea=is_area, @@ -506,9 +521,13 @@ class Polyline(bonsai.core.tool.Polyline): for point in polyline_points[1:]: # The first can be repeated to form a wall loop if (x, y, z) == (point.x, point.y, point.z): return "Cannot create two points at the same location" - # Avoids duplicating an edge + # Avoids creating overlapping edges if len(polyline_points) > 1: - if Vector((x, y, z)) == Vector((polyline_points[-2].x, polyline_points[-2].y, polyline_points[-2].z)): + v1 = Vector((x, y, z)) + v2 = Vector((polyline_points[-1].x, polyline_points[-1].y, polyline_points[-1].z)) + v3 = Vector((polyline_points[-2].x, polyline_points[-2].y, polyline_points[-2].z)) + angle = tool.Cad.angle_3_vectors(v1, v2, v3, new_angle=None, degrees=True) + if tool.Cad.is_x(angle, 0): return # TODO move this limitation to be Wall tool specific. Right now it also affects Measure tool # Avoids creating segments smaller then 0.1. This is a limitation from create_wall_from_2_points diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index 7b529cdea9..4741e16a59 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -70,7 +70,8 @@ class Project(bonsai.core.tool.Project): @classmethod def load_pset_templates(cls): - pset_dir = tool.Ifc.resolve_uri(bpy.context.scene.BIMProperties.pset_dir) + props = tool.Blender.get_bim_props() + pset_dir = tool.Ifc.resolve_uri(props.pset_dir) if os.path.isdir(pset_dir): for path in Path(pset_dir).glob("*.ifc"): bonsai.bim.schema.ifc.psetqto.templates.append(ifcopenshell.open(path)) @@ -136,13 +137,15 @@ class Project(bonsai.core.tool.Project): @classmethod def set_context(cls, context): bonsai.bim.handler.refresh_ui_data() - bpy.context.scene.BIMRootProperties.contexts = str(context.id()) + rprops = tool.Root.get_root_props() + rprops.contexts = str(context.id()) @classmethod def set_default_context(cls): context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") if context: - bpy.context.scene.BIMRootProperties.contexts = str(context.id()) + rprops = tool.Root.get_root_props() + rprops.contexts = str(context.id()) @classmethod def set_default_modeling_dimensions(cls): @@ -235,7 +238,7 @@ class Project(bonsai.core.tool.Project): @classmethod def load_linked_models_from_ifc(cls) -> None: - links = bpy.context.scene.BIMProjectProperties.links + links = tool.Project.get_project_props().links links.clear() links_document = cls.get_linked_models_document() if not links_document: @@ -252,7 +255,7 @@ class Project(bonsai.core.tool.Project): @classmethod def save_linked_models_to_ifc(cls) -> None: ifc_file = tool.Ifc.get() - links = bpy.context.scene.BIMProjectProperties.links + links = tool.Project.get_project_props().links filepaths: set[Path] = set() for link in links: filepaths.add(Path(link.name)) @@ -372,8 +375,12 @@ class Project(bonsai.core.tool.Project): props = cls.get_project_props() for project_library in libraries: library_elements = tool.Project.get_project_library_elements(project_library) + subhierarchy = libraries[project_library] + for sublibrary in subhierarchy: + sublibrary_elements = tool.Project.get_project_library_elements(sublibrary) + library_elements.update(sublibrary_elements) props.add_library_project_library( - project_library.Name or "Unnamed", len(library_elements), project_library.id() + project_library.Name or "Unnamed", len(library_elements), project_library.id(), bool(subhierarchy) ) @classmethod diff --git a/src/bonsai/bonsai/tool/pset_template.py b/src/bonsai/bonsai/tool/pset_template.py index e2b69705b6..8977d1e32c 100644 --- a/src/bonsai/bonsai/tool/pset_template.py +++ b/src/bonsai/bonsai/tool/pset_template.py @@ -133,7 +133,8 @@ class PsetTemplate(bonsai.core.tool.PsetTemplate): for f in tool.Blender.get_data_dir_paths("pset", "*.ifc"): paths.append((f, "Global Pset Template")) - pset_dir = Path(tool.Ifc.resolve_uri(bpy.context.scene.BIMProperties.pset_dir)) + props = tool.Blender.get_bim_props() + pset_dir = Path(tool.Ifc.resolve_uri(props.pset_dir)) if pset_dir.is_dir(): for path in Path(pset_dir).glob("*.ifc"): paths.append((path, "Project Pset Template")) diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index b18e649338..6e92d8cf5a 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -16,9 +16,11 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.style import ifcopenshell.util.representation import ifcopenshell.util.element import ifcopenshell.util.placement @@ -26,12 +28,19 @@ import bonsai.core.tool import bonsai.core.aggregate import bonsai.core.geometry import bonsai.tool as tool -from typing import Union, Optional, Any, Literal +from typing import Union, Optional, Any, Literal, TYPE_CHECKING from bonsai.bim.module.spatial.decorator import GridDecorator from bonsai.bim.module.geometry.decorator import ItemDecorator +if TYPE_CHECKING: + from bonsai.bim.module.root.prop import BIMRootProperties + class Root(bonsai.core.tool.Root): + @classmethod + def get_root_props(cls) -> BIMRootProperties: + return bpy.context.scene.BIMRootProperties + @classmethod def add_tracked_opening(cls, obj: bpy.types.Object, opening_type: Literal["OPENING", "BOOLEAN"]) -> None: """Add tracked opening or boolean object.""" @@ -49,12 +58,12 @@ class Root(bonsai.core.tool.Root): tool.Geometry.run_style_add_style(obj=mat) for mat in tool.Geometry.get_object_materials_without_styles(obj) ] - ifcopenshell.api.run( - "style.assign_representation_styles", + props = tool.Geometry.get_geometry_props() + ifcopenshell.api.style.assign_representation_styles( tool.Ifc.get(), shape_representation=body, styles=tool.Geometry.get_styles(obj), - should_use_presentation_style_assignment=bpy.context.scene.BIMGeometryProperties.should_use_presentation_style_assignment, + should_use_presentation_style_assignment=props.should_use_presentation_style_assignment, ) @classmethod @@ -111,7 +120,7 @@ class Root(bonsai.core.tool.Root): @classmethod def get_default_container(cls) -> Optional[ifcopenshell.entity_instance]: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = tool.Spatial.get_spatial_props() if container := props.default_container: try: return tool.Ifc.get().by_id(container) @@ -169,8 +178,8 @@ class Root(bonsai.core.tool.Root): @classmethod def get_object_representation(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]: - if obj.data and obj.data.BIMMeshProperties.ifc_definition_id: - return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + if obj.data and (mesh_props := tool.Geometry.get_mesh_props(obj.data)).ifc_definition_id: + return tool.Ifc.get().by_id(mesh_props.ifc_definition_id) element = tool.Ifc.get_entity(obj) if element.is_a("IfcTypeProduct"): if element.RepresentationMaps: @@ -232,7 +241,7 @@ class Root(bonsai.core.tool.Root): @classmethod def reload_grid_decorator(cls) -> None: - axes = bpy.context.scene.BIMGridProperties.grid_axes + axes = tool.Spatial.get_grid_props().grid_axes axes.clear() for axis in tool.Ifc.get().by_type("IfcGridAxis"): if obj := tool.Ifc.get_object(axis): @@ -302,8 +311,8 @@ class Root(bonsai.core.tool.Root): voided_objs.append(subobj) for voided_obj in voided_objs: - if voided_obj.data: - representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id) + if data := voided_obj.data: + representation = tool.Ifc.get().by_id(tool.Geometry.get_mesh_props(data).ifc_definition_id) bonsai.core.geometry.switch_representation( tool.Ifc, tool.Geometry, @@ -421,10 +430,36 @@ class Root(bonsai.core.tool.Root): to unlink them. """ tool.Ifc.unlink(obj=obj) - if hasattr(obj.data, "BIMMeshProperties"): - obj.data.BIMMeshProperties.ifc_definition_id = 0 + if tool.Geometry.has_mesh_properties((data := obj.data)): + tool.Geometry.get_mesh_props(data).ifc_definition_id = 0 for material_slot in obj.material_slots: if material := material_slot.material: tool.Ifc.unlink(obj=material) if "Ifc" in obj.name and "/" in obj.name: obj.name = obj.name.split("/", 1)[1] + + @classmethod + def get_ifc_products(cls) -> tuple[str, ...]: + version = tool.Ifc.get_schema() + if version == "IFC2X3": + products = ( + "IfcElementType", + "IfcElement", + "IfcFeatureElement", + "IfcSpatialStructureElement", + "IfcStructuralItem", + "IfcAnnotation", + "IfcRelSpaceBoundary", + ) + else: + products = ( + "IfcElementType", + "IfcElement", + "IfcFeatureElement", + "IfcSpatialElement", + "IfcSpatialElementType", + "IfcStructuralItem", + "IfcAnnotation", + "IfcRelSpaceBoundary", + ) + return products diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py index 3b90961559..a48e1404b6 100644 --- a/src/bonsai/bonsai/tool/search.py +++ b/src/bonsai/bonsai/tool/search.py @@ -30,9 +30,14 @@ from typing import Union, Literal, TYPE_CHECKING if TYPE_CHECKING: from bonsai.bim.prop import BIMFilterGroup + from bonsai.bim.module.search.prop import BIMSearchProperties class Search(bonsai.core.tool.Search): + @classmethod + def get_search_props(cls) -> BIMSearchProperties: + return bpy.context.scene.BIMSearchProperties + @classmethod def get_group_query(cls, group: ifcopenshell.entity_instance) -> str: return json.loads(group.Description)["query"] @@ -40,7 +45,7 @@ class Search(bonsai.core.tool.Search): @classmethod def get_filter_groups(cls, module: str) -> bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]: if module == "search": - return bpy.context.scene.BIMSearchProperties.filter_groups + return cls.get_search_props().filter_groups elif module == "csv": return bpy.context.scene.CsvProperties.filter_groups elif module == "diff": diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index b06d9a65bb..8f89088e63 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -25,6 +25,7 @@ import math import mathutils from mathutils import Matrix, Vector from lark import Lark, Transformer +from typing import Union class Snap(bonsai.core.tool.Snap): @@ -192,7 +193,7 @@ class Snap(bonsai.core.tool.Snap): rot_intersection = rot_mat @ translated_intersection proximity = rot_intersection.y if tool_state.plane_method == "XZ": - proximity = rot_intersection.z + proximity = rot_intersection.x is_on_rot_axis = abs(proximity) <= stick_factor if is_on_rot_axis: @@ -207,10 +208,14 @@ class Snap(bonsai.core.tool.Snap): # If lock axis is on it will use the snap angle so there is no need to search for eligible axis if elegible_axis or tool_state.lock_axis: # Adapt axis to make snap angle work with other plane method - if tool_state.plane_method == "XZ": - axis = -axis - if tool_state.plane_method == "YZ": - axis = 90 - (axis * -1) + if elegible_axis: + if tool_state.plane_method == "XZ": + axis = 90 - (axis * -1) + else: + if tool_state.plane_method == "XZ": + axis = -axis + if tool_state.plane_method == "YZ": + axis = 90 - (axis * -1) rot_mat = Matrix.Rotation(math.radians(360 - axis), 3, pivot_axis) rot_mat = tool.Polyline.use_transform_orientations(rot_mat) rot_intersection = rot_mat @ translated_intersection @@ -313,7 +318,9 @@ class Snap(bonsai.core.tool.Snap): plane_normal = tool.Polyline.use_transform_orientations(plane_normal) return plane_origin, plane_normal - def cast_rays_to_single_object(obj, mouse_pos): + def cast_rays_to_single_object( + obj: bpy.types.Object, mouse_pos: tuple[int, int] + ) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]: if obj.type != "MESH": return None, None, None hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj) @@ -332,7 +339,9 @@ class Snap(bonsai.core.tool.Snap): else: return None, None, None - def cast_rays_and_get_best_object(objs_to_raycast, mouse_pos): + def cast_rays_and_get_best_object( + objs_to_raycast: list[bpy.types.Object], mouse_pos: tuple[int, int] + ) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]: best_length_squared = 1.0 best_obj = None best_hit = None @@ -568,7 +577,7 @@ class Snap(bonsai.core.tool.Snap): "object": obj, } snaps_by_type.insert(0, snap_point) - cls.update_snapping_point(snap_point["point"], snap_point["type"]) + cls.update_snapping_point(snap_point["point"], snap_point["type"]) return snaps_by_type cls.update_snapping_point(point["point"], point["type"]) return snaps_by_type diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index 5976462dc3..54d4f45f7b 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -42,12 +42,31 @@ import numpy as np from math import pi from mathutils import Vector, Matrix from shapely import Polygon -from typing import Generator, Optional, Union, Literal, List, Any, Iterable +from typing import Generator, Optional, Union, Literal, List, Any, Iterable, TYPE_CHECKING from collections import defaultdict from natsort import natsorted +if TYPE_CHECKING: + from bonsai.bim.module.spatial.prop import ( + BIMGridProperties, + BIMSpatialDecompositionProperties, + BIMObjectSpatialProperties, + ) + class Spatial(bonsai.core.tool.Spatial): + @classmethod + def get_spatial_props(cls) -> BIMSpatialDecompositionProperties: + return bpy.context.scene.BIMSpatialDecompositionProperties + + @classmethod + def get_object_spatial_props(cls, obj: bpy.types.Object) -> BIMObjectSpatialProperties: + return obj.BIMObjectSpatialProperties + + @classmethod + def get_grid_props(cls) -> BIMGridProperties: + return bpy.context.scene.BIMGridProperties + @classmethod def can_contain(cls, container: ifcopenshell.entity_instance, element_obj: Union[bpy.types.Object, None]) -> bool: if not (element := tool.Ifc.get_entity(element_obj)): @@ -82,7 +101,8 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def disable_editing(cls, obj: bpy.types.Object) -> None: - obj.BIMObjectSpatialProperties.is_editing = False + props = cls.get_object_spatial_props(obj) + props.is_editing = False @classmethod def duplicate_object_and_data(cls, obj: bpy.types.Object) -> bpy.types.Object: @@ -93,8 +113,9 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def enable_editing(cls, obj: bpy.types.Object) -> None: - obj.BIMObjectSpatialProperties.is_editing = True - obj.BIMObjectSpatialProperties.relating_container_object = None + props = cls.get_object_spatial_props(obj) + props.is_editing = True + props.relating_container_object = None @classmethod def get_container(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: @@ -208,7 +229,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def get_container_elements_grouped_by_classification(cls, container: ifcopenshell.entity_instance) -> dict: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() results = {} if props.should_include_children: elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True) @@ -255,7 +276,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def get_container_elements_grouped_by_type(cls, container: ifcopenshell.entity_instance) -> dict: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() results: defaultdict[str, dict[int, Any]] = defaultdict(dict) if props.should_include_children: elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True) @@ -281,7 +302,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def load_contained_elements(cls) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() props.elements.clear() if not (container := props.active_container): return @@ -296,7 +317,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def load_contained_elements_by_type(cls, container: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() results = cls.get_container_elements_grouped_by_type(container) expanded_elements = json.loads(props.expanded_elements) @@ -351,7 +372,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def load_contained_elements_by_decomposition(cls, container: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() expanded_elements = json.loads(props.expanded_elements) expanded_ifc_ids = expanded_elements.get("IFC_ID", []) @@ -382,7 +403,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def load_contained_elements_by_classification(cls, container: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() expanded_elements = json.loads(props.expanded_elements) expanded_classifications = expanded_elements.get("CLASSIFICATION", []) expanded_classifications_r = expanded_elements.get("CLASSIFICATION_R", []) @@ -446,7 +467,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def import_spatial_decomposition(cls) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() previous_container_index = props.active_container_index props.containers.clear() cls.contracted_containers = json.loads(props.contracted_containers) @@ -457,7 +478,7 @@ class Spatial(bonsai.core.tool.Spatial): def import_spatial_element(cls, element: ifcopenshell.entity_instance, level_index: int) -> None: if not element.is_a("IfcProject") and not tool.Root.is_spatial_element(element): return - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() new = props.containers.add() new.ifc_class = element.is_a() new["name"] = element.Name or "Unnamed" @@ -556,28 +577,28 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def get_active_container(cls) -> Union[ifcopenshell.entity_instance, None]: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() if props.active_container_index < len(props.containers): container = tool.Ifc.get().by_id(props.containers[props.active_container_index].ifc_definition_id) return container @classmethod def contract_container(cls, container: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() contracted_containers = json.loads(props.contracted_containers) contracted_containers.append(container.id()) props.contracted_containers = json.dumps(contracted_containers) @classmethod def expand_container(cls, container: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() contracted_containers = json.loads(props.contracted_containers) contracted_containers.remove(container.id()) props.contracted_containers = json.dumps(contracted_containers) @classmethod def toggle_container_element(cls, element_index: int, is_recursive: bool) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() if props.element_mode == "TYPE": cls.toggle_container_element_by_type(element_index, is_recursive) elif props.element_mode == "DECOMPOSITION": @@ -587,7 +608,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def toggle_container_element_by_type(cls, element_index: int, is_recursive: bool) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() expanded_elements: dict[str, list[Union[str, int]]] = json.loads(props.expanded_elements) element = props.elements[element_index] @@ -636,7 +657,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def toggle_container_element_by_decomposition(cls, element_index: int, is_recursive: bool) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() element = props.elements[element_index] expanded_elements: dict[str, list[Union[str, int]]] = json.loads(props.expanded_elements) expanded_elements_list: list[Union[str, int]] = expanded_elements.setdefault("IFC_ID", []) @@ -669,7 +690,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def toggle_container_element_by_classification(cls, element_index: int, is_recursive: bool) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() expanded_elements: dict[str, list[str]] = json.loads(props.expanded_elements) expanded_elements_list: list[str] = expanded_elements.setdefault("CLASSIFICATION", []) @@ -750,7 +771,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def get_boundary_lines_from_context_visible_objects(cls) -> list[shapely.LineString]: - props = props = tool.Model.get_model_props() + props = tool.Model.get_model_props() calculation_rl = props.rl3 container = tool.Root.get_default_container() container_obj = tool.Ifc.get_object(container) @@ -761,9 +782,10 @@ class Spatial(bonsai.core.tool.Spatial): for obj in bpy.context.visible_objects: visible_element = tool.Ifc.get_entity(obj) + old_mesh = obj.data if ( not visible_element - or obj.type != "MESH" + or not isinstance(old_mesh, bpy.types.Mesh) or not cls.is_bounding_class(visible_element) or not tool.Drawing.is_intersecting_plane(obj, cut_point, cut_normal) ): @@ -1021,7 +1043,7 @@ class Spatial(bonsai.core.tool.Spatial): old_mesh = active_obj.data old_mesh_name = old_mesh.name assert active_obj and isinstance(old_mesh, bpy.types.Mesh) - mesh.BIMMeshProperties.ifc_definition_id = old_mesh.BIMMeshProperties.ifc_definition_id + tool.Geometry.get_mesh_props(mesh).ifc_definition_id = tool.Geometry.get_mesh_props(old_mesh).ifc_definition_id tool.Geometry.change_object_data(active_obj, mesh, is_global=True) tool.Ifc.edit(active_obj) tool.Blender.remove_data_block(old_mesh) @@ -1224,8 +1246,8 @@ class Spatial(bonsai.core.tool.Spatial): def set_default_container(cls, container: ifcopenshell.entity_instance) -> None: from bonsai.bim.module.spatial.data import SpatialDecompositionData - assert bpy.context - bpy.context.scene.BIMSpatialDecompositionProperties.default_container = container.id() + props = cls.get_spatial_props() + props.default_container = container.id() SpatialDecompositionData.data["default_container"] = SpatialDecompositionData.default_container() project = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) @@ -1282,16 +1304,16 @@ class Spatial(bonsai.core.tool.Spatial): def set_target_container_as_default(cls) -> None: if ( (container := tool.Root.get_default_container()) - and (obj := tool.Ifc.get_object(container)) - and bpy.context.active_object + and (container_obj := tool.Ifc.get_object(container)) + and (obj := bpy.context.active_object) ): - props = bpy.context.active_object.BIMObjectSpatialProperties - props.container_obj = obj + props = cls.get_object_spatial_props(obj) + props.container_obj = container_obj @classmethod def get_filtered_elements(cls, should_filter: bool = True) -> Iterable[ifcopenshell.entity_instance]: ifc_file = tool.Ifc.get() - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() container = ifc_file.by_id(props.active_container.ifc_definition_id) element_filter = props.element_filter active_element = props.active_element diff --git a/src/bonsai/bonsai/tool/structural.py b/src/bonsai/bonsai/tool/structural.py index 7dfa3dbdd0..30cbb00191 100644 --- a/src/bonsai/bonsai/tool/structural.py +++ b/src/bonsai/bonsai/tool/structural.py @@ -23,8 +23,6 @@ import json import bonsai.bim.helper import bonsai.core.tool import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore -from pprint import pprint from typing import Union, Any @@ -129,7 +127,8 @@ class Structural(bonsai.core.tool.Structural): def load_structural_analysis_model_attributes(cls, data: dict[str, Any]) -> None: props = bpy.context.scene.BIMStructuralProperties props.structural_analysis_model_attributes.clear() - for attribute in IfcStore.get_schema().declaration_by_name("IfcStructuralAnalysisModel").all_attributes(): + schema = tool.Ifc.schema() + for attribute in schema.declaration_by_name("IfcStructuralAnalysisModel").all_attributes(): data_type = str(attribute.type_of_attribute) if " npt.NDArray[np.float64]: M_TRANSLATION = (slice(0, 3), 3) matrix = np.array(obj.matrix_world) - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset and obj.BIMObjectProperties.blender_offset_type != "NOT_APPLICABLE": unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) coordinate_offset = tool.Geometry.get_cartesian_point_offset(obj) diff --git a/src/bonsai/bonsai/tool/unit.py b/src/bonsai/bonsai/tool/unit.py index 37597cf95c..808ea5ead0 100644 --- a/src/bonsai/bonsai/tool/unit.py +++ b/src/bonsai/bonsai/tool/unit.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import json import math @@ -23,24 +24,34 @@ import ifcopenshell import bonsai.bim.helper import bonsai.core.tool import bonsai.tool as tool -from typing import Union, Literal, Any +from typing import Union, Literal, Any, TYPE_CHECKING from typing_extensions import assert_never +if TYPE_CHECKING: + from bonsai.bim.module.unit.prop import BIMUnitProperties + class Unit(bonsai.core.tool.Unit): UNIT_TYPE = Literal["LENGTHUNIT", "AREAUNIT", "VOLUMEUNIT"] + @classmethod + def get_unit_props(cls) -> BIMUnitProperties: + return bpy.context.scene.BIMUnitProperties + @classmethod def clear_active_unit(cls) -> None: - bpy.context.scene.BIMUnitProperties.active_unit_id = 0 + props = cls.get_unit_props() + props.active_unit_id = 0 @classmethod def disable_editing_units(cls) -> None: - bpy.context.scene.BIMUnitProperties.is_editing = False + props = cls.get_unit_props() + props.is_editing = False @classmethod def enable_editing_units(cls) -> None: - bpy.context.scene.BIMUnitProperties.is_editing = True + props = cls.get_unit_props() + props.is_editing = True @classmethod def export_unit_attributes(cls) -> dict[str, Any]: @@ -52,11 +63,12 @@ class Unit(bonsai.core.tool.Unit): attributes[prop.name] = (0, 0, 0, 0, 0, 0, 0) return True - props = bpy.context.scene.BIMUnitProperties + props = cls.get_unit_props() return bonsai.bim.helper.export_attributes(props.unit_attributes, callback=callback) @classmethod def get_scene_unit_name(cls, unit_type: UNIT_TYPE) -> str: + bim_props = tool.Blender.get_bim_props() if unit_type == "LENGTHUNIT": props = bpy.context.scene.unit_settings if props.length_unit == "MILES": @@ -69,23 +81,24 @@ class Unit(bonsai.core.tool.Unit): return "thou" return "foot" elif unit_type == "AREAUNIT": - return bpy.context.scene.BIMProperties.area_unit + return bim_props.area_unit elif unit_type == "VOLUMEUNIT": - return bpy.context.scene.BIMProperties.volume_unit + return bim_props.volume_unit else: assert_never() @classmethod def get_scene_unit_si_prefix(cls, unit_type: UNIT_TYPE) -> Union[str, None]: + bim_props = tool.Blender.get_bim_props() if unit_type == "LENGTHUNIT": props = bpy.context.scene.unit_settings if props.length_unit == "ADAPTIVE" or props.length_unit == "METERS": return return props.length_unit.replace("METERS", "") elif unit_type == "AREAUNIT": - unit = bpy.context.scene.BIMProperties.area_unit + unit = bim_props.area_unit elif unit_type == "VOLUMEUNIT": - unit = bpy.context.scene.BIMProperties.volume_unit + unit = bim_props.volume_unit else: assert_never(unit_type) if "/" in unit: @@ -93,9 +106,11 @@ class Unit(bonsai.core.tool.Unit): @classmethod def import_unit_attributes(cls, unit: ifcopenshell.entity_instance) -> None: + props = cls.get_unit_props() + def callback(name, prop, data): if name == "Dimensions" and data["type"] != "IfcSIUnit": - new = bpy.context.scene.BIMUnitProperties.unit_attributes.add() + new = props.unit_attributes.add() new.name = name new.is_null = data[name] is None new.is_optional = False @@ -103,13 +118,12 @@ class Unit(bonsai.core.tool.Unit): new.string_value = json.dumps([e for e in tool.Ifc.get().by_id(data["id"]).Dimensions]) return True - props = bpy.context.scene.BIMUnitProperties props.unit_attributes.clear() bonsai.bim.helper.import_attributes2(unit, props.unit_attributes, callback=callback) @classmethod def import_units(cls) -> None: - props = bpy.context.scene.BIMUnitProperties + props = tool.Unit.get_unit_props() props.units.clear() units = [] @@ -158,7 +172,8 @@ class Unit(bonsai.core.tool.Unit): @classmethod def set_active_unit(cls, unit: ifcopenshell.entity_instance) -> None: - bpy.context.scene.BIMUnitProperties.active_unit_id = unit.id() + props = cls.get_unit_props() + props.active_unit_id = unit.id() @classmethod def get_project_currency_unit(cls) -> Union[ifcopenshell.entity_instance, None]: diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py index 13f5ff7897..e37911b75e 100644 --- a/src/bonsai/bonsai/tool/web.py +++ b/src/bonsai/bonsai/tool/web.py @@ -730,7 +730,8 @@ class Web(bonsai.core.tool.Web): if operator_data["type"] == "getDrawings": drawings_data = [] sheets_data = [] - ifc_file_dir = os.path.dirname(bpy.context.scene.BIMProperties.ifc_file) + props = tool.Blender.get_bim_props() + ifc_file_dir = os.path.dirname(props.ifc_file) sheets = [d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SHEET"] for sheet in sorted(sheets, key=lambda s: getattr(s, "Identification", getattr(s, "DocumentId", None))): diff --git a/src/bonsai/docs/guides/development/undo_system.rst b/src/bonsai/docs/guides/development/undo_system.rst index c4e95bf28a..a123e8f96e 100644 --- a/src/bonsai/docs/guides/development/undo_system.rst +++ b/src/bonsai/docs/guides/development/undo_system.rst @@ -62,7 +62,7 @@ Instead, stuff happens in Blender operators. return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - ifcopenshell.api.run("foo.bar", IfcStore.get_file()) + ifcopenshell.api.run("foo.bar", tool.Ifc.get()) return {"FINISHED"} When your operator manipulates (creates, removes, or edits) IFC data directly or diff --git a/src/bonsai/scripts/headless_import.py b/src/bonsai/scripts/headless_import.py index cda13e3878..111b07d78b 100644 --- a/src/bonsai/scripts/headless_import.py +++ b/src/bonsai/scripts/headless_import.py @@ -1,12 +1,12 @@ # This can be run using `blender -b -P headless_import.py` import bpy -from bonsai.bim.ifc import IfcStore +import bonsai.tool as tool # When federating, you may wish to manually specify the origin to ensure models # with different or arbitrary origin conventions will turn up in the right spot. -props = bpy.context.scene.BIMGeoreferenceProperties +props = tool.Georeference.get_georeference_props() # A good idea it to test import a portion of the model (or grids only) and check # georeferencing coordinates in the IFC Georeferencing panel before filling out @@ -19,7 +19,7 @@ props = bpy.context.scene.BIMGeoreferenceProperties # props.blender_x_axis_ordinate = '0.989063862448262' # props.has_blender_offset = True -props = bpy.context.scene.BIMProjectProperties +props = tool.Project.get_project_props() # Generally recommended to disable caching for stability right now props.should_cache = False diff --git a/src/bonsai/scripts/waldo.py b/src/bonsai/scripts/waldo.py new file mode 100644 index 0000000000..18c6267d39 --- /dev/null +++ b/src/bonsai/scripts/waldo.py @@ -0,0 +1,392 @@ +import numpy as np +import ifcopenshell +import ifcopenshell.api.root +import ifcopenshell.api.type +import ifcopenshell.api.unit +import ifcopenshell.api.project +import ifcopenshell.api.context +import ifcopenshell.api.spatial +import ifcopenshell.api.material +import ifcopenshell.api.geometry +import ifcopenshell.util.shape_builder +import ifcopenshell.util.element + +# from ifcopenshell.util.shape_builder import VectorType, SequenceOfVectors +from collections import namedtuple + +f = ifcopenshell.api.project.create_file() + +ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject") +meters = ifcopenshell.api.unit.add_si_unit(f) +ifcopenshell.api.unit.assign_unit(f, units=[meters]) + +model = ifcopenshell.api.context.add_context(f, context_type="Model") +plan = ifcopenshell.api.context.add_context(f, context_type="Plan") +axis = ifcopenshell.api.context.add_context( + f, context_type="Plan", context_identifier="Axis", target_view="GRAPH_VIEW", parent=plan +) +body = ifcopenshell.api.context.add_context( + f, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model +) +material1 = ifcopenshell.api.material.add_material(f, name="material1", category="material1") +material2 = ifcopenshell.api.material.add_material(f, name="material2", category="material2") +site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite") +builder = ifcopenshell.util.shape_builder.ShapeBuilder(f) + +style = ifcopenshell.api.style.add_style(f) +attributes = {"SurfaceColour": {"Name": None, "Red": 1.0, "Green": 0.5, "Blue": 0.5}, "Transparency": 0.0} +ifcopenshell.api.style.add_surface_style(f, style=style, ifc_class="IfcSurfaceStyleShading", attributes=attributes) +ifcopenshell.api.style.assign_material_style(f, material=material1, style=style, context=body) + +style = ifcopenshell.api.style.add_style(f) +attributes = {"SurfaceColour": {"Name": None, "Red": 0.5, "Green": 0.5, "Blue": 1.0}, "Transparency": 0.0} +ifcopenshell.api.style.add_surface_style(f, style=style, ifc_class="IfcSurfaceStyleShading", attributes=attributes) +ifcopenshell.api.style.assign_material_style(f, material=material2, style=style, context=body) + + +def test_wall(offset, p1, p2, p3, p4): + offset *= 1.5 + wall_type_a = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWallType", name="A") + wall_type_b = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWallType", name="B") + + set_a = ifcopenshell.api.material.add_material_set(f, set_type="IfcMaterialLayerSet") + structure = ifcopenshell.api.material.add_layer(f, layer_set=set_a, material=material1, name="structure") + structure.Priority = p1 + structure.LayerThickness = 0.1 + cladding = ifcopenshell.api.material.add_layer(f, layer_set=set_a, material=material2, name="cladding") + cladding.Priority = p2 + cladding.LayerThickness = 0.05 + + set_b = ifcopenshell.api.material.add_material_set(f, set_type="IfcMaterialLayerSet") + structure = ifcopenshell.api.material.add_layer(f, layer_set=set_b, material=material1, name="structure") + structure.Priority = p3 + structure.LayerThickness = 0.1 + cladding = ifcopenshell.api.material.add_layer(f, layer_set=set_b, material=material2, name="cladding") + cladding.Priority = p4 + cladding.LayerThickness = 0.05 + + ifcopenshell.api.material.assign_material(f, products=[wall_type_a], material=set_a) + ifcopenshell.api.material.assign_material(f, products=[wall_type_b], material=set_b) + + for i, rotation in enumerate((-90, -60, -120, 90, 60, 120)): + for i2, connection in enumerate(("ATEND", "ATSTART", "MIX")): + wall_a = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"A{p1}{p2}") + wall_b = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"B{p3}{p4}") + + ifcopenshell.api.spatial.assign_container(f, products=[wall_a, wall_b], relating_structure=site) + + ifcopenshell.api.type.assign_type(f, related_objects=[wall_a], relating_type=wall_type_a) + ifcopenshell.api.type.assign_type(f, related_objects=[wall_b], relating_type=wall_type_b) + + axis_a = builder.polyline(((0.0, 0.0), (1.0, 0.0))) + axis_b = builder.polyline(((0.0, 0.0), (1.0, 0.0))) + rep_a = builder.get_representation(axis, [axis_a]) + rep_b = builder.get_representation(axis, [axis_b]) + + ifcopenshell.api.geometry.assign_representation(f, product=wall_a, representation=rep_a) + ifcopenshell.api.geometry.assign_representation(f, product=wall_b, representation=rep_b) + + x_offset = i * 2 + x_offset += i2 * (2 * 6) + if connection == "ATEND": + sign_offset = 0 if rotation < 0 else 1 + matrix_a = np.eye(4) + matrix_a[:, 3][0:3] = (0 + x_offset, 0 + offset + sign_offset, 0) + matrix_b = np.eye(4) + matrix_b = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_b + matrix_b[:, 3][0:3] = (1 + x_offset, 1 + offset - sign_offset, 0) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_a, matrix=matrix_a) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_b, matrix=matrix_b) + + ifcopenshell.api.geometry.connect_path( + f, + relating_element=wall_a, + related_element=wall_b, + relating_connection="ATEND", + related_connection="ATEND", + ) + elif connection == "ATSTART": + sign_offset = 0 if rotation < 0 else 1 + matrix_a = np.eye(4) + matrix_a[:, 3][0:3] = (0 + x_offset, 1 + offset - sign_offset, 0) + matrix_b = np.eye(4) + matrix_b = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_b + matrix_b[:, 3][0:3] = (0 + x_offset, 1 + offset - sign_offset, 0) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_a, matrix=matrix_a) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_b, matrix=matrix_b) + + ifcopenshell.api.geometry.connect_path( + f, + relating_element=wall_a, + related_element=wall_b, + relating_connection="ATSTART", + related_connection="ATSTART", + ) + elif connection == "MIX": + sign_offset = 0 if rotation < 0 else 1 + matrix_a = np.eye(4) + matrix_a[:, 3][0:3] = (0 + x_offset, 1 + offset - sign_offset, 0) + matrix_b = np.eye(4) + matrix_b = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_b + matrix_b[:, 3][0:3] = (1 + x_offset, 1 + offset - sign_offset, 0) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_a, matrix=matrix_a) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_b, matrix=matrix_b) + + ifcopenshell.api.geometry.connect_path( + f, + relating_element=wall_a, + related_element=wall_b, + relating_connection="ATEND", + related_connection="ATSTART", + ) + + Foo(f, body, axis).regenerate(wall_a) + Foo(f, body, axis).regenerate(wall_b) + + +PrioritisedLayer = namedtuple("PrioritisedLayer", "priority thickness") + + +class Foo: + def __init__(self, file, body, axis): + self.file = file + self.body = body + self.axis = axis + + def regenerate(self, wall): + print("-" * 100) + print(wall) + layers = self.get_layers(wall) + if not layers: + return + reference = self.get_reference_line(wall) + self.reference_p1, self.reference_p2 = reference + axes = self.get_axes(wall, reference, layers) + self.end_point = None + self.start_points = [] + self.end_points = [] + for rel in wall.ConnectedTo: + if rel.is_a("IfcRelConnectsPathElements"): + wall2 = rel.RelatedElement + layers1 = self.combine_layers(layers.copy(), rel.RelatingPriorities) + layers2 = self.combine_layers(self.get_layers(wall2), rel.RelatedPriorities) + if not layers1 or not layers2: + continue + self.join(wall, wall2, layers1, layers2, rel.RelatingConnectionType, rel.RelatedConnectionType) + + for rel in wall.ConnectedFrom: + if rel.is_a("IfcRelConnectsPathElements"): + wall2 = rel.RelatingElement + layers1 = self.combine_layers(layers.copy(), rel.RelatedPriorities) + layers2 = self.combine_layers(self.get_layers(wall2), rel.RelatingPriorities) + if not layers1 or not layers2: + continue + self.join(wall, wall2, layers1, layers2, rel.RelatedConnectionType, rel.RelatingConnectionType) + + # for rel in wall.ConnectedFrom: + # if rel.is_a("IfcRelConnectsPathElements"): + # connection = rel.RelatedConnectionType + if not self.start_points: + minx = axes[0][0][0] + self.start_points = [ + np.array((minx, axes[0][0][1])), + np.array((minx, axes[-1][0][1])), + ] + if not self.end_points: + maxx = axes[0][1][0] + self.end_points = [ + np.array((maxx, axes[0][0][1])), + np.array((maxx, axes[-1][0][1])), + ] + print("FINISHED") + print(self.start_points) + print(self.end_points) + + points = [] + if self.start_points[0][1] < self.start_points[-1][1]: + points.extend((self.start_points)) + else: + points.extend(reversed(self.start_points)) + if self.end_points[0][1] > self.end_points[-1][1]: + points.extend((self.end_points)) + else: + points.extend(reversed(self.end_points)) + + builder = ifcopenshell.util.shape_builder.ShapeBuilder(wall.file) + item = builder.extrude(builder.polyline(points, closed=True), magnitude=1.0) + rep = builder.get_representation(self.body, items=[item]) + if old_rep := ifcopenshell.util.representation.get_representation(wall, self.body): + ifcopenshell.util.element.replace_element(old_rep, rep) + else: + ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep) + + item = builder.polyline([self.reference_p1, self.reference_p2]) + rep = builder.get_representation(self.axis, items=[item]) + if old_rep := ifcopenshell.util.representation.get_representation(wall, self.axis): + ifcopenshell.util.element.replace_element(old_rep, rep) + else: + ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep) + + def join(self, wall1, wall2, layers1, layers2, connection1, connection2): + if connection1 == "NOTDEFINED" or connection2 == "NOTDEFINED": + return + print("joining", wall1, layers1, connection1) + print("to", wall2, layers2, connection2) + + # axes = self.get_axes(wall2, layers2) + reference1 = self.get_reference_line(wall1) + reference2 = self.get_reference_line(wall2) + axes1 = self.get_axes(wall1, reference1, layers1) + axes2 = self.get_axes(wall2, reference2, layers2) + matrix1i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement)) + matrix2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement) + print(axes1) + print(axes2) + + # Convert wall2 data to wall1 local coordinates + for axis in axes2: + axis[0] = (matrix1i @ matrix2 @ np.concatenate((axis[0], (0, 1))))[:2] + axis[1] = (matrix1i @ matrix2 @ np.concatenate((axis[1], (0, 1))))[:2] + reference2[0] = (matrix1i @ matrix2 @ np.concatenate((reference2[0], (0, 1))))[:2] + reference2[1] = (matrix1i @ matrix2 @ np.concatenate((reference2[1], (0, 1))))[:2] + + # Sort axes from interior to exterior + if connection1 == "ATEND": + if axes2[0][0][0] > axes2[-1][0][0]: # We process layers in a +X direction + axes2 = list(reversed(axes2)) + layers2 = list(reversed(layers2)) + elif connection1 == "ATSTART": + if axes2[-1][0][0] > axes2[0][0][0]: # We process layers in a -X direction + axes2 = list(reversed(axes2)) + layers2 = list(reversed(layers2)) + + # wall2_x = matrix2[:,0][:2] + axis2 = axes2[0] # Take an arbitrary axis + if connection2 == "ATSTART": + axis2 = [axis2[1], axis2[0]] # Flip direction so the axis "points" in the direction of join + if axis2[0][1] < axis2[1][1]: # Pointing +Y + if axes1[-1][0][1] < axes1[0][0][1]: # We process layers1 in a +Y direction + axes1 = list(reversed(axes1)) + layers1 = list(reversed(layers1)) + else: # Pointing -Y + if axes1[0][0][1] < axes1[-1][0][1]: # We process layers1 in a -Y direction + axes1 = list(reversed(axes1)) + layers1 = list(reversed(layers1)) + + print("modified") + print(axes1) + print(axes2) + # Checked + + last_y = axes1[-1][0][1] + ys = iter([a[0][1] for a in axes1]) + print("ys are", [a[0][1] for a in axes1]) + + last_axis2 = axes2[-1] + axes2 = iter(axes2) + axis2 = next(axes2) + y = next(ys) + x = self.intersect_axis(*axis2, y=y) + points = [np.array((x, y))] + print("first point", points) + + layers1 = iter(layers1) + layers2 = iter(layers2) + layer1 = next(layers1, None) + layer2 = next(layers2, None) + + while layer1 and layer2: + print("considering", layer1, layer2) + if layer1.priority > layer2.priority: + axis2 = next(axes2) + x = self.intersect_axis(*axis2, y=y) + layer2 = next(layers2, None) + elif layer2.priority > layer1.priority: + y = next(ys) + x = self.intersect_axis(*axis2, y=y) + layer1 = next(layers1, None) + else: + y = next(ys) + x = self.intersect_axis(*next(axes2), y=y) + layer1 = next(layers1, None) + layer2 = next(layers2, None) + points.append(np.array((x, y))) + + print("points", points) + if points[-1][1] != last_y: + points.append(np.array((self.intersect_axis(*last_axis2, y=last_y), last_y))) + print("fpoints", points) + if connection1 == "ATSTART": + self.start_points = points + self.reference_p1[0] = self.intersect_axis(*reference2, y=reference1[0][1]) + elif connection1 == "ATEND": + self.end_points = points + self.reference_p2[0] = self.intersect_axis(*reference2, y=reference1[0][1]) + + def get_layers(self, wall) -> list: + material = ifcopenshell.util.element.get_material(wall, should_skip_usage=True) + if not material or not material.is_a("IfcMaterialLayerSet"): + return [] + return [PrioritisedLayer(l.Priority or 0, l.LayerThickness) for l in material.MaterialLayers] + + def combine_layers(self, layers, override_priorities): + results = [] + if override_priorities: + for i, priority in enumerate(override_priorities[: len(layers)]): + layers[i][0] = priority + if not layers: + return [] + results = [layers.pop(0)] + for layer in layers: + if not layer.thickness: + continue + if layer.priority == results[-1].priority: + results[-1] = PrioritisedLayer(layer.priority, results[-1].thickness + layer.thickness) + else: + results.append(layer) + return results + + def intersect_axis(self, p1, p2, y=0): + # Assumes lines are horizontal + x1, y1 = p1 + x2, y2 = p2 + t = (y - y1) / (y2 - y1) + return x1 + t * (x2 - x1) + + def get_reference_line(self, wall): + if axis := ifcopenshell.util.representation.get_representation(wall, "Plan", "Axis", "GRAPH_VIEW"): + for item in ifcopenshell.util.representation.resolve_representation(axis).Items: + if item.is_a("IfcPolyline"): + points = item.Points + elif item.is_a("IfcIndexedPolyCurve"): + points = item.Points.CoordList + else: + continue + if points[0][0] < points[1][0]: # An axis always goes in the +X direction + return [np.array(points[0]), np.array(points[1])] + return [np.array(points[1]), np.array(points[0])] + return [np.array((0.0, 0.0)), np.array((1.0, 0.0))] + + def get_axes(self, wall, reference, layers: list[PrioritisedLayer]): + axes = [[p.copy() for p in reference]] + # Apply usage to convert the Reference line into MlsBase + sense_factor = 1 + if (usage := ifcopenshell.util.element.get_material(wall)) and usage.is_a("IfcMaterialLayerSetUage"): + for point in axes[0]: + point[1] += usage.OffsetFromReferenceLine + sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1 + + for layer in layers: + axes.append([p.copy() + np.array((0.0, layer.thickness * sense_factor)) for p in axes[-1]]) + return axes + + +test_wall(0, 1, 1, 1, 1) +test_wall(1, 2, 1, 1, 2) +test_wall(2, 2, 1, 1, 1) +test_wall(3, 1, 2, 1, 1) +test_wall(4, 1, 2, 1, 2) +test_wall(5, 3, 1, 2, 4) + + +f.write("/home/dion/wall.ifc") diff --git a/src/bonsai/test/bim/bootstrap.py b/src/bonsai/test/bim/bootstrap.py index bea49df413..308d140298 100644 --- a/src/bonsai/test/bim/bootstrap.py +++ b/src/bonsai/test/bim/bootstrap.py @@ -23,6 +23,7 @@ import bpy import pytest import webbrowser import bonsai.bim.handler +import bonsai.tool as tool import ifcopenshell import ifcopenshell.util.element import ifcopenshell.util.representation @@ -66,7 +67,8 @@ class NewIfc4X3: bpy.data.batch_remove(bpy.data.objects) bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) bonsai.bim.handler.load_post(None) - bpy.context.scene.BIMProjectProperties.export_schema = "IFC4X3_ADD2" + props = tool.Project.get_project_props() + props.export_schema = "IFC4X3_ADD2" bpy.ops.bim.create_project() @@ -172,14 +174,14 @@ def the_object_name_exists(name): def an_ifc_file_exists(): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() if not ifc: assert False, "No IFC file is available" return ifc def an_ifc_file_does_not_exist(): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() if ifc: assert False, "An IFC is available" @@ -281,7 +283,7 @@ def the_object_name1_has_no_boolean_difference_by_name2(name1, name2): def the_object_name_is_voided_by_void(name, void): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) for rel in element.HasOpenings: if rel.RelatedOpeningElement.Name == void: @@ -290,7 +292,7 @@ def the_object_name_is_voided_by_void(name, void): def the_object_name_is_not_voided_by_void(name, void): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) for rel in element.HasOpenings: if rel.RelatedOpeningElement.Name == void: @@ -298,21 +300,21 @@ def the_object_name_is_not_voided_by_void(name, void): def the_object_name_is_not_voided(name): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) if any(element.HasOpenings): assert False, "An opening was found" def the_object_name_is_not_a_void(name): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) if any(element.VoidsElements): assert False, "A void was found" def the_void_name_is_filled_by_filling(name, filling): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) if any(rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings): return True @@ -320,14 +322,14 @@ def the_void_name_is_filled_by_filling(name, filling): def the_void_name_is_not_filled_by_filling(name, filling): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) if any(rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings): assert False, "A filling was found" def the_object_name_is_not_a_filling(name): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) if any(element.FillsVoids): assert False, "A filling was found" diff --git a/src/bonsai/test/bim/feature/type.feature b/src/bonsai/test/bim/feature/type.feature index 6b68fe7566..c32f7a8cfb 100644 --- a/src/bonsai/test/bim/feature/type.feature +++ b/src/bonsai/test/bim/feature/type.feature @@ -202,11 +202,11 @@ Scenario: Select similar type Scenario: Purge unused types Given an empty IFC project And I press "bim.launch_type_manager" - And I set "scene.BIMModelProperties.type_class" to "IfcWallType" - And I set "scene.BIMModelProperties.type_predefined_type" to "SOLIDWALL" - And I set "scene.BIMModelProperties.type_template" to "EMPTY" - When I press "bim.add_type" - Then the object "IfcWallType/TYPEX" is an "IfcWallType" - And the object "IfcWallType/TYPEX" has no data - When I press "bim.purge_unused_types" - Then the object "IfcWallType/TYPEX" does not exist + And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" + And I set "scene.BIMRootProperties.ifc_predefined_type" to "SOLIDWALL" + And I set "scene.BIMRootProperties.representation_template" to "EMPTY" + When I press "bim.add_element" + Then the object "IfcWallType/Unnamed" is an "IfcWallType" + And the object "IfcWallType/Unnamed" has no data + When I press "bim.purge_unused_objects(object_type='TYPE')" + Then the object "IfcWallType/Unnamed" does not exist diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 95945ced81..1332c63e8e 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -39,7 +39,7 @@ scenarios("feature") variables = { "cwd": Path.cwd().as_posix(), - "ifc": "IfcStore.get_file()", + "ifc": "tool.Ifc.get()", "pset_ifc": "IfcStore.pset_template_file", "classification_ifc": "IfcStore.classification_file", } @@ -190,7 +190,8 @@ def an_empty_blender_session(): # default project settings bpy.context.scene.unit_settings.system = "METRIC" bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" - bpy.context.scene.BIMProjectProperties.template_file = "0" + props = tool.Project.get_project_props() + props.template_file = "0" tool.Blender.get_addon_preferences().should_play_chaching_sound = False @@ -203,7 +204,8 @@ def an_empty_ifc_project(): @given("an empty IFC2X3 project") def an_empty_ifc_2x3_project(): an_empty_blender_session() - bpy.context.scene.BIMProjectProperties.export_schema = "IFC2X3" + props = tool.Project.get_project_props() + props.export_schema = "IFC2X3" bpy.ops.bim.create_project() @@ -742,7 +744,7 @@ def the_object_name_has_a_representation_type_of_context(name, type, context): def the_object_name_data_is_a_type_representation_of_context(name, type, context): ifc = an_ifc_file_exists() context, subcontext, target_view = context.split("/") - rep = ifc.by_id(the_object_name_exists(name).data.BIMMeshProperties.ifc_definition_id) + rep = ifc.by_id(tool.Geometry.get_mesh_props(the_object_name_exists(name).data).ifc_definition_id) assert rep assert rep.RepresentationType == type, f"The object {name} is not a {type} representation" assert rep.ContextOfItems.ContextType == context @@ -784,14 +786,14 @@ def the_ifc_material_name_does_not_exist(name): @then("an IFC file does not exist") def an_ifc_file_does_not_exist(): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() if ifc: assert False, "An IFC is available" @then("an IFC file exists") def an_ifc_file_exists(): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() if not ifc: assert False, "No IFC file is available" return ifc @@ -805,7 +807,7 @@ def the_object_name_should_display_as_mode(name, mode): @then(parsers.parse('the object "{name}" is voided by "{void}"')) def the_object_name_is_voided_by_void(name, void): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) assert any((rel for rel in element.HasOpenings if rel.RelatedOpeningElement.Name == void)), "No void found" @@ -821,14 +823,14 @@ def the_object_name_is_not_voided_by_void(name, void): @then(parsers.parse('the object "{name}" is not voided')) def the_object_name_is_not_voided(name): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) assert not element.HasOpenings, "A void was found" @then(parsers.parse('the object "{name}" is a void')) def the_object_name_is_a_void(name): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() obj = the_object_name_exists(name) element = ifc.by_id(obj.BIMObjectProperties.ifc_definition_id) assert any((element.VoidsElements)), "No void was found" @@ -888,7 +890,7 @@ def the_object_name_has_no_data(name): @then(parsers.parse('the object "{name}" has data which is an IFC representation')) def the_object_name_has_ifc_representation_data(name): - id = the_object_name_exists(name).data.BIMMeshProperties.ifc_definition_id + id = tool.Geometry.get_mesh_props(the_object_name_exists(name).data).ifc_definition_id assert id != 0, f"The ID is {id}" @@ -934,7 +936,7 @@ def the_object_name_has_number_vertices(name, number): @then(parsers.parse('the void "{name}" is filled by "{filling}"')) def the_void_name_is_filled_by_filling(name, filling): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) assert any((rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings)), "No filling found" @@ -951,7 +953,7 @@ def the_void_name_is_not_filled_by_filling(name, filling): @when(parsers.parse('the object "{name}" is not a filling')) @then(parsers.parse('the object "{name}" is not a filling')) def the_object_name_is_not_a_filling(name): - ifc = IfcStore.get_file() + ifc = tool.Ifc.get() element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) assert not any(element.FillsVoids), "A filling was found" diff --git a/src/bonsai/test/tool/test_brick.py b/src/bonsai/test/tool/test_brick.py index c2d10d699f..b26a2acd43 100644 --- a/src/bonsai/test/tool/test_brick.py +++ b/src/bonsai/test/tool/test_brick.py @@ -122,11 +122,10 @@ class TestAddBrickifcProject(NewFile): result = subject.add_brickifc_project("http://example.org/digitaltwin#") assert result == f"http://example.org/digitaltwin#{project.GlobalId}" brick = URIRef(result) + props = tool.Blender.get_bim_props() assert list(BrickStore.graph.triples((brick, A, REF.ifcProject))) assert list(BrickStore.graph.triples((brick, REF.ifcProjectID, Literal(project.GlobalId)))) - assert list( - BrickStore.graph.triples((brick, REF.ifcFileLocation, Literal(bpy.context.scene.BIMProperties.ifc_file))) - ) + assert list(BrickStore.graph.triples((brick, REF.ifcFileLocation, Literal(props.ifc_file)))) assert list( BrickStore.graph.triples( (brick, URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("My Project")) diff --git a/src/bonsai/test/tool/test_debug.py b/src/bonsai/test/tool/test_debug.py index d7d6a95c2c..888a717f4d 100644 --- a/src/bonsai/test/tool/test_debug.py +++ b/src/bonsai/test/tool/test_debug.py @@ -55,7 +55,8 @@ class TestLoadExpress(NewFile): class TestPurgeHdf5Cache(NewFile): def test_run(self): - cache_dir = Path(bpy.context.scene.BIMProperties.cache_dir) + props = tool.Blender.get_bim_props() + cache_dir = Path(props.cache_dir) test_file = cache_dir / "test.h5" test_file.parent.mkdir(parents=True, exist_ok=True) test_file.touch() diff --git a/src/bonsai/test/tool/test_document.py b/src/bonsai/test/tool/test_document.py index 30ce8b5721..d60b9fbe31 100644 --- a/src/bonsai/test/tool/test_document.py +++ b/src/bonsai/test/tool/test_document.py @@ -36,13 +36,13 @@ class TestAddBreadcrumb(NewFile): tool.Ifc().set(ifc) document = ifc.createIfcDocumentInformation() subject.add_breadcrumb(document) - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() assert props.breadcrumbs[0].name == str(document.id()) class TestClearBreadcrumbs(NewFile): def test_run(self): - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() props.breadcrumbs.add() subject.clear_breadcrumbs() assert len(props.breadcrumbs) == 0 @@ -50,7 +50,7 @@ class TestClearBreadcrumbs(NewFile): class TestClearDocumentTree(NewFile): def test_run(self): - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() new = props.documents.add() subject.clear_document_tree() assert len(props.documents) == 0 @@ -58,23 +58,26 @@ class TestClearDocumentTree(NewFile): class TestDisableEditingDocument(NewFile): def test_run(self): - bpy.context.scene.BIMDocumentProperties.active_document_id = 1 + props = tool.Document.get_document_props() + props.active_document_id = 1 subject.disable_editing_document() - assert bpy.context.scene.BIMDocumentProperties.active_document_id == 0 + assert props.active_document_id == 0 class TestDisableEditingUI(NewFile): def test_run(self): - bpy.context.scene.BIMDocumentProperties.is_editing = True + props = tool.Document.get_document_props() + props.is_editing = True subject.disable_editing_ui() - assert bpy.context.scene.BIMDocumentProperties.is_editing == False + assert props.is_editing == False class TestEnableEditingUI(NewFile): def test_run(self): - bpy.context.scene.BIMDocumentProperties.is_editing = False + props = tool.Document.get_document_props() + props.is_editing = False subject.enable_editing_ui() - assert bpy.context.scene.BIMDocumentProperties.is_editing == True + assert props.is_editing == True class TestExportDocumentAttributes(NewFile): @@ -129,22 +132,22 @@ class TestImportDocumentAttributes(NewFile): document.Confidentiality = "CONFIDENTIAL" document.Status = "DRAFT" subject().import_document_attributes(document) - props = bpy.context.scene.BIMDocumentProperties - assert props.document_attributes.get("Identification").string_value == "Identification" - assert props.document_attributes.get("Name").string_value == "Name" - assert props.document_attributes.get("Description").string_value == "Description" - assert props.document_attributes.get("Location").string_value == "Location" - assert props.document_attributes.get("Purpose").string_value == "Purpose" - assert props.document_attributes.get("IntendedUse").string_value == "IntendedUse" - assert props.document_attributes.get("Scope").string_value == "Scope" - assert props.document_attributes.get("Revision").string_value == "Revision" - assert props.document_attributes.get("CreationTime").string_value == "CreationTime" - assert props.document_attributes.get("LastRevisionTime").string_value == "LastRevisionTime" - assert props.document_attributes.get("ElectronicFormat").string_value == "ElectronicFormat" - assert props.document_attributes.get("ValidFrom").string_value == "ValidFrom" - assert props.document_attributes.get("ValidUntil").string_value == "ValidUntil" - assert props.document_attributes.get("Confidentiality").enum_value == "CONFIDENTIAL" - assert props.document_attributes.get("Status").enum_value == "DRAFT" + props = tool.Document.get_document_props() + assert props.document_attributes["Identification"].string_value == "Identification" + assert props.document_attributes["Name"].string_value == "Name" + assert props.document_attributes["Description"].string_value == "Description" + assert props.document_attributes["Location"].string_value == "Location" + assert props.document_attributes["Purpose"].string_value == "Purpose" + assert props.document_attributes["IntendedUse"].string_value == "IntendedUse" + assert props.document_attributes["Scope"].string_value == "Scope" + assert props.document_attributes["Revision"].string_value == "Revision" + assert props.document_attributes["CreationTime"].string_value == "CreationTime" + assert props.document_attributes["LastRevisionTime"].string_value == "LastRevisionTime" + assert props.document_attributes["ElectronicFormat"].string_value == "ElectronicFormat" + assert props.document_attributes["ValidFrom"].string_value == "ValidFrom" + assert props.document_attributes["ValidUntil"].string_value == "ValidUntil" + assert props.document_attributes["Confidentiality"].enum_value == "CONFIDENTIAL" + assert props.document_attributes["Status"].enum_value == "DRAFT" def test_importing_reference(self): ifc = ifcopenshell.file() @@ -155,11 +158,11 @@ class TestImportDocumentAttributes(NewFile): document.Name = "Name" document.Description = "Description" subject().import_document_attributes(document) - props = bpy.context.scene.BIMDocumentProperties - assert props.document_attributes.get("Location").string_value == "Location" - assert props.document_attributes.get("Identification").string_value == "Identification" - assert props.document_attributes.get("Name").string_value == "Name" - assert props.document_attributes.get("Description").string_value == "Description" + props = tool.Document.get_document_props() + assert props.document_attributes["Location"].string_value == "Location" + assert props.document_attributes["Identification"].string_value == "Identification" + assert props.document_attributes["Name"].string_value == "Name" + assert props.document_attributes["Description"].string_value == "Description" class TestImportProjectDocuments(NewFile): @@ -169,7 +172,7 @@ class TestImportProjectDocuments(NewFile): ifc.createIfcProject() document = ifcopenshell.api.run("document.add_information", ifc) subject.import_project_documents() - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() assert len(props.documents) == 1 assert props.documents[0].ifc_definition_id == document.id() assert props.documents[0].name == "Unnamed" @@ -185,7 +188,7 @@ class TestImportReferences(NewFile): document = ifcopenshell.api.run("document.add_information", ifc) reference = ifcopenshell.api.run("document.add_reference", ifc, information=document) subject.import_references(document) - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() assert len(props.documents) == 1 assert props.documents[0].ifc_definition_id == reference.id() assert props.documents[0].name == "Unnamed" @@ -201,7 +204,7 @@ class TestImportSubdocuments(NewFile): document = ifcopenshell.api.run("document.add_information", ifc) subdocument = ifcopenshell.api.run("document.add_information", ifc, parent=document) subject.import_subdocuments(document) - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() assert len(props.documents) == 1 assert props.documents[0].ifc_definition_id == subdocument.id() assert props.documents[0].name == "Unnamed" @@ -220,7 +223,7 @@ class TestIsDocumentInformation(NewFile): class TestRemoveLatestBreadcrumb(NewFile): def test_run(self): - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() props.breadcrumbs.add() props.breadcrumbs.add() subject.remove_latest_breadcrumb() @@ -232,4 +235,5 @@ class TestSetActiveDocument(NewFile): ifc = ifcopenshell.file() document = ifc.createIfcDocumentInformation() subject.set_active_document(document) - assert bpy.context.scene.BIMDocumentProperties.active_document_id == document.id() + props = tool.Document.get_document_props() + assert props.active_document_id == document.id() diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index 5e3c239e03..b004a92efb 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -27,7 +27,6 @@ import bonsai.core.tool import bonsai.tool as tool from test.bim.bootstrap import NewFile from bonsai.tool.drawing import Drawing as subject -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.drawing.data import DecoratorData from mathutils import Vector @@ -113,30 +112,34 @@ class TestDeleteDrawingElements(NewFile): class TestDisableEditingDrawings(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_drawings = True + props = tool.Drawing.get_document_props() + props.is_editing_drawings = True subject.disable_editing_drawings() - assert bpy.context.scene.DocProperties.is_editing_drawings == False + assert props.is_editing_drawings == False class TestDisableEditingSchedules(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_schedules = True + props = tool.Drawing.get_document_props() + props.is_editing_schedules = True subject.disable_editing_schedules() - assert bpy.context.scene.DocProperties.is_editing_schedules == False + assert props.is_editing_schedules == False class TestDisableEditingReferences(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_references = True + props = tool.Drawing.get_document_props() + props.is_editing_references = True subject.disable_editing_references() - assert bpy.context.scene.DocProperties.is_editing_references == False + assert props.is_editing_references == False class TestDisableEditingSheets(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_sheets = True + props = tool.Drawing.get_document_props() + props.is_editing_sheets = True subject.disable_editing_sheets() - assert bpy.context.scene.DocProperties.is_editing_sheets == False + assert props.is_editing_sheets == False class TestDisableEditingText(NewFile): @@ -166,30 +169,34 @@ class TestEnableEditing(NewFile): class TestEnableEditingDrawings(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_drawings = False + props = tool.Drawing.get_document_props() + props.is_editing_drawings = False subject.enable_editing_drawings() - assert bpy.context.scene.DocProperties.is_editing_drawings == True + assert props.is_editing_drawings == True class TestEnableEditingSchedules(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_schedules = False + props = tool.Drawing.get_document_props() + props.is_editing_schedules = False subject.enable_editing_schedules() - assert bpy.context.scene.DocProperties.is_editing_schedules == True + assert props.is_editing_schedules == True class TestEnableEditingReferences(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_references = False + props = tool.Drawing.get_document_props() + props.is_editing_references = False subject.enable_editing_references() - assert bpy.context.scene.DocProperties.is_editing_references == True + assert props.is_editing_references == True class TestEnableEditingSheets(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_sheets = False + props = tool.Drawing.get_document_props() + props.is_editing_sheets = False subject.enable_editing_sheets() - assert bpy.context.scene.DocProperties.is_editing_sheets == True + assert props.is_editing_sheets == True class TestEnableEditingText(NewFile): @@ -492,7 +499,7 @@ class TestImportDrawings(NewFile): pset = ifcopenshell.api.run("pset.add_pset", ifc, product=drawing, name="EPset_Drawing") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"TargetView": "PLAN_VIEW"}) subject.import_drawings() - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() for d in props.drawings: d.is_expanded = True subject.import_drawings() @@ -508,7 +515,7 @@ class TestImportSchedules(NewFile): ifc.createIfcDocumentInformation(Identification="Y", Name="FOOBAZ") document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SCHEDULE") subject.import_documents("SCHEDULE") - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() assert props.schedules[0].ifc_definition_id == document.id() assert props.schedules[0].identification == "X" assert props.schedules[0].name == "FOOBAR" @@ -519,7 +526,7 @@ class TestImportSchedules(NewFile): ifc.createIfcDocumentInformation(DocumentId="Y", Name="FOOBAZ") document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="SCHEDULE") subject.import_documents("SCHEDULE") - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() assert props.schedules[0].ifc_definition_id == document.id() assert props.schedules[0].identification == "X" assert props.schedules[0].name == "FOOBAR" @@ -532,7 +539,7 @@ class TestImportReferences(NewFile): ifc.createIfcDocumentInformation(Identification="Y", Name="FOOBAZ") document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="REFERENCE") subject.import_documents("REFERENCE") - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() assert props.references[0].ifc_definition_id == document.id() assert props.references[0].identification == "X" assert props.references[0].name == "FOOBAR" @@ -543,7 +550,7 @@ class TestImportReferences(NewFile): ifc.createIfcDocumentInformation(DocumentId="Y", Name="FOOBAZ") document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="REFERENCE") subject.import_documents("REFERENCE") - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() assert props.references[0].ifc_definition_id == document.id() assert props.references[0].identification == "X" assert props.references[0].name == "FOOBAR" @@ -556,7 +563,7 @@ class TestImportSheets(NewFile): ifc.createIfcDocumentInformation(Identification="Y", Name="FOOBAZ") document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SHEET") subject.import_sheets() - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() assert props.sheets[0].ifc_definition_id == document.id() assert props.sheets[0].identification == "X" assert props.sheets[0].name == "FOOBAR" @@ -567,7 +574,7 @@ class TestImportSheets(NewFile): ifc.createIfcDocumentInformation(DocumentId="Y", Name="FOOBAZ") document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="SHEET") subject.import_sheets() - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() assert props.sheets[0].ifc_definition_id == document.id() assert props.sheets[0].identification == "X" assert props.sheets[0].name == "FOOBAR" @@ -657,9 +664,10 @@ class TestSetName(NewFile): class TestShowDecorations(NewFile): def test_run(self): - bpy.context.scene.DocProperties.should_draw_decorations = False + props = tool.Drawing.get_document_props() + props.should_draw_decorations = False subject.show_decorations() - assert bpy.context.scene.DocProperties.should_draw_decorations is True + assert props.should_draw_decorations is True class TestDrawingMaintainingSheetPosition(NewFile): @@ -680,7 +688,7 @@ class TestDrawingMaintainingSheetPosition(NewFile): return drawing_data def test_run(self): - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() bpy.ops.bim.create_project() ifc = tool.Ifc.get() sheet_path = Path.cwd() / "layouts" / "A00 - UNTITLED.svg" @@ -845,10 +853,11 @@ class TestDrawingStyles(NewFile): ifc = tool.Ifc.get() drawing = ifc.by_type("IfcAnnotation")[0] bpy.ops.bim.expand_target_view(target_view="PLAN_VIEW") - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() props.active_drawing_index = 2 bpy.ops.bim.activate_drawing(drawing=drawing.id()) - self.drawing_styles = bpy.context.scene.DocProperties.drawing_styles + props = tool.Drawing.get_document_props() + self.drawing_styles = props.drawing_styles def test_drawing_styles_not_loaded_if_underlay_is_inactive(self): self.setup_project_with_drawing() @@ -867,7 +876,8 @@ class TestDrawingStyles(NewFile): class TestAddReferenceImage(NewFile): def test_run(self): - bpy.context.scene.BIMProjectProperties.template_file = "0" + props = tool.Project.get_project_props() + props.template_file = "0" bpy.ops.bim.create_project() ifc_path = Path("test/files/temp/test.ifc").absolute() bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True) diff --git a/src/bonsai/test/tool/test_geometry.py b/src/bonsai/test/tool/test_geometry.py index f1ca935f30..63721bcdb7 100644 --- a/src/bonsai/test/tool/test_geometry.py +++ b/src/bonsai/test/tool/test_geometry.py @@ -171,14 +171,14 @@ class TestGetCartesianPointCoordinateOffset(NewFile): def test_run(self): obj = bpy.data.objects.new("Object", None) obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT" - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = True obj.BIMObjectProperties.cartesian_point_offset = "1,2,3" assert np.allclose(subject.get_cartesian_point_offset(obj), np.array((1.0, 2.0, 3.0))) def test_get_null_if_not_a_cartesian_point_offset_type(self): obj = bpy.data.objects.new("Object", None) - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = True obj.BIMObjectProperties.cartesian_point_offset = "1,2,3" assert subject.get_cartesian_point_offset(obj) is None @@ -186,7 +186,7 @@ class TestGetCartesianPointCoordinateOffset(NewFile): def test_get_null_if_no_blender_offset(self): obj = bpy.data.objects.new("Object", None) obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT" - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = False assert subject.get_cartesian_point_offset(obj) is None @@ -237,14 +237,15 @@ class TestImportRepresentationParameters(NewFile): item = ifc.createIfcExtrudedAreaSolid(SweptArea=swept_area, Depth=2) representation = ifc.createIfcShapeRepresentation(Items=[item]) data = bpy.data.meshes.new("Mesh") - data.BIMMeshProperties.ifc_definition_id = representation.id() + mprops = tool.Geometry.get_mesh_props(data) + mprops.ifc_definition_id = representation.id() subject.import_representation_parameters(data) - assert data.BIMMeshProperties.ifc_parameters[0].name == "IfcExtrudedAreaSolid/Depth" - assert data.BIMMeshProperties.ifc_parameters[0].step_id == item.id() - assert data.BIMMeshProperties.ifc_parameters[0].index == 3 - assert data.BIMMeshProperties.ifc_parameters[1].name == "IfcCircleProfileDef/Radius" - assert data.BIMMeshProperties.ifc_parameters[1].step_id == swept_area.id() - assert data.BIMMeshProperties.ifc_parameters[1].index == 3 + assert mprops.ifc_parameters[0].name == "IfcExtrudedAreaSolid/Depth" + assert mprops.ifc_parameters[0].step_id == item.id() + assert mprops.ifc_parameters[0].index == 3 + assert mprops.ifc_parameters[1].name == "IfcCircleProfileDef/Radius" + assert mprops.ifc_parameters[1].step_id == swept_area.id() + assert mprops.ifc_parameters[1].index == 3 class TestIsBodyRepresentation(NewFile): @@ -293,7 +294,7 @@ class TestLink(NewFile): element = ifc.createIfcShapeRepresentation() obj = bpy.data.meshes.new("Mesh") subject.link(element, obj) - assert obj.BIMMeshProperties.ifc_definition_id == element.id() + assert tool.Geometry.get_mesh_props(obj).ifc_definition_id == element.id() class TestRecordObjectMaterials(NewFile): @@ -306,7 +307,7 @@ class TestRecordObjectMaterials(NewFile): material.BIMStyleProperties.ifc_definition_id = style.id() obj.data.materials.append(material) subject.record_object_materials(obj) - assert obj.data.BIMMeshProperties.material_checksum == str([style.id()]) + assert tool.Geometry.get_mesh_props(obj).material_checksum == str([style.id()]) class TestRecordObjectPosition(NewFile): diff --git a/src/bonsai/test/tool/test_georeference.py b/src/bonsai/test/tool/test_georeference.py index 4b22a84781..96e498482c 100644 --- a/src/bonsai/test/tool/test_georeference.py +++ b/src/bonsai/test/tool/test_georeference.py @@ -39,7 +39,7 @@ class TestImportProjectedCRS(NewFile): ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") ifcopenshell.api.run("context.add_context", ifc, context_type="Model") subject.import_projected_crs() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert len(props.projected_crs) == 0 def test_importing_projected_crs(self): @@ -58,7 +58,7 @@ class TestImportProjectedCRS(NewFile): unit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT") projected_crs.MapUnit = unit subject.import_projected_crs() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert props.projected_crs.get("Name").string_value == "Name" assert props.projected_crs.get("Description").string_value == "Description" assert props.projected_crs.get("GeodeticDatum").string_value == "GeodeticDatum" @@ -71,7 +71,7 @@ class TestImportProjectedCRS(NewFile): ifc = ifcopenshell.file(schema="IFC2X3") tool.Ifc.set(ifc) subject.import_projected_crs() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert len(props.projected_crs) == 0 @@ -82,7 +82,7 @@ class TestImportCoordinateOperation(NewFile): ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") ifcopenshell.api.run("context.add_context", ifc, context_type="Model") subject.import_coordinate_operation() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert len(props.coordinate_operation) == 0 def test_importing_coordinate_operation(self): @@ -99,7 +99,7 @@ class TestImportCoordinateOperation(NewFile): map_conversion.XAxisOrdinate = 5 map_conversion.Scale = 6 subject.import_coordinate_operation() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert props.coordinate_operation.get("Eastings").string_value == "1.0" assert props.coordinate_operation.get("Northings").string_value == "2.0" assert props.coordinate_operation.get("OrthogonalHeight").string_value == "3.0" @@ -112,7 +112,7 @@ class TestImportCoordinateOperation(NewFile): ifc = ifcopenshell.file(schema="IFC2X3") tool.Ifc.set(ifc) subject.import_coordinate_operation() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert len(props.coordinate_operation) == 0 @@ -123,7 +123,7 @@ class TestImportTrueNorth(NewFile): ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") ifcopenshell.api.run("context.add_context", ifc, context_type="Model") subject.import_true_north() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert props.true_north_abscissa == "0" assert props.true_north_ordinate == "1" assert props.true_north_angle == "0" @@ -135,7 +135,7 @@ class TestImportTrueNorth(NewFile): context = ifcopenshell.api.run("context.add_context", ifc, context_type="Model") context.TrueNorth = ifc.createIfcDirection((1.0, 2.0, 0.0)) subject.import_true_north() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert props.true_north_abscissa == "1.0" assert props.true_north_ordinate == "2.0" assert props.true_north_angle == "-26.5650512" @@ -176,35 +176,39 @@ class TestGetTrueNorthAttributes(NewFile): class TestEnableEditing(NewFile): def test_run(self): - bpy.context.scene.BIMGeoreferenceProperties.is_editing = False + props = tool.Georeference.get_georeference_props() + props.is_editing = False subject.enable_editing() - assert bpy.context.scene.BIMGeoreferenceProperties.is_editing is True + assert props.is_editing is True class TestDisableEditing(NewFile): def test_run(self): - bpy.context.scene.BIMGeoreferenceProperties.is_editing = True + props = tool.Georeference.get_georeference_props() + props.is_editing = True subject.disable_editing() - assert bpy.context.scene.BIMGeoreferenceProperties.is_editing is False + assert props.is_editing is False class TestSetCoordinates(NewFile): def test_run(self): + props = tool.Georeference.get_georeference_props() subject.set_coordinates("local", [1.0, 2.0, 3.0]) - assert bpy.context.scene.BIMGeoreferenceProperties.local_coordinates == "1.0,2.0,3.0" + assert props.local_coordinates == "1.0,2.0,3.0" subject.set_coordinates("blender", [4.0, 5.0, 6.0]) - assert bpy.context.scene.BIMGeoreferenceProperties.blender_coordinates == "4.0,5.0,6.0" + assert props.blender_coordinates == "4.0,5.0,6.0" subject.set_coordinates("map", [7.0, 8.0, 9.0]) - assert bpy.context.scene.BIMGeoreferenceProperties.map_coordinates == "7.0,8.0,9.0" + assert props.map_coordinates == "7.0,8.0,9.0" class TestGetCoordinates(NewFile): def test_run(self): - bpy.context.scene.BIMGeoreferenceProperties.local_coordinates = "1.0,2.0,3.0" + props = tool.Georeference.get_georeference_props() + props.local_coordinates = "1.0,2.0,3.0" assert subject.get_coordinates("local") == [1.0, 2.0, 3.0] - bpy.context.scene.BIMGeoreferenceProperties.blender_coordinates = "4.0,5.0,6.0" + props.blender_coordinates = "4.0,5.0,6.0" assert subject.get_coordinates("blender") == [4.0, 5.0, 6.0] - bpy.context.scene.BIMGeoreferenceProperties.map_coordinates = "7.0,8.0,9.0" + props.map_coordinates = "7.0,8.0,9.0" assert subject.get_coordinates("map") == [7.0, 8.0, 9.0] @@ -231,7 +235,7 @@ class TestXyz2Enh(NewFile): ifc = ifcopenshell.file() ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") tool.Ifc.set(ifc) - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = True props.blender_offset_x = "1.0" assert subject.xyz2enh([0.0, 0.0, 0.0]) == (1.0, 0.0, 0.0) @@ -247,7 +251,7 @@ class TestXyz2Enh(NewFile): assert subject.xyz2enh([0.0, 0.0, 0.0]) == (1.0, 0.0, 0.0) def test_applying_both_blender_offset_and_map_conversion(self): - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = True props.blender_offset_x = "1.0" ifc = ifcopenshell.file() @@ -271,7 +275,7 @@ class TestEnh2Xyz(NewFile): ifc = ifcopenshell.file() ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") tool.Ifc.set(ifc) - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = True props.blender_offset_x = "1.0" assert subject.enh2xyz([0.0, 0.0, 0.0]) == (-1.0, 0.0, 0.0) @@ -287,7 +291,7 @@ class TestEnh2Xyz(NewFile): assert subject.enh2xyz([0.0, 0.0, 0.0]) == (-1.0, 0.0, 0.0) def test_applying_both_blender_offset_and_map_conversion(self): - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = True props.blender_offset_x = "1.0" ifc = ifcopenshell.file() diff --git a/src/bonsai/test/tool/test_ifc.py b/src/bonsai/test/tool/test_ifc.py index 429dd76212..aedcea3465 100644 --- a/src/bonsai/test/tool/test_ifc.py +++ b/src/bonsai/test/tool/test_ifc.py @@ -40,12 +40,14 @@ class TestSet(test.bim.bootstrap.NewFile): class TestGet(test.bim.bootstrap.NewFile): def test_getting_an_ifc_dataset_from_a_ifc_spf_filepath(self): assert subject.get() is None - bpy.context.scene.BIMProperties.ifc_file = "test/files/basic.ifc" + props = tool.Blender.get_bim_props() + props.ifc_file = "test/files/basic.ifc" result = subject.get() assert isinstance(result, ifcopenshell.file) def test_getting_the_active_ifc_dataset_regardless_of_ifc_path(self): - bpy.context.scene.BIMProperties.ifc_file = "test/files/basic.ifc" + props = tool.Blender.get_bim_props() + props.ifc_file = "test/files/basic.ifc" ifc = ifcopenshell.file() subject.set(ifc) assert subject.get() == ifc @@ -189,7 +191,7 @@ class TestLink(test.bim.bootstrap.NewFile): element = ifc.create_entity("IfcShapeRepresentation") obj = bpy.data.meshes.new("Material") subject.link(element, obj) - assert obj.BIMMeshProperties.ifc_definition_id == element.id() + assert tool.Geometry.get_mesh_props(obj).ifc_definition_id == element.id() class TestUnlink(test.bim.bootstrap.NewFile): diff --git a/src/bonsai/test/tool/test_loader.py b/src/bonsai/test/tool/test_loader.py index c8235ed52b..30ef9e22b9 100644 --- a/src/bonsai/test/tool/test_loader.py +++ b/src/bonsai/test/tool/test_loader.py @@ -520,7 +520,8 @@ class TestLoadingIndexedMap(NewFile): class TestSetupActiveBsddClassification(NewFile): def run_test(self, schema: ifcopenshell.util.schema.IFC_SCHEMA) -> None: schema_ = "IFC4X3_ADD2" if schema == "IFC4X3" else schema - bpy.context.scene.BIMProjectProperties.export_schema = schema_ + props = tool.Project.get_project_props() + props.export_schema = schema_ bpy.ops.bim.create_project() ifc_file = tool.Ifc.get() name = "CCI Construction" diff --git a/src/bonsai/test/tool/test_misc.py b/src/bonsai/test/tool/test_misc.py index a274c89386..78a3c34cc5 100644 --- a/src/bonsai/test/tool/test_misc.py +++ b/src/bonsai/test/tool/test_misc.py @@ -24,7 +24,6 @@ import test.bim.bootstrap import bonsai.core.tool import bonsai.tool as tool from bonsai.tool.misc import Misc as subject -from bonsai.bim.ifc import IfcStore class TestImplementsTool(test.bim.bootstrap.NewFile): diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py index 13a847f94a..f01399e88c 100644 --- a/src/bonsai/test/tool/test_model.py +++ b/src/bonsai/test/tool/test_model.py @@ -378,12 +378,13 @@ class TestGenerateStair2DProfile(NewFile): class TestUsingArrays(NewFile): def setup_array(self, add_second_layer=False, sync_children=False): - bpy.context.scene.BIMProjectProperties.template_file = "0" + tool.Project.get_project_props().template_file = "0" bpy.ops.bim.create_project() bpy.ops.mesh.primitive_cube_add() obj = bpy.context.active_object - bpy.context.scene.BIMRootProperties.ifc_product = "IfcElement" + rprops = tool.Root.get_root_props() + rprops.ifc_product = "IfcElement" bpy.ops.bim.assign_class(ifc_class="IfcActuator", predefined_type="ELECTRICACTUATOR", userdefined_type="") bpy.ops.bim.add_array() @@ -467,7 +468,8 @@ class TestApplyIfcMaterialChanges(NewFile): return mesh def setup_test(self, and_elements: bool = True) -> None: - bpy.context.scene.BIMProjectProperties.template_file = "0" + props = tool.Project.get_project_props() + props.template_file = "0" bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" bpy.ops.bim.create_project() ifc_file = tool.Ifc.get() @@ -519,7 +521,7 @@ class TestApplyIfcMaterialChanges(NewFile): bpy.ops.bim.add_constr_type_instance(relating_type_id=relating_type_id) with_opening = bpy.context.active_object with_opening.name = "With Opening" - props = bpy.context.scene.BIMRootProperties + props = tool.Root.get_root_props() props.representation_obj = with_opening bpy.ops.bim.add_element(ifc_product="IfcFeatureElement", ifc_class="IfcOpeningElement") diff --git a/src/bonsai/test/tool/test_project.py b/src/bonsai/test/tool/test_project.py index 4af7494a85..a87b00f753 100644 --- a/src/bonsai/test/tool/test_project.py +++ b/src/bonsai/test/tool/test_project.py @@ -97,7 +97,8 @@ class TestSetContext(NewFile): tool.Ifc.set(ifc) context = ifc.createIfcGeometricRepresentationContext() subject.set_context(context) - assert bpy.context.scene.BIMRootProperties.contexts == str(context.id()) + rprops = tool.Root.get_root_props() + assert rprops.contexts == str(context.id()) class TestSetDefaultContext(NewFile): @@ -115,7 +116,8 @@ class TestSetDefaultContext(NewFile): target_view="MODEL_VIEW", ) subject.set_default_context() - assert bpy.context.scene.BIMRootProperties.contexts == str(body.id()) + rprops = tool.Root.get_root_props() + assert rprops.contexts == str(body.id()) class TestSetDefaultModelingDimensions(NewFile): @@ -244,25 +246,25 @@ class TestLoadProject(NewFile): class TestLoadLinkedModels(NewFile): def test_load_linked_models_no_document(self): - links = bpy.context.scene.BIMProjectProperties.links + props = tool.Project.get_project_props() ifc = ifcopenshell.file() tool.Ifc.set(ifc) subject.load_linked_models_from_ifc() - assert len(links) == 0 + assert len(props.links) == 0 def test_load_linked_models_document_no_references(self): ifc = ifcopenshell.file() - links = bpy.context.scene.BIMProjectProperties.links + props = tool.Project.get_project_props() ifcopenshell.api.root.create_entity(ifc, "IfcProject") document = ifcopenshell.api.document.add_information(ifc) document.Name = "BBIM_Linked_Models" tool.Ifc.set(ifc) subject.load_linked_models_from_ifc() - assert len(links) == 0 + assert len(props.links) == 0 def test_load_linked_models_document_with_references(self): ifc = ifcopenshell.file() - links = bpy.context.scene.BIMProjectProperties.links + props = tool.Project.get_project_props() ifcopenshell.api.root.create_entity(ifc, "IfcProject") document = ifcopenshell.api.document.add_information(ifc) document.Name = "BBIM_Linked_Models" @@ -271,8 +273,8 @@ class TestLoadLinkedModels(NewFile): reference.Location = linked_model_path tool.Ifc.set(ifc) subject.load_linked_models_from_ifc() - assert len(links) == 1 - assert links[0].name == linked_model_path + assert len(props.links) == 1 + assert props.links[0].name == linked_model_path class TestSaveLinkedModelsToIfc(NewFile): @@ -286,8 +288,8 @@ class TestSaveLinkedModelsToIfc(NewFile): def test_save_linked_models_to_ifc_paths_to_add(self): ifc = ifcopenshell.file() ifcopenshell.api.root.create_entity(ifc, "IfcProject") - links = bpy.context.scene.BIMProjectProperties.links - link = links.add() + props = tool.Project.get_project_props() + link = props.links.add() linked_model_path = "test.ifc" link.name = linked_model_path tool.Ifc.set(ifc) @@ -299,7 +301,7 @@ class TestSaveLinkedModelsToIfc(NewFile): def test_save_linked_models_to_ifc_already_created_references(self): ifc = ifcopenshell.file() - links = bpy.context.scene.BIMProjectProperties.links + links = tool.Project.get_project_props().links ifcopenshell.api.root.create_entity(ifc, "IfcProject") document = ifcopenshell.api.document.add_information(ifc) @@ -326,7 +328,7 @@ class TestSaveLinkedModelsToIfc(NewFile): def test_save_linked_models_to_ifc_references_to_remove(self): ifc = ifcopenshell.file() - links = bpy.context.scene.BIMProjectProperties.links + links = tool.Project.get_project_props().links ifcopenshell.api.root.create_entity(ifc, "IfcProject") document = ifcopenshell.api.document.add_information(ifc) diff --git a/src/bonsai/test/tool/test_root.py b/src/bonsai/test/tool/test_root.py index 19cba02029..2c89a4e5fa 100644 --- a/src/bonsai/test/tool/test_root.py +++ b/src/bonsai/test/tool/test_root.py @@ -120,8 +120,8 @@ class TestGetObjectRepresentation(NewFile): ifc = ifcopenshell.file() tool.Ifc.set(ifc) representation = ifc.createIfcShapeRepresentation() - obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) - obj.data.BIMMeshProperties.ifc_definition_id = representation.id() + obj = bpy.data.objects.new("Object", (mesh := bpy.data.meshes.new("Mesh"))) + tool.Geometry.get_mesh_props(mesh).ifc_definition_id = representation.id() assert subject.get_object_representation(obj) == representation @@ -175,7 +175,7 @@ class TestSetObjectName(NewFile): class TestReassignClass(NewFile): def test_reassigning_multiple_occurrences_of_the_same_type(self): - bpy.context.scene.BIMProjectProperties.template_file = "IFC4 Demo Template.ifc" + tool.Project.get_project_props().template_file = "IFC4 Demo Template.ifc" bpy.ops.bim.create_project() ifc_file = tool.Ifc.get() context = bpy.context @@ -191,8 +191,10 @@ class TestReassignClass(NewFile): slabs = [tool.Ifc.get_object(e) for e in ifc_file.by_type("IfcSlab")] assert len(slabs) == 3 tool.Blender.set_objects_selection(context, slabs[0], (slabs[1],)) - context.scene.BIMRootProperties.ifc_product = "IfcElement" - context.scene.BIMRootProperties.ifc_class = "IfcWall" + + props = tool.Root.get_root_props() + props.ifc_product = "IfcElement" + props.ifc_class = "IfcWall" bpy.ops.bim.reassign_class() assert len(ifc_file.by_type("IfcWall")) == 3 diff --git a/src/bonsai/test/tool/test_spatial.py b/src/bonsai/test/tool/test_spatial.py index 2985b458d1..9bcc84c6dc 100644 --- a/src/bonsai/test/tool/test_spatial.py +++ b/src/bonsai/test/tool/test_spatial.py @@ -129,7 +129,8 @@ class TestDisableEditing(NewFile): obj = bpy.data.objects.new("Object", None) subject.enable_editing(obj) subject.disable_editing(obj) - assert obj.BIMObjectSpatialProperties.is_editing is False + props = tool.Spatial.get_object_spatial_props(obj) + assert props.is_editing is False class TestDuplicateObjectAndData(NewFile): @@ -148,7 +149,8 @@ class TestEnableEditing(NewFile): def test_run(self): obj = bpy.data.objects.new("Object", None) subject.enable_editing(obj) - assert obj.BIMObjectSpatialProperties.is_editing is True + props = tool.Spatial.get_object_spatial_props(obj) + assert props.is_editing is True class TestGetContainer(NewFile): diff --git a/src/bonsai/test/tool/test_surveyor.py b/src/bonsai/test/tool/test_surveyor.py index 0a12bd3903..bfba0cb480 100644 --- a/src/bonsai/test/tool/test_surveyor.py +++ b/src/bonsai/test/tool/test_surveyor.py @@ -20,6 +20,7 @@ import bpy import numpy as np import ifcopenshell import ifcopenshell.api +import ifcopenshell.util.geolocation import test.bim.bootstrap import bonsai.core.tool @@ -34,7 +35,7 @@ class TestImplementsTool(test.bim.bootstrap.NewFile): class TestGetGlobalMatrix(test.bim.bootstrap.NewFile): def test_getting_an_absolute_matrix_if_no_blender_offset(self): - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = False obj = bpy.data.objects.new("Object", None) assert (subject.get_absolute_matrix(obj) == np.array(obj.matrix_world)).all() @@ -45,7 +46,7 @@ class TestGetGlobalMatrix(test.bim.bootstrap.NewFile): unit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", prefix="MILLI") ifcopenshell.api.run("unit.assign_unit", ifc, units=[unit]) tool.Ifc.set(ifc) - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = True props.blender_offset_x = "1000" props.blender_offset_y = "2000" diff --git a/src/bonsai/test/tool/test_unit.py b/src/bonsai/test/tool/test_unit.py index 4a8512c360..a3bad12ab3 100644 --- a/src/bonsai/test/tool/test_unit.py +++ b/src/bonsai/test/tool/test_unit.py @@ -18,6 +18,9 @@ import bpy import ifcopenshell +import ifcopenshell.api.project +import ifcopenshell.api.root +import ifcopenshell.api.unit import bonsai.core.tool import bonsai.tool as tool from test.bim.bootstrap import NewFile @@ -31,23 +34,26 @@ class TestImplementsTool(NewFile): class TestClearActiveUnit(NewFile): def test_run(self): - bpy.context.scene.BIMUnitProperties.active_unit_id = 1 + props = tool.Unit.get_unit_props() + props.active_unit_id = 1 subject.clear_active_unit() - assert bpy.context.scene.BIMUnitProperties.active_unit_id == 0 + assert props.active_unit_id == 0 class TestDisableEditingUnits(NewFile): def test_run(self): - bpy.context.scene.BIMUnitProperties.is_editing = True + props = tool.Unit.get_unit_props() + props.is_editing = True subject.disable_editing_units() - assert bpy.context.scene.BIMUnitProperties.is_editing == False + assert props.is_editing == False class TestEnableEditingUnits(NewFile): def test_run(self): - bpy.context.scene.BIMUnitProperties.is_editing = False + props = tool.Unit.get_unit_props() + props.is_editing = False subject.enable_editing_units() - assert bpy.context.scene.BIMUnitProperties.is_editing == True + assert props.is_editing == True class TestExportUnitAttributes(NewFile): @@ -98,10 +104,11 @@ class TestExportUnitAttributes(NewFile): class TestGetSceneUnitName(NewFile): def test_getting_an_imperial_name(self): + props = tool.Blender.get_bim_props() bpy.context.scene.unit_settings.system = "IMPERIAL" bpy.context.scene.unit_settings.length_unit = "MILES" - bpy.context.scene.BIMProperties.area_unit = "square foot" - bpy.context.scene.BIMProperties.volume_unit = "cubic inch" + props.area_unit = "square foot" + props.volume_unit = "cubic inch" assert subject.get_scene_unit_name("LENGTHUNIT") == "mile" assert subject.get_scene_unit_name("AREAUNIT") == "square foot" assert subject.get_scene_unit_name("VOLUMEUNIT") == "cubic inch" @@ -142,13 +149,14 @@ class TestGetSceneUnitSIPrefix: assert subject.get_scene_unit_si_prefix("LENGTHUNIT") == "KILO" bpy.context.scene.unit_settings.length_unit = "ADAPTIVE" assert subject.get_scene_unit_si_prefix("LENGTHUNIT") is None - bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE" + props = tool.Blender.get_bim_props() + props.area_unit = "SQUARE_METRE" assert subject.get_scene_unit_si_prefix("AREAUNIT") is None - bpy.context.scene.BIMProperties.area_unit = "MILLI/SQUARE_METRE" + props.area_unit = "MILLI/SQUARE_METRE" assert subject.get_scene_unit_si_prefix("AREAUNIT") == "MILLI" - bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE" + props.volume_unit = "CUBIC_METRE" assert subject.get_scene_unit_si_prefix("VOLUMEUNIT") is None - bpy.context.scene.BIMProperties.volume_unit = "MILLI/CUBIC_METRE" + props.volume_unit = "MILLI/CUBIC_METRE" assert subject.get_scene_unit_si_prefix("VOLUMEUNIT") == "MILLI" @@ -159,25 +167,25 @@ class TestImportUnitAttributes(NewFile): unit.UnitType = "ANGULARVELOCITYUNIT" unit.UserDefinedType = "UserDefinedType" subject.import_unit_attributes(unit) - props = bpy.context.scene.BIMUnitProperties - assert props.unit_attributes.get("UnitType").enum_value == "ANGULARVELOCITYUNIT" - assert props.unit_attributes.get("UserDefinedType").string_value == "UserDefinedType" + props = tool.Unit.get_unit_props() + assert props.unit_attributes["UnitType"].enum_value == "ANGULARVELOCITYUNIT" + assert props.unit_attributes["UserDefinedType"].string_value == "UserDefinedType" def test_importing_monetary_units(self): tool.Ifc.set(ifc := ifcopenshell.file()) unit = ifc.createIfcMonetaryUnit() unit.Currency = "Currency" subject.import_unit_attributes(unit) - props = bpy.context.scene.BIMUnitProperties - assert props.unit_attributes.get("Currency").string_value == "Currency" + props = tool.Unit.get_unit_props() + assert props.unit_attributes["Currency"].string_value == "Currency" def test_importing_monetary_units_ifc2x3(self): tool.Ifc.set(ifc := ifcopenshell.file(schema="IFC2X3")) unit = ifc.createIfcMonetaryUnit() unit.Currency = "USD" subject.import_unit_attributes(unit) - props = bpy.context.scene.BIMUnitProperties - assert props.unit_attributes.get("Currency").enum_value == "USD" + props = tool.Unit.get_unit_props() + assert props.unit_attributes["Currency"].enum_value == "USD" def test_importing_context_dependent_units(self): ifc = ifcopenshell.file() @@ -187,10 +195,10 @@ class TestImportUnitAttributes(NewFile): unit.Name = "Name" unit.Dimensions = ifc.createIfcDimensionalExponents(1, 2, 3, 4, 5, 6, 7) subject.import_unit_attributes(unit) - props = bpy.context.scene.BIMUnitProperties - assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT" - assert props.unit_attributes.get("Name").string_value == "Name" - assert props.unit_attributes.get("Dimensions").string_value == "[1, 2, 3, 4, 5, 6, 7]" + props = tool.Unit.get_unit_props() + assert props.unit_attributes["UnitType"].enum_value == "ABSORBEDDOSEUNIT" + assert props.unit_attributes["Name"].string_value == "Name" + assert props.unit_attributes["Dimensions"].string_value == "[1, 2, 3, 4, 5, 6, 7]" def test_importing_conversion_based_units(self): ifc = ifcopenshell.file() @@ -200,10 +208,10 @@ class TestImportUnitAttributes(NewFile): unit.Name = "Name" unit.Dimensions = ifc.createIfcDimensionalExponents(1, 2, 3, 4, 5, 6, 7) subject.import_unit_attributes(unit) - props = bpy.context.scene.BIMUnitProperties - assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT" - assert props.unit_attributes.get("Name").string_value == "Name" - assert props.unit_attributes.get("Dimensions").string_value == "[1, 2, 3, 4, 5, 6, 7]" + props = tool.Unit.get_unit_props() + assert props.unit_attributes["UnitType"].enum_value == "ABSORBEDDOSEUNIT" + assert props.unit_attributes["Name"].string_value == "Name" + assert props.unit_attributes["Dimensions"].string_value == "[1, 2, 3, 4, 5, 6, 7]" def test_importing_conversion_based_with_offset_units(self): ifc = ifcopenshell.file() @@ -214,11 +222,11 @@ class TestImportUnitAttributes(NewFile): unit.Dimensions = ifc.createIfcDimensionalExponents(1, 2, 3, 4, 5, 6, 7) unit.ConversionOffset = 1 subject.import_unit_attributes(unit) - props = bpy.context.scene.BIMUnitProperties - assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT" - assert props.unit_attributes.get("Name").string_value == "Name" - assert props.unit_attributes.get("Dimensions").string_value == "[1, 2, 3, 4, 5, 6, 7]" - assert props.unit_attributes.get("ConversionOffset").float_value == 1 + props = tool.Unit.get_unit_props() + assert props.unit_attributes["UnitType"].enum_value == "ABSORBEDDOSEUNIT" + assert props.unit_attributes["Name"].string_value == "Name" + assert props.unit_attributes["Dimensions"].string_value == "[1, 2, 3, 4, 5, 6, 7]" + assert props.unit_attributes["ConversionOffset"].float_value == 1 def test_importing_si_units(self): ifc = ifcopenshell.file() @@ -228,11 +236,11 @@ class TestImportUnitAttributes(NewFile): unit.Prefix = "EXA" unit.Name = "AMPERE" subject.import_unit_attributes(unit) - props = bpy.context.scene.BIMUnitProperties - assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT" - assert props.unit_attributes.get("Prefix").enum_value == "EXA" - assert props.unit_attributes.get("Name").enum_value == "AMPERE" - assert props.unit_attributes.get("Dimensions") is None + props = tool.Unit.get_unit_props() + assert props.unit_attributes["UnitType"].enum_value == "ABSORBEDDOSEUNIT" + assert props.unit_attributes["Prefix"].enum_value == "EXA" + assert props.unit_attributes["Name"].enum_value == "AMPERE" + assert props.unit_attributes["Dimensions"] is None class TestImportUnits(NewFile): @@ -248,7 +256,7 @@ class TestImportUnits(NewFile): ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject") ifcopenshell.api.unit.assign_unit(ifc, units=[unit2]) subject.import_units() - props = bpy.context.scene.BIMUnitProperties + props = tool.Unit.get_unit_props() assert len(props.units) == 6 assert props.units[0].ifc_definition_id == unit1.id() @@ -311,4 +319,5 @@ class TestSetActiveUnit(NewFile): ifc = ifcopenshell.file() unit = ifc.createIfcSIUnit() subject.set_active_unit(unit) - assert bpy.context.scene.BIMUnitProperties.active_unit_id == unit.id() + props = tool.Unit.get_unit_props() + assert props.active_unit_id == unit.id() diff --git a/src/ifcgeom/ConversionSettings.h b/src/ifcgeom/ConversionSettings.h index 65b11ba394..ed172cad69 100644 --- a/src/ifcgeom/ConversionSettings.h +++ b/src/ifcgeom/ConversionSettings.h @@ -407,7 +407,11 @@ namespace ifcopenshell { static constexpr TriangulationMethod defaultvalue = TRIANGLE_MESH; }; - + struct CgalEmitOriginalEdges : public SettingBase { + static constexpr const char* const name = "cgal-original-edges"; + static constexpr const char* const description = "Try to emit original edge face boundary edges instead of recomputed ones based on face normal. Falls back to triangulated data in case of boolean operands and faces with holes."; + static constexpr bool defaultvalue = false; + }; } template @@ -500,7 +504,7 @@ namespace ifcopenshell { }; class IFC_GEOM_API Settings : public SettingsContainer< - std::tuple + std::tuple > {}; } diff --git a/src/ifcgeom/function_item_evaluator.cpp b/src/ifcgeom/function_item_evaluator.cpp index 9ca71b3450..1d5c0d186a 100644 --- a/src/ifcgeom/function_item_evaluator.cpp +++ b/src/ifcgeom/function_item_evaluator.cpp @@ -1,8 +1,16 @@ #include "function_item_evaluator.h" #include "profile_helper.h" +#include + using namespace ifcopenshell::geometry; +double ifcopenshell::geometry::polynomial_length(double A, double B, double C, double horizontal_length) { + auto fn = [A, B, C](double x) -> double { return sqrt(pow(B + 2 * C * x, 2.0) + 1.0); }; + auto l = boost::math::quadrature::trapezoidal(fn, 0.0, horizontal_length); + return l; +} + struct functor_fn_evaluator : public fn_evaluator { functor_fn_evaluator(taxonomy::functor_item::const_ptr fn, const ifcopenshell::geometry::Settings& settings) : fn_evaluator(settings), diff --git a/src/ifcgeom/function_item_evaluator.h b/src/ifcgeom/function_item_evaluator.h index 0e29c6149c..3107b615dc 100644 --- a/src/ifcgeom/function_item_evaluator.h +++ b/src/ifcgeom/function_item_evaluator.h @@ -7,6 +7,17 @@ namespace ifcopenshell { namespace geometry { +/// @brief Computes the curve length of a polynomial of the form y = A + Bx + Cx^2 +/// This function is needed on the python side. To do this computation, a large library like scipy +/// is needed. That is too much overhead. For this reason, a simple function is here on the C++ side +/// that the python side can call +/// @param A constant term +/// @param B linear term +/// @param C quadradic term +/// @param horizontal_length length of the polynomal projected onto the horizontal axis +/// @return curve length +double polynomial_length(double A, double B, double C,double horizontal_length); + /// @brief Abstract class for evaluating a function_item. This class is specialized for each of the function_item types. struct fn_evaluator { fn_evaluator(const ifcopenshell::geometry::Settings& settings) : settings_(settings) { diff --git a/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp b/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp index ac991e40a3..e7a3b2a9ce 100644 --- a/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp +++ b/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp @@ -144,10 +144,15 @@ ifcopenshell::geometry::CgalShape::CgalShape(const cgal_shape_t& shape, bool con } if (shape.size_of_facets() != 1) { - // this is for handling the specical case of storing a single point in a polyhedron, + // the size_of_facets() == 1 check is for handling the specical case of + // storing a single point in a polyhedron as a degenerate triangle + // // @todo come up with a proper variant for storing lower dimensional entities - CGAL::Polygon_mesh_processing::triangulate_faces(*shape_); - CGAL::Polygon_mesh_processing::remove_degenerate_faces(*shape_); + + // @todo we don't have access to settings here so we don't know whether we should triangulate + // remove_degenerate_faces() is also called in the triangulate() call below though... + // CGAL::Polygon_mesh_processing::triangulate_faces(*shape_); + // CGAL::Polygon_mesh_processing::remove_degenerate_faces(*shape_); } } @@ -183,6 +188,15 @@ void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Sett // ... also becuase of transforming the vertex positions, right? cgal_shape_t s = *this; + const bool setting_use_original_edges = settings.get().get(); + + std::set> original_edges; + if (setting_use_original_edges) { + for (auto it = s.edges_begin(); it != s.edges_end(); ++it) { + original_edges.insert({ it->vertex()->point(), it->prev()->vertex()->point() }); + } + } + if (!place.is_identity()) { const auto& m = place.ccomponents(); @@ -199,14 +213,11 @@ void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Sett } if (!std::all_of(s.facets_begin(), s.facets_end(), [](auto f) { return f.is_triangle(); })) { - if (!s.is_valid()) { Logger::Message(Logger::LOG_ERROR, "Invalid Polyhedron_3 in object (before triangulation)"); return; } - CGAL::Polygon_mesh_processing::remove_degenerate_faces(s); - bool success = false; try { success = CGAL::Polygon_mesh_processing::triangulate_faces(s); @@ -215,27 +226,29 @@ void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Sett return; } + CGAL::Polygon_mesh_processing::remove_degenerate_faces(s); + if (!success) { Logger::Message(Logger::LOG_ERROR, "Triangulation failed"); return; } - // std::cout << "Triangulated model: " << s.size_of_facets() << " facets and " << s.size_of_vertices() << " vertices" << std::endl; if (!s.is_valid()) { Logger::Message(Logger::LOG_ERROR, "Invalid Polyhedron_3 in object (after triangulation)"); // return; } - } // Facet -> planar component map for determining which // edges are to be registered. std::vector> components; - partition_coplanar_components(s, components); std::map facet_to_component; - for (auto it = components.begin(); it != components.end(); ++it) { - for (auto& f : *it) { - facet_to_component[f] = it; + if (!setting_use_original_edges) { + partition_coplanar_components(s, components); + for (auto it = components.begin(); it != components.end(); ++it) { + for (auto& f : *it) { + facet_to_component[f] = it; + } } } @@ -305,8 +318,10 @@ void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Sett } vertexidx[i] = (int)vidx; - is_face_boundary[i] = facet_to_component[face] != facet_to_component[current_halfedge->opposite()->face()]; - + is_face_boundary[i] = setting_use_original_edges + ? original_edges.find({ current_halfedge->vertex()->point(), current_halfedge->prev()->vertex()->point() }) != original_edges.end() + : facet_to_component[face] != facet_to_component[current_halfedge->opposite()->face()]; + ++i; ++num_vertices; ++current_halfedge; diff --git a/src/ifcgeom/mapping/IfcArbitraryClosedProfileDef.cpp b/src/ifcgeom/mapping/IfcArbitraryClosedProfileDef.cpp index 6a51091f99..3cce6f31dc 100644 --- a/src/ifcgeom/mapping/IfcArbitraryClosedProfileDef.cpp +++ b/src/ifcgeom/mapping/IfcArbitraryClosedProfileDef.cpp @@ -26,6 +26,10 @@ using namespace ifcopenshell::geometry; taxonomy::ptr mapping::map_impl(const IfcSchema::IfcArbitraryClosedProfileDef* inst) { auto loop = taxonomy::cast(map(inst->OuterCurve())); if (loop) { + if (inst->ProfileType() == IfcSchema::IfcProfileTypeEnum::IfcProfileType_CURVE) { + return loop; + } + auto face = taxonomy::make(); loop->external = true; face->children = { loop }; diff --git a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp index 14ee9899ad..52187d7e5e 100644 --- a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp @@ -31,10 +31,10 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in std::vector cross_sections; auto dir = map(inst->Directrix()); - auto pwf = taxonomy::dcast(dir); - if (!pwf) { + auto fn = taxonomy::dcast(dir); + if (!fn) { // Only implement on alignment curves - Logger::Warning("IfcSectionedSolidHorizontal is only implemented for piecewise function Directrix curves", inst); + Logger::Warning("IfcSectionedSolidHorizontal is only implemented for Directrix curves based on taxonomy::function_item", inst); return nullptr; } @@ -70,9 +70,6 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in profile_offsets.push_back(po); } -#else - return nullptr; -#endif if (faces.size() != profile_offsets.size()) { Logger::Warning("Expected CrossSections and CrossSectionPositions to be equal length, but got " + std::to_string(faces.size()) + " and " + std::to_string(profile_offsets.size()) + " respectively", inst); return nullptr; @@ -85,9 +82,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in for (size_t i = 0; i < faces.size(); ++i) { cross_sections.push_back({ longitudes[i], faces[i], profile_offsets[i] }); } - } +#else + return nullptr; +#endif + } - return make_loft(settings_, inst, pwf, cross_sections); + return make_loft(settings_, inst, fn, cross_sections); } #endif diff --git a/src/ifcgeom/mapping/IfcSweptDiskSolid.cpp b/src/ifcgeom/mapping/IfcSweptDiskSolid.cpp index 1eec8c6bc7..bd779fd6cd 100644 --- a/src/ifcgeom/mapping/IfcSweptDiskSolid.cpp +++ b/src/ifcgeom/mapping/IfcSweptDiskSolid.cpp @@ -58,8 +58,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSweptDiskSolid* inst) { auto ep = inst->EndParam(); #else boost::optional sp, ep; - sp = inst->StartParam(); - ep = inst->EndParam(); + try { + sp = inst->StartParam(); + ep = inst->EndParam(); + } catch (const IfcParse::IfcException& e) { + Logger::Warning(e); + } #endif const double tol = settings_.get().get(); diff --git a/src/ifcgeom/taxonomy.cpp b/src/ifcgeom/taxonomy.cpp index b2d48478f3..6fc7288dda 100644 --- a/src/ifcgeom/taxonomy.cpp +++ b/src/ifcgeom/taxonomy.cpp @@ -800,13 +800,14 @@ boost::optional ifcopenshell::geometry::taxonomy::curve_to_face_upgra } -boost::optional ifcopenshell::geometry::taxonomy::loop_to_piecewise_function_upgrade_impl(ptr item) { - boost::optional pwf_; +boost::optional ifcopenshell::geometry::taxonomy::loop_to_function_item_upgrade_impl(ptr item) { + boost::optional fi_; auto loop_ = dcast(item); if (loop_) { - if (loop_->pwf.is_initialized()) { - pwf_ = loop_->pwf; + if (loop_->fi.is_initialized()) { + fi_ = loop_->fi; } else { + // piecewise_function is a specialization of function_item - callers don't need to know this detail piecewise_function::spans_t spans; spans.reserve(loop_->children.size()); for (auto& edge_ : loop_->children) { @@ -828,9 +829,9 @@ boost::optional ifcopenshell::geometry::taxonomy::loop_ }; spans.emplace_back(taxonomy::make(l, fn)); } - pwf_ = make(0.0,spans); - loop_->pwf = pwf_; + fi_ = make(0.0,spans); + loop_->fi = fi_; } } - return pwf_; + return fi_; } diff --git a/src/ifcgeom/taxonomy.h b/src/ifcgeom/taxonomy.h index 4f73f10a66..703504abc2 100644 --- a/src/ifcgeom/taxonomy.h +++ b/src/ifcgeom/taxonomy.h @@ -892,7 +892,7 @@ typedef item const* ptr; DECLARE_PTR(loop) boost::optional external, closed; - boost::optional pwf; + boost::optional fi; bool is_polyhedron() const { for (auto& e : children) { @@ -1374,27 +1374,27 @@ typedef item const* ptr; } }; - boost::optional loop_to_piecewise_function_upgrade_impl(ptr item); + boost::optional loop_to_function_item_upgrade_impl(ptr item); template - class loop_to_piecewise_function_upgrade { + class loop_to_function_item_upgrade { private: - boost::optional pwf_; + boost::optional fi_; public: - loop_to_piecewise_function_upgrade(taxonomy::ptr item) { - if constexpr (std::is_same_v) { - pwf_ = loop_to_piecewise_function_upgrade_impl(item); + loop_to_function_item_upgrade(taxonomy::ptr item) { + if constexpr (std::is_same_v) { + fi_ = loop_to_function_item_upgrade_impl(item); } } operator bool() const { - return pwf_.is_initialized(); + return fi_.is_initialized(); } operator typename T::ptr() const { - if constexpr (std::is_same_v) { - if (pwf_) { - return *pwf_; + if constexpr (std::is_same_v) { + if (fi_) { + return *fi_; } } return nullptr; @@ -1435,7 +1435,7 @@ typedef item const* ptr; } } { - loop_to_piecewise_function_upgrade upg(u); + loop_to_function_item_upgrade upg(u); if (upg) { return upg; } @@ -1479,7 +1479,7 @@ typedef item const* ptr; } } { - loop_to_piecewise_function_upgrade upg(u); + loop_to_function_item_upgrade upg(u); if (upg) { return upg; } diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst index df7ae3321e..0c4753807a 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst @@ -306,39 +306,43 @@ In addition to geometry settings, serialisation has its own set of .. code-block:: python - import ifcopenshell - import ifcopenshell.geom - import multiprocessing - - settings = ifcopenshell.geom.settings() - - # Settings for glTF / glb - settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) - # Note that applying default materials is required in glTF serialisation. - settings.set("apply-default-materials", True) - - # Settings for obj - # settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) - # settings.set("apply-default-materials", True) - # settings.set("use-world-coords", True) - - # Serialise to glTF / glb - serialiser = ifcopenshell.geom.serializers.gltf("output.glb", settings) - self.serialiser_settings = ifcopenshell.geom.serializer_settings() - # Setting element GUIDs is optional, but useful to uniquely identify objects in non-semantic formats. - serialiser_settings.set("use-element-guids", True) - - # Serialise to obj - # serialiser = ifcopenshell.geom.serializers.obj('output.obj', 'output.mtl', settings, serialiser_settings) - - serialiser.setFile(self.file) - serialiser.setUnitNameAndMagnitude("METER", 1.0) - serialiser.writeHeader() - - iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count()) - if iterator.initialize(): - while True: - serialiser.write(iterator.get()) - if not iterator.next(): - break - serialiser.finalize() + import multiprocessing + + import ifcopenshell + import ifcopenshell.geom + + ifc_file = ifcopenshell.open("model.ifc") + + settings = ifcopenshell.geom.settings() + + # Settings for glTF / glb + settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) + # Note that applying default materials is required in glTF serialisation. + settings.set("apply-default-materials", True) + + # Settings for obj + # settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) + # settings.set("apply-default-materials", True) + # settings.set("use-world-coords", True) + + serialiser_settings = ifcopenshell.geom.serializer_settings() + # Setting element GUIDs is optional, but useful to uniquely identify objects in non-semantic formats. + serialiser_settings.set("use-element-guids", True) + + # Serialise to glTF / glb + serialiser = ifcopenshell.geom.serializers.gltf("output.glb", settings, serialiser_settings) + + # Serialise to obj + # serialiser = ifcopenshell.geom.serializers.obj('output.obj', 'output.mtl', settings, serialiser_settings) + + serialiser.setFile(ifc_file) + serialiser.setUnitNameAndMagnitude("METER", 1.0) + serialiser.writeHeader() + + iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count()) + if iterator.initialize(): + while True: + serialiser.write(iterator.get()) + if not iterator.next(): + break + serialiser.finalize() diff --git a/src/ifcopenshell-python/ifcopenshell/alignment.py b/src/ifcopenshell-python/ifcopenshell/alignment.py index a40151068d..45eb8bc054 100644 --- a/src/ifcopenshell-python/ifcopenshell/alignment.py +++ b/src/ifcopenshell-python/ifcopenshell/alignment.py @@ -28,6 +28,8 @@ import ifcopenshell.guid import ifcopenshell.template from ifcopenshell import entity_instance from ifcopenshell import ifcopenshell_wrapper +import ifcopenshell.util +import ifcopenshell.util.stationing def evaluate_representation(shape_rep: entity_instance, dist_along: float) -> np.ndarray: @@ -46,10 +48,10 @@ def evaluate_representation(shape_rep: entity_instance, dist_along: float) -> np # TODO: confirm point is not beyond limits of alignment s = ifcopenshell.geom.settings() - piecewise_function = ifcopenshell_wrapper.map_shape(s, shape_rep.wrapped_data) - pwf_evaluator = ifcopenshell_wrapper.piecewise_function_evaluator(piecewise_function, s) + function_item = ifcopenshell_wrapper.map_shape(s, shape_rep.wrapped_data) + evaluator = ifcopenshell_wrapper.function_item_evaluator(s, function_item) - trans_matrix = pwf_evaluator.evaluate(dist_along) + trans_matrix = evaluator.evaluate(dist_along) return np.array(trans_matrix, dtype=np.float64).T @@ -68,10 +70,10 @@ def evaluate_segment(segment: entity_instance, dist_along: float) -> np.ndarray: raise ValueError(f"Provided value {dist_along=} is beyond the end of the segment ({segment.SegmentLength}).") s = ifcopenshell.geom.settings() - piecewise_function = ifcopenshell_wrapper.map_shape(s, segment.wrapped_data) - pwf_evaluator = ifcopenshell_wrapper.piecewise_function_evaluator(piecewise_function, s) + function_item = ifcopenshell_wrapper.map_shape(s, segment.wrapped_data) + evaluator = ifcopenshell_wrapper.function_item_evaluator(s, function_item) - trans_matrix = pwf_evaluator.evaluate(dist_along) + trans_matrix = evaluator.evaluate(dist_along) return np.array(trans_matrix, dtype=np.float64).T @@ -90,7 +92,8 @@ def generate_vertices(rep_curve: entity_instance, distance_interval: float = 5.0 raise ValueError("Alignment representation not found.") s = ifcopenshell.geom.settings() - s.set("PIECEWISE_STEP_PARAM", distance_interval) + s.set("piecewise-step-type", 0) # 0 = step-size is maximum step size, 1 = step-size is mininimum number of steps + s.set("piecewise-step-size", distance_interval) shape = ifcopenshell.geom.create_shape(s, rep_curve) vertices = shape.verts if len(vertices) == 0: @@ -185,6 +188,148 @@ class IfcAlignmentHelper: alignment_segment.ObjectPlacement = global_placement alignment_segment.Representation = product + def _map_alignment_vertical_segment(self, segment: entity_instance) -> Sequence[entity_instance]: + segment_type = segment.is_a().upper() + expected_type = "IFCALIGNMENTVERTICALSEGMENT" + if not segment_type == expected_type: + raise TypeError(f"Expected to see type '{expected_type}', instead received '{segment_type}'.") + + start_distance_along = segment.StartDistAlong + horizontal_length = segment.HorizontalLength + start_height = segment.StartHeight + start_gradient = segment.StartGradient + end_gradient = segment.EndGradient + radius_of_curvature = segment.RadiusOfCurvature + + if math.isclose(horizontal_length, 0): + # set transition value based on whether this is the final zero-length segment + transition = "DISCONTINUOUS" + else: + transition = "CONTSAMEGRADIENTSAMECURVATURE" + + _type = segment.PredefinedType + + match _type: + case "CONSTANTGRADIENT": + parent_curve = self._file.create_entity( + type="IfcLine", + Pnt=self._file.create_entity( + type="IfcCartesianPoint", + Coordinates=(0.0, 0.0), + ), + Dir=self._file.create_entity( + type="IfcVector", + Orientation=self._file.create_entity( + type="IfcDirection", + DirectionRatios=(1.0, 0.0), + ), + Magnitude=1.0, + ), + ) + + dx = math.cos(math.atan(start_gradient)) + dy = math.sin(math.atan(start_gradient)) + curve_segment_length = horizontal_length / dx + + curve_segment = self._file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=self._file.create_entity( + type="IfcAxis2Placement2D", + Location=self._file.create_entity( + type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height) + ), + RefDirection=self._file.createIfcDirection((dx, dy)), + ), + SegmentStart=self._file.createIfcLengthMeasure(0.0), + SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length), + ParentCurve=parent_curve, + ) + result = (curve_segment, None) + + case "PARABOLICARC": + A = start_height + B = start_gradient + C = (end_gradient - start_gradient) / (2.0 * horizontal_length) + + parent_curve = self._file.create_entity( + type="IfcPolynomialCurve", + Position=self._file.create_entity( + type="IfcAxis2Placement2D", + Location=self._file.create_entity(type="IfcCartesianPoint", Coordinates=(0.0, 0.0)), + RefDirection=self._file.createIfcDirection( + (1.0, 0.0), + ), + ), + CoefficientsX=(0.0, 1.0), + CoefficientsY=(A, B, C), + ) + + dx = math.cos(math.atan(start_gradient)) + dy = math.sin(math.atan(start_gradient)) + curve_segment_length = ifcopenshell_wrapper.polynomial_length(A, B, C, horizontal_length) + + curve_segment = self._file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=self._file.create_entity( + type="IfcAxis2Placement2D", + Location=self._file.create_entity( + type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height) + ), + RefDirection=self._file.createIfcDirection((dx, dy)), + ), + SegmentStart=self._file.createIfcLengthMeasure(0.0), + SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length), + ParentCurve=parent_curve, + ) + result = (curve_segment, None) + + case "CIRCULARARC": + start_angle = math.atan(start_gradient) + end_angle = math.atan(end_gradient) + if start_angle < end_angle: + radius = horizontal_length / (math.sin(end_angle) - math.sin(start_angle)) + else: + radius = horizontal_length / (math.sin(start_angle) - math.sin(end_angle)) + + parent_curve = self._file.create_entity( + type="IfcCircle", + Position=self._file.create_entity( + type="IfcAxis2Placement2D", + Location=self._file.create_entity(type="IfcCartesianPoint", Coordinates=(0.0, 0.0)), + RefDirection=self._file.createIfcDirection( + (1.0, 0.0), + ), + ), + Radius=radius, + ) + + segment_curve_length = radius * math.fabs(end_angle - start_angle) + + curve_segment = self._file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=self._file.create_entity( + type="IfcAxis2Placement2D", + Location=self._file.create_entity( + type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height) + ), + RefDirection=self._file.createIfcDirection( + (1.0, 0.0), + ), + ), + SegmentStart=self._file.createIfcLengthMeasure(0.0), + SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length), + ParentCurve=parent_curve, + ) + result = (curve_segment, None) + + case _: + result = (None, None) + + return result + def _map_alignment_horizontal_segment(self, segment: entity_instance) -> Sequence[entity_instance]: segment_type = segment.is_a().upper() expected_type = "IFCALIGNMENTHORIZONTALSEGMENT" @@ -203,67 +348,62 @@ class IfcAlignmentHelper: else: transition = "CONTSAMEGRADIENTSAMECURVATURE" - match _type: - case "LINE": - parent_curve = self._file.create_entity( - type="IfcLine", - Pnt=self._file.create_entity( - type="IfcCartesianPoint", - Coordinates=(0.0, 0.0), + if _type == "LINE": + parent_curve = self._file.create_entity( + type="IfcLine", + Pnt=self._file.create_entity( + type="IfcCartesianPoint", + Coordinates=(0.0, 0.0), + ), + Dir=self._file.create_entity( + type="IfcVector", + Orientation=self._file.create_entity( + type="IfcDirection", + DirectionRatios=(1.0, 0.0), ), - Dir=self._file.create_entity( - type="IfcVector", - Orientation=self._file.create_entity( - type="IfcDirection", - DirectionRatios=(1.0, 0.0), - ), - Magnitude=1.0, + Magnitude=1.0, + ), + ) + curve_segment = self._file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=self._file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=self._file.createIfcDirection( + (math.cos(start_direction), math.sin(start_direction)), ), - ) - curve_segment = self._file.create_entity( - type="IfcCurveSegment", - Transition=transition, - Placement=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=start_point, - RefDirection=self._file.createIfcDirection( - (math.cos(start_direction), math.sin(start_direction)), - ), - ), - SegmentStart=self._file.createIfcLengthMeasure(0.0), - SegmentLength=self._file.createIfcLengthMeasure(length), - ParentCurve=parent_curve, - ) - result = (curve_segment, None) - case "CIRCULARARC": - parent_curve = self._file.createIfcCircle( - Position=self._file.createIfcAxis2Placement2D( - Location=self._file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)), - RefDirection=self._file.createIfcDirection( - (math.cos(start_direction), math.sin(start_direction)) - ), - ), - Radius=abs(start_radius), - ) + ), + SegmentStart=self._file.createIfcLengthMeasure(0.0), + SegmentLength=self._file.createIfcLengthMeasure(length), + ParentCurve=parent_curve, + ) + result = (curve_segment, None) + elif _type == "CIRCULARARC": + parent_curve = self._file.createIfcCircle( + Position=self._file.createIfcAxis2Placement2D( + Location=self._file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)), + RefDirection=self._file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + Radius=abs(start_radius), + ) - curve_segment = self._file.create_entity( - type="IfcCurveSegment", - Transition=transition, - Placement=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=start_point, - RefDirection=self._file.createIfcDirection( - (math.cos(start_direction), math.sin(start_direction)) - ), - ), - SegmentStart=self._file.createIfcLengthMeasure(0.0), - SegmentLength=self._file.createIfcLengthMeasure(length * start_radius / abs(start_radius)), - ParentCurve=parent_curve, - ) - result = (curve_segment, None) + curve_segment = self._file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=self._file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=self._file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=self._file.createIfcLengthMeasure(0.0), + SegmentLength=self._file.createIfcLengthMeasure(length * start_radius / abs(start_radius)), + ParentCurve=parent_curve, + ) + result = (curve_segment, None) - case _: - result = (None, None) + else: + result = (None, None) return result @@ -555,10 +695,13 @@ class IfcAlignmentHelper: alignment.Representation = product_definition_shape # create referent for start station + start_station_name = "Start Station ({})".format( + ifcopenshell.util.stationing.station_as_string(start_station) + ) start_referent = self._file.createIfcReferent( GlobalId=ifcopenshell.guid.new(), OwnerHistory=None, - Name="Start Station", + Name=start_station_name, Description=None, ObjectType=None, ObjectPlacement=self._file.createIfcLinearPlacement( @@ -576,6 +719,8 @@ class IfcAlignmentHelper: Representation=None, PredefinedType="STATION", ) + pset_stationing = ifcopenshell.api.pset.add_pset(self._file, product=start_referent, name="Pset_Stationing") + ifcopenshell.api.pset.edit_pset(self._file, pset=pset_stationing, properties={"Station": start_station}) # nest the horizontal and the referent under the alignment nesting_of_alignment = self._file.create_entity( @@ -599,12 +744,11 @@ class IfcAlignmentHelper: return alignment - def add_vertical_alignment( + def _create_vertical_alignment( self, - name: str, - description: str, + composite_curve: entity_instance, vpoints: Sequence[Sequence[float]], - vclengths: Sequence[Sequence[float]], + lengths: Sequence[float], include_geometry: bool = True, ): """ @@ -613,12 +757,358 @@ class IfcAlignmentHelper: @param name: value for Name attribute @param description: value for Description attribute @param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end. - @param vclengths: radii values to use for transition + @param vclengths: horizontal length of parabolic vertical curves @param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic """ - pass + vertical_segments = list() # business logic + vertical_curve_segments = list() # geometry + xPBG, yPBG = vpoints[0] + xPVI, yPVI = vpoints[1] + i = 1 + for length in lengths: + # back gradient + dxBG = xPVI - xPBG + dyBG = yPVI - yPBG + start_slope = math.tan(math.atan2(dyBG, dxBG)) - def add_alignment( + # forward gradient + i += 1 + xPFG, yPFG = vpoints[i] + dxFG = xPFG - xPVI + dyFG = yPFG - yPVI + end_slope = math.tan(math.atan2(dyFG, dxFG)) + + xEVC = xPVI + length / 2.0 + yEVC = yPVI + end_slope * length / 2.0 + + # create gradient + gradient_length = dxBG - length / 2.0 + design_parameters = self._file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag=None, + EndTag=None, + StartDistAlong=xPBG, + HorizontalLength=gradient_length, + StartHeight=yPBG, + StartGradient=start_slope, + EndGradient=start_slope, + RadiusOfCurvature=None, + PredefinedType="CONSTANTGRADIENT", + ) + alignment_segment = self._file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + vertical_segments.append(alignment_segment) + + if include_geometry: + vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) + + # create vertical curve + k = (end_slope - start_slope) / length + xBVC = xPVI - length / 2.0 + yBVC = yPVI - start_slope * length / 2.0 + + design_parameters = self._file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag=None, + EndTag=None, + StartDistAlong=xBVC, + HorizontalLength=length, + StartHeight=yBVC, + StartGradient=start_slope, + EndGradient=end_slope, + RadiusOfCurvature=1 / k, + PredefinedType="PARABOLICARC", + ) + alignment_segment = self._file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + vertical_segments.append(alignment_segment) + + if include_geometry: + vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) + + # start of next curve is end of this curve + xPBG = xEVC + yPBG = yEVC + xPVI = xPFG + yPVI = yPFG + + # create last gradient run + dx = xPVI - xPBG + dy = yPVI - yPBG + slope = math.tan(math.atan2(dy, dx)) + gradient_length = dx + + design_parameters = self._file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag=None, + EndTag=None, + StartDistAlong=xPBG, + HorizontalLength=gradient_length, + StartHeight=yPBG, + StartGradient=slope, + EndGradient=slope, + RadiusOfCurvature=None, + PredefinedType="CONSTANTGRADIENT", + ) + alignment_segment = self._file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + vertical_segments.append(alignment_segment) + + if include_geometry: + vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) + + # create zero length terminator segment + design_parameters = self._file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag="VPOE", + EndTag="VPOE", + StartDistAlong=xPVI, + HorizontalLength=0.0, + StartHeight=yPVI, + StartGradient=slope, + EndGradient=slope, + RadiusOfCurvature=None, + PredefinedType="CONSTANTGRADIENT", + ) + alignment_segment = self._file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + vertical_segments.append(alignment_segment) + + if include_geometry: + vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) + + if include_geometry: + gradient_curve = self._file.create_entity( + type="IfcGradientCurve", + Segments=vertical_curve_segments, + SelfIntersect=False, + BaseCurve=composite_curve, + EndPoint=None, + ) + else: + gradient_curve = None + + return vertical_segments, vertical_curve_segments, gradient_curve + + def create_alignment_by_pi_method( + self, + alignment_name: str, + points: Sequence[Sequence[float]], + radii: Sequence[float], + vpoints: Sequence[Sequence[float]], + lengths: Sequence[float], + alignment_description: str = None, + start_station: float = 1000.0, + include_geometry: bool = True, + ): + """ + Create an alignment using the PI layout method for both horizontal and vertical alignments. + + @param alignment_name: value for Name attribute + @param alignment_description: value for Description attribute + @param points: (X,Y) pairs denoting the location of the horizontal PIs, including start and end + @param radii: radii values to use for transition + @param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end. + @param lengths: parabolic vertical curve horizontal length values to use for transition + @param start_station: ??? NOT USED AT THIS TIME ??? + @param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic + """ + + horizontal_segments, horizontal_curve_segments, composite_curve = self._create_horizontal_alignment( + alignment_name, alignment_description, points, radii, include_geometry + ) + vertical_segments, vertical_curve_segments, gradient_curve = self._create_vertical_alignment( + composite_curve, vpoints, lengths + ) + + name_segments(prefix="H", segments=horizontal_segments) + name_segments(prefix="V", segments=vertical_segments) + + # Create the horizontal alignment (IfcAlignmentHorizontal) and nest alignment segments + horizontal_alignment = self._file.create_entity( + type="IfcAlignmentHorizontal", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=f"{alignment_name} - Horizontal", + Description=alignment_description, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + ) + + nests_horizontal_segments = self._file.create_entity( + type="IfcRelNests", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name="Nests horizontal alignment segments under horizontal alignment", + RelatingObject=horizontal_alignment, + RelatedObjects=horizontal_segments, + ) + + # Create the vertical alignment (IfcAlignmentVertical) and nest alignment segments + vertical_alignment = self._file.create_entity( + type="IfcAlignmentVertical", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=f"{alignment_name} - Vertical", + Description=alignment_description, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + ) + + nests_vertical_segments = self._file.create_entity( + type="IfcRelNests", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name="Nests vertical alignment segments under vertical alignment", + RelatingObject=vertical_alignment, + RelatedObjects=vertical_segments, + ) + + # create the alignment + placement = self._file.createIfcLocalPlacement( + PlacementRelTo=None, + RelativePlacement=self._file.createIfcAxis2Placement2D( + Location=self._file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)) + ), + ) + + alignment = self._file.create_entity( + type="IfcAlignment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=alignment_name, + Description=alignment_description, + ObjectType=None, + ObjectPlacement=placement, + Representation=None, + PredefinedType=None, + ) + + # create referent for start station + start_station_name = "Start Station ({})".format(ifcopenshell.util.stationing.station_as_string(start_station)) + start_referent = self._file.createIfcReferent( + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=start_station_name, + Description=None, + ObjectType=None, + ObjectPlacement=self._file.createIfcLinearPlacement( + RelativePlacement=self._file.createIfcAxis2PlacementLinear( + Location=self._file.createIfcPointByDistanceExpression( + DistanceAlong=self._file.createIfcLengthMeasure(0.0), + OffsetLateral=None, + OffsetVertical=None, + OffsetLongitudinal=None, + BasisCurve=composite_curve, + ), + ), + CartesianPosition=None, + ), + Representation=None, + PredefinedType="STATION", + ) + pset_stationing = ifcopenshell.api.pset.add_pset(self._file, product=start_referent, name="Pset_Stationing") + ifcopenshell.api.pset.edit_pset(self._file, pset=pset_stationing, properties={"Station": start_station}) + + # nest the horizontal, vertical and the referent under the alignment + nesting_of_alignment = self._file.create_entity( + type="IfcRelNests", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name="Nests horizontal alignment, vertical alginment, and referents under overall alignment", + RelatingObject=alignment, + RelatedObjects=(horizontal_alignment, vertical_alignment, start_referent), + ) + + # aggregate the alignment under the project + project = self._file.by_type("IfcProject")[0] + alignment_within_project = self._file.createIfcRelAggregates( + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name="Aggregates alignment under the project", + RelatingObject=project, + RelatedObjects=(alignment,), + ) + + # create geometric representation + if include_geometry: + # create the footprint representation + footprint_shape_representation = self._file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=self._axis_geom_subcontext, + RepresentationIdentifier="FootPrint", + RepresentationType="Curve2D", + Items=(composite_curve,), + ) + + # create the Curve3D representation + axis3d_shape_representation = self._file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=self._axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Curve3D", + Items=(gradient_curve,), + ) + + # create the alignment product definition + product_definition_shape = self._file.create_entity( + type="IfcProductDefinitionShape", + Name="Alignment Product Definition Shape", + Description=None, + Representations=( + footprint_shape_representation, + axis3d_shape_representation, + ), + ) + + # create representations for each segment + self._create_segment_representations(placement, horizontal_curve_segments, horizontal_segments) + self._create_segment_representations(placement, vertical_curve_segments, vertical_segments) + + # add the representation to the alignment + alignment.Representation = product_definition_shape + + return alignment + + def create_horizontal_alignment_by_pi_method( self, name: str, hpoints: Sequence[Sequence[float]], @@ -630,7 +1120,7 @@ class IfcAlignmentHelper: """ Create a new alignment with a horizontal alignment using the PI layout method """ - self._add_horizontal_alignment( + return self._add_horizontal_alignment( alignment_name=name, points=hpoints, radii=radii, @@ -647,7 +1137,17 @@ if __name__ == "__main__": import sys from matplotlib import pyplot as plt - f = ifcopenshell.open(sys.argv[1]) + f = ifcopenshell.file(schema="IFC4X3_ADD2") + project = f.create_entity(type="IfcProject", GlobalId=ifcopenshell.guid.new()) + context = f.create_entity(type="IfcGeometricRepresentationContext") + + points = [(0.0, 0.0), (100.0, 0.0), (200.0, 150.0)] + radii = [50.0] + + helper = IfcAlignmentHelper(f) + helper.create_horizontal_alignment_by_pi_method(name="MyAlignment", hpoints=points, radii=radii) + + # f = ifcopenshell.open(sys.argv[1]) print_structure(f.by_type("IfcAlignment")[0]) al_hor_rep = f.by_type("IfcCompositeCurve")[0] diff --git a/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py b/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py index 3cbab7f38d..ff0e2b11b7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py +++ b/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py @@ -39,11 +39,8 @@ def edit_attributes(file: ifcopenshell.file, product: ifcopenshell.entity_instan :param product: The product you want to edit. This may be any rooted IFC entity. - :type product: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: diff --git a/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py b/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py index d77776f04a..b9a2630442 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py +++ b/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py @@ -17,17 +17,20 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.util.unit +import numpy as np +import numpy.typing as npt from typing import Optional +from ifcopenshell.util.shape_builder import SequenceOfVectors, V, ifc_safe_vector_type def assign_connection_geometry( file: ifcopenshell.file, rel_space_boundary: ifcopenshell.entity_instance, - outer_boundary: list[tuple[float, float]], + outer_boundary: SequenceOfVectors, location: tuple[float, float, float], axis: tuple[float, float, float], ref_direction: tuple[float, float, float], - inner_boundaries: Optional[list[list[tuple[float, float]]]] = None, + inner_boundaries: Optional[SequenceOfVectors] = None, unit_scale: Optional[float] = None, ) -> None: """Create and assign a connection geometry to a space boundary relationship @@ -40,35 +43,28 @@ def assign_connection_geometry( :param rel_space_boundary: The space boundary relationship to assign the connection geometry to. - :type rel_space_boundary: ifcopenshell.entity_instance :param outer_boundary: A list of 2D points representing an open polyline. The last point will connect to the first point. Each point is represented by an interable of 2 floats. The coordinates of the points are relative to the positional matrix arguments. - :type outer_boundary: list[tuple[float, float]] :param inner_boundaries: A list of zero or more inner boundaries to use for the plane. Each boundary is represented by an open polyline, as defined by the outer_boundary argument. - :type inner_boundaries: list[list[tuple[float, float]]], optional :param location: The local origin of the connection geometry, defined as an XYZ coordinate relative to the placement of the space that is being bounded. - :type location: tuple[float, float, float] :param axis: The local X axis of the connection geometry, defined as an XYZ vector relative to the placement of the space that is being bounded. - :type axis: tuple[float, float, float] :param ref_direction: The local Z axis of the connection geometry, defined as an XYZ vector relative to the placement of the space that is being bounded. The Y vector is automatically derived using the right hand rule. - :type ref_direction: tuple[float, float, float] :param unit_scale: The unit scale as calculated by ifcopenshell.util.unit.calculate_unit_scale. If not provided, it will be automatically calculated for you. :type unit_scale: float, optional :return: None - :rtype: None Example: @@ -83,19 +79,26 @@ def assign_connection_geometry( usecase = Usecase() usecase.file = file usecase.rel_space_boundary = rel_space_boundary - usecase.outer_boundary = outer_boundary - usecase.inner_boundaries = inner_boundaries or () - usecase.location = location - usecase.axis = axis - usecase.ref_direction = ref_direction - usecase.unit_scale = unit_scale + usecase.outer_boundary = V(outer_boundary) + usecase.inner_boundaries = V(inner_boundaries or []) + usecase.location = V(location) + usecase.axis = V(axis) + usecase.ref_direction = V(ref_direction) + usecase.unit_scale = unit_scale if unit_scale is not None else ifcopenshell.util.unit.calculate_unit_scale(file) return usecase.execute() class Usecase: + file: ifcopenshell.file + rel_space_boundary: ifcopenshell.entity_instance + outer_boundary: npt.NDArray + inner_boundaries: npt.NDArray + location: npt.NDArray + axis: npt.NDArray + ref_direction: npt.NDArray + unit_scale: float + def execute(self): - if self.unit_scale is None: - self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) outer_boundary = self.create_polyline(self.outer_boundary) inner_boundaries = tuple(self.create_polyline(boundary) for boundary in self.inner_boundaries) plane = self.create_plane(self.location, self.axis, self.ref_direction) @@ -103,18 +106,23 @@ class Usecase: connection_geometry = self.file.createIfcConnectionSurfaceGeometry(curve_bounded_plane) self.rel_space_boundary.ConnectionGeometry = connection_geometry - def create_point(self, point): - return self.file.createIfcCartesianPoint(point / self.unit_scale) + def create_point(self, point: npt.NDArray) -> ifcopenshell.entity_instance: + return self.file.create_enitty("IfcCartesianPoint", ifc_safe_vector_type(point / self.unit_scale)) - def close_polyline(self, points): + def close_polyline( + self, points: tuple[ifcopenshell.entity_instance, ...] + ) -> tuple[ifcopenshell.entity_instance, ...]: return points + (points[0],) - def create_polyline(self, points): - if points[0] == points[-1]: + def create_polyline(self, points: npt.NDArray) -> ifcopenshell.entity_instance: + if np.allclose(points[0], points[-1]): points = points[0 : len(points) - 1] - return self.file.createIfcPolyline(self.close_polyline(tuple(self.create_point(point) for point in points))) + ifc_points = tuple(self.create_point(point) for point in points) + return self.file.createIfcPolyline(self.close_polyline(ifc_points)) - def create_plane(self, location, axis, ref_direction): + def create_plane( + self, location: npt.NDArray, axis: npt.NDArray, ref_direction: npt.NDArray + ) -> ifcopenshell.entity_instance: return self.file.createIfcPlane( self.file.createIfcAxis2Placement3D( self.create_point(location), diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py b/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py index af886a0014..2932a5bb93 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py @@ -20,7 +20,7 @@ import ifcopenshell import ifcopenshell.guid import ifcopenshell.util.schema import ifcopenshell.util.date -from typing import Union +from typing import Union, Any def add_classification( @@ -61,9 +61,7 @@ def add_classification( classification library. The latter approach is preferred if you are using a commonly known system such as Uniclass, as this will ensure all metadata is added correctly. - :type classification: str,ifcopenshell.entity_instance :return: The added IfcClassification element - :rtype: ifcopenshell.entity_instance Example: @@ -81,28 +79,28 @@ def add_classification( """ usecase = Usecase() usecase.file = file - usecase.settings = { - "classification": classification, - } - return usecase.execute() + return usecase.execute(classification) class Usecase: - def execute(self): - if isinstance(self.settings["classification"], str): - classification = self.file.createIfcClassification(Name=self.settings["classification"]) + file: ifcopenshell.file + + def execute(self, classification: Union[str, ifcopenshell.entity_instance]) -> ifcopenshell.entity_instance: + self.classification = classification + if isinstance(self.classification, str): + classification = self.file.create_entity("IfcClassification", Name=self.classification) self.relate_to_project(classification) return classification return self.add_from_library() - def add_from_library(self): + def add_from_library(self) -> ifcopenshell.entity_instance: edition_date = None - if self.settings["classification"].EditionDate: - edition_date = ifcopenshell.util.date.ifc2datetime(self.settings["classification"].EditionDate) - self.settings["classification"].EditionDate = None + if self.classification.EditionDate: + edition_date = ifcopenshell.util.date.ifc2datetime(self.classification.EditionDate) + self.classification.EditionDate = None migrator = ifcopenshell.util.schema.Migrator() - result = migrator.migrate(self.settings["classification"], self.file) + result = migrator.migrate(self.classification, self.file) # TODO: should auto date migration be part of the migrator? if self.file.schema == "IFC2X3" and edition_date: @@ -118,7 +116,7 @@ class Usecase: return result - def relate_to_project(self, classification): + def relate_to_project(self, classification: ifcopenshell.entity_instance) -> None: self.file.create_entity( "IfcRelAssociatesClassification", GlobalId=ifcopenshell.guid.new(), diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py index 1e0ae329bf..817d15b5cf 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py @@ -21,7 +21,7 @@ import ifcopenshell.api.owner import ifcopenshell.guid import ifcopenshell.util.element import ifcopenshell.util.schema -from typing import Optional, Union +from typing import Optional, Union, Any def add_reference( @@ -65,23 +65,18 @@ def add_reference( :param product: The list of IFC objects, properties, or resources you want to associate the classification reference to. - :type product: list[ifcopenshell.entity_instance] :param reference: The classification reference entity taken from an IFC classification library. If you supply this parameter, you will use option 2. - :type reference: ifcopenshell.entity_instance, optional :param identification: If you choose option 1 and do not specify a reference, you may manually specify an identification code. The code is typically a short identifier and may have punctuation to separate the levels of hierarchy in the classificaion (e.g. Pr_12_23_34). - :type identification: str, optional :param name: If you choose option 1 and do not specify a reference, you may manually specify a name. The name is typically human readable. - :type name: str, optional :param classification: The IfcClassification entity in your IFC model (not the library, if you are doing option 2) that the reference is part of. - :type classification: ifcopenshell.entity_instance :param is_lightweight: If you are doing option 2, choose whether or not to only add that particular reference (lighweight) or also add all of its parent references in the classification hierarchy (not @@ -91,13 +86,11 @@ def add_reference( references merely help describe the "tree" of classifications, but is generally unnecessary. Using lightweight classifications are recommended and is the default. - :type is_lightweight: bool, optional :raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements. :return: The newly added IfcClassificationReference or `None` if `products` was empty list. - :rtype: Union[ifcopenshell.entity_instance, None] Example: @@ -136,6 +129,9 @@ def add_reference( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): if not self.settings["products"]: return diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/edit_classification.py b/src/ifcopenshell-python/ifcopenshell/api/classification/edit_classification.py index d6c8ef8bc4..b2091c0eca 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/edit_classification.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/edit_classification.py @@ -28,11 +28,8 @@ def edit_classification( IfcClassification, consult the IFC documentation. :param classification: The IfcClassification entity you want to edit - :type classification: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -43,7 +40,5 @@ def edit_classification( ifcopenshell.api.classification.edit_classification(model, classification=classification, attributes={"Name": "Foo"}) """ - settings = {"classification": classification, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["classification"], name, value) + for name, value in attributes.items(): + setattr(classification, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/edit_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/edit_reference.py index 80d978e7d3..f5a85d40f9 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/edit_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/edit_reference.py @@ -28,11 +28,8 @@ def edit_reference( IfcClassificationReference, consult the IFC documentation. :param reference: The IfcClassificationReference entity you want to edit - :type reference: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -43,7 +40,5 @@ def edit_reference( ifcopenshell.api.classification.edit_reference(model, reference=reference, attributes={"Name": "Foo"}) """ - settings = {"reference": reference, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["reference"], name, value) + for name, value in attributes.items(): + setattr(reference, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_classification.py b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_classification.py index 90e6abbbe7..38382eda91 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_classification.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_classification.py @@ -28,9 +28,7 @@ def remove_classification(file: ifcopenshell.file, classification: ifcopenshell. removed from a project. :param classification: The IfcClassification entity you want to remove - :type classification: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -42,16 +40,17 @@ def remove_classification(file: ifcopenshell.file, classification: ifcopenshell. """ usecase = Usecase() usecase.file = file - usecase.settings = {"classification": classification} - return usecase.execute() + return usecase.execute(classification) class Usecase: - def execute(self): - references = self.get_references(self.settings["classification"]) + file: ifcopenshell.file + + def execute(self, classification: ifcopenshell.entity_instance) -> None: + references = self.get_references(classification) for reference in references: self.file.remove(reference) - self.file.remove(self.settings["classification"]) + self.file.remove(classification) for rel in self.file.by_type("IfcRelAssociatesClassification"): if not rel.RelatingClassification: history = rel.OwnerHistory diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py index 0bdd347bfa..55dfd11b08 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py @@ -33,15 +33,12 @@ def remove_reference( :param reference: The IfcClassificationReference entity of the relationship you want to remove. - :type reference: ifcopenshell.entity_instance :param product: The list fo object entities of the relationship you want to remove. - :type product: list[ifcopenshell.entity_instance] :raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements. :return: None - :rtype: None Example: @@ -56,20 +53,18 @@ def remove_reference( ifcopenshell.api.classification.remove_reference(model, reference=reference, products=[wall_type]) """ - settings = {"reference": reference, "products": products} - is_ifc2x3 = file.schema == "IFC2X3" - products = set(settings["products"]) - referenced = ifcopenshell.util.element.get_referenced_elements(settings["reference"]) - products -= products.difference(referenced) + products_set = set(products) + referenced = ifcopenshell.util.element.get_referenced_elements(reference) + products_set -= products_set.difference(referenced) # all products are already unassigned from a reference - if not products: + if not products_set: return rooted_products: set[ifcopenshell.entity_instance] = set() non_rooted_products: set[ifcopenshell.entity_instance] = set() - for product in settings["products"]: + for product in products: if product.is_a("IfcRoot"): rooted_products.add(product) else: @@ -86,7 +81,7 @@ def remove_reference( reference_rels = { rel for rel in reference_rels - if rel.is_a("IfcRelAssociatesClassification") and rel.RelatingClassification == settings["reference"] + if rel.is_a("IfcRelAssociatesClassification") and rel.RelatingClassification == reference } for rel in reference_rels: @@ -108,7 +103,7 @@ def remove_reference( rels = getattr(product, "HasExternalReference", []) reference_rels.update(rels) - reference_rels = {rel for rel in reference_rels if rel.RelatingReference == settings["reference"]} + reference_rels = {rel for rel in reference_rels if rel.RelatingReference == reference} for rel in reference_rels: related_objects = set(rel.RelatedResourceObjects) - non_rooted_products if related_objects: @@ -117,6 +112,6 @@ def remove_reference( file.remove(rel) # TODO: we only handle lightweight classifications here - referenced_elements = ifcopenshell.util.element.get_referenced_elements(settings["reference"]) + referenced_elements = ifcopenshell.util.element.get_referenced_elements(reference) if not referenced_elements: - file.remove(settings["reference"]) + file.remove(reference) diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py index fdcdf50f6f..63ecfeb3c0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py @@ -26,16 +26,14 @@ def add_metric_reference( Adds a chain of references to a metric. The reference path is a string of the form "attribute.attribute.attribute" Used to reference a value of an attribute of an instance through a metric objective entity. """ - settings = {"metric": metric, "reference_path": reference_path} - references_created = [] - if settings["reference_path"]: - attributes = settings["reference_path"].split(".") + if reference_path: + attributes = reference_path.split(".") for i in range(len(attributes)): if i == 0: reference = file.create_entity("IfcReference") reference.AttributeIdentifier = attributes[i] - settings["metric"].ReferencePath = reference + metric.ReferencePath = reference references_created.append(reference) else: reference = file.create_entity("IfcReference") diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py index 74bf3d9a04..f4daad0abb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py @@ -39,36 +39,29 @@ def assign_constraint( :param products: The list of products the constraint applies to. This is anything which can have properties or quantities. - :type products: list[ifcopenshell.entity_instance] :param constraint: The IfcObjective constraint - :type constraint: ifcopenshell.entity_instance :return: The new or updated IfcRelAssociatesConstraint relationship or `None` if `products` was an empty list. - :rtype: ifcopenshell.entity_instance """ usecase = Usecase() usecase.file = file - usecase.settings = { - "products": products, - "constraint": constraint, - } - return usecase.execute() + return usecase.execute(products, constraint) class Usecase: - def execute(self): - products = set(self.settings["products"]) + file: ifcopenshell.file + + def execute(self, products: list[ifcopenshell.entity_instance], constraint: ifcopenshell.entity_instance): if not products: return + products_set = set(products) - self.constraint = self.settings["constraint"] - - rels = self.get_constraint_rels() + rels = self.get_constraint_rels(constraint) related_objects = set() for rel in rels: related_objects.update(rel.RelatedObjects) - products_to_assign = products - related_objects + products_to_assign = products_set - related_objects if not products_to_assign: return rels[0] @@ -85,14 +78,14 @@ class Usecase: **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(self.file), - "RelatingConstraint": self.constraint, + "RelatingConstraint": constraint, "RelatedObjects": list(products_to_assign), } ) - def get_constraint_rels(self) -> list[ifcopenshell.entity_instance]: + def get_constraint_rels(self, constraint: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: rels = [] - for rel in self.file.get_inverse(self.constraint): + for rel in self.file.get_inverse(constraint): if rel.is_a("IfcRelAssociatesConstraint"): rels.append(rel) return rels diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_metric.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_metric.py index 89a0cbef7d..3967846759 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_metric.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_metric.py @@ -26,11 +26,8 @@ def edit_metric(file: ifcopenshell.file, metric: ifcopenshell.entity_instance, a IfcMetric, consult the IFC documentation. :param metric: The IfcMetric you want to edit. - :type metric: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,7 +39,5 @@ def edit_metric(file: ifcopenshell.file, metric: ifcopenshell.entity_instance, a ifcopenshell.api.constraint.edit_metric(model, metric=metric, attributes={"ConstraintGrade": "HARD"}) """ - settings = {"metric": metric, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["metric"], name, value) + for name, value in attributes.items(): + setattr(metric, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_objective.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_objective.py index ce4e76ba9e..3cd75356a7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_objective.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_objective.py @@ -28,11 +28,8 @@ def edit_objective( IfcObjective, consult the IFC documentation. :param objective: The IfcObjective you want to edit. - :type objective: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,7 +39,5 @@ def edit_objective( ifcopenshell.api.constraint.edit_objective(model, objective=objective, attributes={"ConstraintGrade": "HARD"}) """ - settings = {"objective": objective, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["objective"], name, value) + for name, value in attributes.items(): + setattr(objective, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py index f700a2c150..76668f714d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.util.element def remove_metric(file: ifcopenshell.file, metric: ifcopenshell.entity_instance) -> None: @@ -41,17 +42,18 @@ def remove_metric(file: ifcopenshell.file, metric: ifcopenshell.entity_instance) """ usecase = Usecase() usecase.file = file - usecase.settings = {"metric": metric} - return usecase.execute() + return usecase.execute(metric) class Usecase: - def execute(self): - if self.settings["metric"].ReferencePath: - reference = self.settings["metric"].ReferencePath + file: ifcopenshell.file + + def execute(self, metric: ifcopenshell.entity_instance) -> None: + if metric.ReferencePath: + reference = metric.ReferencePath self.delete_reference(reference) - self.file.remove(self.settings["metric"]) + self.file.remove(metric) for rel in self.file.by_type("IfcRelAssociatesConstraint"): if not rel.RelatingConstraint: history = rel.OwnerHistory @@ -62,7 +64,7 @@ class Usecase: if not resource_rel.RelatingConstraint: self.file.remove(resource_rel) - def delete_reference(self, reference): + def delete_reference(self, reference: ifcopenshell.entity_instance) -> None: if reference.InnerReference: self.delete_reference(reference.InnerReference) self.file.remove(reference) diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py index cff9716809..d9e84cc826 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py @@ -32,41 +32,35 @@ def unassign_constraint( other products. :param products: The list of products the constraint applies to. - :type products: list[ifcopenshell.entity_instance] :param constraint: The IfcObjective constraint - :type constraint: ifcopenshell.entity_instance :return: None - :rtype: None """ usecase = Usecase() usecase.file = file - usecase.settings = { - "products": products, - "constraint": constraint, - } - return usecase.execute() + return usecase.execute(products, constraint) class Usecase: - def execute(self): - products = set(self.settings["products"]) - if not products: - return + file: ifcopenshell.file - self.constraint = self.settings["constraint"] - rels = self.get_constraint_rels() + def execute(self, products_: list[ifcopenshell.entity_instance], constraint: ifcopenshell.entity_instance): + if not products_: + return + products_set = set(products_) + + rels = self.get_constraint_rels(constraint) related_objects = set() for rel in rels: related_objects.update(rel.RelatedObjects) - if not related_objects.intersection(products): + if not related_objects.intersection(products_set): return for rel in rels: related_objects = set(rel.RelatedObjects) - if not related_objects.intersection(products): + if not related_objects.intersection(products_set): continue - related_objects -= products + related_objects -= products_set if related_objects: rel.RelatedObjects = list(related_objects) ifcopenshell.api.owner.update_owner_history(self.file, **{"element": rel}) @@ -77,9 +71,9 @@ class Usecase: if history: ifcopenshell.util.element.remove_deep2(self.file, history) - def get_constraint_rels(self) -> list[ifcopenshell.entity_instance]: + def get_constraint_rels(self, cosntraint: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: rels = [] - for rel in self.file.get_inverse(self.constraint): + for rel in self.file.get_inverse(cosntraint): if rel.is_a("IfcRelAssociatesConstraint"): rels.append(rel) return rels diff --git a/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py b/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py index ba52e83a9e..1a7d567c58 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py +++ b/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py @@ -27,11 +27,8 @@ def edit_context(file: ifcopenshell.file, context: ifcopenshell.entity_instance, IfcGeometricRepresentationContext, consult the IFC documentation. :param context: The IfcGeometricRepresentationContext entity you want to edit - :type context: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -47,7 +44,5 @@ def edit_context(file: ifcopenshell.file, context: ifcopenshell.entity_instance, ifcopenshell.api.context.edit_context(model, context=body, attributes={"ContextIdentifier": "Body"}) """ - settings = {"context": context, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["context"], name, value) + for name, value in attributes.items(): + setattr(context, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py index ac6b563960..fcf298ff7c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py @@ -25,8 +25,6 @@ from typing import Union def copy_cost_item( file: ifcopenshell.file, cost_item: ifcopenshell.entity_instance ) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]: - # TODO: currently it never returns list of duplicated cost items - # though it is stated in the docs """Copies all cost items and related relationships The following relationships are also duplicated: @@ -36,9 +34,7 @@ def copy_cost_item( * The copy will have duplicated nested cost items :param cost_item: The cost item to be duplicated - :type cost_item: ifcopenshell.entity_instance :return: The duplicated cost item or the list of duplicated cost items if the latter has children - :rtype: ifcopenshell.entity_instance or list[ifcopenshell.entity_instance] Example: .. code:: python @@ -53,22 +49,29 @@ def copy_cost_item( """ usecase = Usecase() usecase.file = file - usecase.settings = {"cost_item": cost_item} - return usecase.execute() + return usecase.execute(cost_item) class Usecase: - def execute(self): - self.new_cost_items = [] - return self.duplicate_cost_item(self.settings["cost_item"]) + file: ifcopenshell.file + new_cost_items: list[ifcopenshell.entity_instance] - def duplicate_cost_item(self, cost_item): + def execute( + self, cost_item: ifcopenshell.entity_instance + ) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]: + self.new_cost_items = [] + self.duplicate_cost_item(cost_item) + return self.new_cost_items[0] if len(self.new_cost_items) == 1 else self.new_cost_items + + def duplicate_cost_item(self, cost_item: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: new_cost_item = ifcopenshell.util.element.copy_deep(self.file, cost_item) self.new_cost_items.append(new_cost_item) self.copy_indirect_attributes(cost_item, new_cost_item) return new_cost_item - def copy_indirect_attributes(self, from_element, to_element): + def copy_indirect_attributes( + self, from_element: ifcopenshell.entity_instance, to_element: ifcopenshell.entity_instance + ) -> None: for inverse in self.file.get_inverse(from_element): if inverse.is_a("IfcRelDefinesByProperties"): inverse = ifcopenshell.util.element.copy(self.file, inverse) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item_values.py b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item_values.py index 6903f41bfe..e62b105c50 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item_values.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item_values.py @@ -30,11 +30,8 @@ def copy_cost_item_values( parametrically linked, so if one value changes, the other will not. :param source: The IfcCostItem to copy cost values from - :type source: ifcopenshell.entity_instance :param destination: The IfcCostItem to copy cost values from - :type destination: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -53,11 +50,9 @@ def copy_cost_item_values( # Let's copy the value from one item to another ifcopenshell.api.cost.copy_cost_item_values(model, source=item1, destination=item2) """ - settings = {"source": source, "destination": destination} - - for cost_value in settings["destination"].CostValues or []: + for cost_value in destination.CostValues or []: ifcopenshell.api.cost.remove_cost_item_value(file, cost_value=cost_value) copied_cost_values = [] - for cost_value in settings["source"].CostValues or []: + for cost_value in source.CostValues or []: copied_cost_values.append(ifcopenshell.util.element.copy_deep(file, cost_value)) - settings["destination"].CostValues = copied_cost_values + destination.CostValues = copied_cost_values diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py index 2417c9ac65..813e306bc8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py @@ -28,11 +28,8 @@ def edit_cost_item( IfcCostItem, consult the IFC documentation. :param cost_item: The IfcCostItem entity you want to edit - :type cost_item: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,7 +39,5 @@ def edit_cost_item( item = ifcopenshell.api.cost.add_cost_item(model, cost_schedule=schedule) ifcopenshell.api.cost.edit_cost_item(model, cost_item=item, attributes={"Name": "Foo"}) """ - settings = {"cost_item": cost_item, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["cost_item"], name, value) + for name, value in attributes.items(): + setattr(cost_item, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item_quantity.py index cb265318ac..697d028a37 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item_quantity.py @@ -28,11 +28,8 @@ def edit_cost_item_quantity( IfcPhysicalQuantity, consult the IFC documentation. :param physical_quantity: The IfcPhysicalQuantity entity you want to edit - :type physical_quantity: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -50,7 +47,5 @@ def edit_cost_item_quantity( ifcopenshell.api.cost.edit_cost_item_quantity(model, physical_quantity=quantity, "attributes": {"VolumeValue": 3.0}) """ - settings = {"physical_quantity": physical_quantity, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["physical_quantity"], name, value) + for name, value in attributes.items(): + setattr(physical_quantity, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py index 1c6f42b123..d55e93330e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py @@ -28,11 +28,8 @@ def edit_cost_schedule( IfcCostSchedule, consult the IFC documentation. :param cost_schedule: The IfcCostSchedule entity you want to edit - :type cost_schedule: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,8 +39,5 @@ def edit_cost_schedule( ifcopenshell.api.cost.edit_cost_schedule(model, cost_schedule=schedule, attributes={"Name": "Foo"}) """ - - settings = {"cost_schedule": cost_schedule, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["cost_schedule"], name, value) + for name, value in attributes.items(): + setattr(cost_schedule, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py index f3e4e01acd..ebc09ed34c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py @@ -31,11 +31,8 @@ def edit_cost_value( IfcCostValue, consult the IFC documentation. :param cost_value: The IfcCostValue entity you want to edit - :type cost_value: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -49,20 +46,18 @@ def edit_cost_value( ifcopenshell.api.cost.edit_cost_value(model, cost_value=value, attributes={"AppliedValue": 42.0}) """ - settings = {"cost_value": cost_value, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): + for name, value in attributes.items(): if name == "AppliedValue" and value is not None: # TODO: support all applied value select types value = file.createIfcMonetaryMeasure(value) elif name == "UnitBasis": - old_unit_basis = settings["cost_value"].UnitBasis + old_unit_basis = cost_value.UnitBasis if value: value_component = file.create_entity( ifcopenshell.util.unit.get_unit_measure_class(value["UnitComponent"].UnitType), value["ValueComponent"], ) value = file.create_entity("IfcMeasureWithUnit", value_component, value["UnitComponent"]) - if old_unit_basis and len(file.get_inverse(old_unit_basis)) == 0: + if old_unit_basis and file.get_total_inverses(old_unit_basis) == 0: ifcopenshell.util.element.remove_deep(file, old_unit_basis) - setattr(settings["cost_value"], name, value) + setattr(cost_value, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py index c9ef89455e..80f330ce0b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py @@ -28,11 +28,8 @@ def remove_cost_item_quantity( removed. :param cost_item: The IfcCostItem that the quantity is assigned to - :type cost_item: ifcopenshell.entity_instance :param physical_quantity: The IfcPhysicalQuantity to remove - :type physical_quantity: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -46,11 +43,9 @@ def remove_cost_item_quantity( ifcopenshell.api.cost.remove_cost_item(model, cost_item=item, physical_quantity=quantity) """ - settings = {"cost_item": cost_item, "physical_quantity": physical_quantity} - - if len(file.get_inverse(settings["physical_quantity"])) == 1: - file.remove(settings["physical_quantity"]) + if file.get_total_inverses(physical_quantity) == 1: + file.remove(physical_quantity) return - quantities = list(settings["cost_item"].CostQuantities or []) - quantities.remove(settings["physical_quantity"]) - settings["cost_item"].CostQuantities = quantities + quantities = list(cost_item.CostQuantities or []) + quantities.remove(physical_quantity) + cost_item.CostQuantities = quantities diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py index 2c7745d7f9..b578cd7134 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py @@ -28,11 +28,8 @@ def remove_cost_value( :param parent: The IfcCostItem, IfcConstructionResource, or IfcCostValue that the IfcCostValue is assigned to. - :type parent: ifcopenshell.entity_instance :param cost_value: The IfcCostValue that you want to remove - :type parent: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -48,20 +45,18 @@ def remove_cost_value( ifcopenshell.api.cost.remove_cost_value(model, parent=item, cost_value=value) """ - settings = {"parent": parent, "cost_value": cost_value} - - if len(file.get_inverse(settings["cost_value"])) == 1: - file.remove(settings["cost_value"]) + if file.get_total_inverses(cost_value) == 1: + file.remove(cost_value) # TODO deep purge - elif settings["parent"].is_a("IfcCostItem"): - values = list(settings["parent"].CostValues) - values.remove(settings["cost_value"]) - settings["parent"].CostValues = values if values else None - elif settings["parent"].is_a("IfcConstructionResource"): - values = list(settings["parent"].BaseCosts) - values.remove(settings["cost_value"]) - settings["parent"].BaseCosts = values if values else None - elif settings["parent"].is_a("IfcCostValue"): - components = list(settings["parent"].Components) - components.remove(settings["cost_value"]) - settings["parent"].Components = components if components else None + elif parent.is_a("IfcCostItem"): + values = list(parent.CostValues) + values.remove(cost_value) + parent.CostValues = values if values else None + elif parent.is_a("IfcConstructionResource"): + values = list(parent.BaseCosts) + values.remove(cost_value) + parent.BaseCosts = values if values else None + elif parent.is_a("IfcCostValue"): + components = list(parent.Components) + components.remove(cost_value) + parent.Components = components if components else None diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py index bb815647fd..de180d65ef 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py @@ -30,12 +30,9 @@ def unassign_cost_item_quantity( have any impact on the cost item. :param cost_item: The IfcCostItem to remove quantities from - :type cost_item: ifcopenshell.entity_instance :param products: A list of IfcProducts that may have parametrically connected quantities to the cost item - :type products: list[ifcopenshell.entity_instance] :return: None - :rtype: None Example: @@ -69,38 +66,39 @@ def unassign_cost_item_quantity( """ usecase = Usecase() usecase.file = file - usecase.settings = {"cost_item": cost_item, "products": products or []} - return usecase.execute() + return usecase.execute(cost_item, products or []) class Usecase: - def execute(self): - self.quantities = set(self.settings["cost_item"].CostQuantities or []) - for quantity in self.settings["cost_item"].CostQuantities or []: + file: ifcopenshell.file + + def execute(self, cost_item: ifcopenshell.entity_instance, products: list[ifcopenshell.entity_instance]) -> None: + quantities = set(cost_item.CostQuantities or []) + for quantity in cost_item.CostQuantities or []: for inverse in self.file.get_inverse(quantity): if not inverse.is_a("IfcElementQuantity"): continue for rel in inverse.DefinesOccurrence or []: for related_object in rel.RelatedObjects: - if related_object in self.settings["products"]: - self.quantities.remove(quantity) - self.settings["cost_item"].CostQuantities = list(self.quantities) - for product in self.settings["products"]: + if related_object in products: + quantities.remove(quantity) + cost_item.CostQuantities = list(quantities) + for product in products: ifcopenshell.api.control.unassign_control( self.file, related_object=product, - relating_control=self.settings["cost_item"], + relating_control=cost_item, ) - self.update_cost_item_count() + self.update_cost_item_count(cost_item) - def update_cost_item_count(self): + def update_cost_item_count(self, cost_item: ifcopenshell.entity_instance) -> None: # This is a bold assumption # https://forums.buildingsmart.org/t/how-does-a-cost-item-know-that-it-is-counting-a-controlled-product/3564 - if len(self.settings["cost_item"].CostQuantities) == 1: - quantity = self.settings["cost_item"].CostQuantities[0] + if len(cost_item.CostQuantities) == 1: + quantity = cost_item.CostQuantities[0] if quantity.is_a("IfcQuantityCount"): count = 0 - for rel in self.settings["cost_item"].Controls: + for rel in cost_item.Controls: count += len(rel.RelatedObjects) if count: quantity[3] = count diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py b/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py index 5627cc1c84..a86a178e86 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py @@ -41,15 +41,12 @@ def assign_document( :param product: The list of objects to associate the document to. This could be almost any sensible object in IFC. - :type product: list[ifcopenshell.entity_instance] :param document: The IfcDocumentReference to associate to, or alternatively an IfcDocumentInformation, though this is not recommended. - :type document: ifcopenshell.entity_instance :return: The IfcRelAssociatesDocument relationship or `None` if `products` was an empty list or all products were already assigned to the `document`. - :rtype: ifcopenshell.entity_instance Example: @@ -65,43 +62,41 @@ def assign_document( # Let's imagine storey represents an IfcBuildingStorey for the ground floor ifcopenshell.api.document.assign_document(model, products=[storey], document=reference) """ - settings = { - "products": products, - "document": document, - } # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference? # NOTE: reuses code from `library.assign_reference` - referenced_elements = ifcopenshell.util.element.get_referenced_elements(settings["document"]) - products: set[ifcopenshell.entity_instance] = set(settings["products"]) - products = products - referenced_elements + referenced_elements = ifcopenshell.util.element.get_referenced_elements(document) + products_set: set[ifcopenshell.entity_instance] = set(products) + products_set = products_set - referenced_elements - if not products: + if not products_set: return if file.schema == "IFC2X3": rel = next( - (r for r in file.by_type("IfcRelAssociatesDocument") if r.RelatingDocument == settings["document"]), + (r for r in file.by_type("IfcRelAssociatesDocument") if r.RelatingDocument == document), None, ) else: - ifc_class = settings["document"].is_a() + ifc_class = document.is_a() if ifc_class == "IfcDocumentReference": - rel = next(iter(settings["document"].DocumentRefForObjects), None) + rel = next(iter(document.DocumentRefForObjects), None) elif ifc_class == "IfcDocumentInformation": - rel = next(iter(settings["document"].DocumentInfoForObjects), None) + rel = next(iter(document.DocumentInfoForObjects), None) + else: + assert False, f"Unexpected document type: {ifc_class}" if not rel: return file.create_entity( "IfcRelAssociatesDocument", GlobalId=ifcopenshell.guid.new(), OwnerHistory=ifcopenshell.api.owner.create_owner_history(file), - RelatedObjects=list(products), - RelatingDocument=settings["document"], + RelatedObjects=list(products_set), + RelatingDocument=document, ) - related_objects = set(rel.RelatedObjects) | products + related_objects = set(rel.RelatedObjects) | products_set rel.RelatedObjects = list(related_objects) ifcopenshell.api.owner.update_owner_history(file, element=rel) return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py b/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py index 09d697f7db..56c1d40df4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py @@ -30,11 +30,8 @@ def edit_information( IfcDocumentInformation, consult the IFC documentation. :param reference: The IfcDocumentInformation entity you want to edit - :type reference: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -46,7 +43,5 @@ def edit_information( attributes={"Identification": "A-GA-6100", "Name": "Overall Plan", "Location": "A-GA-6100 - Overall Plan.pdf"}) """ - settings = {"information": information, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["information"], name, value) + for name, value in attributes.items(): + setattr(information, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py b/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py index 5a00827305..51ad195bf3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py @@ -30,11 +30,8 @@ def edit_reference( IfcDocumentReference, consult the IFC documentation. :param reference: The IfcDocumentReference entity you want to edit - :type reference: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -49,7 +46,5 @@ def edit_reference( ifcopenshell.api.document.edit_reference(model, reference=reference, attributes={"Identification": "2.1.15"}) """ - settings = {"reference": reference, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["reference"], name, value) + for name, value in attributes.items(): + setattr(reference, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/drawing/edit_text_literal.py b/src/ifcopenshell-python/ifcopenshell/api/drawing/edit_text_literal.py index 1f7356e989..c083960e59 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/drawing/edit_text_literal.py +++ b/src/ifcopenshell-python/ifcopenshell/api/drawing/edit_text_literal.py @@ -28,11 +28,8 @@ def edit_text_literal( IfcTextLiteral, consult the IFC documentation. :param reference: The IfcTextLiteral entity you want to edit - :type reference: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,7 +39,5 @@ def edit_text_literal( ifcopenshell.api.drawing.edit_text_literal(model, text_literal=text, attributes={"Literal": "MY ANNOTATION"}) """ - settings = {"text_literal": text_literal, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["text_literal"], name, value) + for name, value in attributes.items(): + setattr(text_literal, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py index 1883374b97..174c475f92 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py @@ -17,7 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.util.unit -from typing import Union +from typing import Union, Any COORD = Union[tuple[float, float], tuple[float, float, float]] @@ -82,6 +82,9 @@ def add_axis_representation( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) is_2d = len(self.settings["axis"][0]) == 2 diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py index 6b48845e4e..9d5fe9f20b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py @@ -26,14 +26,9 @@ def add_footprint_representation( # A list of IFC curves to include in the curve set curves: list[ifcopenshell.entity_instance], ) -> ifcopenshell.entity_instance: - settings = { - "context": context, - "curves": curves, - } - return file.createIfcShapeRepresentation( - settings["context"], - settings["context"].ContextIdentifier, + context, + context.ContextIdentifier, "GeometricCurveSet", - [file.createIfcGeometricCurveSet(settings["curves"])], + [file.createIfcGeometricCurveSet(curves)], ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py index efe6aa5c89..940315621a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py @@ -17,7 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.util.unit -from typing import Optional +from typing import Optional, Any COORD_3D = tuple[float, float, float] @@ -60,6 +60,9 @@ def add_mesh_representation( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): if self.settings["unit_scale"] is None: self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py index 17cdec58c8..77bb515996 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py @@ -52,6 +52,9 @@ def add_profile_representation( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) self.settings["clippings"] = [Clipping.parse(c) for c in self.settings["clippings"]] diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py index 13d06e59c8..8244c3e434 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py @@ -111,10 +111,12 @@ class Usecase: settings: dict[str, Any] ifc_vertices: list[ifcopenshell.entity_instance] coordinate_offset: Union[npt.NDArray[np.float64], None] + geometry: Union[bpy.types.Mesh, bpy.types.Curve] def execute(self) -> Union[ifcopenshell.entity_instance, None]: self.is_manifold = None self.coordinate_offset = self.settings["coordinate_offset"] + self.geometry = self.settings["geometry"] if ( isinstance(self.settings["geometry"], bpy.types.Mesh) and self.settings["geometry"] == self.settings["blender_object"].data @@ -636,9 +638,7 @@ class Usecase: dim = (lambda v: v.xy) if is_2d else (lambda v: v.xyz) results = [] for spline in curve_object_data.splines: - points = spline.bezier_points[:] + spline.points[:] - if spline.use_cyclic_u: - points.append(points[0]) + points = self.get_spline_points(spline) ifc_points = [self.create_cartesian_point(*dim(point.co)) for point in points] results.append(self.file.createIfcPolyline(ifc_points)) return results @@ -981,12 +981,12 @@ class Usecase: ) def create_structural_reference_representation(self) -> ifcopenshell.entity_instance: - if len(self.settings["geometry"].vertices) == 1: + if isinstance(self.geometry, bpy.types.Mesh) and len(self.geometry.vertices) == 1: return self.file.createIfcTopologyRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "Vertex", - [self.create_vertex_point(self.settings["geometry"].vertices[0].co)], + [self.create_vertex_point(self.geometry.vertices[0].co)], ) return self.file.createIfcTopologyRepresentation( self.settings["context"], @@ -998,11 +998,22 @@ class Usecase: def create_vertex_point(self, point: Vector) -> ifcopenshell.entity_instance: return self.file.createIfcVertexPoint(self.create_cartesian_point(point.x, point.y, point.z)) + def get_spline_points( + self, spline: bpy.types.Spline + ) -> list[Union[bpy.types.SplinePoint, bpy.types.BezierSplinePoint]]: + points = spline.bezier_points[:] + spline.points[:] + if spline.use_cyclic_u: + points.append(points[0]) + return points + def create_edge(self) -> Union[ifcopenshell.entity_instance, None]: - if hasattr(self.settings["geometry"], "splines"): - points = self.get_spline_points(self.settings["geometry"].splines[0]) + geometry = self.geometry + if isinstance(geometry, bpy.types.Curve): + points = self.get_spline_points(geometry.splines[0]) + elif isinstance(geometry, bpy.types.Mesh): + points = geometry.vertices else: - points = self.settings["geometry"].vertices + assert False, type(geometry) if not points: return return self.file.createIfcEdge(self.create_vertex_point(points[0].co), self.create_vertex_point(points[1].co)) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py index 22fb99d583..fdc457367b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py @@ -426,6 +426,9 @@ def add_window_representation( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): builder = ShapeBuilder(self.file) np_X, np_Y, np_Z = 0, 1, 2 diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py index 571c5212ba..81fa276a2c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py @@ -31,53 +31,33 @@ def connect_path( related_connection: str = "NOTDEFINED", description: Optional[str] = None, ) -> ifcopenshell.entity_instance: - settings = { - "relating_element": relating_element, - "related_element": related_element, - "relating_connection": relating_connection, - "related_connection": related_connection, - "description": description, - } - - incompatible_connections = [] - for rel in settings["relating_element"].ConnectedTo: + incompatible_connections: list[ifcopenshell.entity_instance] = [] + for rel in relating_element.ConnectedTo: if not rel.is_a("IfcRelConnectsPathElements"): continue - if rel.RelatedElement == settings["related_element"]: + if rel.RelatedElement == related_element: incompatible_connections.append(rel) - elif ( - rel.RelatingConnectionType in ["ATSTART", "ATEND"] - and rel.RelatingConnectionType == settings["relating_connection"] - ): + elif rel.RelatingConnectionType in ["ATSTART", "ATEND"] and rel.RelatingConnectionType == relating_connection: incompatible_connections.append(rel) - for rel in settings["relating_element"].ConnectedFrom: + for rel in relating_element.ConnectedFrom: if not rel.is_a("IfcRelConnectsPathElements"): continue - if ( - rel.RelatedConnectionType in ["ATSTART", "ATEND"] - and rel.RelatedConnectionType == settings["relating_connection"] - ): + if rel.RelatedConnectionType in ["ATSTART", "ATEND"] and rel.RelatedConnectionType == relating_connection: incompatible_connections.append(rel) - for rel in settings["related_element"].ConnectedFrom: + for rel in related_element.ConnectedFrom: if not rel.is_a("IfcRelConnectsPathElements"): continue - if ( - rel.RelatedConnectionType in ["ATSTART", "ATEND"] - and rel.RelatedConnectionType == settings["related_connection"] - ): + if rel.RelatedConnectionType in ["ATSTART", "ATEND"] and rel.RelatedConnectionType == related_connection: incompatible_connections.append(rel) - for rel in settings["related_element"].ConnectedTo: + for rel in related_element.ConnectedTo: if not rel.is_a("IfcRelConnectsPathElements"): continue - if rel.RelatedElement == settings["relating_element"]: + if rel.RelatedElement == relating_element: incompatible_connections.append(rel) - elif ( - rel.RelatingConnectionType in ["ATSTART", "ATEND"] - and rel.RelatingConnectionType == settings["related_connection"] - ): + elif rel.RelatingConnectionType in ["ATSTART", "ATEND"] and rel.RelatingConnectionType == related_connection: incompatible_connections.append(rel) if incompatible_connections: @@ -87,14 +67,15 @@ def connect_path( if history: ifcopenshell.util.element.remove_deep2(file, history) - return file.createIfcRelConnectsPathElements( + return file.create_entity( + "IfcRelConnectsPathElements", ifcopenshell.guid.new(), OwnerHistory=ifcopenshell.api.owner.create_owner_history(file), - Description=settings["description"], - RelatingElement=settings["relating_element"], - RelatedElement=settings["related_element"], - RelatingConnectionType=settings["relating_connection"], - RelatedConnectionType=settings["related_connection"], + Description=description, + RelatingElement=relating_element, + RelatedElement=related_element, + RelatingConnectionType=relating_connection, + RelatedConnectionType=related_connection, RelatingPriorities=[], RelatedPriorities=[], ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py index e8cd98aefc..0618b407ec 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py @@ -37,8 +37,10 @@ def remove_representation( :param should_keep_named_profiles: If true, named profile defs will not be removed as they are assumed to be significant. """ + is_ifc2x3 = file.schema == "IFC2X3" styled_items = set() - presentation_layer_assignments = set() + presentation_layer_assignments_items: set[ifcopenshell.entity_instance] = set() + presentation_layer_assignments_reps: set[ifcopenshell.entity_instance] = set() textures = set() colours = set() named_profiles = set() @@ -46,16 +48,14 @@ def remove_representation( if subelement.is_a("IfcRepresentationItem"): [styled_items.add(s) for s in subelement.StyledByItem or []] # IFC2X3 is using LayerAssignments - for s in ( - subelement.LayerAssignment if hasattr(subelement, "LayerAssignment") else subelement.LayerAssignments - ): - presentation_layer_assignments.add(s) + for s in subelement.LayerAssignment if not is_ifc2x3 else subelement.LayerAssignments: + presentation_layer_assignments_items.add(s) # IfcTessellatedFaceSet inverses [textures.add(t) for t in getattr(subelement, "HasTextures", []) or []] [colours.add(t) for t in getattr(subelement, "HasColours", []) or []] elif subelement.is_a("IfcRepresentation"): for layer in subelement.LayerAssignments: - presentation_layer_assignments.add(layer) + presentation_layer_assignments_reps.add(layer) elif subelement.is_a("IfcProfileDef") and subelement.ProfileName: named_profiles.add(subelement) @@ -63,11 +63,16 @@ def remove_representation( if should_keep_named_profiles: do_not_delete += named_profiles + # Order matters - layer assignments may reference representation directly. + also_consider = list(presentation_layer_assignments_reps) + also_consider.extend(presentation_layer_assignments_items - presentation_layer_assignments_reps) + also_consider.extend(styled_items) + also_consider.extend(textures) ifcopenshell.util.element.remove_deep2( file, representation, - also_consider=list(styled_items | presentation_layer_assignments | colours), - do_not_delete=do_not_delete, + also_consider=also_consider, + do_not_delete=set(do_not_delete), ) for texture in textures: @@ -77,8 +82,10 @@ def remove_representation( to_delete = file.to_delete or set() for element in styled_items: - if not element.Item or element.Item in to_delete: + item = element.Item + if not item or item in to_delete: file.remove(element) + presentation_layer_assignments = presentation_layer_assignments_reps | presentation_layer_assignments_items for element in presentation_layer_assignments: if all(item in to_delete for item in element.AssignedItems): file.remove(element) diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py index 63d46b840b..3cbcae5d60 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py @@ -43,7 +43,7 @@ def remove_georeferencing(file: ifcopenshell.file) -> None: ifcopenshell.api.pset.remove_pset(file, project, file.by_id(pset["id"])) return for projected_crs in file.by_type("IfcProjectedCRS"): - if (unit := projected_crs.MapUnit) and len(file.get_inverse(unit)) == 1: + if (unit := projected_crs.MapUnit) and file.get_total_inverses(unit) == 1: projected_crs.MapUnit = None ifcopenshell.util.element.remove_deep2(file, unit) file.remove(projected_crs) diff --git a/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py b/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py index 8239ab9614..652fc89f56 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py +++ b/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py @@ -23,9 +23,7 @@ def remove_grid_axis(file: ifcopenshell.file, axis: ifcopenshell.entity_instance """Removes a grid axis from a grid :param axis: The IfcGridAxis you want to remove. - :type axis: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -44,7 +42,7 @@ def remove_grid_axis(file: ifcopenshell.file, axis: ifcopenshell.entity_instance ifcopenshell.api.grid.remove_grid_axis(model, axis=axis_2) """ axis_curve = axis.AxisCurve - if len(file.get_inverse(axis_curve)) == 1: + if file.get_total_inverses(axis_curve) == 1: ifcopenshell.util.element.remove_deep(file, axis_curve) file.remove(axis_curve) file.remove(axis) diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py index 64a4f571dc..4972058fb4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py @@ -46,29 +46,25 @@ def assign_group( ifcopenshell.api.group.assign_group(model, products=model.by_type("IfcFurniture"), group=group) """ - settings = { - "products": products, - "group": group, - } - - if not settings["products"]: + if not products: return - if not settings["group"].IsGroupedBy: + is_grouped_by: tuple[ifcopenshell.entity_instance, ...] + if not (is_grouped_by := group.IsGroupedBy): return file.create_entity( "IfcRelAssignsToGroup", **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatedObjects": settings["products"], - "RelatingGroup": settings["group"], - } + "RelatedObjects": products, + "RelatingGroup": group, + }, ) - rel = settings["group"].IsGroupedBy[0] + rel = is_grouped_by[0] related_objects = set(rel.RelatedObjects) or set() - products = set(settings["products"]) - if products.issubset(related_objects): + products_set = set(products) + if products_set.issubset(related_objects): return rel - rel.RelatedObjects = list(related_objects | products) + rel.RelatedObjects = list(related_objects | products_set) ifcopenshell.api.owner.update_owner_history(file, **{"element": rel}) return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py index a404ef0f88..5a7a2b2018 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py @@ -26,11 +26,8 @@ def edit_group(file: ifcopenshell.file, group: ifcopenshell.entity_instance, att IfcGroup, consult the IFC documentation. :param group: The IfcGroup entity you want to edit - :type group: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -40,7 +37,5 @@ def edit_group(file: ifcopenshell.file, group: ifcopenshell.entity_instance, att ifcopenshell.api.group.edit_group(model, group=group, attributes={"Description": "All furniture and joinery included in the unit"}) """ - settings = {"group": group, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["group"], name, value) + for name, value in attributes.items(): + setattr(group, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py index ee31336eec..cac87106aa 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py @@ -46,17 +46,12 @@ def unassign_group( bad_furniture = furniture[0] ifcopenshell.api.group.unassign_group(model, products=[bad_furniture], group=group) """ - settings = { - "products": products, - "group": group, - } - - if not settings["group"].IsGroupedBy: + if not group.IsGroupedBy: return - rel = settings["group"].IsGroupedBy[0] + rel = group.IsGroupedBy[0] related_objects = set(rel.RelatedObjects) or set() - products = set(settings["products"]) - related_objects -= products + products_set = set(products) + related_objects -= products_set if related_objects: rel.RelatedObjects = list(related_objects) ifcopenshell.api.owner.update_owner_history(file, **{"element": rel}) diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py b/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py index 61b3ec7657..468c193ff7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py @@ -45,24 +45,19 @@ def update_group_products( ifcopenshell.api.group.update_group_products(model, products=model.by_type("IfcFurniture"), group=group) """ - settings = { - "group": group, - "products": products, - } - - if not settings["group"].IsGroupedBy: + if not group.IsGroupedBy: return file.create_entity( "IfcRelAssignsToGroup", **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatedObjects": settings["products"], - "RelatingGroup": settings["group"], + "RelatedObjects": products, + "RelatingGroup": group, } ) else: - rels = settings["group"].IsGroupedBy - objects = set(settings["products"]) + rels = group.IsGroupedBy + objects = set(products) for rel in rels: objects.update([g for g in rel.RelatedObjects if g.is_a("IfcGroup")]) to_purge = rels[1:] diff --git a/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py b/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py index fa57e526cc..57117b6625 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py @@ -62,15 +62,10 @@ def assign_layer( # only one item) to the layer. ifcopenshell.api.layer.assign_layer(model, items=[representation.Items[0]], layer=layer) """ - settings = { - "items": items, - "layer": layer, - } - # support AssignedItems == None since layer might just got created - layer = settings["layer"] + assigned_items: set[ifcopenshell.entity_instance] assigned_items = set(layer.AssignedItems or []) - items = set(settings["items"]) - if items.issubset(assigned_items): + items_set = set(items) + if items_set.issubset(assigned_items): return - layer.AssignedItems = list(assigned_items | items) + layer.AssignedItems = list(assigned_items | items_set) diff --git a/src/ifcopenshell-python/ifcopenshell/api/layer/edit_layer.py b/src/ifcopenshell-python/ifcopenshell/api/layer/edit_layer.py index 6f55f5da1d..7203fabb0c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/layer/edit_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/layer/edit_layer.py @@ -37,7 +37,5 @@ def edit_layer(file: ifcopenshell.file, layer: ifcopenshell.entity_instance, att ifcopenshell.api.layer.edit_layer(model, layer=layer, attributes={"Description": "All walls, based on the AIA standard."}) """ - settings = {"layer": layer, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["layer"], name, value) + for name, value in attributes.items(): + setattr(layer, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py b/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py index 97ae094a50..3eb0ed8fa1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py @@ -61,17 +61,11 @@ def unassign_layer( # Let's undo it! ifcopenshell.api.layer.unassign_layer(model, items=[representation.Items[0]], layer=layer) """ - settings = { - "items": items, - "layer": layer, - } - - layer = settings["layer"] assigned_items = set(layer.AssignedItems) or set() - items = set(settings["items"]) - if not items.issubset(assigned_items): + items_set = set(items) + if not items_set.issubset(assigned_items): return - assigned_items = list(assigned_items - items) + assigned_items = list(assigned_items - items_set) # keep IFC valid in case if there are no items left if assigned_items: diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/edit_library.py b/src/ifcopenshell-python/ifcopenshell/api/library/edit_library.py index b3bc87b1c3..4e12195bda 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/edit_library.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/edit_library.py @@ -28,11 +28,8 @@ def edit_library(file: ifcopenshell.file, library: ifcopenshell.entity_instance, IfcLibraryInformation, consult the IFC documentation. :param library: The IfcLibraryInformation entity you want to edit - :type library: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/edit_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/edit_reference.py index f3016fd9cf..e6acacbd8f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/edit_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/edit_reference.py @@ -28,11 +28,8 @@ def edit_reference( IfcLibraryReference, consult the IFC documentation. :param reference: The IfcLibraryReference entity you want to edit - :type reference: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -44,7 +41,5 @@ def edit_reference( ifcopenshell.api.library.edit_reference(model, reference=reference, attributes={"Identification": "http://example.org/digitaltwin#AHU01"}) """ - settings = {"reference": reference, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["reference"], name, value) + for name, value in attributes.items(): + setattr(reference, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py index 7719fd86c8..1e7c209e6c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py @@ -17,6 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.util.representation +from typing import Any def assign_profile( @@ -91,18 +92,17 @@ def assign_profile( """ usecase = Usecase() usecase.file = file - usecase.settings = {"material_profile": material_profile, "profile": profile} - return usecase.execute() + return usecase.execute(material_profile, profile) class Usecase: file: ifcopenshell.file - def execute(self) -> None: + def execute(self, material_profile: ifcopenshell.entity_instance, profile: ifcopenshell.entity_instance) -> None: # TODO: handle composite profiles - old_profile = self.settings["material_profile"].Profile - self.settings["material_profile"].Profile = self.settings["profile"] - for profile_set in self.settings["material_profile"].ToMaterialProfileSet: + old_profile = material_profile.Profile + material_profile.Profile = profile + for profile_set in material_profile.ToMaterialProfileSet: for inverse in self.file.get_inverse(profile_set): if not inverse.is_a("IfcMaterialProfileSetUsage"): continue @@ -111,20 +111,20 @@ class Usecase: if not rel.is_a("IfcRelAssociatesMaterial"): continue for element in rel.RelatedObjects: - self.change_profile(element) + self.change_profile(element, profile) else: for rel in inverse.AssociatedTo: for element in rel.RelatedObjects: - self.change_profile(element) + self.change_profile(element, profile) - if old_profile and len(self.file.get_inverse(old_profile)) == 0: + if old_profile and self.file.get_total_inverses(old_profile) == 0: # TODO: check remove deep self.file.remove(old_profile) - def change_profile(self, element: ifcopenshell.entity_instance) -> None: + def change_profile(self, element: ifcopenshell.entity_instance, profile: ifcopenshell.entity_instance) -> None: representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if not representation: return for subelement in self.file.traverse(representation): if subelement.is_a("IfcSweptAreaSolid"): - subelement.SweptArea = self.settings["profile"] + subelement.SweptArea = profile diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py index 939e995a06..9a01322743 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py @@ -28,11 +28,8 @@ def edit_assigned_material( IfcMaterial, consult the IFC documentation. :param element: The IfcMaterial entity you want to edit - :type element: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,7 +39,5 @@ def edit_assigned_material( ifcopenshell.api.material.edit_assigned_material(model, element=concrete, attributes={"Description": "40MPA concrete with broom finish"}) """ - settings = {"element": element, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["element"], name, value) + for name, value in attributes.items(): + setattr(element, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_constituent.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_constituent.py index eda33d349b..a7398ca5f0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_constituent.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_constituent.py @@ -31,13 +31,9 @@ def edit_constituent( IfcMaterialConstituent, consult the IFC documentation. :param constituent: The IfcMaterialConstituent entity you want to edit - :type constituent: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict, optional :param material: The IfcMaterial entity you want to change the constituent to - :type material: ifcopenshell.entity_instance, optional :return: None - :rtype: None Example: @@ -65,8 +61,6 @@ def edit_constituent( ifcopenshell.api.material.edit_constituent(model, constituent=constituent, attributes={"Name": "Glazing"}) """ - settings = {"constituent": constituent, "attributes": attributes or {}, "material": material} - - for name, value in settings["attributes"].items(): - setattr(settings["constituent"], name, value) - settings["constituent"].Material = settings["material"] + for name, value in (attributes or {}).items(): + setattr(constituent, name, value) + constituent.Material = material diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer.py index d691d6b154..68f3c6d832 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer.py @@ -31,14 +31,10 @@ def edit_layer( IfcMaterialLayer, consult the IFC documentation. :param layer: The IfcMaterialLayer entity you want to edit - :type layer: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict, optional :param material: The IfcMaterial entity you want the layer to be made from. - :type material: ifcopenshell.entity_instance, optional :return: None - :rtype: None Example: @@ -63,9 +59,7 @@ def edit_layer( layer = ifcopenshell.api.material.add_layer(model, layer_set=material_set, material=gypsum) ifcopenshell.api.material.edit_layer(model, layer=layer, attributes={"LayerThickness": 13}) """ - settings = {"layer": layer, "attributes": attributes or {}, "material": material} - - for name, value in settings["attributes"].items(): - setattr(settings["layer"], name, value) - if settings["material"]: - settings["layer"].Material = settings["material"] + for name, value in (attributes or {}).items(): + setattr(layer, name, value) + if material: + layer.Material = material diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py index 6c581c1374..9d1ffb14ec 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py @@ -29,11 +29,8 @@ def edit_layer_usage(file: ifcopenshell.file, usage: ifcopenshell.entity_instanc IfcMaterialLayerSetUsage, consult the IFC documentation. :param usage: The IfcMaterialLayerSetUsage entity you want to edit - :type usage: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -75,7 +72,5 @@ def edit_layer_usage(file: ifcopenshell.file, usage: ifcopenshell.entity_instanc ifcopenshell.api.material.edit_layer_usage(model, usage=rel.RelatingMaterial, attributes={"OffsetFromReferenceLine": 200}) """ - settings = {"usage": usage, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["usage"], name, value) + for name, value in attributes.items(): + setattr(usage, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py index 17f925997e..c35755a960 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py @@ -34,17 +34,12 @@ def edit_profile( IfcMaterialProfile, consult the IFC documentation. :param profile: The IfcMaterialProfile entity you want to edit - :type profile: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict, optional :param profile_def: The IfcProfileDef entity the profile curve should be extruded from. - :type profile_def: ifcopenshell.entity_instance, optional :param material: The IfcMaterial entity you want to change the profile to be made from. - :type material: ifcopenshell.entity_instance, optional :return: None - :rtype: None Example: @@ -80,16 +75,9 @@ def edit_profile( ifcopenshell.api.material.edit_profile(model, profile=profile_item, profile_def=hea200, material=steel2) """ - settings = { - "profile": profile, - "attributes": attributes or {}, - "profile_def": profile_def, - "material": material, - } - - for name, value in settings["attributes"].items(): - setattr(settings["profile"], name, value) - if settings["material"]: - settings["profile"].Material = settings["material"] - if settings["profile_def"]: - settings["profile"].Profile = settings["profile_def"] + for name, value in (attributes or {}).items(): + setattr(profile, name, value) + if material: + profile.Material = material + if profile_def: + profile.Profile = profile_def diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py index f9894db5b3..8a583d64b4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py @@ -17,6 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.geom import ifcopenshell.util.representation +from ifcopenshell.geom import ShapeType from typing import Any @@ -34,11 +35,8 @@ def edit_profile_usage( IfcMaterialProfileSetUsage, consult the IFC documentation. :param usage: The IfcMaterialProfileSetUsage entity you want to edit - :type usage: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -93,21 +91,23 @@ def edit_profile_usage( usecase = Usecase() usecase.file = file - usecase.settings = {"usage": usage, "attributes": attributes} - return usecase.execute() + return usecase.execute(usage, attributes) class Usecase: - def execute(self): - self.cardinal_point = self.settings["attributes"].get("CardinalPoint") - if self.cardinal_point and self.cardinal_point != self.settings["usage"].CardinalPoint: + file: ifcopenshell.file + + def execute(self, usage: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: + self.attributes = attributes + self.cardinal_point = attributes.get("CardinalPoint") + if self.cardinal_point and self.cardinal_point != usage.CardinalPoint: self.update_cardinal_point() - for name, value in self.settings["attributes"].items(): - setattr(self.settings["usage"], name, value) + for name, value in attributes.items(): + setattr(usage, name, value) def update_cardinal_point(self): - material_set = self.settings["usage"].ForProfileSet + material_set = self.attributes["usage"].ForProfileSet self.profile = material_set.CompositeProfile if not self.profile and material_set.MaterialProfiles: self.profile = material_set.MaterialProfiles[0].Profile @@ -117,13 +117,13 @@ class Usecase: self.position = self.calculate_position() if self.file.schema == "IFC2X3": - for rel in self.file.get_inverse(self.settings["usage"]): + for rel in self.file.get_inverse(self.attributes["usage"]): if not rel.is_a("IfcRelAssociatesMaterial"): continue for element in rel.RelatedObjects: self.update_representation(element) else: - for rel in self.settings["usage"].AssociatedTo: + for rel in self.attributes["usage"].AssociatedTo: for element in rel.RelatedObjects: self.update_representation(element) @@ -166,7 +166,7 @@ class Usecase: elif self.cardinal_point == 9: return self.get_top_right(shape) - def get_bottom_left(self, shape): + def get_bottom_left(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts x = [v[i] for i in range(0, len(v), 3)] y = [v[i + 1] for i in range(0, len(v), 3)] @@ -174,13 +174,13 @@ class Usecase: height = max(y) - min(y) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((-width / 2, height / 2, 0.0))) - def get_bottom_centre(self, shape): + def get_bottom_centre(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts y = [v[i + 1] for i in range(0, len(v), 3)] height = max(y) - min(y) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((0.0, height / 2, 0.0))) - def get_bottom_right(self, shape): + def get_bottom_right(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts x = [v[i] for i in range(0, len(v), 3)] y = [v[i + 1] for i in range(0, len(v), 3)] @@ -188,22 +188,22 @@ class Usecase: height = max(y) - min(y) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((width / 2, height / 2, 0.0))) - def get_mid_depth_left(self, shape): + def get_mid_depth_left(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts x = [v[i] for i in range(0, len(v), 3)] width = max(x) - min(x) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((-width / 2, 0.0, 0.0))) - def get_mid_depth_centre(self, shape): + def get_mid_depth_centre(self, shape: ShapeType) -> ifcopenshell.entity_instance: return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))) - def get_mid_depth_right(self, shape): + def get_mid_depth_right(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts x = [v[i] for i in range(0, len(v), 3)] width = max(x) - min(x) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((width / 2, 0.0, 0.0))) - def get_top_left(self, shape): + def get_top_left(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts x = [v[i] for i in range(0, len(v), 3)] y = [v[i + 1] for i in range(0, len(v), 3)] @@ -211,13 +211,13 @@ class Usecase: height = max(y) - min(y) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((-width / 2, -height / 2, 0.0))) - def get_top_centre(self, shape): + def get_top_centre(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts y = [v[i + 1] for i in range(0, len(v), 3)] height = max(y) - min(y) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((0.0, -height / 2, 0.0))) - def get_top_right(self, shape): + def get_top_right(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts x = [v[i] for i in range(0, len(v), 3)] y = [v[i + 1] for i in range(0, len(v), 3)] @@ -225,7 +225,7 @@ class Usecase: height = max(y) - min(y) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((width / 2, -height / 2, 0.0))) - def update_representation(self, element): + def update_representation(self, element: ifcopenshell.entity_instance) -> None: representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if not representation: return @@ -234,5 +234,5 @@ class Usecase: if subelement.is_a("IfcSweptAreaSolid") and subelement.SweptArea == self.profile: self.update_swept_area_solid(subelement) - def update_swept_area_solid(self, element): + def update_swept_area_solid(self, element: ifcopenshell.entity_instance) -> None: element.Position = self.position diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py index 7521c48a15..8367f24d0e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.util.element def remove_constituent( @@ -51,7 +52,7 @@ def remove_constituent( # invalid. ifcopenshell.api.material.remove_constituent(model, constituent=glazing) """ - material = layer.Material + material = constituent.Material file.remove(constituent) if material and should_remove_material: - ifcopenshell.util.element.remove_deep2(file, subelement) + ifcopenshell.util.element.remove_deep2(file, material) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py index 7055beaf26..443ad122c0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py @@ -57,4 +57,4 @@ def remove_layer( material = layer.Material file.remove(layer) if material and should_remove_material: - ifcopenshell.util.element.remove_deep2(file, subelement) + ifcopenshell.util.element.remove_deep2(file, material) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_list_item.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_list_item.py index cd4f2f1cd6..6af15a7d3a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_list_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_list_item.py @@ -54,8 +54,6 @@ def remove_list_item( # Let's remove the glass ifcopenshell.api.material.remove_list_item(model, material_list=material_set, material_index=1) """ - settings = {"material_list": material_list, "material_index": material_index} - - materials = list(settings["material_list"].Materials) - materials.pop(settings["material_index"]) - settings["material_list"].Materials = materials + materials = list(material_list.Materials) + materials.pop(material_index) + material_list.Materials = materials diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/reorder_set_item.py b/src/ifcopenshell-python/ifcopenshell/api/material/reorder_set_item.py index 0015d3558a..aa06433e66 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/reorder_set_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/reorder_set_item.py @@ -57,16 +57,17 @@ def reorder_set_item( ifcopenshell.api.material.reorder_set_item(model, material_set=material_set, old_index=0, new_index=1) """ - settings = {"material_set": material_set, "old_index": old_index, "new_index": new_index} - - if settings["material_set"].is_a("IfcMaterialConstituentSet"): + if material_set.is_a("IfcMaterialConstituentSet"): set_name = "MaterialConstituents" - elif settings["material_set"].is_a("IfcMaterialLayerSet"): + elif material_set.is_a("IfcMaterialLayerSet"): set_name = "MaterialLayers" - elif settings["material_set"].is_a("IfcMaterialProfileSet"): + elif material_set.is_a("IfcMaterialProfileSet"): set_name = "MaterialProfiles" - elif settings["material_set"].is_a("IfcMaterialList"): + elif material_set.is_a("IfcMaterialList"): set_name = "Materials" - items = list(getattr(settings["material_set"], set_name) or []) - items.insert(settings["new_index"], items.pop(settings["old_index"])) - setattr(settings["material_set"], set_name, items) + else: + raise ValueError(f"Unexpected material set type: '{material_set.is_a()}'.") + + items = list(getattr(material_set, set_name) or []) + items.insert(new_index, items.pop(old_index)) + setattr(material_set, set_name, items) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py index 8db2060df0..f8386490c7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py @@ -19,6 +19,7 @@ import ifcopenshell import ifcopenshell.api.owner import ifcopenshell.util.element +from typing import Any def unassign_material(file: ifcopenshell.file, products: list[ifcopenshell.entity_instance]) -> None: @@ -58,6 +59,9 @@ def unassign_material(file: ifcopenshell.file, products: list[ifcopenshell.entit class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): self.products = set(self.settings["products"]) if not self.products: diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/add_application.py b/src/ifcopenshell-python/ifcopenshell/api/owner/add_application.py index 991006be01..3f18754287 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/add_application.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/add_application.py @@ -17,7 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.api -from typing import Optional +from typing import Optional, Any def add_application( @@ -68,6 +68,9 @@ def add_application( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): if not self.settings["application_developer"]: self.settings["application_developer"] = self.create_application_organisation() diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py b/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py index 7105e5e78d..75bb5eb517 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py @@ -48,16 +48,14 @@ def add_role( identification="AWB", name="Architects Without Ballpens") ifcopenshell.api.owner.add_role(model, assigned_object=organisation, role="ARCHITECT") """ - settings = {"assigned_object": assigned_object, "role": role} - - element = file.createIfcActorRole("ARCHITECT") - if settings["role"]: + element = file.create_entity("IfcActorRole", Role="ARCHITECT") + if role: try: - element.Role = settings["role"] + element.Role = role except: element.Role = "USERDEFINED" - element.UserDefinedRole = settings["role"] - roles = list(settings["assigned_object"].Roles) if settings["assigned_object"].Roles else [] + element.UserDefinedRole = role + roles = list(assigned_object.Roles) if assigned_object.Roles else [] roles.append(element) - settings["assigned_object"].Roles = roles + assigned_object.Roles = roles return element diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_actor.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_actor.py index 799b37462c..8ad238f994 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_actor.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_actor.py @@ -26,11 +26,8 @@ def edit_actor(file: ifcopenshell.file, actor: ifcopenshell.entity_instance, att IfcActor, consult the IFC documentation. :param actor: The IfcActor entity you want to edit - :type actor: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -49,7 +46,5 @@ def edit_actor(file: ifcopenshell.file, actor: ifcopenshell.entity_instance, att ifcopenshell.api.actor.edit_actor(model, actor=actor, attributes={"Description": "Responsible for buildings A, B, and C."}) """ - settings = {"actor": actor, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["actor"], name, value) + for name, value in attributes.items(): + setattr(actor, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_address.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_address.py index 43776fa711..52e80d6ef1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_address.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_address.py @@ -26,11 +26,8 @@ def edit_address(file: ifcopenshell.file, address: ifcopenshell.entity_instance, IfcAddress, consult the IFC documentation. :param address: The IfcAddress entity you want to edit - :type address: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -51,7 +48,5 @@ def edit_address(file: ifcopenshell.file, address: ifcopenshell.entity_instance, "ElectronicMailAddresses": ["bobthebuilder@example.com"], "WWWHomePageURL": "https://thinkmoult.com"}) """ - settings = {"address": address, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["address"], name, value) + for name, value in attributes.items(): + setattr(address, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py index 9cfbfff48d..a2bb5e3eb6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py @@ -28,11 +28,8 @@ def edit_organisation( IfcOrganization, consult the IFC documentation. :param organisation: The IfcOrganization entity you want to edit - :type organisation: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -43,7 +40,5 @@ def edit_organisation( ifcopenshell.api.owner.edit_organisation(model, organisation=organisation, attributes={"name": "Architects Without Ballpens"}) """ - settings = {"organisation": organisation, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["organisation"], name, value) + for name, value in attributes.items(): + setattr(organisation, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_person.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_person.py index 0e4bd15293..6b697bd674 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_person.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_person.py @@ -26,11 +26,8 @@ def edit_person(file: ifcopenshell.file, person: ifcopenshell.entity_instance, a IfcPerson, consult the IFC documentation. :param person: The IfcPerson entity you want to edit - :type person: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -41,7 +38,5 @@ def edit_person(file: ifcopenshell.file, person: ifcopenshell.entity_instance, a ifcopenshell.api.owner.edit_person(model, person=person, attributes={"MiddleNames": ["The"], "FamilyName": "Builder"}) """ - settings = {"person": person, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["person"], name, value) + for name, value in attributes.items(): + setattr(person, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_role.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_role.py index 885d4b9b9b..fca80a16b0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_role.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_role.py @@ -26,11 +26,8 @@ def edit_role(file: ifcopenshell.file, role: ifcopenshell.entity_instance, attri IfcActorRole, consult the IFC documentation. :param role: The IfcActorRole entity you want to edit - :type role: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -45,7 +42,5 @@ def edit_role(file: ifcopenshell.file, role: ifcopenshell.entity_instance, attri # But Bob is not an architect ifcopenshell.api.owner.edit_role(model, role=role, attributes={"Role": "CONSTRUCTIONMANAGER"}) """ - settings = {"role": role, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["role"], name, value) + for name, value in attributes.items(): + setattr(role, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py index fbc25adcbe..acee11d9f5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py @@ -27,9 +27,7 @@ def remove_organisation(file: ifcopenshell.file, organisation: ifcopenshell.enti removed. :param organisation: The IfcOrganization to remove - :type organisation: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -39,31 +37,29 @@ def remove_organisation(file: ifcopenshell.file, organisation: ifcopenshell.enti identification="AWB", name="Architects Without Ballpens") ifcopenshell.api.owner.remove_organisation(model, organisation=organisation) """ - settings = {"organisation": organisation} - - for role in settings["organisation"].Roles or []: - if len(file.get_inverse(role)) == 1: + for role in organisation.Roles or []: + if (file.get_total_inverses(role)) == 1: ifcopenshell.api.owner.remove_role(file, role=role) - for address in settings["organisation"].Addresses or []: - if len(file.get_inverse(address)) == 1: + for address in organisation.Addresses or []: + if (file.get_total_inverses(address)) == 1: ifcopenshell.api.owner.remove_address(file, address=address) - for inverse in file.get_inverse(settings["organisation"]): + for inverse in file.get_inverse(organisation): if inverse.is_a("IfcOrganizationRelationship"): - if inverse.RelatingOrganization == settings["organisation"]: + if inverse.RelatingOrganization == organisation: file.remove(inverse) - elif inverse.RelatedOrganizations == (settings["organisation"],): + elif inverse.RelatedOrganizations == (organisation,): file.remove(inverse) elif inverse.is_a("IfcDocumentInformation"): - if inverse.Editors == (settings["organisation"],): + if inverse.Editors == (organisation,): inverse.Editors = None elif inverse.is_a("IfcPersonAndOrganization"): ifcopenshell.api.owner.remove_person_and_organisation(file, person_and_organisation=inverse) elif inverse.is_a("IfcActor"): ifcopenshell.api.root.remove_product(file, product=inverse) elif inverse.is_a("IfcResourceLevelRelationship") and not inverse.is_a("IfcOrganizationRelationship"): - if inverse.RelatedResourceObjects == (settings["organisation"],): + if inverse.RelatedResourceObjects == (organisation,): file.remove(inverse) elif inverse.is_a("IfcApplication"): ifcopenshell.api.owner.remove_application(file, application=inverse) - file.remove(settings["organisation"]) + file.remove(organisation) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py index a4f4d64f1f..0c618c1e74 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py @@ -29,9 +29,7 @@ def remove_person(file: ifcopenshell.file, person: ifcopenshell.entity_instance) the only responsile person for them. :param person: The IfcPerson to remove - :type person: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -41,31 +39,30 @@ def remove_person(file: ifcopenshell.file, person: ifcopenshell.entity_instance) identification="bobthebuilder", family_name="Thebuilder", given_name="Bob") ifcopenshell.api.owner.remove_person(model, person=person) """ - settings = {"person": person} - for role in settings["person"].Roles or []: - if len(file.get_inverse(role)) == 1: + for role in person.Roles or []: + if file.get_total_inverses(role) == 1: ifcopenshell.api.owner.remove_role(file, role=role) - for address in settings["person"].Addresses or []: - if len(file.get_inverse(address)) == 1: + for address in person.Addresses or []: + if file.get_total_inverses(address) == 1: ifcopenshell.api.owner.remove_address(file, address=address) - for inverse in file.get_inverse(settings["person"]): + for inverse in file.get_inverse(person): if inverse.is_a("IfcWorkControl"): - if inverse.Creators == (settings["person"],): + if inverse.Creators == (person,): inverse.Creators = None elif inverse.is_a("IfcInventory"): - if inverse.ResponsiblePersons == (settings["person"],): + if inverse.ResponsiblePersons == (person,): # in IFC2X3 ResponsiblePersons is not optional and without it IfcInventory is not valid if file.schema == "IFC2X3": ifcopenshell.api.root.remove_product(file, product=inverse) elif inverse.is_a("IfcDocumentInformation"): - if inverse.Editors == (settings["person"],): + if inverse.Editors == (person,): inverse.Editors = None elif inverse.is_a("IfcPersonAndOrganization"): ifcopenshell.api.owner.remove_person_and_organisation(file, person_and_organisation=inverse) elif inverse.is_a("IfcActor"): ifcopenshell.api.root.remove_product(file, product=inverse) elif inverse.is_a("IfcResourceLevelRelationship"): - if inverse.RelatedResourceObjects == (settings["person"],): + if inverse.RelatedResourceObjects == (person,): file.remove(inverse) - file.remove(settings["person"]) + file.remove(person) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py b/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py index 493f4077f4..b5dedfff49 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py @@ -30,17 +30,15 @@ def get_application(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instanc IfcApplication. See ifcopenshell.api.owner.create_owner_history for details. :param ifc: The IFC file object that is being edited. - :type ifc: ifcopenshell.file :return: The IfcApplication with metadata of the authoring software. - :rtype: ifcopenshell.entity_instance """ - app = ifc.by_type("IfcApplication") + app = next(iter(ifc.by_type("IfcApplication")), None) if not app and ifc.schema == "IFC2X3": raise Exception( "Please create an application to continue. See the owner.create_owner_history docs for more info." "https://docs.ifcopenshell.org/autoapi/ifcopenshell/api/owner/create_owner_history/index.html" ) - return (app or [None])[0] + return app def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]: @@ -50,17 +48,15 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None IfcApplication. See ifcopenshell.api.owner.create_owner_history for details. :param ifc: The IFC file object that is being edited. - :type ifc: ifcopenshell.file :return: The IfcPersonAndOrganization with metadata of the authoring user. - :rtype: ifcopenshell.entity_instance """ - pao = ifc.by_type("IfcPersonAndOrganization") + pao = next(iter(ifc.by_type("IfcPersonAndOrganization")), None) if not pao and ifc.schema == "IFC2X3": raise Exception( "Please create a user to continue. See the owner.create_owner_history docs for more info." "https://docs.ifcopenshell.org/autoapi/ifcopenshell/api/owner/create_owner_history/index.html" ) - return (pao or [None])[0] + return pao get_application_factory = get_application diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile.py b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile.py index 08892a555b..80f3b8cf71 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile.py @@ -16,12 +16,14 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import numpy.typing as npt import ifcopenshell.util.unit -from typing import Optional +from ifcopenshell.util.shape_builder import V, SequenceOfVectors, ifc_safe_vector_type +from typing import Optional, Union def add_arbitrary_profile( - file: ifcopenshell.file, profile: list[tuple[float, float]], name: Optional[str] = None + file: ifcopenshell.file, profile: SequenceOfVectors, name: Optional[str] = None ) -> ifcopenshell.entity_instance: """Adds a new arbitrary polyline-based profile @@ -33,13 +35,10 @@ def add_arbitrary_profile( identical. :param profile: A list of coordinates - :type profile: list[tuple[float, float]] :param name: If the profile is semantically significant (i.e. to be managed and reused by the user) then it must be named. Otherwise, this may be left as none. - :type name: str, optional :return: The newly created IfcArbitraryClosedProfileDef - :rtype: ifcopenshell.entity_instance Example: @@ -53,26 +52,30 @@ def add_arbitrary_profile( """ usecase = Usecase() usecase.file = file - usecase.settings = {"profile": profile, "name": name} - return usecase.execute() + return usecase.execute(V(profile), name) class Usecase: - def execute(self): - self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) - points = [self.convert_si_to_unit(p) for p in self.settings["profile"]] - if self.file.schema == "IFC2X3": - curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points]) - else: - dimensions = len(points[0]) - if dimensions == 2: - ifc_points = self.file.createIfcCartesianPointList2D(points) - elif dimensions == 3: - ifc_points = self.file.createIfcCartesianPointList3D(points) - curve = self.file.createIfcIndexedPolyCurve(ifc_points) - return self.file.createIfcArbitraryClosedProfileDef("AREA", self.settings["name"], curve) + file: ifcopenshell.file - def convert_si_to_unit(self, co): - if isinstance(co, (tuple, list)): - return [self.convert_si_to_unit(o) for o in co] - return co / self.settings["unit_scale"] + def execute(self, profile: npt.NDArray, name: Union[str, None]): + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) + points = self.convert_si_to_unit(profile) + if self.file.schema == "IFC2X3": + curve = self.file.create_entity( + "IfcPolyline", + [self.file.create_entity("IfcCartesianPoint", ifc_safe_vector_type(p)) for p in points], + ) + else: + dimensions = points.shape[1] + if dimensions == 2: + ifc_points = self.file.create_entity("IfcCartesianPointList2D", ifc_safe_vector_type(points)) + elif dimensions == 3: + ifc_points = self.file.create_entity("IfcCartesianPointList3D", ifc_safe_vector_type(points)) + else: + assert False, f"Invalid dimensions: {dimensions}." + curve = self.file.create_entity("IfcIndexedPolyCurve", ifc_points) + return self.file.create_entity("IfcArbitraryClosedProfileDef", "AREA", name, curve) + + def convert_si_to_unit(self, co: npt.NDArray) -> npt.NDArray: + return co / self.unit_scale diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py index 8412d40e23..b813a3c65f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py @@ -17,13 +17,15 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.util.unit -from typing import Optional +import numpy.typing as npt +from ifcopenshell.util.shape_builder import SequenceOfVectors, ifc_safe_vector_type, V +from typing import Optional, Union def add_arbitrary_profile_with_voids( file: ifcopenshell.file, - outer_profile: list[tuple[float, float]], - inner_profiles: list[list[tuple[float, float]]], + outer_profile: SequenceOfVectors, + inner_profiles: list[SequenceOfVectors], name: Optional[str] = None, ) -> ifcopenshell.entity_instance: """Adds a new arbitrary polyline-based profile with voids @@ -41,15 +43,11 @@ def add_arbitrary_profile_with_voids( provided in SI meters. :param outer_profile: A list of coordinates - :type profile: list[tuple[float, float]] :param inner_profiles: A list of polylines - :type profile: list[list[tuple[float, float]]] :param name: If the profile is semantically significant (i.e. to be managed and reused by the user) then it must be named. Otherwise, this may be left as none. - :type name: str, optional :return: The newly created IfcArbitraryProfileDefWithVoids - :rtype: ifcopenshell.entity_instance Example: @@ -63,37 +61,52 @@ def add_arbitrary_profile_with_voids( """ usecase = Usecase() usecase.file = file - usecase.settings = {"outer_profile": outer_profile, "inner_profiles": inner_profiles, "name": name} - return usecase.execute() + return usecase.execute(V(outer_profile), [V(p) for p in inner_profiles], name) class Usecase: - def execute(self): - self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) - outer_points = [self.convert_si_to_unit(p) for p in self.settings["outer_profile"]] - inner_points = [] - for inner_profile in self.settings["inner_profiles"]: - inner_points.append([self.convert_si_to_unit(p) for p in inner_profile]) + file: ifcopenshell.file + + def execute( + self, + outer_profile: npt.NDArray, + inner_profiles: list[npt.NDArray], + name: Union[str, None], + ): + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) + outer_points = self.convert_si_to_unit(outer_profile) + inner_points: list[npt.NDArray] = [] + for inner_profile in inner_profiles: + inner_points.append(self.convert_si_to_unit(inner_profile)) + + inner_curves: list[ifcopenshell.entity_instance] = [] if self.file.schema == "IFC2X3": - outer_curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in outer_points]) - inner_curves = [] + outer_curve = self.file.create_entity( + "IfcPolyline", + [self.file.create_entity("IfcCartesianPoint", ifc_safe_vector_type(p)) for p in outer_points], + ) for inner_point in inner_points: inner_curves.append( - self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in inner_point]) + self.file.create_entity( + "IfcPolyline", + [self.file.create_entity("IfcCartesianPoint", ifc_safe_vector_type(p)) for p in inner_point], + ) ) else: - outer_curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList3D(outer_points)) - inner_curves = [] + outer_curve = self.file.create_entity( + "IfcIndexedPolyCurve", + (self.file.create_entity("IfcCartesianPointList3D", ifc_safe_vector_type(outer_points))), + ) for inner_point in inner_points: - dimensions = len(inner_point[0]) + dimensions = inner_point.shape[1] if dimensions == 2: - ifc_points = self.file.createIfcCartesianPointList2D(inner_point) + ifc_points = self.file.create_entity("IfcCartesianPointList2D", ifc_safe_vector_type(inner_point)) elif dimensions == 3: - ifc_points = self.file.createIfcCartesianPointList3D(inner_point) - inner_curves.append(self.file.createIfcIndexedPolyCurve(ifc_points)) - return self.file.createIfcArbitraryProfileDefWithVoids("AREA", self.settings["name"], outer_curve, inner_curves) + ifc_points = self.file.create_entity("IfcCartesianPointList3D", ifc_safe_vector_type(inner_point)) + else: + assert False, f"Invalid dimensions: {dimensions}." + inner_curves.append(self.file.create_entity("IfcIndexedPolyCurve", ifc_points)) + return self.file.create_entity("IfcArbitraryProfileDefWithVoids", "AREA", name, outer_curve, inner_curves) - def convert_si_to_unit(self, co): - if isinstance(co, (tuple, list)): - return [self.convert_si_to_unit(o) for o in co] - return co / self.settings["unit_scale"] + def convert_si_to_unit(self, co: npt.NDArray) -> npt.NDArray: + return co / self.unit_scale diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/edit_profile.py b/src/ifcopenshell-python/ifcopenshell/api/profile/edit_profile.py index b1fda065f0..ba7bc3cfeb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/profile/edit_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/edit_profile.py @@ -26,11 +26,8 @@ def edit_profile(file: ifcopenshell.file, profile: ifcopenshell.entity_instance, IfcProfileDef, consult the IFC documentation. :param profile: The IfcProfileDef entity you want to edit - :type profile: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -43,7 +40,5 @@ def edit_profile(file: ifcopenshell.file, profile: ifcopenshell.entity_instance, ifcopenshell.api.profile.edit_profile(model, profile=circle, attributes={"ProfileName": "1000mm Dia"}) """ - settings = {"profile": profile, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["profile"], name, value) + for name, value in attributes.items(): + setattr(profile, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py index ffa31faa8e..db585c3276 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py @@ -20,7 +20,7 @@ import ifcopenshell import ifcopenshell.api.owner import ifcopenshell.api.pset import ifcopenshell.guid -from typing import Optional +from typing import Optional, Any def add_pset( @@ -92,12 +92,11 @@ def add_pset( # Add a fire rating property standardised by buildingSMART. ifcopenshell.api.pset.edit_pset(model, pset=pset, properties={"FireRating": "2HR"}) """ - settings = {"product": product, "name": name} is_ifc2x3 = file.schema == "IFC2X3" - if settings["product"].is_a("IfcObject") or settings["product"].is_a("IfcContext"): - for rel in settings["product"].IsDefinedBy or []: - if rel.is_a("IfcRelDefinesByProperties") and rel.RelatingPropertyDefinition.Name == settings["name"]: + if product.is_a("IfcObject") or product.is_a("IfcContext"): + for rel in product.IsDefinedBy or []: + if rel.is_a("IfcRelDefinesByProperties") and rel.RelatingPropertyDefinition.Name == name: return rel.RelatingPropertyDefinition pset = file.create_entity( @@ -105,15 +104,15 @@ def add_pset( **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "Name": settings["name"], + "Name": name, }, ) - ifcopenshell.api.pset.assign_pset(file, [settings["product"]], pset) + ifcopenshell.api.pset.assign_pset(file, [product], pset) return pset - elif settings["product"].is_a("IfcTypeObject"): - for definition in settings["product"].HasPropertySets or []: - if definition.Name == settings["name"]: + elif product.is_a("IfcTypeObject"): + for definition in product.HasPropertySets or []: + if definition.Name == name: return definition pset = file.create_entity( @@ -121,42 +120,43 @@ def add_pset( **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "Name": settings["name"], + "Name": name, }, ) - ifcopenshell.api.pset.assign_pset(file, [settings["product"]], pset) + ifcopenshell.api.pset.assign_pset(file, [product], pset) return pset # in IFC2X3 IfcMaterialDefinition not yet existed - elif settings["product"].is_a("IfcMaterialDefinition") or settings["product"].is_a("IfcMaterial"): - kwargs = {"Material": settings["product"]} + elif product.is_a("IfcMaterialDefinition") or product.is_a("IfcMaterial"): + kwargs: dict[str, Any] + kwargs = {"Material": product} if file.schema == "IFC2X3": ifc_class = ifc2x3_subclass or "IfcExtendedMaterialProperties" - definitions = (d for d in file.by_type("IfcMaterialProperties") if d.Material == settings["product"]) + definitions = (d for d in file.by_type("IfcMaterialProperties") if d.Material == product) if ifc_class == "IfcExtendedMaterialProperties": - kwargs["Name"] = settings["name"] + kwargs["Name"] = name else: ifc_class = "IfcMaterialProperties" - definitions = settings["product"].HasProperties - kwargs["Name"] = settings["name"] + definitions = product.HasProperties + kwargs["Name"] = name for definition in definitions: # In IFC2X3 not all IfcMaterialProperties has Name - if getattr(definition, "Name", None) == settings["name"]: + if getattr(definition, "Name", None) == name: return definition return file.create_entity(ifc_class, **kwargs) - elif settings["product"].is_a("IfcProfileDef"): + elif product.is_a("IfcProfileDef"): # in IFC2X3 IfcProfileProperties doesn't have Name and we cannot identify them if file.schema != "IFC2X3": - for definition in settings["product"].HasProperties or []: - if definition.Name == settings["name"]: + for definition in product.HasProperties or []: + if definition.Name == name: return definition kwargs = {} - kwargs["ProfileDefinition"] = settings["product"] + kwargs["ProfileDefinition"] = product if file.schema != "IFC2X3": - kwargs["Name"] = settings["name"] + kwargs["Name"] = name if is_ifc2x3: ifc_class = ifc2x3_subclass or "IfcGeneralProfileProperties" @@ -164,4 +164,4 @@ def add_pset( ifc_class = "IfcProfileProperties" return file.create_entity(ifc_class, **kwargs) - raise TypeError(f"Class '{settings['product'].is_a(True)}' doesn't support adding a property set.") + raise TypeError(f"Class '{product.is_a(True)}' doesn't support adding a property set.") diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py b/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py index 15646fe9c7..415b8ec187 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py @@ -19,6 +19,7 @@ import ifcopenshell import ifcopenshell.api.owner import ifcopenshell.guid +from typing import Any def add_qto(file: ifcopenshell.file, product: ifcopenshell.entity_instance, name: str) -> ifcopenshell.entity_instance: @@ -83,9 +84,15 @@ def add_qto(file: ifcopenshell.file, product: ifcopenshell.entity_instance, name class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): - if self.settings["product"].is_a("IfcObject") or self.settings["product"].is_a("IfcContext"): - for rel in self.settings["product"].IsDefinedBy or []: + product: ifcopenshell.entity_instance = self.settings["product"] + name: str = self.settings["name"] + + if product.is_a("IfcObject") or product.is_a("IfcContext"): + for rel in product.IsDefinedBy or []: if ( rel.is_a("IfcRelDefinesByProperties") and rel.RelatingPropertyDefinition.Name == self.settings["name"] @@ -103,14 +110,14 @@ class Usecase: } ) return qto - elif self.settings["product"].is_a("IfcTypeObject"): - for definition in self.settings["product"].HasPropertySets or []: - if definition.Name == self.settings["name"]: + elif product.is_a("IfcTypeObject"): + for definition in product.HasPropertySets or []: + if definition.Name == name: return definition qto = self.create_qto() - has_property_sets = list(self.settings["product"].HasPropertySets or []) + has_property_sets = list(product.HasPropertySets or []) has_property_sets.append(qto) - self.settings["product"].HasPropertySets = has_property_sets + product.HasPropertySets = has_property_sets return qto def create_qto(self): diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py b/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py index 1984506c30..3677a8e63e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py @@ -28,11 +28,8 @@ def edit_prop_template( IfcSimplePropertyTemplate, consult the IFC documentation. :param prop_template: The IfcSimplePropertyTemplate entity you want to edit - :type prop_template: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_pset_template.py b/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_pset_template.py index 1682fa150d..d143b5554e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_pset_template.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_pset_template.py @@ -28,11 +28,8 @@ def edit_pset_template( IfcPropertySetTemplate, consult the IFC documentation. :param pset_template: The IfcPropertySetTemplate entity you want to edit - :type pset_template: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -45,7 +42,5 @@ def edit_pset_template( ifcopenshell.api.pset_template.edit_pset_template(model, pset_template=template, attributes={"Name": "ABC_RiskFactors"}) """ - settings = {"pset_template": pset_template, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["pset_template"], name, value) + for name, value in attributes.items(): + setattr(pset_template, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource_quantity.py index b445c1d1ad..fc67a8d3c7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource_quantity.py @@ -36,15 +36,12 @@ def add_resource_quantity( This base quantity is then used in other calculations. :param resource: The IfcConstructionResource to add a quantity to. - :type resource: ifcopenshell.entity_instance :param ifc_class: The type of quantity to add, chosen from IfcQuantityArea (for material), IfcQuantityCount (for products), IfcQuantityLength (for material), IfcQuantityTime (for equipment or labour), IfcQuantityVolume (for material), and IfcQuantityWeight (for material). - :type ifc_class: str,optional :return: The newly created quantity depending on the IFC class - :rtype: ifcopenshell.entity_instance Example: @@ -65,8 +62,6 @@ def add_resource_quantity( ifcopenshell.api.resource.edit_resource_quantity(model, physical_quantity=quantity, attributes={"TimeValue": 8.0}) """ - settings = {"resource": resource, "ifc_class": ifc_class} - resource_type = resource.is_a() supported_quantities = ifcopenshell.util.resource.RESOURCES_TO_QUANTITIES[resource_type] if ifc_class not in supported_quantities: @@ -75,14 +70,14 @@ def add_resource_quantity( f"Supported quantities: {','.join(supported_quantities)}" ) - quantity = file.create_entity(settings["ifc_class"], Name="Unnamed") + quantity = file.create_entity(ifc_class, Name="Unnamed") # 3 IfcPhysicalSimpleQuantity Value - if settings["ifc_class"] == "IfcQuantityCount": + if ifc_class == "IfcQuantityCount": quantity[3] = 0 else: quantity[3] = 0.0 - old_quantity = settings["resource"].BaseQuantity - settings["resource"].BaseQuantity = quantity + old_quantity = resource.BaseQuantity + resource.BaseQuantity = quantity if old_quantity: ifcopenshell.util.element.remove_deep(file, old_quantity) return quantity diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource.py b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource.py index b5bb899ead..3476032c1a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource.py @@ -26,11 +26,8 @@ def edit_resource(file: ifcopenshell.file, resource: ifcopenshell.entity_instanc IfcResource, consult the IFC documentation. :param resource: The IfcResource entity you want to edit - :type resource: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,7 +39,5 @@ def edit_resource(file: ifcopenshell.file, resource: ifcopenshell.entity_instanc # Change the name of the resource to "Zone A Crew" ifcopenshell.api.resource.edit_resource(model, resource=resource, attributes={"Name": "Foo"}) """ - settings = {"resource": resource, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["resource"], name, value) + for name, value in attributes.items(): + setattr(resource, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_quantity.py index af8f55f94f..87a14774d8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_quantity.py @@ -28,11 +28,8 @@ def edit_resource_quantity( IfC quantity, consult the IFC documentation. :param physical_quantity: The IfC quantity entity you want to edit - :type physical_quantity: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -53,10 +50,5 @@ def edit_resource_quantity( ifcopenshell.api.resource.edit_resource_quantity(model, physical_quantity=time, attributes={"TimeValue": 8.0}) """ - settings = { - "physical_quantity": physical_quantity, - "attributes": attributes, - } - - for name, value in settings["attributes"].items(): - setattr(settings["physical_quantity"], name, value) + for name, value in attributes.items(): + setattr(physical_quantity, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py index 687fe0490d..7326a5fd90 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py @@ -18,6 +18,9 @@ import ifcopenshell import ifcopenshell.api.sequence +import ifcopenshell.util.constraint +import ifcopenshell.util.date +import ifcopenshell.util.resource from typing import Any @@ -30,11 +33,8 @@ def edit_resource_time( IfcResourceTime, consult the IFC documentation. :param resource_time: The IfcResourceTime entity you want to edit - :type resource_time: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -62,25 +62,23 @@ def edit_resource_time( """ usecase = Usecase() usecase.file = file - usecase.settings = {"resource_time": resource_time, "attributes": attributes} - return usecase.execute() + return usecase.execute(resource_time, attributes) class Usecase: - def execute(self): - self.resource = self.get_resource() + file: ifcopenshell.file + + def execute(self, resource_time: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: + resource = self.get_resource(resource_time) # If the user specifies both an end date and a duration, the duration takes priority - if ( - self.settings["attributes"].get("ScheduleWork", None) - and "ScheduleFinish" in self.settings["attributes"].keys() - ): - del self.settings["attributes"]["ScheduleFinish"] - if self.settings["attributes"].get("ActualWork", None) and "ActualFinish" in self.settings["attributes"].keys(): - del self.settings["attributes"]["ActualFinish"] + if attributes.get("ScheduleWork", None) and "ScheduleFinish" in attributes.keys(): + del attributes["ScheduleFinish"] + if attributes.get("ActualWork", None) and "ActualFinish" in attributes.keys(): + del attributes["ActualFinish"] - for name, value in self.settings["attributes"].items(): - metrics = ifcopenshell.util.constraint.get_metric_constraints(self.resource, "Usage." + name) + for name, value in attributes.items(): + metrics = ifcopenshell.util.constraint.get_metric_constraints(resource, "Usage." + name) if metrics and ifcopenshell.util.constraint.is_hard_constraint(metrics[0]): continue if value: @@ -88,13 +86,13 @@ class Usecase: value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime") elif name == "ScheduleWork" or name == "ActualWork" or name == "RemainingTime": value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration") - setattr(self.settings["resource_time"], name, value) + setattr(resource_time, name, value) if name == "ScheduleUsage" and ifcopenshell.util.constraint.get_metric_constraints( - self.resource, "Usage.ScheduleWork" + resource, "Usage.ScheduleWork" ): - task = ifcopenshell.util.resource.get_task_assignments(self.resource) + task = ifcopenshell.util.resource.get_task_assignments(resource) if task: ifcopenshell.api.sequence.calculate_task_duration(self.file, task=task) - def get_resource(self): - return [e for e in self.file.get_inverse(self.settings["resource_time"]) if e.is_a("IfcResource")][0] + def get_resource(self, resource_time: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: + return next(e for e in self.file.get_inverse(resource_time) if e.is_a("IfcResource")) diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py b/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py index a55c711e53..6f02a4531d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py @@ -23,6 +23,7 @@ import ifcopenshell.api.geometry import ifcopenshell.util.system import ifcopenshell.util.element import ifcopenshell.util.placement +from typing import Any def copy_class(file: ifcopenshell.file, product: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: @@ -74,6 +75,9 @@ def copy_class(file: ifcopenshell.file, product: ifcopenshell.entity_instance) - class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): result = ifcopenshell.util.element.copy(self.file, self.settings["product"]) self.copy_direct_attributes(result) diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py b/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py index a31dc4c00a..67d67ed7f2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py @@ -19,7 +19,7 @@ import ifcopenshell import ifcopenshell.api.owner import ifcopenshell.guid -from typing import Optional +from typing import Optional, Any def create_entity( @@ -80,6 +80,9 @@ def create_entity( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): element = self.file.create_entity( self.settings["ifc_class"], diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py index 3e4cd3faf4..7682856864 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py @@ -27,7 +27,7 @@ import ifcopenshell.util.representation import ifcopenshell.util.type import ifcopenshell.util.schema import ifcopenshell.util.element -from typing import Optional, Union, Literal +from typing import Optional, Union, Literal, Any def reassign_class( @@ -87,6 +87,7 @@ def reassign_class( class Usecase: file: ifcopenshell.file + settings: dict[str, Any] def execute(self): ifc_class: str = self.settings["ifc_class"] diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py index 2df1b4eaa5..1ab4bc432e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py @@ -62,42 +62,38 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc # No we don't. ifcopenshell.api.root.remove_product(model, product=wall) """ - settings = {"product": product} - - representations = [] - if settings["product"].is_a("IfcProduct"): - if settings["product"].Representation: - representations = settings["product"].Representation.Representations or [] + representations: list[ifcopenshell.entity_instance] = [] + if product.is_a("IfcProduct"): + if product.Representation: + representations = product.Representation.Representations or [] else: representations = [] # remove object placements - object_placement = settings["product"].ObjectPlacement + object_placement = product.ObjectPlacement if object_placement: if file.get_total_inverses(object_placement) == 1: - settings["product"].ObjectPlacement = None # remove the inverse for remove_deep2 to work + product.ObjectPlacement = None # remove the inverse for remove_deep2 to work ifcopenshell.util.element.remove_deep2(file, object_placement) - elif settings["product"].is_a("IfcTypeProduct"): - representations = [rm.MappedRepresentation for rm in settings["product"].RepresentationMaps or []] + elif product.is_a("IfcTypeProduct"): + representations = [rm.MappedRepresentation for rm in product.RepresentationMaps or []] # remove psets - psets = settings["product"].HasPropertySets or [] + psets = product.HasPropertySets or [] for pset in psets: if file.get_total_inverses(pset) != 1: continue - ifcopenshell.api.pset.remove_pset(file, product=settings["product"], pset=pset) + ifcopenshell.api.pset.remove_pset(file, product=product, pset=pset) for representation in representations: - ifcopenshell.api.geometry.unassign_representation( - file, product=settings["product"], representation=representation - ) - ifcopenshell.api.geometry.remove_representation(file, **{"representation": representation}) - for opening in getattr(settings["product"], "HasOpenings", []) or []: + ifcopenshell.api.geometry.unassign_representation(file, product=product, representation=representation) + ifcopenshell.api.geometry.remove_representation(file, representation=representation) + for opening in getattr(product, "HasOpenings", []) or []: ifcopenshell.api.feature.remove_feature(file, feature=opening.RelatedOpeningElement) - if settings["product"].is_a("IfcGrid"): - for axis in settings["product"].UAxes + settings["product"].VAxes + (settings["product"].WAxes or ()): + if product.is_a("IfcGrid"): + for axis in product.UAxes + product.VAxes + (product.WAxes or ()): ifcopenshell.api.grid.remove_grid_axis(file, axis=axis) def element_exists(element_id): @@ -108,22 +104,20 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc return False # TODO: remove object placement and other relationships - for inverse_id in [i.id() for i in file.get_inverse(settings["product"])]: + for inverse_id in [i.id() for i in file.get_inverse(product)]: try: inverse = file.by_id(inverse_id) except: continue if inverse.is_a("IfcRelDefinesByProperties"): - ifcopenshell.api.pset.remove_pset( - file, product=settings["product"], pset=inverse.RelatingPropertyDefinition - ) + ifcopenshell.api.pset.remove_pset(file, product=product, pset=inverse.RelatingPropertyDefinition) elif inverse.is_a("IfcRelAssociatesMaterial"): - ifcopenshell.api.material.unassign_material(file, products=[settings["product"]]) + ifcopenshell.api.material.unassign_material(file, products=[product]) elif inverse.is_a("IfcRelDefinesByType"): - if inverse.RelatingType == settings["product"]: + if inverse.RelatingType == product: ifcopenshell.api.type.unassign_type(file, related_objects=inverse.RelatedObjects) else: - ifcopenshell.api.type.unassign_type(file, related_objects=[settings["product"]]) + ifcopenshell.api.type.unassign_type(file, related_objects=[product]) elif inverse.is_a("IfcRelSpaceBoundary"): ifcopenshell.api.boundary.remove_boundary(file, boundary=inverse) elif inverse.is_a("IfcRelFillsElement"): @@ -142,7 +136,7 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelNests"): - if inverse.RelatingObject == settings["product"]: + if inverse.RelatingObject == product: inverse_id = inverse.id() for subelement in inverse.RelatedObjects: if subelement.is_a("IfcDistributionPort"): @@ -153,27 +147,27 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) - elif inverse.RelatedObjects == (settings["product"],): + elif inverse.RelatedObjects == (product,): history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelAggregates"): - if inverse.RelatingObject == settings["product"] or len(inverse.RelatedObjects) == 1: + if inverse.RelatingObject == product or len(inverse.RelatedObjects) == 1: history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelContainedInSpatialStructure"): - if inverse.RelatingStructure == settings["product"] or len(inverse.RelatedElements) == 1: + if inverse.RelatingStructure == product or len(inverse.RelatedElements) == 1: history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelConnectsElements"): if inverse.is_a("IfcRelConnectsWithRealizingElements"): - if settings["product"] not in (inverse.RelatingElement, inverse.RelatedElement) and any( - el for el in inverse.RealizingElements if el != settings["product"] + if product not in (inverse.RelatingElement, inverse.RelatedElement) and any( + el for el in inverse.RealizingElements if el != product ): continue history = inverse.OwnerHistory @@ -181,15 +175,15 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelConnectsPortToElement"): - if inverse.RelatedElement == settings["product"]: + if inverse.RelatedElement == product: ifcopenshell.api.root.remove_product(file, product=inverse.RelatingPort) - elif inverse.RelatingPort == settings["product"]: + elif inverse.RelatingPort == product: history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelConnectsPorts"): - if settings["product"] not in (inverse.RelatingPort, inverse.RelatedPort): + if product not in (inverse.RelatingPort, inverse.RelatedPort): # if it's not RelatingPort/RelatedPort then it's optional RealizingElement # so we keep the relationship continue @@ -204,7 +198,7 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelAssignsToProduct"): - if inverse.RelatingProduct == settings["product"]: + if inverse.RelatingProduct == product: history = inverse.OwnerHistory file.remove(inverse) if history: @@ -215,17 +209,17 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelFlowControlElements"): - if inverse.RelatingFlowElement == settings["product"]: + if inverse.RelatingFlowElement == product: history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) - elif inverse.RelatedControlElements == (settings["product"],): + elif inverse.RelatedControlElements == (product,): history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) - history = settings["product"].OwnerHistory - file.remove(settings["product"]) + history = product.OwnerHistory + file.remove(product) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py index 89037d4eb0..38d64aa1bc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py @@ -29,11 +29,8 @@ def add_task_time( (especially for maintenance tasks). :param task: The task to add time data to. - :type task: ifcopenshell.entity_instance :param is_recurring: Whether or not the time should recur. - :type is_recurring: bool :return: The newly created IfcTaskTime. - :rtype: ifcopenshell.entity_instance Example: @@ -61,11 +58,9 @@ def add_task_time( ifcopenshell.api.sequence.edit_task_time(model, task_time=time, attributes={"ScheduleStart": "2000-01-01", "ScheduleDuration": "P2D"}) """ - settings = {"task": task, "is_recurring": is_recurring} - - if settings["is_recurring"]: + if is_recurring: task_time = file.create_entity("IfcTaskTimeRecurring") else: task_time = file.create_entity("IfcTaskTime") - settings["task"].TaskTime = task_time + task.TaskTime = task_time return task_time diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py index 65ea1db9bd..2c08ab00a3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py @@ -34,17 +34,13 @@ def assign_lag_time( are allowed. :param rel_sequence: The IfcRelSequence to assign the lag time to. - :type rel_sequence: ifcopenshell.entity_instance :param lag_value: An ISO standardised duration string. - :type lag_value: str :param duration_type: Choose from WORKTIME for the associated calendar-based lag times (this is the most common scenario and is recommended as a default), or ELAPSEDTIME to not follow the calendar. You may also choose NOTDEFINED but the behaviour of this is unclear. - :type duration_type: str :return: The newly created IfcLagTime - :rtype: ifcopenshell.entity_instance Example: @@ -84,16 +80,10 @@ def assign_lag_time( # for whatever reason. ifcopenshell.api.sequence.assign_lag_time(model, rel_sequence=sequence, lag_value="P1D") """ - settings = { - "rel_sequence": rel_sequence, - "lag_value": lag_value, - "duration_type": duration_type, - } - - lag_value = file.createIfcDuration(ifcopenshell.util.date.datetime2ifc(settings["lag_value"], "IfcDuration")) - lag_time = file.create_entity("IfcLagTime", DurationType=settings["duration_type"], LagValue=lag_value) - if settings["rel_sequence"].is_a("IfcRelSequence"): - if settings["rel_sequence"].TimeLag and len(file.get_inverse(settings["rel_sequence"].TimeLag)) == 1: - file.remove(settings["rel_sequence"].TimeLag) - settings["rel_sequence"].TimeLag = lag_time + duration = file.create_entity("IfcDuration", ifcopenshell.util.date.datetime2ifc(lag_value, "IfcDuration")) + lag_time = file.create_entity("IfcLagTime", DurationType=duration_type, LagValue=duration) + if rel_sequence.is_a("IfcRelSequence"): + if (current_lag_time := rel_sequence.TimeLag) and file.get_total_inverses(current_lag_time) == 1: + file.remove(current_lag_time) + rel_sequence.TimeLag = lag_time return lag_time diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py index 0c64fb09e2..7f74fb300b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py @@ -65,11 +65,8 @@ def assign_recurrence_pattern( :param parent: Either an IfcTaskTimeRecurring if you are defining a recurring schedule for a task, or IfcWorkTime if you are defining a recurring pattern for a workdays or holidays in a calendar. - :type parent: ifcopenshell.entity_instance :param recurrence_type: One of the types of recurrences. - :type recurrence_type: str :return: The newly created IfcRecurrencePattern - :rtype: ifcopenshell.entity_instance Example: @@ -108,16 +105,14 @@ def assign_recurrence_pattern( ifcopenshell.api.sequence.edit_recurrence_pattern(model, recurrence_pattern=pattern, attributes={"DayComponent": [1], "Interval": 6}) """ - settings = {"parent": parent, "recurrence_type": recurrence_type} + recurrence = file.create_entity("IfcRecurrencePattern", recurrence_type) - recurrence = file.createIfcRecurrencePattern(settings["recurrence_type"]) - - if settings["parent"].is_a("IfcWorkTime"): - if settings["parent"].RecurrencePattern and len(file.get_inverse(settings["parent"].RecurrencePattern)) == 1: - file.remove(settings["parent"].RecurrencePattern) - settings["parent"].RecurrencePattern = recurrence - elif settings["parent"].is_a("IfcTaskTimeRecurring"): - if recurrence_old := settings["parent"].Recurrence and len(file.get_inverse(recurrence_old)) == 1: + if parent.is_a("IfcWorkTime"): + if (old_recurrence := parent.RecurrencePattern) and file.get_total_inverses(old_recurrence) == 1: + file.remove(old_recurrence) + parent.RecurrencePattern = recurrence + elif parent.is_a("IfcTaskTimeRecurring"): + if (recurrence_old := parent.Recurrence) and file.get_total_inverses(recurrence_old) == 1: file.remove(recurrence_old) - settings["parent"].Recurrence = recurrence + parent.Recurrence = recurrence return recurrence diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py index 664d5780ec..65491604aa 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py @@ -51,13 +51,10 @@ def assign_sequence( predecessor and successor tasks in the planning profession. :param relating_process: The previous / predecessor task. - :type relating_process: ifcopenshell.entity_instance :param related_process: The next / successor task. - :type related_process: ifcopenshell.entity_instance :param sequence_type: Choose from FINISH_START, FINISH_FINISH, START_START, or START_FINISH. :return: The newly created IfcRelSequence - :rtype: ifcopenshell.entity_instance Example: @@ -109,24 +106,18 @@ def assign_sequence( # to be 2000-01-05. ifcopenshell.api.sequence.cascade_schedule(model, task=formwork) """ - settings = { - "relating_process": relating_process, - "related_process": related_process, - "sequence_type": sequence_type, - } - - for rel in settings["related_process"].IsSuccessorFrom or []: - if rel.RelatingProcess == settings["relating_process"]: + for rel in related_process.IsSuccessorFrom or []: + if rel.RelatingProcess == relating_process: return rel rel = file.create_entity( "IfcRelSequence", **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatingProcess": settings["relating_process"], - "RelatedProcess": settings["related_process"], - "SequenceType": settings["sequence_type"], + "RelatingProcess": relating_process, + "RelatedProcess": related_process, + "SequenceType": sequence_type, } ) - ifcopenshell.api.sequence.cascade_schedule(file, task=settings["relating_process"]) + ifcopenshell.api.sequence.cascade_schedule(file, task=relating_process) return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py index 7c3f821e7e..d5f0e5947e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py @@ -20,6 +20,7 @@ import math import ifcopenshell.api.sequence import ifcopenshell.util.date import ifcopenshell.util.element +from typing import Union def calculate_task_duration(file: ifcopenshell.file, task: ifcopenshell.entity_instance) -> None: @@ -35,9 +36,7 @@ def calculate_task_duration(file: ifcopenshell.file, task: ifcopenshell.entity_i then nothing happens. :param task: The IfcTask to calculate the duration for. - :type task: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -82,18 +81,20 @@ def calculate_task_duration(file: ifcopenshell.file, task: ifcopenshell.entity_i """ usecase = Usecase() usecase.file = file - usecase.settings = {"task": task} - return usecase.execute() + return usecase.execute(task) class Usecase: - def execute(self): + file: ifcopenshell.file + + def execute(self, task: ifcopenshell.entity_instance) -> None: + self.task = task self.seconds_per_workday = self.calculate_seconds_per_workday() duration = self.calculate_max_resource_usage_duration() if duration: self.set_task_duration(duration) - def calculate_seconds_per_workday(self): + def calculate_seconds_per_workday(self) -> float: def get_work_schedule(task): for rel in task.HasAssignments or []: if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkSchedule"): @@ -102,7 +103,7 @@ class Usecase: return get_work_schedule(rel.RelatingObject) default_seconds_per_workday = 8 * 60 * 60 - work_schedule = get_work_schedule(self.settings["task"]) + work_schedule = get_work_schedule(self.task) if not work_schedule: return default_seconds_per_workday psets = ifcopenshell.util.element.get_psets(work_schedule) @@ -115,9 +116,9 @@ class Usecase: work_day_duration = ifcopenshell.util.date.ifc2datetime(psets["Pset_WorkControlCommon"]["WorkDayDuration"]) return work_day_duration.seconds - def calculate_max_resource_usage_duration(self): + def calculate_max_resource_usage_duration(self) -> float: max_duration = 0 - for rel in self.settings["task"].OperatesOn or []: + for rel in self.task.OperatesOn or []: for related_object in rel.RelatedObjects: if related_object.is_a("IfcConstructionResource"): duration = self.calculate_duration_in_days(related_object) @@ -125,7 +126,7 @@ class Usecase: max_duration = duration return max_duration - def calculate_duration_in_days(self, resource): + def calculate_duration_in_days(self, resource: ifcopenshell.entity_instance) -> Union[float, None]: def is_hourly_work(schedule_work): return "T" in schedule_work @@ -140,7 +141,7 @@ class Usecase: schedule_seconds = (schedule_duration.days + partial_days) * self.seconds_per_workday return math.ceil((schedule_seconds / self.seconds_per_workday) / schedule_usage) - def set_task_duration(self, duration): - if not self.settings["task"].TaskTime: - ifcopenshell.api.sequence.add_task_time(self.file, task=self.settings["task"]) - self.settings["task"].TaskTime.ScheduleDuration = f"P{duration}D" + def set_task_duration(self, duration: float) -> None: + if not (task_time := self.task.TaskTime): + task_time = ifcopenshell.api.sequence.add_task_time(self.file, task=self.task) + task_time.ScheduleDuration = f"P{duration}D" diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py index 8a989ecb55..65cbd5a58a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py @@ -19,6 +19,8 @@ import datetime import ifcopenshell.util.date import ifcopenshell.util.sequence +from ifcopenshell.util.sequence import DURATION_TYPE +from typing import Union, Optional def cascade_schedule(file: ifcopenshell.file, task: ifcopenshell.entity_instance) -> None: @@ -41,9 +43,7 @@ def cascade_schedule(file: ifcopenshell.file, task: ifcopenshell.entity_instance be equivalent to be Tuesday 8am, for instance. :param task: The start task to begin cascading from. - :type task: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -103,16 +103,22 @@ def cascade_schedule(file: ifcopenshell.file, task: ifcopenshell.entity_instance """ usecase = Usecase() usecase.file = file - usecase.settings = {"task": task} - return usecase.execute() + return usecase.execute(task) class Usecase: - def execute(self): - self.calendar_cache = {} - self.cascade_task(self.settings["task"], is_first_task=True) + file: ifcopenshell.file - def cascade_task(self, task, is_first_task=False, task_sequence=None): + def execute(self, task: ifcopenshell.entity_instance): + self.calendar_cache = {} + self.cascade_task(task, is_first_task=True) + + def cascade_task( + self, + task: ifcopenshell.entity_instance, + is_first_task: bool = False, + task_sequence: Optional[list[ifcopenshell.entity_instance]] = None, + ) -> None: if task_sequence is None: task_sequence = [] @@ -316,18 +322,22 @@ class Usecase: for nested_task in rel.RelatedObjects or [] ] - def get_lag_time_days(self, lag_time): + def get_lag_time_days(self, lag_time: ifcopenshell.entity_instance) -> int: return ifcopenshell.util.date.ifc2datetime(lag_time.LagValue.wrappedValue).days - def get_calendar(self, task): + def get_calendar(self, task: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: if task.id() not in self.calendar_cache: self.calendar_cache[task.id()] = ifcopenshell.util.sequence.derive_calendar(task) return self.calendar_cache[task.id()] - def offset_date(self, date, days, duration_type, calendar): + def offset_date( + self, date: datetime.datetime, days: int, duration_type: DURATION_TYPE, calendar: ifcopenshell.entity_instance + ) -> datetime.datetime: return ifcopenshell.util.sequence.offset_date(date, datetime.timedelta(days=days), duration_type, calendar) - def get_task_time_attribute(self, task, attribute): + def get_task_time_attribute( + self, task: ifcopenshell.entity_instance, attribute: str + ) -> Union[datetime.datetime, None]: if task.TaskTime: value = getattr(task.TaskTime, attribute) if value: diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py index d0670b76e8..ab1035bbab 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py @@ -24,7 +24,7 @@ import ifcopenshell.guid import ifcopenshell.util.element import ifcopenshell.util.sequence import ifcopenshell.util.system -from typing import Optional +from typing import Optional, Union def create_baseline( @@ -44,11 +44,8 @@ def create_baseline( * Same Resource Relationships :param work_schedule: The planned work_schedule to baseline - :type work_schedule: ifcopenshell.entity_instance :param name: baseline work schedule name - :type name: str, optional :return: The baseline work_schedule - :rtype: ifcopenshell.entity_instance Example: @@ -62,33 +59,34 @@ def create_baseline( """ usecase = Usecase() usecase.file = file - usecase.settings = {"work_schedule": work_schedule, "name": name} - return usecase.execute() + return usecase.execute(work_schedule, name) class Usecase: - def execute(self): - result = self.create_baseline_work_schedule(self.settings["work_schedule"]) - return result + file: ifcopenshell.file - def create_baseline_work_schedule(self, work_schedule): + def execute(self, work_schedule: ifcopenshell.entity_instance, name: Union[str, None]) -> None: # create work schedule if not work_schedule.PredefinedType == "PLANNED": return baseline_work_schedule = ifcopenshell.api.sequence.add_work_schedule( self.file, name=work_schedule.Name, predefined_type="BASELINE" ) - baseline_work_schedule.Name = self.settings["name"] + baseline_work_schedule.Name = name self.create_baseline_reference(work_schedule, baseline_work_schedule) for summary_task in ifcopenshell.util.sequence.get_root_tasks(work_schedule): - current, duplicate = ifcopenshell.api.sequence.duplicate_task(self.file, task=summary_task) + res = ifcopenshell.api.sequence.duplicate_task(self.file, task=summary_task) + assert isinstance(res, list) + current, duplicate = res ifcopenshell.api.control.assign_control( self.file, relating_control=baseline_work_schedule, related_object=duplicate[0] ) for i, task in enumerate(current): self.create_baseline_reference(task, duplicate[i]) - def create_baseline_reference(self, relating_object, related_object): + def create_baseline_reference( + self, relating_object: ifcopenshell.entity_instance, related_object: ifcopenshell.entity_instance + ) -> ifcopenshell.entity_instance: referenced_by = None if relating_object.Declares: referenced_by = relating_object.Declares[0] diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py index db47ccd5ec..7f974bb84e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py @@ -23,9 +23,12 @@ import ifcopenshell.api.owner import ifcopenshell.api.sequence import ifcopenshell.util.element import ifcopenshell.util.sequence +from typing import Union, Any -def duplicate_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: +def duplicate_task( + file: ifcopenshell.file, task: ifcopenshell.entity_instance +) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]: """Duplicates a task in the project The following relationships are also duplicated: @@ -35,9 +38,7 @@ def duplicate_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance) * The copy will have duplicated nested tasks :param task: The task to be duplicated - :type task: ifcopenshell.entity_instance :return: The duplicated task or the list of duplicated tasks if the latter has children - :rtype: ifcopenshell.entity_instance or list of ifcopenshell.entity_instance Example: .. code:: python @@ -55,6 +56,9 @@ def duplicate_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance) class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): self.tracker = {"current": [], "duplicate": []} self.duplicate_task(self.settings["task"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_lag_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_lag_time.py index d3c8a4418a..d55bf0e4b5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_lag_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_lag_time.py @@ -28,11 +28,8 @@ def edit_lag_time(file: ifcopenshell.file, lag_time: ifcopenshell.entity_instanc IfcLagTime, consult the IFC documentation. :param lag_time: The IfcLagTime entity you want to edit - :type lag_time: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -75,14 +72,12 @@ def edit_lag_time(file: ifcopenshell.file, lag_time: ifcopenshell.entity_instanc # Or, let's make it 2 days instead. ifcopenshell.api.sequence.edit_lag_time(model, lag_time=lag, attributes={"LagValue": "P2D"}) """ - settings = {"lag_time": lag_time, "attributes": attributes} - - for name, value in settings["attributes"].items(): + for name, value in attributes.items(): if name == "LagValue" and value is not None: if isinstance(value, float): value = file.createIfcRatioMeasure(value) else: value = file.createIfcDuration(ifcopenshell.util.date.datetime2ifc(value, "IfcDuration")) - setattr(settings["lag_time"], name, value) - for rel in [r for r in file.get_inverse(settings["lag_time"]) if r.is_a("IfcRelSequence")]: + setattr(lag_time, name, value) + for rel in [r for r in file.get_inverse(lag_time) if r.is_a("IfcRelSequence")]: ifcopenshell.api.sequence.cascade_schedule(file, task=rel.RelatedProcess) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_recurrence_pattern.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_recurrence_pattern.py index 6b165f716a..519c5cdf25 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_recurrence_pattern.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_recurrence_pattern.py @@ -30,11 +30,8 @@ def edit_recurrence_pattern( IfcRecurrencePattern, consult the IFC documentation. :param recurrence_pattern: The IfcRecurrencePattern entity you want to edit - :type recurrence_pattern: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -55,13 +52,8 @@ def edit_recurrence_pattern( ifcopenshell.api.sequence.edit_recurrence_pattern(model, recurrence_pattern=pattern, attributes={"WeekdayComponent": [1, 2, 3, 4, 5]}) """ - settings = { - "recurrence_pattern": recurrence_pattern, - "attributes": attributes, - } - - for name, value in settings["attributes"].items(): - setattr(settings["recurrence_pattern"], name, value) + for name, value in attributes.items(): + setattr(recurrence_pattern, name, value) ifcopenshell.util.sequence.is_working_day.cache_clear() ifcopenshell.util.sequence.is_calendar_applicable.cache_clear() diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_sequence.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_sequence.py index dee8a32e64..2686ec5ace 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_sequence.py @@ -30,11 +30,8 @@ def edit_sequence( IfcRelSequence, consult the IFC documentation. :param rel_sequence: The IfcRelSequence entity you want to edit - :type rel_sequence: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -62,9 +59,7 @@ def edit_sequence( ifcopenshell.api.sequence.edit_sequence(model, rel_sequence=sequence, attributes={"SequenceType": "START_START"}) """ - settings = {"rel_sequence": rel_sequence, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["rel_sequence"], name, value) - if "SequenceType" in settings["attributes"].keys(): - ifcopenshell.api.sequence.cascade_schedule(file, task=settings["rel_sequence"].RelatedProcess) + for name, value in attributes.items(): + setattr(rel_sequence, name, value) + if "SequenceType" in attributes.keys(): + ifcopenshell.api.sequence.cascade_schedule(file, task=rel_sequence.RelatedProcess) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py index dcd218059f..3fa1b88caf 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py @@ -26,11 +26,8 @@ def edit_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance, attri IfcTask, consult the IFC documentation. :param task: The IfcTask entity you want to edit - :type task: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -48,7 +45,5 @@ def edit_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance, attri # Change the identification ifcopenshell.api.sequence.edit_task(model, task=task, attributes={"Identification": "M"}) """ - settings = {"task": task, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["task"], name, value) + for name, value in attributes.items(): + setattr(task, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py index 6d9d3404d1..119ab82a14 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py @@ -36,11 +36,8 @@ def edit_task_time( IfcTaskTime, consult the IFC documentation. :param task_time: The IfcTaskTime entity you want to edit - :type task_time: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -61,94 +58,89 @@ def edit_task_time( """ usecase = Usecase() usecase.file = file - usecase.settings = {"task_time": task_time, "attributes": attributes} - return usecase.execute() + return usecase.execute(task_time, attributes) class Usecase: - def execute(self): + file: ifcopenshell.file + + def execute(self, task_time: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: + self.task_time = task_time self.task = self.get_task() self.calendar = ifcopenshell.util.sequence.derive_calendar(self.task) # If the user specifies both an end date and a duration, the duration takes priority - if ( - self.settings["attributes"].get("ScheduleDuration", None) - and "ScheduleFinish" in self.settings["attributes"].keys() - ): - del self.settings["attributes"]["ScheduleFinish"] + if attributes.get("ScheduleDuration", None) and "ScheduleFinish" in attributes.keys(): + del attributes["ScheduleFinish"] - duration_type = self.settings["attributes"].get("DurationType", self.settings["task_time"].DurationType) - finish = self.settings["attributes"].get("ScheduleFinish", None) + duration_type = attributes.get("DurationType", self.task_time.DurationType) + finish = attributes.get("ScheduleFinish", None) if finish: if isinstance(finish, str): finish = datetime.datetime.fromisoformat(finish) - self.settings["attributes"]["ScheduleFinish"] = datetime.datetime.combine( + attributes["ScheduleFinish"] = datetime.datetime.combine( ifcopenshell.util.sequence.get_soonest_working_day(finish, duration_type, self.calendar), datetime.time(17), ) - start = self.settings["attributes"].get("ScheduleStart", None) + start = attributes.get("ScheduleStart", None) if start: if isinstance(start, str): start = datetime.datetime.fromisoformat(start) - self.settings["attributes"]["ScheduleStart"] = datetime.datetime.combine( + attributes["ScheduleStart"] = datetime.datetime.combine( ifcopenshell.util.sequence.get_soonest_working_day(start, duration_type, self.calendar), datetime.time(9), ) - for name, value in self.settings["attributes"].items(): + for name, value in attributes.items(): if value is not None: if "Start" in name or "Finish" in name or name == "StatusTime": value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime") elif name == "ScheduleDuration" or name == "ActualDuration" or name == "RemainingTime": value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration") - setattr(self.settings["task_time"], name, value) + setattr(self.task_time, name, value) - if ( - "ScheduleDuration" in self.settings["attributes"].keys() - and self.settings["task_time"].ScheduleDuration - and self.settings["task_time"].ScheduleStart - ): + if "ScheduleDuration" in attributes.keys() and task_time.ScheduleDuration and task_time.ScheduleStart: self.calculate_finish() - elif self.settings["attributes"].get("ScheduleStart", None) and self.settings["task_time"].ScheduleDuration: + elif attributes.get("ScheduleStart", None) and task_time.ScheduleDuration: self.calculate_finish() - elif self.settings["attributes"].get("ScheduleFinish", None) and self.settings["task_time"].ScheduleStart: + elif attributes.get("ScheduleFinish", None) and task_time.ScheduleStart: self.calculate_duration() - if self.settings["task_time"].ScheduleDuration and ( - "ScheduleStart" in self.settings["attributes"].keys() - or "ScheduleFinish" in self.settings["attributes"].keys() - or "ScheduleDuration" in self.settings["attributes"].keys() + if task_time.ScheduleDuration and ( + "ScheduleStart" in attributes.keys() + or "ScheduleFinish" in attributes.keys() + or "ScheduleDuration" in attributes.keys() ): ifcopenshell.api.sequence.cascade_schedule(self.file, task=self.task) - if self.settings["task_time"].ScheduleDuration: + if task_time.ScheduleDuration: self.handle_resource_calculation() def calculate_finish(self): finish = ifcopenshell.util.sequence.get_start_or_finish_date( - ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleStart), - ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleDuration), - self.settings["task_time"].DurationType, + ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleStart), + ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleDuration), + self.task_time.DurationType, self.calendar, date_type="FINISH", ) - self.settings["task_time"].ScheduleFinish = ifcopenshell.util.date.datetime2ifc(finish, "IfcDateTime") + self.task_time.ScheduleFinish = ifcopenshell.util.date.datetime2ifc(finish, "IfcDateTime") def calculate_duration(self): - start = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleStart) - finish = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleFinish) + start = ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleStart) + finish = ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleFinish) current_date = datetime.date(start.year, start.month, start.day) finish_date = datetime.date(finish.year, finish.month, finish.day) duration = datetime.timedelta(days=1) while current_date < finish_date: - if self.settings["task_time"].DurationType == "ELAPSEDTIME" or not self.calendar: + if self.task_time.DurationType == "ELAPSEDTIME" or not self.calendar: duration += datetime.timedelta(days=1) elif ifcopenshell.util.sequence.is_working_day(current_date, self.calendar): duration += datetime.timedelta(days=1) current_date += datetime.timedelta(days=1) - self.settings["task_time"].ScheduleDuration = ifcopenshell.util.date.datetime2ifc(duration, "IfcDuration") + self.task_time.ScheduleDuration = ifcopenshell.util.date.datetime2ifc(duration, "IfcDuration") def get_task(self) -> ifcopenshell.entity_instance: - return next(e for e in self.file.get_inverse(self.settings["task_time"]) if e.is_a("IfcTask")) + return next(e for e in self.file.get_inverse(self.task_time) if e.is_a("IfcTask")) def handle_resource_calculation(self): resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_deep=False) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_calendar.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_calendar.py index f82f53e534..83e56a4372 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_calendar.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_calendar.py @@ -28,11 +28,8 @@ def edit_work_calendar( IfcWorkCalendar, consult the IFC documentation. :param work_calendar: The IfcWorkCalendar entity you want to edit - :type work_calendar: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -45,7 +42,5 @@ def edit_work_calendar( ifcopenshell.api.sequence.edit_work_calendar(model, work_calendar=calendar, attributes={"Description": "Monday to Friday 8 hour days"}) """ - settings = {"work_calendar": work_calendar, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["work_calendar"], name, value) + for name, value in attributes.items(): + setattr(work_calendar, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_plan.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_plan.py index 920e2c671e..145460e11c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_plan.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_plan.py @@ -29,11 +29,8 @@ def edit_work_plan( IfcWorkPlan, consult the IFC documentation. :param work_plan: The IfcWorkPlan entity you want to edit - :type work_plan: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -46,12 +43,10 @@ def edit_work_plan( ifcopenshell.api.sequence.edit_work_plan(model, work_plan=work_plan, attributes={"Description": "Construction of phase 1"}) """ - settings = {"work_plan": work_plan, "attributes": attributes} - - for name, value in settings["attributes"].items(): + for name, value in attributes.items(): if value: if "Date" in name or "Time" in name: value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime") elif name == "Duration" or name == "TotalFloat": value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration") - setattr(settings["work_plan"], name, value) + setattr(work_plan, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py index 5ed392ddff..98905b9a1f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py @@ -29,11 +29,8 @@ def edit_work_schedule( IfcWorkSchedule, consult the IFC documentation. :param work_schedule: The IfcWorkSchedule entity you want to edit - :type work_schedule: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -50,12 +47,10 @@ def edit_work_schedule( ifcopenshell.api.sequence.edit_work_schedule(model, work_schedule=work_schedule, attributes={"Description": "3 crane design option"}) """ - settings = {"work_schedule": work_schedule, "attributes": attributes} - - for name, value in settings["attributes"].items(): + for name, value in attributes.items(): if value: if "Date" in name or "Time" in name: value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime") elif name == "Duration" or name == "TotalFloat": value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration") - setattr(settings["work_schedule"], name, value) + setattr(work_schedule, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py index 07c1f818d5..7fae8b4c18 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py @@ -31,11 +31,8 @@ def edit_work_time( IfcWorkTime, consult the IFC documentation. :param work_time: The IfcWorkTime entity you want to edit - :type work_time: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -54,16 +51,14 @@ def edit_work_time( ifcopenshell.api.sequence.edit_work_time(model, work_time=work_time, attributes={"StartDate": "2000-01-01", "FinishDate": "2000-01-02"}) """ - settings = {"work_time": work_time, "attributes": attributes} - - for name, value in settings["attributes"].items(): + for name, value in attributes.items(): if name in ("Start", "StartDate"): value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate") # 4 IfcWorktime Start - settings["work_time"][4] = value + work_time[4] = value elif name in ("Finish", "FinishDate"): value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate") # 5 IfcWorktime Finish - settings["work_time"][5] = value + work_time[5] = value else: - setattr(settings["work_time"], name, value) + setattr(work_time, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py index a7fb9d01e6..14e82822e3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py @@ -35,9 +35,7 @@ def recalculate_schedule(file: ifcopenshell.file, work_schedule: ifcopenshell.en error. :param work_schedule: The IfcWorkSchedule to perform the calculation on. - :type work_schedule: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -50,12 +48,14 @@ def recalculate_schedule(file: ifcopenshell.file, work_schedule: ifcopenshell.en """ usecase = Usecase() usecase.file = file - usecase.settings = {"work_schedule": work_schedule} - return usecase.execute() + return usecase.execute(work_schedule) class Usecase: - def execute(self): + file: ifcopenshell.file + + def execute(self, work_schedule: ifcopenshell.entity_instance) -> None: + self.work_schedule = work_schedule # The method implemented is the same as shown here: # https://www.youtube.com/watch?v=qTErIV6OqLg self.start_dates = [] @@ -88,7 +88,6 @@ class Usecase: if is_cyclic: raise RecursionError("Task graph is cyclic and so critical path method cannot be performed.") - return self.pending_nodes = set(self.g.nodes) while self.pending_nodes: @@ -100,7 +99,7 @@ class Usecase: self.update_task_times() - def build_network_graph(self): + def build_network_graph(self) -> None: self.sequence_type_map = { None: "FS", "START_START": "SS", @@ -114,14 +113,14 @@ class Usecase: self.edges = [] self.g.add_node("start", duration=0, duration_type="ELAPSEDTIME", calendar=None) self.g.add_node("finish", duration=0, duration_type="ELAPSEDTIME", calendar=None) - for rel in self.settings["work_schedule"].Controls: + for rel in self.work_schedule.Controls: for related_object in rel.RelatedObjects: if not related_object.is_a("IfcTask"): continue self.add_node(related_object) self.g.add_edges_from(self.edges) - def add_node(self, task): + def add_node(self, task: ifcopenshell.entity_instance) -> None: if task.IsNestedBy: for rel in task.IsNestedBy: [self.add_node(o) for o in rel.RelatedObjects] @@ -176,7 +175,7 @@ class Usecase: if not successor_types: self.edges.append((task.id(), "finish", {"lag_time": 0, "type": "FF"})) - def update_task_times(self): + def update_task_times(self) -> None: for ifc_definition_id in self.g.nodes: if ifc_definition_id in ("start", "finish"): continue @@ -198,12 +197,12 @@ class Usecase: }, ) - def offset_date(self, date, days, node): + def offset_date(self, date: datetime.datetime, days: int, node: dict) -> datetime.datetime: return ifcopenshell.util.sequence.offset_date( date, datetime.timedelta(days=days), node["duration_type"], node["calendar"] ) - def forward_pass(self, node): + def forward_pass(self, node) -> bool: successors = self.g.successors(node) predecessors = list(self.g.predecessors(node)) data = self.g.nodes[node] @@ -326,7 +325,7 @@ class Usecase: return True - def backward_pass(self, node): + def backward_pass(self, node) -> bool: successors = list(self.g.successors(node)) predecessors = self.g.predecessors(node) data = self.g.nodes[node] @@ -496,12 +495,12 @@ class Usecase: def calculate_free_float( self, - predecessor_date, - successor_date, - lag_time, - predecessor_data, - successor_data, - ): + predecessor_date: datetime.datetime, + successor_date: datetime.datetime, + lag_time: int, + predecessor_data: dict, + successor_data: dict, + ) -> datetime.timedelta: if not lag_time: min_successor_date = successor_date else: diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py index 08ffcd9f40..b07a630684 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py @@ -25,9 +25,7 @@ def unassign_lag_time(file: ifcopenshell.file, rel_sequence: ifcopenshell.entity The schedule is cascaded afterwards. :param rel_sequence: The sequence to remove the lag time from. - :type rel_sequence: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -57,12 +55,8 @@ def unassign_lag_time(file: ifcopenshell.file, rel_sequence: ifcopenshell.entity # What if you didn't? ifcopenshell.api.sequence.unassign_lag_time(model, rel_sequence=sequence) """ - settings = { - "rel_sequence": rel_sequence, - } - - if len(file.get_inverse(settings["rel_sequence"].TimeLag)) == 1: - file.remove(settings["rel_sequence"].TimeLag) + if file.get_total_inverses((current_lag_time := rel_sequence.TimeLag)) == 1: + file.remove(current_lag_time) else: - settings["rel_sequence"].TimeLag = None - ifcopenshell.api.sequence.cascade_schedule(file, task=settings["rel_sequence"].RelatedProcess) + rel_sequence.TimeLag = None + ifcopenshell.api.sequence.cascade_schedule(file, task=rel_sequence.RelatedProcess) diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py index bb31cd7c7d..abd66e7fa8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py @@ -29,12 +29,10 @@ def dereference_structure( """Dereferences a list of products and space :param products: The list of physical IfcElements that exists in the space. - :type products: list[ifcopenshell.entity_instance] :param relating_structure: The IfcSpatialStructureElement element, such as IfcBuilding, IfcBuildingStorey, or IfcSpace that the element exists in. :return: None - :rtype: None Example: @@ -68,14 +66,12 @@ def dereference_structure( # Actually, it only goes up to storey 2. ifcopenshell.api.spatial.dereference_structure(model, products=[column], relating_structure=storey3) """ - settings = {"products": products, "relating_structure": relating_structure} - - products = set(settings["products"]) - for rel in settings["relating_structure"].ReferencesElements: + products_set = set(products) + for rel in relating_structure.ReferencesElements: related_elements = set(rel.RelatedElements) - if not related_elements.intersection(products): + if not related_elements.intersection(products_set): continue - related_elements = related_elements - products + related_elements = related_elements - products_set if related_elements: rel.RelatedElements = list(related_elements) ifcopenshell.api.owner.update_owner_history(file, **{"element": rel}) diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py index 2897219e68..41452244d1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py @@ -46,14 +46,11 @@ def reference_structure( spaces simultaneously. :param products: The list of physical IfcElements that exists in the space. - :type products: list[ifcopenshell.entity_instance] :param relating_structure: The IfcSpatialStructureElement element, such as IfcBuilding, IfcBuildingStorey, or IfcSpace that the element exists in. - :type relating_structure: ifcopenshell.entity_instance :return: The IfcRelReferencedInSpatialStructure relationship instance or `None` if `products` was an empty list. - :rtype: Union[ifcopenshell.entity_instance, None] Example: @@ -85,19 +82,16 @@ def reference_structure( model, products=[column], relating_structure=[storey2, storey3] ) """ - settings = { - "products": products, - "relating_structure": relating_structure, - } - structure = settings["relating_structure"] - products = set(settings["products"]) + structure = relating_structure + products_set = set(products) - if not products: + if not products_set: return referenced = ifcopenshell.util.element.get_structure_referenced_elements(structure) - products_to_assign = products - referenced + products_to_assign = products_set - referenced + rel: Union[ifcopenshell.entity_instance, None] rel = next(iter(structure.ReferencesElements), None) if not products_to_assign: diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_analysis_model.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_analysis_model.py index 4b7d94a4a2..65c03eb238 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_analysis_model.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_analysis_model.py @@ -28,14 +28,8 @@ def edit_structural_analysis_model( IfcStructuralAnalysisModel, consult the IFC documentation. :param structural_analysis_model: The IfcStructuralAnalysisModel entity you want to edit - :type structural_analysis_model: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None """ - settings = {"structural_analysis_model": structural_analysis_model, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["structural_analysis_model"], name, value) - return settings["structural_analysis_model"] + for name, value in attributes.items(): + setattr(structural_analysis_model, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py index 54963415df..3d33e4e8ad 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py @@ -28,19 +28,14 @@ def edit_structural_boundary_condition( IfcBoundaryCondition, consult the IFC documentation. :param condition: The IfcBoundaryCondition entity you want to edit - :type condition: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None """ - settings = {"condition": condition, "attributes": attributes} - - for name, data in settings["attributes"].items(): + for name, data in attributes.items(): if data["type"] == "string" or data["type"] == "null": value = data["value"] elif data["type"] == "IfcBoolean": value = file.createIfcBoolean(data["value"]) else: value = file.create_entity(data["type"], data["value"]) - setattr(settings["condition"], name, value) + setattr(condition, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py index 0d569c5f56..38befd9c6f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py @@ -16,42 +16,33 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell +from ifcopenshell.util.shape_builder import VectorType, ifc_safe_vector_type def edit_structural_connection_cs( file: ifcopenshell.file, structural_item: ifcopenshell.entity_instance, - axis: tuple[float, float, float] = (0.0, 0.0, 1.0), - ref_direction: tuple[float, float, float] = (1.0, 0.0, 0.0), + axis: VectorType = (0.0, 0.0, 1.0), + ref_direction: VectorType = (1.0, 0.0, 0.0), ) -> None: """Edits the coordinate system of a structural connection :param structural_item: The IfcStructuralItem you want to modify. - :type structural_item: ifcopenshell.entity_instance :param axis: The unit Z axis vector defined as a list of 3 floats. Defaults to (0., 0., 1.). - :type axis: tuple[float, float, float] :param ref_direction: The unit X axis vector defined as a list of 3 floats. Defaults to (1., 0., 0.). - :type ref_direction: tuple[float, float, float] :return: None - :rtype: None """ - settings = { - "structural_item": structural_item, - "axis": axis, - "ref_direction": ref_direction, - } - - if settings["structural_item"].ConditionCoordinateSystem is None: + if structural_item.ConditionCoordinateSystem is None: point = file.createIfcCartesianPoint((0.0, 0.0, 0.0)) ccs = file.createIfcAxis2Placement3D(point, None, None) - settings["structural_item"].ConditionCoordinateSystem = ccs + structural_item.ConditionCoordinateSystem = ccs - ccs = settings["structural_item"].ConditionCoordinateSystem - if ccs.Axis and len(file.get_inverse(ccs.Axis)) == 1: - file.remove(ccs.Axis) - ccs.Axis = file.createIfcDirection(settings["axis"]) - if ccs.RefDirection and len(file.get_inverse(ccs.RefDirection)) == 1: - file.remove(ccs.RefDirection) - ccs.RefDirection = file.createIfcDirection(settings["ref_direction"]) + ccs = structural_item.ConditionCoordinateSystem + if (current_axis := ccs.Axis) and file.get_total_inverses(current_axis) == 1: + file.remove(current_axis) + ccs.Axis = file.create_entity("IfcDirection", ifc_safe_vector_type(axis)) + if (prev_ref_direction := ccs.RefDirection) and file.get_total_inverses(prev_ref_direction) == 1: + file.remove(prev_ref_direction) + ccs.RefDirection = file.create_entity("IfcDirection", ifc_safe_vector_type(ref_direction)) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py index 8b90457a47..d3858bec95 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py @@ -16,25 +16,21 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell +from ifcopenshell.util.shape_builder import VectorType, ifc_safe_vector_type def edit_structural_item_axis( file: ifcopenshell.file, structural_item: ifcopenshell.entity_instance, - axis: tuple[float, float, float] = (0.0, 0.0, 1.0), + axis: VectorType = (0.0, 0.0, 1.0), ) -> None: """Edits the coordinate system of a structural connection :param structural_item: The IfcStructuralItem you want to modify. - :type structural_item: ifcopenshell.entity_instance :param axis: The unit Z axis vector defined as a list of 3 floats. Defaults to (0., 0., 1.). - :type axis: tuple[float, float, float] :return: None - :rtype: None """ - settings = {"structural_item": structural_item, "axis": axis} - - if len(file.get_inverse(settings["structural_item"].Axis)) == 1: - file.remove(settings["structural_item"].Axis) - settings["structural_item"].Axis = file.createIfcDirection(settings["axis"]) + if file.get_total_inverses(axis_dir := structural_item.Axis) == 1: + file.remove(axis_dir) + structural_item.Axis = file.create_entity("IfcDirection", ifc_safe_vector_type(axis)) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load.py index 20298d1c0f..faa218a7a3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load.py @@ -28,13 +28,8 @@ def edit_structural_load( IfcStructuralLoad, consult the IFC documentation. :param structural_load: The IfcStructuralLoad entity you want to edit - :type structural_load: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None """ - settings = {"structural_load": structural_load, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["structural_load"], name, value) + for name, value in attributes.items(): + setattr(structural_load, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load_case.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load_case.py index 59231fb9e4..02948c637d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load_case.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load_case.py @@ -28,13 +28,8 @@ def edit_structural_load_case( IfcStructuralLoadCase, consult the IFC documentation. :param load_case: The IfcStructuralLoadCase entity you want to edit - :type load_case: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None """ - settings = {"load_case": load_case, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["load_case"], name, value) + for name, value in attributes.items(): + setattr(load_case, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py index 8135bc0255..144b96f5a9 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py @@ -29,18 +29,14 @@ def remove_structural_analysis_model( :param structural_analysis_model: The IfcStructuralAnalysisModel to remove. - :type structural_analysis_model: ifcopenshell.entity_instance :return: None - :rtype: None """ - settings = {"structural_analysis_model": structural_analysis_model} - - for rel in settings["structural_analysis_model"].IsGroupedBy or []: + for rel in structural_analysis_model.IsGroupedBy or []: history = rel.OwnerHistory file.remove(rel) if history: ifcopenshell.util.element.remove_deep2(file, history) - history = settings["structural_analysis_model"].OwnerHistory - file.remove(settings["structural_analysis_model"]) + history = structural_analysis_model.OwnerHistory + file.remove(structural_analysis_model) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py index bd9018c1e2..d31811a739 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py @@ -24,27 +24,25 @@ def remove_structural_boundary_condition( connection: Optional[ifcopenshell.entity_instance] = None, boundary_condition: Optional[ifcopenshell.entity_instance] = None, ) -> None: - """Removes a condition from a connection, or an orphased boundary condition + """Removes a condition from a connection, or an orphaned boundary condition :param connection: The IfcStructuralConnection to remove the condition from. If omitted, it is assumed to be an orphaned condition. - :type connection: ifcopenshell.entity_instance,optional :param boundary_condition: The IfcBoundaryCondition to remove. - :type boundary_condition: ifcopenshell.entity_instance, optional. :return: None - :rtype: None """ - settings = {"connection": connection, "boundary_condition": boundary_condition} - if settings["connection"]: + if connection: # remove boundary condition from a connection - if not settings["connection"].AppliedCondition: + if not connection.AppliedCondition: return - if len(file.get_inverse(settings["connection"].AppliedCondition)) == 1: - file.remove(settings["connection"].AppliedCondition) - settings["connection"].AppliedCondition = None + applied_condition = connection.AppliedCondition + if file.get_total_inverses(applied_condition) == 1: + file.remove(applied_condition) + connection.AppliedCondition = None else: + assert boundary_condition, "Either connection or boundary_condition must be provided." # remove the boundary condition - for conn in file.get_inverse(settings["boundary_condition"]): + for conn in file.get_inverse(boundary_condition): conn.AppliedCondition = None - file.remove(settings["boundary_condition"]) + file.remove(boundary_condition) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py index 1d406a16e8..0c1a6ad3a1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py @@ -22,10 +22,6 @@ def remove_structural_load(file: ifcopenshell.file, structural_load: ifcopenshel """Removes a structural load :param structural_load: The IfcStructuralLoad to remove. - :type structural_load: ifcopenshell.entity_instance :return: None - :rtype: None """ - settings = {"structural_load": structural_load} - - file.remove(settings["structural_load"]) + file.remove(structural_load) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py index 1d9473515a..401db40dbc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py @@ -25,18 +25,14 @@ def remove_structural_load_case(file: ifcopenshell.file, load_case: ifcopenshell """Removes a structural load case :param load_case: The IfcStructuralLoadCase to remove. - :type load_case: ifcopenshell.entity_instance :return: None - :rtype: None """ - settings = {"load_case": load_case} - # TODO: do a deep purge - for rel in settings["load_case"].IsGroupedBy or []: + for rel in load_case.IsGroupedBy or []: history = rel.OwnerHistory file.remove(rel) if history: ifcopenshell.util.element.remove_deep2(file, history) - history = settings["load_case"].OwnerHistory - file.remove(settings["load_case"]) + history = load_case.OwnerHistory + file.remove(load_case) ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py index 281630fd7a..25acbbe9cc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py @@ -25,20 +25,16 @@ def remove_structural_load_group(file: ifcopenshell.file, load_group: ifcopenshe """Removes a structural load group :param load_group: The IfcStructuralLoadGroup to remove. - :type load_group: ifcopenshell.entity_instance :return: None - :rtype: None """ - settings = {"load_group": load_group} - # TODO: do a deep purge - for inverse in file.get_inverse(settings["load_group"]): + for inverse in file.get_inverse(load_group): if inverse.is_a("IfcRelAssignsToGroup") and len(inverse.RelatedObjects) == 1: history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) - history = settings["load_group"].OwnerHistory - file.remove(settings["load_group"]) + history = load_group.OwnerHistory + file.remove(load_group) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py index 408f8e7906..944240e618 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py @@ -35,7 +35,7 @@ def add_surface_style( style: ifcopenshell.entity_instance, ifc_class: SURFACE_STYLE_TYPES = "IfcSurfaceStyleShading", attributes: Optional[dict[str, Any]] = None, -) -> None: +) -> ifcopenshell.entity_instance: """Adds a new presentation item to a surface style A surface style can have multiple different types of presentation items @@ -82,17 +82,13 @@ def add_surface_style( :param style: The IfcSurfaceStyle you want to add to presentation item to. See ifcopenshell.api.style.add_style. - :type style: ifcopenshell.entity_instance :param ifc_class: Choose from IfcSurfaceStyleShading, IfcSurfaceStyleRendering, IfcSurfaceStyleWithTextures, IfcSurfaceStyleLighting, IfcSurfaceStyleReflectance, or IfcExternallyDefinedSurfaceStyle. - :type ifc_class: str :param attributes: a dictionary of attribute names and values. - :type attributes: dict, optional :return: The newly created presentation item based on the provided ifc_class. - :rtype: ifcopenshell.entity_instance Example: @@ -127,20 +123,20 @@ def add_surface_style( "SpecularHighlight": {"SpecularRoughness": 0.5}, # Roughness factor }) """ - settings = {"style": style, "ifc_class": ifc_class, "attributes": attributes or {}} + attributes = attributes or {} + style_item = file.create_entity(ifc_class) + ifcopenshell.api.style.edit_surface_style(file, style=style_item, attributes=attributes) + styles: list[ifcopenshell.entity_instance] + styles = list(style.Styles or []) - style_item = file.create_entity(settings["ifc_class"]) - ifcopenshell.api.style.edit_surface_style(file, style=style_item, attributes=settings["attributes"]) - styles = list(settings["style"].Styles or []) - - select_class = settings["ifc_class"] + select_class = ifc_class if select_class == "IfcSurfaceStyleRendering": select_class = "IfcSurfaceStyleShading" duplicate_items = [s for s in styles if s.is_a(select_class)] for duplicate_item in duplicate_items: ifcopenshell.api.style.remove_surface_style(file, style=duplicate_item) - styles = list(settings["style"].Styles or []) + styles = list(style.Styles or []) styles.append(style_item) - settings["style"].Styles = styles + style.Styles = styles return style_item diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py index 3ecc91177f..fbd706df68 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py @@ -19,6 +19,7 @@ import ifcopenshell import ifcopenshell.api.style import ifcopenshell.util.element +from typing import Any def assign_material_style( @@ -115,6 +116,9 @@ def assign_material_style( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): self.style = self.settings["style"] if self.file.schema == "IFC2X3" or self.settings["should_use_presentation_style_assignment"]: @@ -170,7 +174,7 @@ class Usecase: new_items.append(self.create_styled_item(item_to_reuse)) representation.Items = new_items for item in same_style_items: - if len(self.file.get_inverse(item)) == 0: + if self.file.get_total_inverses(item) == 0: self.file.remove(item) else: representations = list(definition_representation.Representations) diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/edit_presentation_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/edit_presentation_style.py index d1a42e5d9e..7a39629528 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/edit_presentation_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/edit_presentation_style.py @@ -28,11 +28,8 @@ def edit_presentation_style( IfcPresentationStyle, consult the IFC documentation. :param style: The IfcPresentationStyle entity you want to edit - :type style: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -44,7 +41,5 @@ def edit_presentation_style( # Change the name of the style to "Foo" ifcopenshell.api.style.edit_presentation_style(model, style=style, attributes={"Name": "Foo"}) """ - settings = {"style": style, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["style"], name, value) + for name, value in attributes.items(): + setattr(style, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py index c5be1d757f..7f7f05948d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell -from typing import Any +from typing import Any, Union def edit_surface_style( @@ -36,11 +36,8 @@ def edit_surface_style( example below. :param style: The IfcPresentationStyle entity you want to edit - :type style: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -74,24 +71,27 @@ def edit_surface_style( """ usecase = Usecase() usecase.file = file - usecase.settings = {"style": style, "attributes": attributes or {}} - return usecase.execute() + return usecase.execute(style, attributes) class Usecase: - def execute(self): - attributes = {} - for attribute in self.settings["style"].wrapped_data.declaration().as_entity().all_attributes(): + file: ifcopenshell.file + + def execute(self, style: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: + self.style = style + + attribute_types: dict[str, str] = {} + for attribute in style.wrapped_data.declaration().as_entity().all_attributes(): attribute_type = attribute.type_of_attribute() if attribute_type.as_aggregation_type() is None: attribute_type = attribute_type.declared_type().name() else: # doesn't have .declared_type() attribute_type = attribute_type.type_of_element() - attributes[attribute.name()] = attribute_type + attribute_types[attribute.name()] = attribute_type - for key, value in self.settings["attributes"].items(): - attribute_class = attributes.get(key) + for key, value in attributes.items(): + attribute_class = attribute_types.get(key) if attribute_class == "IfcColourRgb": self.edit_colour_rgb(key, value) elif key == "SpecularHighlight": @@ -99,41 +99,39 @@ class Usecase: elif attribute_class == "IfcColourOrFactor": self.edit_colour_or_factor(key, value) else: - setattr(self.settings["style"], key, value) + setattr(style, key, value) - def edit_colour_rgb(self, name, value: dict): - if (attribute := getattr(self.settings["style"], name)) is None: + def edit_colour_rgb(self, name: str, value: dict[str, Any]): + if (attribute := getattr(self.style, name)) is None: attribute = self.file.createIfcColourRgb() - setattr(self.settings["style"], name, attribute) + setattr(self.style, name, attribute) attribute.Name = value.get("Name", None) attribute.Red = value["Red"] attribute.Green = value["Green"] attribute.Blue = value["Blue"] - def edit_colour_or_factor(self, name, value): + def edit_colour_or_factor(self, name: str, value: Union[dict[str, Any], ifcopenshell.entity_instance, None]): if isinstance(value, dict): - attribute = getattr(self.settings["style"], name) + attribute = getattr(self.style, name) if not attribute or not attribute.is_a("IfcColourRgb"): colour = self.file.createIfcColourRgb(None, 0, 0, 0) - setattr(self.settings["style"], name, colour) - attribute = getattr(self.settings["style"], name) + setattr(self.style, name, colour) + attribute = getattr(self.style, name) attribute[1] = value["Red"] attribute[2] = value["Green"] attribute[3] = value["Blue"] else: # assume it's float value for IfcNormalisedRatioMeasure or None - existing_value = getattr(self.settings["style"], name) + existing_value = getattr(self.style, name) if existing_value and existing_value.id(): self.file.remove(existing_value) if value is not None: - value = self.file.createIfcNormalisedRatioMeasure(value) - setattr(self.settings["style"], name, value) + value = self.file.create_entity("IfcNormalisedRatioMeasure", value) + setattr(self.style, name, value) - def edit_specular_highlight(self, value): + def edit_specular_highlight(self, value: Union[dict[str, Any], None]) -> None: if value is None: - self.settings["style"].SpecularHighlight = None + self.style.SpecularHighlight = None elif value.get("IfcSpecularExponent", None): - self.settings["style"].SpecularHighlight = self.file.createIfcSpecularExponent(value["IfcSpecularExponent"]) + self.style.SpecularHighlight = self.file.createIfcSpecularExponent(value["IfcSpecularExponent"]) elif value.get("IfcSpecularRoughness", None): - self.settings["style"].SpecularHighlight = self.file.createIfcSpecularRoughness( - value["IfcSpecularRoughness"] - ) + self.style.SpecularHighlight = self.file.createIfcSpecularRoughness(value["IfcSpecularRoughness"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/remove_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/remove_style.py index 3ee4cbf6d5..c72cca657e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/remove_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/remove_style.py @@ -41,7 +41,6 @@ def remove_style(file: ifcopenshell.file, style: ifcopenshell.entity_instance) - """ usecase = Usecase() usecase.file = file - usecase.settings = {"style": style} return usecase.execute(style) diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/remove_surface_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/remove_surface_style.py index bad29c878f..19bb9e1b79 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/remove_surface_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/remove_surface_style.py @@ -24,9 +24,7 @@ def remove_surface_style(file: ifcopenshell.file, style: ifcopenshell.entity_ins """Removes a presentation item from a presentation style :param style: The IfcPresentationItem to remove. - :type style: ifcopenshell.entity_instance :return: None - :rtype: None Example: diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/unassign_representation_styles.py b/src/ifcopenshell-python/ifcopenshell/api/style/unassign_representation_styles.py index f56fad763c..4141adbacc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/unassign_representation_styles.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/unassign_representation_styles.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell +from typing import Any def unassign_representation_styles( @@ -63,6 +64,9 @@ def unassign_representation_styles( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): if not self.settings["styles"]: return [] diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py index 9a1bc76f43..b0b39cfd2b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py @@ -20,7 +20,7 @@ import ifcopenshell import ifcopenshell.api.owner import ifcopenshell.guid import ifcopenshell.util.element -from typing import Optional +from typing import Optional, Any def connect_port( @@ -110,6 +110,9 @@ def connect_port( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): # Note: there are a number of ambiguities with port connectivity. We # assume system topology is represented by a directed graph. In other diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/edit_system.py b/src/ifcopenshell-python/ifcopenshell/api/system/edit_system.py index 00311b7ac0..c90894ec7f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/edit_system.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/edit_system.py @@ -26,11 +26,8 @@ def edit_system(file: ifcopenshell.file, system: ifcopenshell.entity_instance, a IfcSystem, consult the IFC documentation. :param system: The IfcSystem entity you want to edit - :type system: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,8 +39,5 @@ def edit_system(file: ifcopenshell.file, system: ifcopenshell.entity_instance, a # Change the name of the system to "HW" for Hot Water ifcopenshell.api.system.edit_system(model, system=system, attributes={"Name": "HW"}) """ - - settings = {"system": system, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["system"], name, value) + for name, value in attributes.items(): + setattr(system, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py index 1bf459920f..b5b69e276c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py @@ -19,6 +19,7 @@ import ifcopenshell import ifcopenshell.api.owner import ifcopenshell.util.element +from typing import Any def unassign_port( @@ -62,6 +63,9 @@ def unassign_port( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): if self.file.schema == "IFC2X3": return self.execute_ifc2x3() diff --git a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py index 2a20f57f2f..f4bfae022e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py +++ b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py @@ -22,7 +22,7 @@ import ifcopenshell.api.owner import ifcopenshell.api.material import ifcopenshell.guid import ifcopenshell.util.element -from typing import Union, Iterable +from typing import Union, Iterable, Any def assign_type( @@ -186,6 +186,9 @@ def assign_type( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): if not self.settings["related_objects"]: return diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/add_derived_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/add_derived_unit.py index 8a68e5727f..0fba187c75 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/add_derived_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/add_derived_unit.py @@ -46,11 +46,8 @@ def add_derived_unit( :type unit_type: str :param userdefinedtype: The user defined type in case of choosing USERDEFINED, or None for no user defined type. - :type userdefinedtype: str or None :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: The newly created IfcDerivedUnit - :rtype: ifcopenshell.entity_instance Example: @@ -69,15 +66,13 @@ def add_derived_unit( #12=IfcDerivedUnit((#10,#11),.LINEARVELOCITY.,$) """ - settings = {"unit_type": unit_type, "attributes": attributes} - derive_unit_elements = [] - for named_unit in settings["attributes"]: + for named_unit in attributes: derive_unit_elements.append( - file.create_entity("IfcDerivedUnitElement", Unit=named_unit, Exponent=settings["attributes"][named_unit]) + file.create_entity("IfcDerivedUnitElement", Unit=named_unit, Exponent=attributes[named_unit]) ) return file.create_entity( - "IfcDerivedUnit", Elements=derive_unit_elements, UnitType=settings["unit_type"], UserDefinedType=userdefinedtype + "IfcDerivedUnit", Elements=derive_unit_elements, UnitType=unit_type, UserDefinedType=userdefinedtype ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py index 4e2dc3f848..d57acbd435 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py @@ -18,7 +18,7 @@ import ifcopenshell import ifcopenshell.util.unit -from typing import Optional +from typing import Optional, Any def assign_unit( @@ -75,6 +75,9 @@ def assign_unit( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): # We're going to refactor this to split unit creation and assignment if self.settings["units"]: diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_derived_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_derived_unit.py index a8b3316dbd..5bc9bbd316 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_derived_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_derived_unit.py @@ -26,13 +26,8 @@ def edit_derived_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instanc IfcDerivedUnit, consult the IFC documentation. :param unit: The IfcDerivedUnit entity you want to edit - :type unit: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None """ - settings = {"unit": unit, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["unit"], name, value) + for name, value in attributes.items(): + setattr(unit, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_monetary_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_monetary_unit.py index 4fdfe6ec29..bccc61afc0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_monetary_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_monetary_unit.py @@ -26,11 +26,8 @@ def edit_monetary_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instan IfcMonetaryUnit, consult the IFC documentation. :param unit: The IfcMonetaryUnit entity you want to edit - :type unit: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -43,7 +40,5 @@ def edit_monetary_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instan # Ah who are we kidding ifcopenshell.api.unit.edit_monetary_unit(model, unit=zwl, attributes={"Currency": "USD"}) """ - settings = {"unit": unit, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["unit"], name, value) + for name, value in attributes.items(): + setattr(unit, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py index e439e2c476..683feb7216 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py @@ -29,11 +29,8 @@ def edit_named_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instance, IfcNamedUnit, consult the IFC documentation. :param unit: The IfcNamedUnit entity you want to edit - :type unit: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -45,15 +42,13 @@ def edit_named_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instance, # Uh, crates? Boxes? Whatever. ifcopenshell.api.unit.edit_named_unit(model, unit=unit, attibutes={"Name": "CRATES"}) """ - settings = {"unit": unit, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): + for name, value in attributes.items(): if name == "Dimensions": - dimensions = settings["unit"].Dimensions - if len(file.get_inverse(dimensions)) > 1: - settings["unit"].Dimensions = file.createIfcDimensionalExponents(*value) + dimensions = unit.Dimensions + if file.get_total_inverses(dimensions) > 1: + unit.Dimensions = file.createIfcDimensionalExponents(*value) else: for i, exponent in enumerate(value): dimensions[i] = exponent continue - setattr(settings["unit"], name, value) + setattr(unit, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py index 17eee6d4be..9651bfa87e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py @@ -23,9 +23,7 @@ def unassign_unit(file: ifcopenshell.file, units: Optional[list[ifcopenshell.ent """Unassigns units as default units for the project :param units: A list of units to assign as project defaults. - :type units: list[ifcopenshell.entity_instance],optional :return: None - :rtype: None Example: @@ -44,15 +42,13 @@ def unassign_unit(file: ifcopenshell.file, units: Optional[list[ifcopenshell.ent # Actually, we don't need areas. ifcopenshell.api.unit.unassign_unit(model, units=[area]) """ - settings = {"units": units} - - unit_assignment = file.by_type("IfcUnitAssignment") - if not unit_assignment: + unit_assignments = file.by_type("IfcUnitAssignment") + if not unit_assignments: + return + unit_assignment = unit_assignments[0] + units_set = set(unit_assignment.Units or []) + units_set = units_set - set(units or []) + if units_set: + unit_assignment.Units = list(units_set) return - unit_assignment = unit_assignment[0] - units = set(unit_assignment.Units or []) - units = units - set(settings["units"]) - if units: - unit_assignment.Units = list(units) - return unit_assignment file.remove(unit_assignment) diff --git a/src/ifcopenshell-python/ifcopenshell/entity_instance.py b/src/ifcopenshell-python/ifcopenshell/entity_instance.py index dcd3b74cb0..15e5980d41 100644 --- a/src/ifcopenshell-python/ifcopenshell/entity_instance.py +++ b/src/ifcopenshell-python/ifcopenshell/entity_instance.py @@ -248,14 +248,10 @@ class entity_instance: :param f: A callable that takes a single argument and returns a boolean value. It represents the condition. - :type f: Callable :param g: A callable that takes a single argument and returns a transformed value. It represents the transformation. - :type g: Callable :param value: Any object, the input value to be processed - :type value: Any :return: Transformed value - :rtype: Any Example: @@ -304,8 +300,6 @@ class entity_instance: """Return the data type of a positional attribute of the element :param attr: The index or name of the attribute - :type attr: Union[int, str] - :rtype: string """ attr_idx = attr if isinstance(attr, numbers.Integral) else self.wrapped_data.get_argument_index(attr) return self.wrapped_data.get_argument_type(attr_idx) @@ -314,8 +308,6 @@ class entity_instance: """Return the name of a positional attribute of the element :param attr_idx: The index of the attribute - :type attr_idx: int - :rtype: string """ return self.wrapped_data.get_argument_name(attr_idx) @@ -405,9 +397,7 @@ class entity_instance: returned IFC class name should include schema name (e.g. "IFC4.IfcWall" if `True` and "IfcWall" if `False`). If omitted will act as `False`. - :type args: Union[str, bool] :returns: Either the name of the class, or a boolean if it passes the check - :rtype: Union[str, bool] Example: @@ -423,10 +413,7 @@ class entity_instance: return self.wrapped_data.is_a(*args) def id(self) -> int: - """Return the STEP numerical identifier - - :rtype: int - """ + """Return the STEP numerical identifier""" return self.wrapped_data.id() def __eq__(self, other: "entity_instance") -> bool: diff --git a/src/ifcopenshell-python/ifcopenshell/file.py b/src/ifcopenshell-python/ifcopenshell/file.py index 04c6c2ce50..328d98b27f 100644 --- a/src/ifcopenshell-python/ifcopenshell/file.py +++ b/src/ifcopenshell-python/ifcopenshell/file.py @@ -24,6 +24,7 @@ import zipfile import functools import ifcopenshell from pathlib import Path +from typing import Any from typing import Callable from typing import Generator from typing import Optional @@ -358,7 +359,6 @@ class file: of those methods. :param type: Case insensitive name of the IFC class - :type type: string :param args: The positional arguments of the IFC class :param kwargs: The keyword arguments of the IFC class :returns: An entity instance @@ -481,12 +481,10 @@ class file: """Return an IFC entity instance filtered by IFC ID. :param id: STEP numerical identifier - :type id: int :raises RuntimeError: If `id` is not found. :returns: An ifcopenshell.entity_instance - :rtype: ifcopenshell.entity_instance """ return self[id] @@ -494,13 +492,10 @@ class file: """Return an IFC entity instance filtered by IFC GUID. :param guid: GlobalId value in 22-character encoded form - :type guid: string :raises RuntimeError: If `guid` is not found. :returns: An ifcopenshell.entity_instance - :rtype: ifcopenshell.entity_instance - """ return self[guid] @@ -509,9 +504,7 @@ class file: If the entity already exists, it is not re-added. Existence of entity is checked by it's `.identity()`. :param inst: The entity instance to add - :type inst: ifcopenshell.entity_instance :returns: An ifcopenshell.entity_instance - :rtype: ifcopenshell.entity_instance """ if self.transaction: @@ -529,14 +522,11 @@ class file: If an IFC type class has subclasses, all entities of those subclasses are also returned. :param type: The case insensitive type of IFC class to return. - :type type: string :param include_subtypes: Whether or not to return subtypes of the IFC class - :type include_subtypes: bool :raises RuntimeError: If `type` is not found in IFC schema. :returns: A list of ifcopenshell.entity_instance objects - :rtype: list[ifcopenshell.entity_instance] """ if include_subtypes: return [entity_instance(e, self) for e in self.wrapped_data.by_type(type)] @@ -624,9 +614,7 @@ class file: significantly faster. :param inst: The entity instance to get inverse relationships - :type inst: ifcopenshell.entity_instance :returns: The total number of references - :rtype: int """ return self.wrapped_data.get_total_inverses(inst.wrapped_data) @@ -638,8 +626,6 @@ class file: the reference to the deleted will be removed from the aggregate. :param inst: The entity instance to delete - :type inst: ifcopenshell.entity_instance - :rtype: None """ if self.transaction: self.transaction.store_delete(inst) @@ -675,9 +661,7 @@ class file: if None. Supported formats : .ifc, .ifcXML, .ifcZIP (equivalent to format=".ifc" with zipped=True) For zipped .ifcXML use format=".ifcXML" with zipped=True - :type format: str :param zipped: zip the file after it is written - :type zipped: bool Example: diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index b8ef0711d5..9034e382be 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -454,7 +454,7 @@ def get_elements_by_pset(pset: ifcopenshell.entity_instance) -> set[ifcopenshell return elements -def get_predefined_type(element: ifcopenshell.entity_instance) -> str: +def get_predefined_type(element: ifcopenshell.entity_instance) -> Union[str, None]: """Retrieves the PrefefinedType attribute of an element. If the predefined type is user defined, the custom type (such as object @@ -1333,7 +1333,7 @@ def get_referenced_elements(reference: ifcopenshell.entity_instance) -> set[ifco def replace_element(element: ifcopenshell.entity_instance, replacement: ifcopenshell.entity_instance) -> None: - for inverse in element.file: + for inverse in element.file.get_inverse(element): replace_attribute(inverse, element, replacement) @@ -1434,7 +1434,7 @@ def remove_deep2( ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance, also_consider: list[ifcopenshell.entity_instance] = [], - do_not_delete: list[ifcopenshell.entity_instance] = [], + do_not_delete: set[ifcopenshell.entity_instance] = set(), ) -> None: """Recursively purges a subgraph safely, starting at an element @@ -1462,28 +1462,34 @@ def remove_deep2( :param ifc_file: The IFC file object :param also_consider: elements to also consider as a part of a subgraph + Order could matter for perfomance - elements that reference `element` + directly should go first for the better performance. :param do_not_delete: elements to protect from deletion :param element: The starting element that defines the subgraph """ # ifc_file.batch() - also_considered_inverses = 0 + total_inverses = ifc_file.get_total_inverses(element) + if total_inverses > 0: - def increment_considered_inverses(_): - nonlocal also_considered_inverses - also_considered_inverses += 1 + def are_inverses_contained() -> bool: + also_considered_inverses = 0 - for considered_element in also_consider: - for attribute in considered_element: - considered_element.walk(lambda x: x == element, increment_considered_inverses, attribute) + for considered_element in also_consider: + traverse = ifc_file.traverse(considered_element, max_levels=1) + if element in traverse: + also_considered_inverses += 1 + if total_inverses == also_considered_inverses: + return True + return False - if ifc_file.get_total_inverses(element) > 0 + also_considered_inverses: - return + if not are_inverses_contained(): + return to_delete = set() subgraph = list(ifc_file.traverse(element, breadth_first=True)) subgraph.extend(also_consider) subgraph_set = set(subgraph) - subelement_queue = ifc_file.traverse(element, max_levels=1) + subelement_queue = [element] while subelement_queue: subelement = subelement_queue.pop(0) if ( diff --git a/src/ifcopenshell-python/ifcopenshell/util/placement.py b/src/ifcopenshell-python/ifcopenshell/util/placement.py index d6caa1b657..5f8ad154b6 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/placement.py +++ b/src/ifcopenshell-python/ifcopenshell/util/placement.py @@ -137,9 +137,7 @@ def get_cartesiantransformationoperator3d(inst: ifcopenshell.entity_instance) -> ``get_mappeditem_transformation`` instead. :param item: The IfcCartesianTransformationOperator entity - :type item: ifcopenshell.entity_instance :return: A 4x4 numpy transformation matrix - :rtype: MatrixType """ origin = np.array(inst.LocalOrigin.Coordinates) axis1 = np.array((1.0, 0.0, 0.0)) diff --git a/src/ifcopenshell-python/ifcopenshell/util/schema.py b/src/ifcopenshell-python/ifcopenshell/util/schema.py index f709af6b5a..70eff36eb9 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/schema.py +++ b/src/ifcopenshell-python/ifcopenshell/util/schema.py @@ -80,12 +80,7 @@ def is_a(declaration: ifcopenshell.ifcopenshell_wrapper.entity, ifc_class: str) declaration = ifcopenshell.util.schema.get_declaration(wall) ifcopenshell.util.schema.is_a(declaration, "IfcRoot") # True """ - ifc_class = ifc_class.upper() - if declaration.name_uc() == ifc_class: - return True - if declaration.supertype(): - return is_a(declaration.supertype(), ifc_class) - return False + return declaration._is(ifc_class) def get_supertypes( diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 37919b9408..f0a1025583 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -17,7 +17,7 @@ # along with IfcOpenShell. If not, see . import re -import types +import sys import lark import numpy as np import ifcopenshell.api.pset @@ -37,6 +37,11 @@ import ifcopenshell.util.unit from decimal import Decimal from typing import Optional, Any, Union, Iterable +if sys.version_info >= (3, 10): + from types import EllipsisType +else: + EllipsisType = type(...) + filter_elements_grammar = lark.Lark( """start: filter_group @@ -659,7 +664,7 @@ def set_element_value( def process_pset_prop_value( pset: ifcopenshell.entity_instance, prop: str, value: Any - ) -> Union[Any, types.EllipsisType]: + ) -> Union[Any, EllipsisType]: """Try to process value for edit_pset. `edit_pset` is expecting a sequence of values @@ -672,7 +677,7 @@ def set_element_value( current_value = element.get(key, ...) # Check if previous value is a list as a fast way to identify enum properties. - if not isinstance(current_value, (types.EllipsisType, list)): + if not isinstance(current_value, (EllipsisType, list)): return value if isinstance(current_value, list): diff --git a/src/ifcopenshell-python/ifcopenshell/util/stationing.py b/src/ifcopenshell-python/ifcopenshell/util/stationing.py new file mode 100644 index 0000000000..1e0517f177 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/util/stationing.py @@ -0,0 +1,50 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Thomas Krijnen +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import math + + +def station_as_string(station: float, plus_seperator=3, accuracy=3): + """ + Returns a stringized version of a station. Example 100.0 is 1+00.00 as a stationing string + @param station: the station to be stringized + @param plus_seperator: location of the '+' symbol relative to the decimal place (typically 2 for US units and 3 for SI units) + @param accuracy: number of digits following the decimal place + """ + value = math.fabs(station) + + shifter = math.pow(10.0, plus_seperator) + v1 = math.floor(value / shifter) + v2 = value - v1 * shifter + + # Check to make sure that v2 is not basically the same as shifter + # If station = 69500.00000, we sometimes get 694+100.00 instead of 695+00.00 + if math.isclose(v2 - shifter, 5.0 * math.pow(10.0, -(accuracy + 1))): + v2 = 0.0 + v1 += 1 + + v1 = -1 * v1 if station < 0 else v1 + + station_string = "{:d}+{:0{}.{}f}".format(v1, v2, plus_seperator + accuracy + 1, accuracy) + + # special case when v1 is 0 and station is negative, the string above doesn't get the leading + # negative sign. this snippet fixes that + if v1 == 0 and station < 0: + station_string = "-" + station_string + + return station_string diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index b19b8154c9..47adbc284e 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -835,6 +835,7 @@ def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = " import ifcopenshell.util.element import ifcopenshell.util.geolocation import ifcopenshell.api.georeference + import ifcopenshell.api.unit prefix = get_prefix(target_units) si_unit = get_unit_name(target_units) diff --git a/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py b/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py index ba8385fa3e..07a4400f5a 100644 --- a/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py +++ b/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py @@ -105,7 +105,7 @@ class TestAddBoolean(test.bootstrap.IFC4): assert len(booleans) == 1 assert len(rep.Items) == 2 - assert len(self.file.get_inverse(first1)) == 1 + assert self.file.get_total_inverses(first1) == 1 result = list(self.file.get_inverse(first1))[0] assert result.FirstOperand == first1 assert result.SecondOperand == second1 @@ -114,7 +114,7 @@ class TestAddBoolean(test.bootstrap.IFC4): # Second2 is now used twice. Reusing is OK (albeit confusing), so long as things don't get recursive. assert result2.SecondOperand == second2 - assert len(self.file.get_inverse(first2)) == 1 + assert self.file.get_total_inverses(first2) == 1 result3 = list(self.file.get_inverse(first2))[0] assert result3.FirstOperand == first2 assert result3.SecondOperand == second2 diff --git a/src/ifcopenshell-python/test/api/structural/test_edit_structural_analysis_model.py b/src/ifcopenshell-python/test/api/structural/test_edit_structural_analysis_model.py index 18f389f488..fddb8a9993 100644 --- a/src/ifcopenshell-python/test/api/structural/test_edit_structural_analysis_model.py +++ b/src/ifcopenshell-python/test/api/structural/test_edit_structural_analysis_model.py @@ -23,7 +23,7 @@ import ifcopenshell.api.structural class TestEditStructuralAnalysisModel(test.bootstrap.IFC4): def test_editing_a_structural_analysis_model(self): subject = ifcopenshell.api.structural.add_structural_analysis_model(self.file) - subject = ifcopenshell.api.structural.edit_structural_analysis_model( + ifcopenshell.api.structural.edit_structural_analysis_model( self.file, structural_analysis_model=subject, attributes={"Name": "My edited model", "Description": "Description of my model"}, diff --git a/src/ifcopenshell-python/test/geom/original_edges.py b/src/ifcopenshell-python/test/geom/original_edges.py new file mode 100644 index 0000000000..41fd6b8aff --- /dev/null +++ b/src/ifcopenshell-python/test/geom/original_edges.py @@ -0,0 +1,98 @@ +import ifcopenshell +import ifcopenshell.geom + +contents = """ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1'); +FILE_NAME('untitled.ifc','2025-02-17T08:13:34+11:00',(''),(''),'IfcOpenShell 0.0.0','Bonsai 0.8.1-alpha250208-8f261be','Nobody'); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPROJECT('3IbgqFUY99IejU9tnNSVCj',$,'My Project',$,$,$,$,(#14,#26),#9); +#2=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.); +#3=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.); +#4=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.); +#5=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0); +#6=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.); +#7=IFCMEASUREWITHUNIT(IFCREAL(0.0174532925199433),#6); +#8=IFCCONVERSIONBASEDUNIT(#5,.PLANEANGLEUNIT.,'degree',#7); +#9=IFCUNITASSIGNMENT((#3,#4,#2,#8)); +#10=IFCCARTESIANPOINT((0.,0.,0.)); +#11=IFCDIRECTION((0.,0.,1.)); +#12=IFCDIRECTION((1.,0.,0.)); +#13=IFCAXIS2PLACEMENT3D(#10,#11,#12); +#14=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#13,$); +#15=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#14,$,.MODEL_VIEW.,$); +#16=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Model',*,*,*,*,#14,$,.GRAPH_VIEW.,$); +#17=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Box','Model',*,*,*,*,#14,$,.MODEL_VIEW.,$); +#18=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.SECTION_VIEW.,$); +#19=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.ELEVATION_VIEW.,$); +#20=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.MODEL_VIEW.,$); +#21=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.PLAN_VIEW.,$); +#22=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Profile','Model',*,*,*,*,#14,$,.ELEVATION_VIEW.,$); +#23=IFCCARTESIANPOINT((0.,0.)); +#24=IFCDIRECTION((1.,0.)); +#25=IFCAXIS2PLACEMENT2D(#23,#24); +#26=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Plan',2,1.E-05,#25,$); +#27=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Plan',*,*,*,*,#26,$,.GRAPH_VIEW.,$); +#28=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Plan',*,*,*,*,#26,$,.PLAN_VIEW.,$); +#29=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Plan',*,*,*,*,#26,$,.PLAN_VIEW.,$); +#30=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Plan',*,*,*,*,#26,$,.REFLECTED_PLAN_VIEW.,$); +#31=IFCSITE('0RWRVKEyv0w8rD30dnxZgC',$,'My Site',$,$,#54,$,$,$,$,$,$,$,$); +#37=IFCBUILDING('39X8AnBtP4gvIIEtolio1r',$,'My Building',$,$,#60,$,$,$,$,$,$); +#43=IFCBUILDINGSTOREY('3u3kxyF4r8PPWfqNxA77Mg',$,'My Storey',$,$,#66,$,$,$,$); +#49=IFCRELAGGREGATES('254xcZnL5DnQg9572XWoGw',$,$,$,#1,(#31)); +#50=IFCCARTESIANPOINT((0.,0.,0.)); +#51=IFCDIRECTION((0.,0.,1.)); +#52=IFCDIRECTION((1.,0.,0.)); +#53=IFCAXIS2PLACEMENT3D(#50,#51,#52); +#54=IFCLOCALPLACEMENT($,#53); +#55=IFCRELAGGREGATES('3mOs7xn1P8ZutyBJCrBHy6',$,$,$,#31,(#37)); +#56=IFCCARTESIANPOINT((0.,0.,0.)); +#57=IFCDIRECTION((0.,0.,1.)); +#58=IFCDIRECTION((1.,0.,0.)); +#59=IFCAXIS2PLACEMENT3D(#56,#57,#58); +#60=IFCLOCALPLACEMENT(#54,#59); +#61=IFCRELAGGREGATES('061R4J$_v02e5$uJ_Zcpwu',$,$,$,#37,(#43)); +#62=IFCCARTESIANPOINT((0.,0.,0.)); +#63=IFCDIRECTION((0.,0.,1.)); +#64=IFCDIRECTION((1.,0.,0.)); +#65=IFCAXIS2PLACEMENT3D(#62,#63,#64); +#66=IFCLOCALPLACEMENT(#60,#65); +#67=IFCACTUATOR('3XPXM$jnf6gQHxVSs1SU0h',$,'Cube',$,$,#83,#78,$,.ELECTRICACTUATOR.); +#68=IFCRELCONTAINEDINSPATIALSTRUCTURE('2uCE41OAzCIh07v_z205CZ',$,$,$,(#67),#43); +#77=IFCSHAPEREPRESENTATION(#15,'Body','Tessellation',(#95)); +#78=IFCPRODUCTDEFINITIONSHAPE($,$,(#77)); +#79=IFCCARTESIANPOINT((0.,0.,0.)); +#80=IFCDIRECTION((0.,0.,1.)); +#81=IFCDIRECTION((1.,0.,0.)); +#82=IFCAXIS2PLACEMENT3D(#79,#80,#81); +#83=IFCLOCALPLACEMENT(#66,#82); +#84=IFCCARTESIANPOINTLIST3D(((-1.,1.,-1.),(-1.,-1.,-1.),(-1.,-1.,1.),(1.,1.,-1.),(-1.,1.,1.),(1.,-1.,-1.),(1.,1.,1.),(1.,-1.,1.),(1.,1.,2.),(1.,-1.,2.),(-1.,1.,2.),(-1.,-1.,2.))); +#85=IFCINDEXEDPOLYGONALFACE((5,7,4,1)); +#86=IFCINDEXEDPOLYGONALFACE((7,8,6,4)); +#87=IFCINDEXEDPOLYGONALFACE((8,3,2,6)); +#88=IFCINDEXEDPOLYGONALFACE((4,6,2,1)); +#89=IFCINDEXEDPOLYGONALFACE((5,3,12,11)); +#90=IFCINDEXEDPOLYGONALFACE((3,5,1,2)); +#91=IFCINDEXEDPOLYGONALFACE((11,12,10,9)); +#92=IFCINDEXEDPOLYGONALFACE((8,7,9,10)); +#93=IFCINDEXEDPOLYGONALFACE((3,8,10,12)); +#94=IFCINDEXEDPOLYGONALFACE((7,5,11,9)); +#95=IFCPOLYGONALFACESET(#84,$,(#85,#86,#87,#88,#89,#90,#91,#92,#93,#94),$); +ENDSEC; +END-ISO-10303-21; +""" + + +def test_original_edges(): + ifc_file = ifcopenshell.file.from_string(contents) + element = ifc_file.by_id(95) + settings = ifcopenshell.geom.settings() + shape = ifcopenshell.geom.create_shape(settings, element, geometry_library="opencascade") + assert (len(shape.edges) // 2) == 20 + shape = ifcopenshell.geom.create_shape(settings, element, geometry_library="cgal") + assert (len(shape.edges) // 2) == 16 + settings.set("cgal-original-edges", True) + shape = ifcopenshell.geom.create_shape(settings, element, geometry_library="cgal") + assert (len(shape.edges) // 2) == 20 diff --git a/src/ifcopenshell-python/test/util/test_stationing.py b/src/ifcopenshell-python/test/util/test_stationing.py new file mode 100644 index 0000000000..a72a1fb6c1 --- /dev/null +++ b/src/ifcopenshell-python/test/util/test_stationing.py @@ -0,0 +1,46 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import ifcopenshell.util.stationing as sta + + +def test_station_as_string(): + # test with a bunch of "random" station values + s = sta.station_as_string(0.0) + assert s == "0+000.000" + + s = sta.station_as_string(0.0, 2, 2) + assert s == "0+00.00" + + s = sta.station_as_string(0.0, 2) + assert s == "0+00.000" + + s = sta.station_as_string(100.00) + assert s == "0+100.000" + + s = sta.station_as_string(-100.00) + assert s == "-0+100.000" + + s = sta.station_as_string(123456.789, 2, 2) + assert s == "1234+56.79" + + s = sta.station_as_string(-123456.789, 2, 2) + assert s == "-1234+56.79" + + s = sta.station_as_string(123456.789, 3, 4) + assert s == "123+456.7890" diff --git a/src/ifcparse/IfcAlignmentHelper.cpp b/src/ifcparse/IfcAlignmentHelper.cpp index 8e26375ab5..a2953c16c2 100644 --- a/src/ifcparse/IfcAlignmentHelper.cpp +++ b/src/ifcparse/IfcAlignmentHelper.cpp @@ -30,6 +30,7 @@ // @todo use std::numbers::pi when upgrading to C++ 20 static const double PI = boost::math::constants::pi(); +#include #ifdef HAS_SCHEMA_4x3_add2 @@ -207,7 +208,7 @@ Ifc4x3_add2::IfcAlignment* addHorizontalAlignment(IfcHierarchyHelper::ptr alignment_representations(new aggregate_of()); alignment_representations->push(footprint_shape_representation); // 2D footprint @@ -243,21 +244,20 @@ std::tuple::ptr, typenam // back gradient auto dxBG = xPVI - xPBG; auto dyBG = yPVI - yPBG; - auto start_slope = atan2(dyBG, dxBG); + auto start_slope = tan(atan2(dyBG,dxBG)); // forward gradient point_iter++; std::tie(xPFG, yPFG) = *point_iter; auto dxFG = xPFG - xPVI; auto dyFG = yPFG - yPVI; - auto end_slope = atan2(dyFG, dxFG); + auto end_slope = tan(atan2(dyFG,dxFG)); double xEVC = xPVI + length / 2; double yEVC = yPVI + end_slope * length / 2; // create gradient { - file.addDoublet(xPBG, yPBG); auto gradient_length = dxBG - length/2; auto design_parameters = new Ifc4x3_add2::IfcAlignmentVerticalSegment(boost::none, boost::none, xPBG, gradient_length, yPBG, start_slope, start_slope, boost::none, Ifc4x3_add2::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_CONSTANTGRADIENT); auto alignment_segment = new Ifc4x3_add2::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters); @@ -273,8 +273,6 @@ std::tuple::ptr, typenam double xBVC = xPVI - length / 2; double yBVC = yPVI - start_slope * length / 2; - file.addDoublet(xBVC, yBVC); - auto design_parameters = new Ifc4x3_add2::IfcAlignmentVerticalSegment(boost::none, boost::none, xBVC, length, yBVC, start_slope, end_slope, 1 / k, Ifc4x3_add2::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_PARABOLICARC); auto alignment_segment = new Ifc4x3_add2::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters); vertical_segments->push(alignment_segment); @@ -292,9 +290,9 @@ std::tuple::ptr, typenam // create last tangent run auto dx = xPVI - xPBG; auto dy = yPVI - yPBG; - auto slope = atan2(dy, dx); - auto gradient_length = sqrt(dx * dx + dy * dy); - file.addDoublet(xPBG, yPBG); + auto slope = tan(atan2(dy,dx)); + auto gradient_length = dx; + auto design_parameters = new Ifc4x3_add2::IfcAlignmentVerticalSegment(boost::none, boost::none, xPBG, gradient_length, yPBG, slope, slope, boost::none, Ifc4x3_add2::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_CONSTANTGRADIENT); auto alignment_segment = new Ifc4x3_add2::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters); vertical_segments->push(alignment_segment); @@ -303,7 +301,6 @@ std::tuple::ptr, typenam } // create zero length terminator segment - file.addDoublet(xPVI, yPVI); design_parameters = new Ifc4x3_add2::IfcAlignmentVerticalSegment(boost::none, boost::none, xPVI, 0.0, yPVI, slope, slope, boost::none, Ifc4x3_add2::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_CONSTANTGRADIENT); alignment_segment = new Ifc4x3_add2::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters); vertical_segments->push(alignment_segment); @@ -356,14 +353,14 @@ Ifc4x3_add2::IfcAlignment* addAlignment(IfcHierarchyHelper& file, c typename aggregate_of::ptr alignment_representation_items(new aggregate_of()); alignment_representation_items->push(composite_curve); - // create the footprint representation - auto footprint_shape_representation = new Ifc4x3_add2::IfcShapeRepresentation(axis_model_representation_subcontext, std::string("FootPrint"), std::string("Curve2D"), alignment_representation_items); - file.addEntity(footprint_shape_representation); - // the gradient curve is a representation item typename aggregate_of::ptr profile_representation_items(new aggregate_of()); profile_representation_items->push(gradient_curve); + // create footprint representation + auto footprint_shape_representation = new Ifc4x3_add2::IfcShapeRepresentation(axis_model_representation_subcontext, std::string("FootPrint"), std::string("Curve2D"), alignment_representation_items); + file.addEntity(footprint_shape_representation); + // create the axis representation auto axis3d_shape_representation = new Ifc4x3_add2::IfcShapeRepresentation(axis_model_representation_subcontext, std::string("Axis"), std::string("Curve3D"), profile_representation_items); file.addEntity(axis3d_shape_representation); @@ -373,10 +370,10 @@ Ifc4x3_add2::IfcAlignment* addAlignment(IfcHierarchyHelper& file, c _createSegmentRepresentations(file, placement, axis_model_representation_subcontext, horizontal_curve_segments, horizontal_segments); _createSegmentRepresentations(file, placement, axis_model_representation_subcontext, vertical_curve_segments, vertical_segments); - // the alignment has two representations, a plan view footprint and a 3d curve + // the alignment has a 3d curve representation typename aggregate_of::ptr alignment_representations(new aggregate_of()); - alignment_representations->push(footprint_shape_representation); // 2D footprint - alignment_representations->push(axis3d_shape_representation); // 3D curve + alignment_representations->push(footprint_shape_representation); // 2D curve + alignment_representations->push(axis3d_shape_representation); // 3D curve // create the alignment product definition product_definition_shape = new Ifc4x3_add2::IfcProductDefinitionShape(std::string("Alignment Product Definition Shape"), boost::none, alignment_representations); @@ -688,13 +685,18 @@ std::pair mapAlign new Ifc4x3_add2::IfcCartesianPoint(std::vector({0, 0})), new Ifc4x3_add2::IfcVector(new Ifc4x3_add2::IfcDirection(std::vector{1, 0}), 1.0)); + // IfcCurveSegment.SegmentLength is the length of the curve segment, not the horizontal length. + auto dx = cos(atan(start_gradient)); + auto dy = sin(atan(start_gradient)); + auto segment_curve_length = horizontal_length / dx; + auto curve_segment = new Ifc4x3_add2::IfcCurveSegment( Ifc4x3_add2::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT, new Ifc4x3_add2::IfcAxis2Placement2D( new Ifc4x3_add2::IfcCartesianPoint({start_distance_along, start_height}), - new Ifc4x3_add2::IfcDirection({sqrt(1.0 - start_gradient * start_gradient), start_gradient})), + new Ifc4x3_add2::IfcDirection({dx,dy})), new Ifc4x3_add2::IfcLengthMeasure(0.0), // start - new Ifc4x3_add2::IfcLengthMeasure(horizontal_length), + new Ifc4x3_add2::IfcLengthMeasure(segment_curve_length), parent_curve); result.first = curve_segment; @@ -710,11 +712,22 @@ std::pair mapAlign std::vector{A, B, C}, boost::none); + // IfcCurveSegment.SegmentLength is the length of the curve segment, not the horizontal length. + // The curve length is calculated by integrating the differential curve length equation sqrt(1 + (dy/dx)^2) from 0 to horizontal_length. + // y = A + Bx + Cx^2 + // dy/dx = B + 2Cx + auto dx = cos(atan(start_gradient)); + auto dy = sin(atan(start_gradient)); + auto curve_length_fn = [B, C](double x) { return sqrt(1 + pow(B + C * x, 2)); }; + auto segment_curve_length = boost::math::quadrature::trapezoidal(curve_length_fn, 0.0, horizontal_length); + auto curve_segment = new Ifc4x3_add2::IfcCurveSegment( Ifc4x3_add2::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT, - new Ifc4x3_add2::IfcAxis2Placement2D(new Ifc4x3_add2::IfcCartesianPoint({start_distance_along, start_height}), new Ifc4x3_add2::IfcDirection({sqrt(1.0 - start_gradient * start_gradient), start_gradient})), + new Ifc4x3_add2::IfcAxis2Placement2D( + new Ifc4x3_add2::IfcCartesianPoint({start_distance_along, start_height}), + new Ifc4x3_add2::IfcDirection({dx,dy})), new Ifc4x3_add2::IfcLengthMeasure(0.0), - new Ifc4x3_add2::IfcLengthMeasure(horizontal_length), + new Ifc4x3_add2::IfcLengthMeasure(segment_curve_length), parent_curve); result.first = curve_segment; @@ -735,11 +748,14 @@ std::pair mapAlign new Ifc4x3_add2::IfcDirection(std::vector{1, 0})), radius); + + auto segment_curve_length = radius * fabs(end_angle - start_angle); + Ifc4x3_add2::IfcCurveSegment* curve_segment = new Ifc4x3_add2::IfcCurveSegment( Ifc4x3_add2::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT, new Ifc4x3_add2::IfcAxis2Placement2D(new Ifc4x3_add2::IfcCartesianPoint({start_distance_along, start_height}), new Ifc4x3_add2::IfcDirection({1.0, 0.0})), new Ifc4x3_add2::IfcLengthMeasure(0.0), - new Ifc4x3_add2::IfcLengthMeasure(horizontal_length), + new Ifc4x3_add2::IfcLengthMeasure(segment_curve_length), parent_curve); result.first = curve_segment; diff --git a/src/ifcparse/IfcHierarchyHelper.h b/src/ifcparse/IfcHierarchyHelper.h index a8fbdf444b..eff35e7259 100644 --- a/src/ifcparse/IfcHierarchyHelper.h +++ b/src/ifcparse/IfcHierarchyHelper.h @@ -404,50 +404,79 @@ class IFC_PARSE_API IfcHierarchyHelper : public IfcParse::IfcFile { template void addRelatedObject(typename Schema::IfcObjectDefinition* relating_object, typename Schema::IfcObjectDefinition* related_object, - typename Schema::IfcOwnerHistory* owner_hist = 0) { - typename T::list::ptr li = instances_by_type(); - bool found = false; - for (typename T::list::it i = li->begin(); i != li->end(); ++i) { - T* rel = *i; - try { - if (get_parent_of_relation(rel) == relating_object) { - aggregate_of_instance::ptr products = get_children_of_relation(rel); - products->push(related_object); - set_children_of_relation(rel, products); + typename Schema::IfcOwnerHistory* owner_hist = 0) + { + if constexpr (std::is_same_v) { + typename Schema::IfcRelDefinesByType::list::ptr li = instances_by_type(); + bool found = false; + for (typename Schema::IfcRelDefinesByType::list::it i = li->begin(); i != li->end(); ++i) { + typename Schema::IfcRelDefinesByType* rel = *i; + if (rel->RelatingType() == related_object) { + typename Schema::IfcObject::list::ptr objects = rel->RelatedObjects(); + objects->push((typename Schema::IfcObject*)related_object); + rel->setRelatedObjects(objects); found = true; break; } - } catch (std::exception& e) { - Logger::Error(e); - } catch (...) { - Logger::Error("Unknown error in addRelatedObject()"); - } - } - if (!found) { - if (!owner_hist) { - owner_hist = getSingle(); - } - if (!owner_hist) { - owner_hist = addOwnerHistory(); } + if (!found) { + if (!owner_hist) { + owner_hist = getSingle(); + } + if (!owner_hist) { + owner_hist = addOwnerHistory(); + } + typename Schema::IfcObject::list::ptr related_objects(new aggregate_of()); + related_objects->push((typename Schema::IfcObject*)related_object); + typename Schema::IfcRelDefinesByType* t = new typename Schema::IfcRelDefinesByType(IfcParse::IfcGlobalId(), owner_hist, boost::none, boost::none, related_objects, related_object->template as); - aggregate_of_instance::ptr related_objects(new aggregate_of_instance); - related_objects->push(related_object); - - IfcEntityInstanceData data = IfcEntityInstanceData(storage_t(T::Class().attribute_count())); - data.storage_.set(0, (std::string)IfcParse::IfcGlobalId()); - data.storage_.set(1, owner_hist); - int relating_index = 4; - int related_index = 5; - if (T::Class().name() == "IfcRelContainedInSpatialStructure" || std::is_base_of::value) { - // some classes have attributes reversed. - std::swap(relating_index, related_index); + addEntity(t); } - data.storage_.set(relating_index, relating_object); - data.storage_.set(related_index, related_objects); + } else { + typename T::list::ptr li = instances_by_type(); + bool found = false; + for (typename T::list::it i = li->begin(); i != li->end(); ++i) { + T* rel = *i; + try { + if (get_parent_of_relation(rel) == relating_object) { + aggregate_of_instance::ptr products = get_children_of_relation(rel); + products->push(related_object); + set_children_of_relation(rel, products); + found = true; + break; + } + } catch (std::exception& e) { + Logger::Error(e); + } catch (...) { + Logger::Error("Unknown error in addRelatedObject()"); + } + } + if (!found) { + if (!owner_hist) { + owner_hist = getSingle(); + } + if (!owner_hist) { + owner_hist = addOwnerHistory(); + } - T* t = (T*)Schema::get_schema().instantiate(&T::Class(), std::move(data)); - addEntity(t); + aggregate_of_instance::ptr related_objects(new aggregate_of_instance); + related_objects->push(related_object); + + IfcEntityInstanceData data = IfcEntityInstanceData(storage_t(T::Class().attribute_count())); + data.storage_.set(0, (std::string)IfcParse::IfcGlobalId()); + data.storage_.set(1, owner_hist); + int relating_index = 4; + int related_index = 5; + if (T::Class().name() == "IfcRelContainedInSpatialStructure" || std::is_base_of::value) { + // some classes have attributes reversed. + std::swap(relating_index, related_index); + } + data.storage_.set(relating_index, relating_object); + data.storage_.set(related_index, related_objects); + + T* t = (T*)Schema::get_schema().instantiate(&T::Class(), std::move(data)); + addEntity(t); + } } } @@ -589,70 +618,4 @@ IFC_PARSE_API void setSurfaceColour(IfcHierarchyHelper& file, Ifc4x IFC_PARSE_API void setSurfaceColour(IfcHierarchyHelper& file, Ifc4x3_add1::IfcRepresentation* shape, Ifc4x3_add1::IfcPresentationStyle* style); #endif -/* -template <> -inline void IfcHierarchyHelper::addRelatedObject (typename Schema::IfcObjectDefinition* relating_structure, - typename Schema::IfcObjectDefinition* related_object, typename Schema::IfcOwnerHistory* owner_hist) -{ - typename Schema::IfcRelContainedInSpatialStructure::list::ptr li = instances_by_type(); - bool found = false; - for (typename Schema::IfcRelContainedInSpatialStructure::list::it i = li->begin(); i != li->end(); ++i) { - typename Schema::IfcRelContainedInSpatialStructure* rel = *i; - if (rel->RelatingStructure() == relating_structure) { - typename Schema::IfcProduct::list::ptr products = rel->RelatedElements(); - products->push((typename Schema::IfcProduct*)related_object); - rel->setRelatedElements(products); - found = true; - break; - } - } - if (! found) { - if (! owner_hist) { - owner_hist = getSingle(); - } - if (! owner_hist) { - owner_hist = addOwnerHistory(); - } - typename Schema::IfcProduct::list::ptr related_objects (new aggregate_of()); - related_objects->push((typename Schema::IfcProduct*)related_object); - typename Schema::IfcRelContainedInSpatialStructure* t = new typename Schema::IfcRelContainedInSpatialStructure(IfcParse::IfcGlobalId(), owner_hist, - boost::none, boost::none, related_objects, (typename Schema::IfcSpatialStructureElement*)relating_structure); - - addEntity(t); - } -} - -template <> -inline void IfcHierarchyHelper::addRelatedObject (typename Schema::IfcObjectDefinition* relating_type, - typename Schema::IfcObjectDefinition* related_object, typename Schema::IfcOwnerHistory* owner_hist) -{ - typename Schema::IfcRelDefinesByType::list::ptr li = instances_by_type(); - bool found = false; - for (typename Schema::IfcRelDefinesByType::list::it i = li->begin(); i != li->end(); ++i) { - typename Schema::IfcRelDefinesByType* rel = *i; - if (rel->RelatingType() == relating_type) { - typename Schema::IfcObject::list::ptr objects = rel->RelatedObjects(); - objects->push((typename Schema::IfcObject*)related_object); - rel->setRelatedObjects(objects); - found = true; - break; - } - } - if (! found) { - if (! owner_hist) { - owner_hist = getSingle(); - } - if (! owner_hist) { - owner_hist = addOwnerHistory(); - } - typename Schema::IfcObject::list::ptr related_objects (new aggregate_of()); - related_objects->push((typename Schema::IfcObject*)related_object); - typename Schema::IfcRelDefinesByType* t = new typename Schema::IfcRelDefinesByType(IfcParse::IfcGlobalId(), owner_hist, - boost::none, boost::none, related_objects, (typename Schema::IfcTypeObject*)relating_type); - - addEntity(t); - } -} -*/ - #endif diff --git a/src/ifcpatch/ifcpatch/recipes/FixArchiCADToRevitSpaces.py b/src/ifcpatch/ifcpatch/recipes/FixArchiCADToRevitSpaces.py index a025709674..be7fac9d71 100644 --- a/src/ifcpatch/ifcpatch/recipes/FixArchiCADToRevitSpaces.py +++ b/src/ifcpatch/ifcpatch/recipes/FixArchiCADToRevitSpaces.py @@ -71,7 +71,6 @@ class Patcher: import bonsai.tool as tool import ifcopenshell import ifcopenshell.util.element - from bonsai.bim.ifc import IfcStore from mathutils import Vector, Matrix if len(bpy.data.objects) > 0: @@ -114,8 +113,8 @@ class Patcher: bpy.ops.bim.update_representation( ifc_representation_class="IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids" ) - for context in IfcStore.get_file().by_type("IfcGeometricRepresentationContext", include_subtypes=False): + for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): if context.Precision: context.Precision = 10 - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() diff --git a/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py b/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py index 07184d1cb2..6a52abd742 100644 --- a/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py +++ b/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py @@ -84,7 +84,8 @@ class Patcher: import bonsai.tool as tool from math import degrees - bpy.context.scene.BIMProjectProperties.should_use_native_meshes = True + props = tool.Project.get_project_props() + props.should_use_native_meshes = True bpy.ops.bim.load_project(filepath=self.filepath) old_history_size = tool.Ifc.get().history_size diff --git a/src/ifcwrap/IfcPython.i b/src/ifcwrap/IfcPython.i index b6f344da1a..196416586c 100644 --- a/src/ifcwrap/IfcPython.i +++ b/src/ifcwrap/IfcPython.i @@ -54,6 +54,11 @@ %include "exception.i" %include "std_shared_ptr.i" +%{ + #include +%} +%template(DoubleArray3) std::array; + %ignore IfcGeom::NumberNativeDouble; %ignore ifcopenshell::geometry::Converter; @@ -86,7 +91,7 @@ %ignore curve_to_loop_upgrade_impl; %ignore edge_to_loop_upgrade_impl; %ignore curve_to_face_upgrade_impl; -%ignore loop_to_piecewise_function_upgrade_impl; +%ignore loop_to_function_item_upgrade_impl; // settings, can this done more generally? %ignore UseElementNames; diff --git a/src/serializers/GltfSerializer.cpp b/src/serializers/GltfSerializer.cpp index 8f694801be..8b1c0ec03b 100644 --- a/src/serializers/GltfSerializer.cpp +++ b/src/serializers/GltfSerializer.cpp @@ -141,13 +141,12 @@ size_t write_accessor(json& j, std::ofstream& ofs, It begin, It end, int bufferV accessor["componentType"] = component_type::value; accessor["count"] = num; - if (N == 1) { + if constexpr (N == 1) { j["bufferViews"].push_back({ {"buffer", 0}, {"byteOffset", (size_t)ofs.tellp()}, { "byteLength", num * 4}, {"target", ELEMENT_ARRAY_BUFFER} }); } else { j["bufferViews"].push_back({ {"buffer", 0}, {"byteStride", 12}, { "byteOffset", (size_t)ofs.tellp()}, { "byteLength", num * 12}, {"target", ARRAY_BUFFER}}); } - std::array min, max; min.fill(std::numeric_limits::max()); max.fill(std::numeric_limits::lowest());