diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index d1c158dc8e..5c36163207 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -1170,7 +1170,7 @@ class IfcImportSettings: @staticmethod def factory(context=None, input_file=None, logger=None): - scene_diff = bpy.context.scene.DiffProperties + scene_diff = tool.Blender.get_diff_props() props = tool.Project.get_project_props() settings = IfcImportSettings() settings.input_file = input_file diff --git a/src/bonsai/bonsai/bim/module/alignment/operator.py b/src/bonsai/bonsai/bim/module/alignment/operator.py index 1fcbdd52c8..fe38ceea40 100644 --- a/src/bonsai/bonsai/bim/module/alignment/operator.py +++ b/src/bonsai/bonsai/bim/module/alignment/operator.py @@ -31,6 +31,8 @@ import isodate import bonsai.core.sequence as core import bonsai.tool as tool import bonsai.bim.module.sequence.helper as helper +import ifcopenshell.api.spatial +import ifcopenshell.geom import ifcopenshell.util.sequence import ifcopenshell.util.selector from datetime import datetime @@ -59,8 +61,6 @@ class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): return True def _execute(self, context): - import ifcopenshell.api.alignment - self.file = tool.Ifc.get() start = time.time() alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath) diff --git a/src/bonsai/bonsai/bim/module/clash/decorator.py b/src/bonsai/bonsai/bim/module/clash/decorator.py index f23b8962c6..764c6e5232 100644 --- a/src/bonsai/bonsai/bim/module/clash/decorator.py +++ b/src/bonsai/bonsai/bim/module/clash/decorator.py @@ -60,8 +60,9 @@ class ClashDecorator: unselected_elements_color = self.addon_prefs.decorator_color_unselected special_elements_color = self.addon_prefs.decorator_color_special - text = context.scene.BIMClashProperties.active_clash_text - p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5) + props = tool.Clash.get_clash_props() + text = props.active_clash_text + p = props.p1.lerp(props.p2, 0.5) font_id = 0 blf.size(font_id, 12) @@ -92,7 +93,8 @@ class ClashDecorator: # general shader self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") - selected_vertices = [context.scene.BIMClashProperties.p1, context.scene.BIMClashProperties.p2] + props = tool.Clash.get_clash_props() + selected_vertices = [props.p1, props.p2] selected_edges = [] if selected_vertices[0] != selected_vertices[1]: selected_edges = [[0, 1]] diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index a3e43c72d6..3b19612c98 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -69,9 +69,10 @@ class ImportClashSets(bpy.types.Operator): def execute(self, context): tool.Clash.load_clash_sets(self.filepath) - context.scene.BIMClashProperties.clash_sets.clear() + props = tool.Clash.get_clash_props() + props.clash_sets.clear() for clash_set in tool.Clash.get_clash_sets(): - new = context.scene.BIMClashProperties.clash_sets.add() + new = props.clash_sets.add() new.name = clash_set["name"] new.mode = clash_set["mode"] if new.mode == "intersection": @@ -106,7 +107,8 @@ class AddClashSet(bpy.types.Operator): bl_description = "Add a clash set" def execute(self, context): - new = context.scene.BIMClashProperties.clash_sets.add() + props = tool.Clash.get_clash_props() + new = props.clash_sets.add() new.name = "New Clash Set" return {"FINISHED"} @@ -119,7 +121,8 @@ class RemoveClashSet(bpy.types.Operator): index: bpy.props.IntProperty() def execute(self, context): - context.scene.BIMClashProperties.clash_sets.remove(self.index) + props = tool.Clash.get_clash_props() + props.clash_sets.remove(self.index) return {"FINISHED"} @@ -131,7 +134,8 @@ class AddClashSource(bpy.types.Operator): group: bpy.props.StringProperty() def execute(self, context): - clash_set = context.scene.BIMClashProperties.active_clash_set + props = tool.Clash.get_clash_props() + clash_set = props.active_clash_set source = getattr(clash_set, self.group).add() return {"FINISHED"} @@ -145,7 +149,8 @@ class RemoveClashSource(bpy.types.Operator): group: bpy.props.StringProperty() def execute(self, context): - clash_set = context.scene.BIMClashProperties.active_clash_set + props = tool.Clash.get_clash_props() + clash_set = props.active_clash_set getattr(clash_set, self.group).remove(self.index) return {"FINISHED"} @@ -161,7 +166,8 @@ class SelectClashSource(bpy.types.Operator): group: bpy.props.StringProperty() def execute(self, context): - clash_set = context.scene.BIMClashProperties.active_clash_set + props = tool.Clash.get_clash_props() + clash_set = props.active_clash_set getattr(clash_set, self.group)[self.index].name = self.filepath return {"FINISHED"} @@ -178,7 +184,8 @@ class SelectClashResults(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - context.scene.BIMClashProperties.clash_results_path = self.filepath + props = tool.Clash.get_clash_props() + props.clash_results_path = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -194,7 +201,8 @@ class SelectSmartGroupedClashesPath(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - context.scene.BIMClashProperties.smart_grouped_clashes_path = self.filepath + props = tool.Clash.get_clash_props() + props.smart_grouped_clashes_path = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -218,7 +226,7 @@ class ExecuteIfcClash(bpy.types.Operator): def execute(self, context): from ifcclash import ifcclash - self.props = context.scene.BIMClashProperties + self.props = tool.Clash.get_clash_props() _, extension = os.path.splitext(self.filepath) if extension != ".bcf": @@ -306,7 +314,9 @@ class SelectIfcClashResults(bpy.types.Operator): self.filepath = bpy.path.ensure_ext(self.filepath, ".json") with open(self.filepath) as f: clash_sets = json.load(f) - clash_set_name = context.scene.BIMClashProperties.active_clash_set.name + clash_props = tool.Clash.get_clash_props() + assert clash_props.active_clash_set + clash_set_name = clash_props.active_clash_set.name global_ids = [] for clash_set in clash_sets: if clash_set["name"] != clash_set_name: @@ -358,7 +368,7 @@ class SelectClash(bpy.types.Operator): index: bpy.props.IntProperty() def execute(self, context): - self.props = context.scene.BIMClashProperties + self.props = tool.Clash.get_clash_props() clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name) active_clash = self.props.active_clash clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id) @@ -392,13 +402,15 @@ class SmartClashGroup(bpy.types.Operator): @classmethod def poll(cls, context): - return context.scene.BIMClashProperties.clash_results_path + props = tool.Clash.get_clash_props() + return bool(props.clash_results_path) def execute(self, context): from ifcclash import ifcclash settings = ifcclash.ClashSettings() - self.filepath = bpy.path.ensure_ext(context.scene.BIMClashProperties.clash_results_path, ".json") + props = tool.Clash.get_clash_props() + self.filepath = bpy.path.ensure_ext(props.clash_results_path, ".json") settings.output = self.filepath settings.logger = logging.getLogger("Clash") settings.logger.setLevel(logging.DEBUG) @@ -408,19 +420,18 @@ class SmartClashGroup(bpy.types.Operator): clash_sets = json.load(f) # execute the smart grouping - save_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json") - smart_grouped_clashes = ifc_clasher.smart_group_clashes( - clash_sets, context.scene.BIMClashProperties.smart_clash_grouping_max_distance - ) + save_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json") + smart_grouped_clashes = ifc_clasher.smart_group_clashes(clash_sets, props.smart_clash_grouping_max_distance) # save smart_groups to json with open(save_path, "w") as f: f.write(json.dumps(smart_grouped_clashes)) - clash_set_name = context.scene.BIMClashProperties.active_clash_set.name + assert props.active_clash_set + clash_set_name = props.active_clash_set.name # Reset the list of smart_clash_groups for the UI - context.scene.BIMClashProperties.smart_clash_groups.clear() + props.smart_clash_groups.clear() for clash_set, smart_groups in smart_grouped_clashes.items(): # Only select the clashes that correspond to the actively selected IFC Clash Set @@ -428,7 +439,7 @@ class SmartClashGroup(bpy.types.Operator): continue else: for smart_group, global_id_pairs in smart_groups[0].items(): - new_group = context.scene.BIMClashProperties.smart_clash_groups.add() + new_group = props.smart_clash_groups.add() new_group.number = f"{smart_group}" for pair in global_id_pairs: @@ -446,18 +457,21 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator): @classmethod def poll(cls, context): - return context.scene.BIMClashProperties.active_clash_set + props = tool.Clash.get_clash_props() + return bool(props.active_clash_set) def execute(self, context): - smart_groups_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json") + props = tool.Clash.get_clash_props() + smart_groups_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json") - clash_set_name = context.scene.BIMClashProperties.active_clash_set.name + assert props.active_clash_set + clash_set_name = props.active_clash_set.name with open(smart_groups_path) as f: smart_grouped_clashes = json.load(f) # Reset the list of smart_clash_groups for the UI - context.scene.BIMClashProperties.smart_clash_groups.clear() + props.smart_clash_groups.clear() for clash_set, smart_groups in smart_grouped_clashes.items(): # Only select the clashes that correspond to the actively selected IFC Clash Set @@ -465,7 +479,7 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator): continue else: for smart_group, global_id_pairs in smart_groups[0].items(): - new_group = context.scene.BIMClashProperties.smart_clash_groups.add() + new_group = props.smart_clash_groups.add() new_group.number = f"{smart_group}" for pair in global_id_pairs: for guid in pair: @@ -482,11 +496,14 @@ class SelectSmartGroup(bpy.types.Operator): @classmethod def poll(cls, context): - return tool.Ifc.get() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group + props = tool.Clash.get_clash_props() + return tool.Ifc.get() and context.visible_objects and props.active_smart_group def execute(self, context): - selected_smart_group = context.scene.BIMClashProperties.active_smart_group - products = [] + props = tool.Clash.get_clash_props() + selected_smart_group = props.active_smart_group + assert selected_smart_group + products: list[ifcopenshell.entity_instance] = [] for global_id in selected_smart_group.global_ids: try: products.append(tool.Ifc.get().by_guid(global_id.guid)) diff --git a/src/bonsai/bonsai/bim/module/clash/prop.py b/src/bonsai/bonsai/bim/module/clash/prop.py index b112964fe4..88b13a0e25 100644 --- a/src/bonsai/bonsai/bim/module/clash/prop.py +++ b/src/bonsai/bonsai/bim/module/clash/prop.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool from bonsai.bim.prop import StrProperty, Attribute, BIMFilterGroup from bpy.types import PropertyGroup from bpy.props import ( @@ -29,6 +30,8 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from mathutils import Vector +from typing import TYPE_CHECKING, Literal, Union class ClashSource(PropertyGroup): @@ -43,6 +46,10 @@ class ClashSource(PropertyGroup): name="Mode", ) + if TYPE_CHECKING: + filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup] + mode: Literal["a", "i", "e"] + class Clash(PropertyGroup): a_global_id: StringProperty(name="A") @@ -51,9 +58,15 @@ class Clash(PropertyGroup): b_name: StringProperty(name="B Name") status: BoolProperty(name="Status", default=False) + if TYPE_CHECKING: + a_global_id: str + b_global_id: str + a_name: str + b_name: str + status: bool + class ClashSet(PropertyGroup): - name: StringProperty(name="Name") mode: EnumProperty( items=[ ( @@ -76,11 +89,25 @@ class ClashSet(PropertyGroup): b: CollectionProperty(name="Group B", type=ClashSource) clashes: CollectionProperty(name="Clashes", type=Clash) + if TYPE_CHECKING: + mode: Literal["intersection", "collision", "clearance"] + tolerance: float + clearance: float + allow_touching: bool + check_all: bool + a: bpy.types.bpy_prop_collection_idprop[ClashSource] + b: bpy.types.bpy_prop_collection_idprop[ClashSource] + clashes: bpy.types.bpy_prop_collection_idprop[Clash] + class SmartClashGroup(PropertyGroup): number: StringProperty(name="Number") global_ids: CollectionProperty(name="GlobalIDs", type=StrProperty) + if TYPE_CHECKING: + number: str + global_ids: bpy.types.bpy_prop_collection_idprop[StrProperty] + class BIMClashProperties(PropertyGroup): blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty) @@ -107,17 +134,33 @@ class BIMClashProperties(PropertyGroup): subtype="FILE_PATH", ) - @property - def active_clash_set(self): - if self.active_clash_set_index < len(self.clash_sets): - return self.clash_sets[self.active_clash_set_index] + if TYPE_CHECKING: + blender_clash_set_a: bpy.types.bpy_prop_collection_idprop[StrProperty] + blender_clash_set_b: bpy.types.bpy_prop_collection_idprop[StrProperty] + clash_sets: bpy.types.bpy_prop_collection_idprop[ClashSet] + should_create_clash_snapshots: bool + clash_results_path: str + smart_grouped_clashes_path: str + active_clash_set_index: int + active_clash_index: int + smart_clash_groups: bpy.types.bpy_prop_collection_idprop[SmartClashGroup] + active_smart_group_index: int + smart_clash_grouping_max_distance: int + p1: Vector + p2: Vector + active_clash_text: str + export_path: str @property - def active_smart_group(self): - if self.active_smart_group_index < len(self.smart_clash_groups): - return self.smart_clash_groups[self.active_smart_group_index] + def active_clash_set(self) -> Union[ClashSet, None]: + return tool.Blender.get_active_uilist_element(self.clash_sets, self.active_clash_set_index) @property - def active_clash(self): - if self.active_clash_index < len(self.active_clash_set.clashes): - return self.active_clash_set.clashes[self.active_clash_index] + def active_smart_group(self) -> Union[SmartClashGroup, None]: + return tool.Blender.get_active_uilist_element(self.smart_clash_groups, self.active_smart_group_index) + + @property + def active_clash(self) -> Union[Clash, None]: + if not (clash_set := self.active_clash_set): + return None + return tool.Blender.get_active_uilist_element(clash_set.clashes, self.active_clash_index) diff --git a/src/bonsai/bonsai/bim/module/clash/ui.py b/src/bonsai/bonsai/bim/module/clash/ui.py index 2a95b54ab3..3943f21f04 100644 --- a/src/bonsai/bonsai/bim/module/clash/ui.py +++ b/src/bonsai/bonsai/bim/module/clash/ui.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool import bonsai.bim.helper from bpy.types import Panel from bonsai.bim.module.clash.data import ClashData @@ -36,9 +37,7 @@ class BIM_PT_ifcclash(Panel): ClashData.load() layout = self.layout - - scene = context.scene - props = scene.BIMClashProperties + props = tool.Clash.get_clash_props() row = layout.row(align=True) row.operator("bim.add_clash_set") @@ -157,7 +156,7 @@ class BIM_PT_smart_clash_manager(Panel): def draw(self, context): layout = self.layout - props = context.scene.BIMClashProperties + props = tool.Clash.get_clash_props() row = layout.row() layout.label(text="Select clash results to group:") diff --git a/src/bonsai/bonsai/bim/module/csv/operator.py b/src/bonsai/bonsai/bim/module/csv/operator.py index 18667381d6..77c58e54ad 100644 --- a/src/bonsai/bonsai/bim/module/csv/operator.py +++ b/src/bonsai/bonsai/bim/module/csv/operator.py @@ -42,7 +42,8 @@ class AddCsvAttribute(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - attribute = context.scene.CsvProperties.csv_attributes.add() + props = tool.Blender.get_csv_props() + props.csv_attributes.add() return {"FINISHED"} @@ -53,7 +54,8 @@ class RemoveCsvAttribute(bpy.types.Operator): index: bpy.props.IntProperty() def execute(self, context): - context.scene.CsvProperties.csv_attributes.remove(self.index) + props = tool.Blender.get_csv_props() + props.csv_attributes.remove(self.index) return {"FINISHED"} @@ -64,7 +66,8 @@ class RemoveAllCsvAttributes(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.scene.CsvProperties.csv_attributes.clear() + props = tool.Blender.get_csv_props() + props.csv_attributes.clear() return {"FINISHED"} @@ -76,8 +79,9 @@ class ReorderCsvAttribute(bpy.types.Operator): new_index: bpy.props.IntProperty() def execute(self, context): - old = context.scene.CsvProperties.csv_attributes[self.old_index] - new = context.scene.CsvProperties.csv_attributes[self.new_index] + props = tool.Blender.get_csv_props() + old = props.csv_attributes[self.old_index] + new = props.csv_attributes[self.new_index] props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"] for prop in props: value = getattr(new, prop) @@ -95,7 +99,7 @@ class ImportCsvAttributes(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() data = json.load(open(self.filepath)) tool.Search.import_filter_query(data["query"], props.filter_groups) @@ -136,7 +140,7 @@ class ExportCsvAttributes(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() settings = {} for prop in [ @@ -191,14 +195,14 @@ class ExportIfcCsv(bpy.types.Operator): @classmethod def poll(cls, context): - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() if not props.should_load_from_memory and not props.csv_ifc_file: cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.") return False return True def invoke(self, context, event): - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() if props.format == "web": return self.execute(context) self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}") @@ -209,7 +213,7 @@ class ExportIfcCsv(bpy.types.Operator): def execute(self, context): import ifccsv - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() self.filepath = bpy.path.ensure_ext(self.filepath, f".{props.format}") if props.should_load_from_memory: ifc_file = tool.Ifc.get() @@ -298,7 +302,7 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() if not props.should_load_from_memory and not props.csv_ifc_file: cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.") return False @@ -313,7 +317,7 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): import ifccsv - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() if props.should_load_from_memory: ifc_file = tool.Ifc.get() else: @@ -349,7 +353,8 @@ class SelectCsvIfcFile(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - context.scene.CsvProperties.csv_ifc_file = self.filepath + props = tool.Blender.get_csv_props() + props.csv_ifc_file = self.filepath return {"FINISHED"} def invoke(self, context, event): diff --git a/src/bonsai/bonsai/bim/module/csv/prop.py b/src/bonsai/bonsai/bim/module/csv/prop.py index 9426eea5a5..f66ee75c51 100644 --- a/src/bonsai/bonsai/bim/module/csv/prop.py +++ b/src/bonsai/bonsai/bim/module/csv/prop.py @@ -29,6 +29,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING, Literal class CsvAttribute(PropertyGroup): @@ -59,6 +60,14 @@ class CsvAttribute(PropertyGroup): ) formatting: StringProperty(default="{{value}}", name="Formatting") + if TYPE_CHECKING: + header: str + sort: Literal["NONE", "ASC", "DESC"] + group: Literal["NONE", "GROUP", "CONCAT", "VARIES", "SUM", "AVERAGE", "MIN", "MAX"] + varies_value: str + summary: Literal["NONE", "SUM", "AVERAGE", "MIN", "MAX"] + formatting: str + class CsvProperties(PropertyGroup): csv_ifc_file: StringProperty(default="", name="IFC File") @@ -104,3 +113,26 @@ class CsvProperties(PropertyGroup): name="Load from Memory", description="Use IFC file currently loaded in Bonsai", ) + + if TYPE_CHECKING: + csv_ifc_file: str + ifc_selector: str + filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup] + csv_attributes: bpy.types.bpy_prop_collection_idprop[CsvAttribute] + should_generate_svg: bool + should_preserve_existing: bool + include_global_id: bool + null_value: str + empty_value: str + true_value: str + false_value: str + concat_value: str + csv_delimiter: Literal["NONE", "ASC", "DESC"] + format: Literal["csv", "xlsx", "ods", "web"] + csv_custom_delimiter: str + should_show_settings: bool + should_show_sort: bool + should_show_group: bool + should_show_summary: bool + should_show_formatting: bool + should_load_from_memory: bool diff --git a/src/bonsai/bonsai/bim/module/csv/ui.py b/src/bonsai/bonsai/bim/module/csv/ui.py index 6aa41177b0..ec46867fd9 100644 --- a/src/bonsai/bonsai/bim/module/csv/ui.py +++ b/src/bonsai/bonsai/bim/module/csv/ui.py @@ -33,9 +33,7 @@ class BIM_PT_ifccsv(Panel): def draw(self, context): layout = self.layout - - scene = context.scene - props = scene.CsvProperties + props = tool.Blender.get_csv_props() if tool.Ifc.get(): row = layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/diff/data.py b/src/bonsai/bonsai/bim/module/diff/data.py index c8fda6850c..ac3e545f43 100644 --- a/src/bonsai/bonsai/bim/module/diff/data.py +++ b/src/bonsai/bonsai/bim/module/diff/data.py @@ -43,7 +43,7 @@ class DiffData: @classmethod def diff_json(cls): - props = bpy.context.scene.DiffProperties + props = tool.Blender.get_diff_props() if not props.diff_json_file: cls.diff = None return diff --git a/src/bonsai/bonsai/bim/module/diff/operator.py b/src/bonsai/bonsai/bim/module/diff/operator.py index 516b26d9ad..a5c097db51 100644 --- a/src/bonsai/bonsai/bim/module/diff/operator.py +++ b/src/bonsai/bonsai/bim/module/diff/operator.py @@ -36,7 +36,8 @@ class SelectDiffJsonFile(bpy.types.Operator): filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) def execute(self, context): - context.scene.DiffProperties.diff_json_file = self.filepath + props = tool.Blender.get_diff_props() + props.diff_json_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -51,7 +52,8 @@ class VisualiseDiff(bpy.types.Operator): def execute(self, context): ifc_file = tool.Ifc.get() - with open(context.scene.DiffProperties.diff_json_file, "r") as file: + props = tool.Blender.get_diff_props() + with open(props.diff_json_file, "r") as file: diff = json.load(file) for obj in context.visible_objects: obj.color = (1.0, 1.0, 1.0, 1.0) @@ -111,7 +113,8 @@ class SelectDiffOldFile(bpy.types.Operator): filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) def execute(self, context): - context.scene.DiffProperties.old_file = self.filepath + props = tool.Blender.get_diff_props() + props.old_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -128,7 +131,8 @@ class SelectDiffNewFile(bpy.types.Operator): filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) def execute(self, context): - context.scene.DiffProperties.new_file = self.filepath + props = tool.Blender.get_diff_props() + props.new_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -153,7 +157,7 @@ class ExecuteIfcDiff(bpy.types.Operator): def execute(self, context): import ifcdiff - self.props = context.scene.DiffProperties + self.props = tool.Blender.get_diff_props() if tool.Ifc.get(): if self.props.active_file == "NONE": @@ -242,7 +246,8 @@ class SelectDiffObjects(bpy.types.Operator): def execute(self, context): ifc_file = tool.Ifc.get() - with open(context.scene.DiffProperties.diff_json_file, "r") as file: + props = tool.Blender.get_diff_props() + with open(props.diff_json_file, "r") as file: diff = json.load(file) for obj in context.visible_objects: obj.select_set(False) diff --git a/src/bonsai/bonsai/bim/module/diff/prop.py b/src/bonsai/bonsai/bim/module/diff/prop.py index 3f09b39900..61da124a7c 100644 --- a/src/bonsai/bonsai/bim/module/diff/prop.py +++ b/src/bonsai/bonsai/bim/module/diff/prop.py @@ -30,18 +30,25 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING, Literal, get_args -def update_diff_json_file(self, context): +def update_diff_json_file(self: "DiffProperties", context: bpy.types.Context) -> None: DiffData.data["diff_json"] = DiffData.diff_json() +RelationshipType = Literal["type", "property", "container", "aggregate", "classification"] + + class Relationships(PropertyGroup): relationship: EnumProperty( name="Relationship", - items=[(r, r.capitalize(), r) for r in ["type", "property", "container", "aggregate", "classification"]], + items=[(r, r.capitalize(), r) for r in get_args(RelationshipType)], ) + if TYPE_CHECKING: + relationship: RelationshipType + class DiffProperties(PropertyGroup): diff_json_file: StringProperty(default="", name="JSON Output", update=update_diff_json_file) @@ -59,3 +66,12 @@ class DiffProperties(PropertyGroup): name="Active File", default="NEW", ) + + if TYPE_CHECKING: + diff_json_file: str + old_file: str + new_file: str + diff_relationships: bpy.types.bpy_prop_collection_idprop[Relationships] + filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup] + should_load_changed_elements: bool + active_file: Literal["NONE", "OLD", "NEW"] diff --git a/src/bonsai/bonsai/bim/module/diff/ui.py b/src/bonsai/bonsai/bim/module/diff/ui.py index 34e66df0f5..91e0a6b48e 100644 --- a/src/bonsai/bonsai/bim/module/diff/ui.py +++ b/src/bonsai/bonsai/bim/module/diff/ui.py @@ -37,9 +37,7 @@ class BIM_PT_diff(Panel): layout = self.layout layout.use_property_split = True - - scene = context.scene - props = scene.DiffProperties + props = tool.Blender.get_diff_props() layout.label(text="IFC Diff Setup:") diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 32d3729d58..e1090b6cf1 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -155,7 +155,7 @@ class Search(Operator): def execute(self, context): if self.property_group == "CsvProperties": - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() elif self.property_group == "BIMSearchProperties": props = tool.Search.get_search_props() else: diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 297b3eb4df..d5269a405f 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -43,6 +43,8 @@ from typing_extensions import assert_never if TYPE_CHECKING: from bonsai.bim.prop import BIMProperties, BIMObjectProperties + from bonsai.bim.module.csv.prop import CsvProperties + from bonsai.bim.module.diff.prop import DiffProperties T = TypeVar("T") @@ -1600,6 +1602,14 @@ class Blender(bonsai.core.tool.Blender): dct = {cls.bl_idname: cls.ifc_element_type for cls in (BimTool.__subclasses__())} return types.MappingProxyType(dct) + @classmethod + def get_csv_props(cls) -> CsvProperties: + return bpy.context.scene.CsvProperties + + @classmethod + def get_diff_props(cls) -> DiffProperties: + return bpy.context.scene.DiffProperties + @classmethod def get_bim_props(cls, scene: Optional[bpy.types.Scene] = None) -> BIMProperties: if scene is None: diff --git a/src/bonsai/bonsai/tool/clash.py b/src/bonsai/bonsai/tool/clash.py index 57b7ffd4a8..2ec7ccb75d 100644 --- a/src/bonsai/bonsai/tool/clash.py +++ b/src/bonsai/bonsai/tool/clash.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import os import bpy import json @@ -25,14 +26,23 @@ import bonsai.tool as tool from contextlib import contextmanager from mathutils import Vector from ifcclash import ifcclash +from typing import TYPE_CHECKING, Union + +if TYPE_CHECKING: + from bonsai.bim.module.clash.prop import BIMClashProperties class Clash(bonsai.core.tool.Clash): + @classmethod + def get_clash_props(cls) -> BIMClashProperties: + return bpy.context.scene.BIMClashProperties + @classmethod def export_clash_sets(cls) -> list[ifcclash.ClashSet]: - clash_sets = [] - for clash_set in bpy.context.scene.BIMClashProperties.clash_sets: + clash_sets: list[ifcclash.ClashSet] = [] + props = cls.get_clash_props() + for clash_set in props.clash_sets: a = [] b = [] for ab in ["a", "b"]: @@ -46,7 +56,7 @@ class Clash(bonsai.core.tool.Clash): a.append(clash_source) elif ab == "b": b.append(clash_source) - clash_set_data = {"name": clash_set.name, "mode": clash_set.mode, "a": a, "b": b} + clash_set_data = ifcclash.ClashSet(name=clash_set.name, mode=clash_set.mode, a=a, b=b) if clash_set.mode == "intersection": clash_set_data["tolerance"] = clash_set.tolerance clash_set_data["check_all"] = clash_set.check_all @@ -55,33 +65,37 @@ class Clash(bonsai.core.tool.Clash): elif clash_set.mode == "clearance": clash_set_data["clearance"] = clash_set.clearance clash_set_data["check_all"] = clash_set.check_all - clash_sets.append(clash_set_data) + clash_sets.append(ifcclash.ClashSet(**clash_set_data)) return clash_sets @classmethod - def get_clash(cls, clash_set, a_global_id, b_global_id): + def get_clash( + cls, clash_set: ifcclash.ClashSet, a_global_id: str, b_global_id: str + ) -> Union[ifcclash.ClashResult, None]: clashes = clash_set.get("clashes", None) if not clashes: return return clashes.get(f"{a_global_id}-{b_global_id}", None) @classmethod - def get_clash_set(cls, name): + def get_clash_set(cls, name: str) -> Union[ifcclash.ClashSet, None]: for clash_set in ClashStore.clash_sets: if clash_set["name"] == name: return clash_set @classmethod - def get_clash_sets(cls): + def get_clash_sets(cls) -> list[ifcclash.ClashSet]: return ClashStore.clash_sets @classmethod - def import_active_clashes(cls): - clash_set = bpy.context.scene.BIMClashProperties.active_clash_set + def import_active_clashes(cls) -> None: + props = cls.get_clash_props() + clash_set = props.active_clash_set if not clash_set: return clash_set.clashes.clear() result = tool.Clash.get_clash_set(clash_set.name) + assert result is not None for clash in sorted(result.get("clashes", {}).values(), key=lambda x: x["distance"]): blender_clash = clash_set.clashes.add() blender_clash.a_global_id = clash["a_global_id"] @@ -91,17 +105,19 @@ class Clash(bonsai.core.tool.Clash): blender_clash.status = False if not "status" in clash.keys() else clash["status"] @classmethod - def load_clash_sets(cls, fn): + def load_clash_sets(cls, fn: str) -> None: with open(fn) as f: ClashStore.clash_sets = json.load(f) @classmethod - def look_at(cls, target, location): + def look_at(cls, target: Vector, location: Vector) -> None: camera_location = location area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") region = next(region for region in area.regions if region.type == "WINDOW") space = next(space for space in area.spaces if space.type == "VIEW_3D") override = {"area": area, "region": region, "space_data": space} + assert isinstance(space, bpy.types.SpaceView3D) + assert space.region_3d space.region_3d.view_location = target space.region_3d.view_rotation = Vector((camera_location - target)).to_track_quat("Z", "Y") space.region_3d.view_distance = (camera_location - target).length @@ -109,10 +125,8 @@ class Clash(bonsai.core.tool.Clash): class ClashStore: - clash_sets = None - path = None + clash_sets: list[ifcclash.ClashSet] = [] @staticmethod def purge(): - ClashStore.clash_sets = None - ClashStore.path = None + ClashStore.clash_sets = [] diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py index a48e1404b6..635b1c01dd 100644 --- a/src/bonsai/bonsai/tool/search.py +++ b/src/bonsai/bonsai/tool/search.py @@ -43,22 +43,21 @@ class Search(bonsai.core.tool.Search): return json.loads(group.Description)["query"] @classmethod - def get_filter_groups(cls, module: str) -> bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]: + def get_filter_groups(cls, module: FilterModule) -> bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]: if module == "search": return cls.get_search_props().filter_groups elif module == "csv": - return bpy.context.scene.CsvProperties.filter_groups + return tool.Blender.get_csv_props().filter_groups elif module == "diff": - return bpy.context.scene.DiffProperties.filter_groups + return tool.Blender.get_diff_props().filter_groups elif module == "drawing_include": return bpy.context.scene.camera.data.BIMCameraProperties.include_filter_groups elif module == "drawing_exclude": return bpy.context.scene.camera.data.BIMCameraProperties.exclude_filter_groups elif module.startswith("clash"): _, clash_set_index, ab, clash_source_index = module.split("_") - return getattr(bpy.context.scene.BIMClashProperties.clash_sets[int(clash_set_index)], ab)[ - int(clash_source_index) - ].filter_groups + props = tool.Clash.get_clash_props() + return getattr(props.clash_sets[int(clash_set_index)], ab)[int(clash_source_index)].filter_groups assert False, f"Unsupported module: {module}" @classmethod diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_geometric_representation.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_geometric_representation.py index 86353c9e82..c15fac39a4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_geometric_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_geometric_representation.py @@ -18,6 +18,7 @@ import ifcopenshell import ifcopenshell.api.alignment +import ifcopenshell.api.geometry from ifcopenshell import entity_instance import math