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..572cfaf117 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -221,7 +221,8 @@ 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() @@ -343,7 +344,7 @@ def load_post(scene): bpy.context.scene.BIMProperties.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/ifc.py b/src/bonsai/bonsai/bim/ifc.py index 1c5d7800b1..044bf74fd2 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -178,6 +178,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 +188,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 +196,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 +247,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) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index d2bdc2ed70..13fce33a93 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -90,7 +90,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 +219,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 +435,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": diff --git a/src/bonsai/bonsai/bim/module/bcf/operator.py b/src/bonsai/bonsai/bim/module/bcf/operator.py index bfcc40b8a9..aed65de52c 100644 --- a/src/bonsai/bonsai/bim/module/bcf/operator.py +++ b/src/bonsai/bonsai/bim/module/bcf/operator.py @@ -1316,7 +1316,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( diff --git a/src/bonsai/bonsai/bim/module/boundary/operator.py b/src/bonsai/bonsai/bim/module/boundary/operator.py index 5d83a88597..75cf32cb66 100644 --- a/src/bonsai/bonsai/bim/module/boundary/operator.py +++ b/src/bonsai/bonsai/bim/module/boundary/operator.py @@ -114,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() 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/classification/operator.py b/src/bonsai/bonsai/bim/module/classification/operator.py index 199fa2d61b..acdffae946 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 diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 33100956ef..bb949b8fd0 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -101,13 +101,13 @@ 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() + tool.Debug.get_debug_props().attributes.clear() IfcStore.purge() bonsai.bim.handler.refresh_ui_data() return {"FINISHED"} @@ -256,7 +256,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 +309,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"} @@ -332,9 +332,9 @@ class InspectFromStepId(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() - debug_props = context.scene.BIMDebugProperties + 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 +385,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 @@ -438,7 +438,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 +453,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 +499,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 +545,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 +805,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..4fbd5c8ea6 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,7 +33,7 @@ class BIM_PT_debug(Panel): def draw(self, context): layout = self.layout - props = context.scene.BIMDebugProperties + props = tool.Debug.get_debug_props() row = self.layout.row(align=True) row.prop(context.scene.BIMProperties, "ifc_file", text="") @@ -87,27 +88,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 +122,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/drawing/data.py b/src/bonsai/bonsai/bim/module/drawing/data.py index be252a29c9..d781b8bd30 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") diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index e8fba1914d..0bf8ab00b8 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] @@ -2022,7 +2023,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__()) 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 720459f85b..c63fd3d910 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -22,6 +22,7 @@ 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 @@ -136,7 +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")) + area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol( + ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT") + ) value *= scaleFactor @@ -326,16 +329,18 @@ def format_distance( 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..6bac483f21 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: @@ -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) @@ -714,7 +714,8 @@ class CreateDrawing(bpy.types.Operator): files = {context.scene.BIMProperties.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] @@ -1141,7 +1142,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: @@ -1458,7 +1460,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 +1477,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 +1486,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 +1511,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 +1533,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 +1554,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 +1568,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,7 +1616,7 @@ 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: return False @@ -1622,8 +1626,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 +1684,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,7 +1718,7 @@ 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 @@ -1731,7 +1735,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 +1842,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 +1862,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 +1876,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 +1912,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 +1976,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 +2033,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 +2056,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 +2072,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 +2105,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 +2119,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 +2192,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 +2218,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 +2234,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 +2291,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 +2318,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 +2348,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 +2367,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 +2403,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 +2519,14 @@ 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 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 +2585,14 @@ 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 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 +2694,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 +2708,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 +2993,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 +3023,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 +3037,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 +3045,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 +3149,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 +3165,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 +3181,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 +3197,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 +3388,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 +3402,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/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..bd679cb065 100644 --- a/src/bonsai/bonsai/bim/module/drawing/workspace.py +++ b/src/bonsai/bonsai/bim/module/drawing/workspace.py @@ -197,6 +197,8 @@ def create_annotation_occurrence(context): class AnnotationToolUI: + layout: bpy.types.UILayout + @classmethod def draw(cls, context, layout): cls.layout = layout @@ -224,7 +226,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): 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 6a234800bf..1d43e584d7 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -23,6 +23,7 @@ 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 @@ -78,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) @@ -86,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") @@ -121,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): @@ -302,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): @@ -457,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: @@ -486,7 +493,8 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator): 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. @@ -545,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) @@ -569,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 @@ -586,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) @@ -598,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() 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) @@ -627,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.") @@ -721,7 +732,7 @@ class OverrideDelete(bpy.types.Operator): row = self.layout.row() row.label(text="Warning: Faster deletion will use more memory.", icon="ERROR") - def _execute(self, context): + def _execute(self, context: bpy.types.Context): start_time = time() if self.is_batch: @@ -730,9 +741,7 @@ class OverrideDelete(bpy.types.Operator): self.process_arrays(context) clear_active_object = True for obj in context.selected_objects: - try: - obj.name - except: + if not tool.Blender.is_valid_data_block(obj): continue element = tool.Ifc.get_entity(obj) if element: @@ -782,7 +791,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: @@ -1030,7 +1039,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: @@ -1054,7 +1064,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) @@ -1090,12 +1100,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) @@ -1671,7 +1683,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] @@ -1694,7 +1707,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() @@ -1702,7 +1715,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: @@ -1741,7 +1755,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( @@ -1892,9 +1907,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: @@ -1950,6 +1966,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]: @@ -1957,7 +1974,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: @@ -2005,12 +2022,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") @@ -2027,21 +2047,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: @@ -2052,10 +2072,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: @@ -2102,14 +2123,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) @@ -2155,24 +2177,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) @@ -2197,9 +2222,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) @@ -2220,11 +2247,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( @@ -2277,9 +2304,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): @@ -2305,10 +2332,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) @@ -2317,7 +2343,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 @@ -2330,10 +2356,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): @@ -2370,12 +2397,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] @@ -2440,7 +2468,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): @@ -2451,9 +2480,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" ) @@ -2488,13 +2520,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): @@ -2512,7 +2548,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): @@ -2547,7 +2583,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() @@ -2566,7 +2603,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() @@ -2588,7 +2626,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 @@ -2604,7 +2642,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 @@ -2671,7 +2710,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 @@ -2687,11 +2727,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) @@ -2744,7 +2786,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 @@ -2755,10 +2798,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) @@ -2841,7 +2886,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 @@ -2871,7 +2916,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() @@ -2888,6 +2933,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) @@ -2902,7 +2951,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 @@ -2972,9 +3021,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() @@ -3020,10 +3069,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() @@ -3091,10 +3140,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) @@ -3140,10 +3189,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 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..b0ad2ea47d 100644 --- a/src/bonsai/bonsai/bim/module/geometry/ui.py +++ b/src/bonsai/bonsai/bim/module/geometry/ui.py @@ -53,9 +53,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): @@ -411,15 +412,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 +431,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 +458,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 +481,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 +575,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 +592,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/layer/operator.py b/src/bonsai/bonsai/bim/module/layer/operator.py index 9c2f4a2249..13b5d79a56 100644 --- a/src/bonsai/bonsai/bim/module/layer/operator.py +++ b/src/bonsai/bonsai/bim/module/layer/operator.py @@ -147,7 +147,7 @@ class AssignPresentationLayer(bpy.types.Operator, tool.Ifc.Operator): "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 +164,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..5fb8db827d 100644 --- a/src/bonsai/bonsai/bim/module/layer/ui.py +++ b/src/bonsai/bonsai/bim/module/layer/ui.py @@ -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/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index 19865be4e8..9ddd5ccd9d 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -351,9 +351,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/opening.py b/src/bonsai/bonsai/bim/module/model/opening.py index 0e73e71a91..d9da756705 100644 --- a/src/bonsai/bonsai/bim/module/model/opening.py +++ b/src/bonsai/bonsai/bim/module/model/opening.py @@ -236,9 +236,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 +465,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 +667,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 +694,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 +821,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 +845,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 +854,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 +915,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/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index 6286422f99..d96e7d0b51 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 @@ -265,7 +266,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 +502,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 +516,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 +529,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( diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index de3078819d..328eb34090 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) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 5a94be7528..d6e6ec7a31 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -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..11f4fe73c5 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -96,7 +96,8 @@ 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() + if props.mode == "ITEM": EditItemUI.draw(context, layout) elif ( active_ifc_object := (context.active_object and tool.Ifc.get_entity(context.active_object)) @@ -430,7 +431,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 +441,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="") @@ -1136,7 +1137,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/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 1a330917db..eab0db8ef2 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -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/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 8510a7d125..0aea8d00a3 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -75,35 +75,36 @@ class NewProject(bpy.types.Operator): def execute(self, context): bpy.ops.wm.read_homefile() + pprops = tool.Project.get_project_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" + 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" + 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" + 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" + pprops.template_file = "IFC4 Demo Template.ifc" if self.preset != "wizard": bpy.ops.bim.create_project() @@ -126,7 +127,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: @@ -592,7 +593,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 @@ -771,8 +773,8 @@ class EnableEditingHeader(bpy.types.Operator): return IfcStore.get_file() 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) @@ -818,8 +820,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}]",) @@ -860,7 +862,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"} @@ -983,9 +986,10 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): 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()) @@ -1051,8 +1055,8 @@ 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") @@ -1084,7 +1088,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() @@ -1156,7 +1161,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"} @@ -1183,7 +1189,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() @@ -1204,7 +1210,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) @@ -1235,9 +1242,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"} @@ -1257,8 +1265,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) @@ -1267,7 +1276,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() @@ -1299,7 +1308,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 @@ -1325,13 +1334,13 @@ 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 + 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}" @@ -1342,7 +1351,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}" @@ -1397,7 +1406,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) @@ -1433,8 +1442,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(): @@ -1460,8 +1469,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) @@ -1507,7 +1516,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: @@ -1549,7 +1558,7 @@ 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 + self.use_relative_path = tool.Project.get_project_props().use_relative_project_path if (filepath := context.scene.BIMProperties.ifc_file) and not self.should_save_as: self.filepath = str(tool.Blender.ensure_blender_path_is_abs(Path(filepath))) return self.execute(context) @@ -1564,7 +1573,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 @@ -1612,10 +1621,12 @@ class ExportIFC(bpy.types.Operator): # 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 @@ -1660,8 +1671,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) @@ -1875,7 +1886,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 @@ -1981,9 +1992,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": @@ -2119,6 +2129,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"} @@ -2128,7 +2139,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 @@ -2275,34 +2286,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 @@ -2326,14 +2339,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 @@ -2372,8 +2386,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"} @@ -2413,7 +2427,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) @@ -2444,7 +2458,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"} @@ -2462,14 +2476,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] @@ -2495,13 +2510,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/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/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 5083fb2445..4022c1d5ff 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -711,14 +711,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/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/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..a1493f2711 100644 --- a/src/bonsai/bonsai/bim/module/void/operator.py +++ b/src/bonsai/bonsai/bim/module/void/operator.py @@ -139,7 +139,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, @@ -269,20 +270,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 +303,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 +343,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/operator.py b/src/bonsai/bonsai/bim/operator.py index be0d07102d..2bd6f445f4 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -828,7 +828,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"} @@ -839,7 +840,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"} @@ -1060,7 +1062,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"} @@ -1070,7 +1073,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: @@ -1082,7 +1085,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 @@ -1106,7 +1108,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 @@ -1163,9 +1165,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 2f095cbb2c..c9b8c759d8 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -525,6 +525,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") @@ -532,6 +539,11 @@ 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") @@ -540,6 +552,9 @@ class GlobalId(PropertyGroup): class BIMCollectionProperties(PropertyGroup): obj: PointerProperty(type=bpy.types.Object) + if TYPE_CHECKING: + obj: Union[bpy.types.Object, None] + class BIMObjectProperties(PropertyGroup): collection: PointerProperty(type=bpy.types.Collection) @@ -564,6 +579,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") @@ -572,7 +590,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) @@ -580,6 +598,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") @@ -599,16 +633,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) @@ -616,3 +664,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/ui.py b/src/bonsai/bonsai/bim/ui.py index fe300719af..a77650a72b 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: @@ -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") @@ -247,7 +248,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): 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.', + 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", @@ -302,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, @@ -312,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() @@ -333,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") @@ -341,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() @@ -357,7 +380,7 @@ 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: row = layout.row(align=True) row.prop(context.scene.BIMProperties, "data_dir") row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.data_dir" @@ -370,25 +393,26 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): 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, context): + def draw_drawing_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: 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") + 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") @@ -496,9 +520,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() @@ -533,7 +558,7 @@ class BIM_PT_tab_new_project_wizard(Panel): if not tool.Blender.is_tab(context, "PROJECT"): return False props = context.scene.BIMProperties - pprops = context.scene.BIMProjectProperties + pprops = tool.Project.get_project_props() if pprops.is_loading: return False elif tool.Ifc.get() or props.ifc_file: @@ -555,7 +580,7 @@ class BIM_PT_tab_project_info(Panel): if not tool.Blender.is_tab(context, "PROJECT"): return False props = context.scene.BIMProperties - pprops = context.scene.BIMProjectProperties + pprops = tool.Project.get_project_props() if pprops.is_loading: return True elif tool.Ifc.get() or props.ifc_file: @@ -854,6 +879,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() @@ -862,7 +888,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) ) ) @@ -1231,7 +1257,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/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index ad62b1fabc..b9699de172 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -913,7 +913,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"): diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py index 0dd9b6c855..ec4490b2c3 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()) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 134f3bcf5f..088e5b7025 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -346,19 +346,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 +387,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 +624,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 +819,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 +1057,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 +1157,33 @@ 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] 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 +1194,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] - resource_path = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", f"{resource}Path") or getattr( - bpy.context.scene.DocProperties, f"{resource.lower()}_path" + props = tool.Drawing.get_document_props() + resource_path = ( + ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", f"{resource}Path") or props.resource_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 +1509,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 +1891,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..6fb121766c 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: @@ -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,18 @@ 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 - ): - tool.Blender.remove_data_block(obj.data) + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) + if not tool.Ifc.get_entity_by_id(tool.Geometry.get_mesh_props(mesh).ifc_definition_id): + tool.Blender.remove_data_block(mesh) 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 +288,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 +301,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 +512,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 +702,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 +858,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 +885,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 +901,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 +1008,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 +1027,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 +1043,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 +1076,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 +1182,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 +1205,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 +1277,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 +1566,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 +1681,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 +1724,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 +1751,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 +1779,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 +1845,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 +1875,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 f2a0a4d866..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 @@ -61,7 +72,6 @@ class Georeference(bonsai.core.tool.Georeference): new.update = "tool.Georeference.update_map_unit" return True - props = bpy.context.scene.BIMGeoreferenceProperties props.projected_crs.clear() if tool.Ifc.get_schema() == "IFC2X3": @@ -84,7 +94,8 @@ class Georeference(bonsai.core.tool.Georeference): result = 1.0 else: result = 1.0 - for attribute in bpy.context.scene.BIMGeoreferenceProperties.coordinate_operation: + props = cls.get_georeference_props() + for attribute in props.coordinate_operation: if attribute.name == "Scale": attribute.set_value(str(result)) @@ -92,7 +103,7 @@ class Georeference(bonsai.core.tool.Georeference): 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" @@ -110,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 @@ -132,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": @@ -151,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" @@ -175,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 @@ -191,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 @@ -206,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: @@ -219,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 @@ -260,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], @@ -279,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], @@ -329,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 @@ -346,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), @@ -361,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}" @@ -375,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..c82ffa80a5 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: @@ -180,7 +180,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 +220,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 +233,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/loader.py b/src/bonsai/bonsai/tool/loader.py index f79f4b4509..17e8764fbf 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" 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 8554e182f1..db8f4e7922 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 @@ -452,7 +453,8 @@ 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: area_unit = bpy.context.scene.BIMProperties.area_unit unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get(), unit_type=area_unit) diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index 7b529cdea9..98f4f76799 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -235,7 +235,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 +252,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)) diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index b18e649338..ac763cdbf4 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -19,6 +19,7 @@ import bpy import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.style import ifcopenshell.util.representation import ifcopenshell.util.element import ifcopenshell.util.placement @@ -49,12 +50,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 @@ -169,8 +170,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: @@ -302,8 +303,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,8 +422,8 @@ 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(mesh).ifc_definition_id = 0 for material_slot in obj.material_slots: if material := material_slot.material: tool.Ifc.unlink(obj=material) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index b06d9a65bb..10636f031a 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): @@ -313,7 +314,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 +335,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 diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index f2c784c464..2f59e0b3da 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -699,9 +699,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) ): @@ -959,7 +960,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) diff --git a/src/bonsai/bonsai/tool/surveyor.py b/src/bonsai/bonsai/tool/surveyor.py index 80aa96a341..28856a19f8 100644 --- a/src/bonsai/bonsai/tool/surveyor.py +++ b/src/bonsai/bonsai/tool/surveyor.py @@ -32,7 +32,7 @@ class Surveyor(bonsai.core.tool.Surveyor): def get_absolute_matrix(cls, obj: bpy.types.Object) -> 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/scripts/headless_import.py b/src/bonsai/scripts/headless_import.py index cda13e3878..6ce5f1f1ad 100644 --- a/src/bonsai/scripts/headless_import.py +++ b/src/bonsai/scripts/headless_import.py @@ -1,12 +1,13 @@ # This can be run using `blender -b -P headless_import.py` import bpy +import bonsai.tool as tool from bonsai.bim.ifc import IfcStore # 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 +20,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/test/bim/bootstrap.py b/src/bonsai/test/bim/bootstrap.py index bea49df413..a973c8a7dc 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() diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 95945ced81..50598d7be1 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 @@ -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}" diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index 5e3c239e03..902e51e75d 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -113,30 +113,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 +170,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 +500,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 +516,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 +527,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 +540,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 +551,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 +564,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 +575,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 +665,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 +689,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 +854,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 +877,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..641a8adfa0 100644 --- a/src/bonsai/test/tool/test_ifc.py +++ b/src/bonsai/test/tool/test_ifc.py @@ -189,7 +189,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_model.py b/src/bonsai/test/tool/test_model.py index 13a847f94a..a77d90ab92 100644 --- a/src/bonsai/test/tool/test_model.py +++ b/src/bonsai/test/tool/test_model.py @@ -378,7 +378,7 @@ 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() @@ -467,7 +467,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() diff --git a/src/bonsai/test/tool/test_project.py b/src/bonsai/test/tool/test_project.py index 4af7494a85..a5f94dbe4b 100644 --- a/src/bonsai/test/tool/test_project.py +++ b/src/bonsai/test/tool/test_project.py @@ -244,25 +244,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 +271,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 +286,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 +299,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 +326,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..e87eb312a7 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 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/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/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/material/edit_profile_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py index 4a659f251c..8a583d64b4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py @@ -91,8 +91,7 @@ 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: 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/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 a6637c7a88..ab1035bbab 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py @@ -75,7 +75,9 @@ class Usecase: 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] ) 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..e9fcc0d660 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 + ccs = 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.Axis = file.create_entity("IfcDirection", ifc_safe_vector_type(axis)) + if (prev_ref_direction := ccs.RefDirection) and len(file.get_inverse(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/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