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