This commit is contained in:
Andrej730
2025-03-24 12:30:58 +05:00
parent 595ee3fa47
commit 7a55e837f7
18 changed files with 239 additions and 100 deletions
+1 -1
View File
@@ -1170,7 +1170,7 @@ class IfcImportSettings:
@staticmethod @staticmethod
def factory(context=None, input_file=None, logger=None): 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() props = tool.Project.get_project_props()
settings = IfcImportSettings() settings = IfcImportSettings()
settings.input_file = input_file settings.input_file = input_file
@@ -31,6 +31,8 @@ import isodate
import bonsai.core.sequence as core import bonsai.core.sequence as core
import bonsai.tool as tool import bonsai.tool as tool
import bonsai.bim.module.sequence.helper as helper import bonsai.bim.module.sequence.helper as helper
import ifcopenshell.api.spatial
import ifcopenshell.geom
import ifcopenshell.util.sequence import ifcopenshell.util.sequence
import ifcopenshell.util.selector import ifcopenshell.util.selector
from datetime import datetime from datetime import datetime
@@ -59,8 +61,6 @@ class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
return True return True
def _execute(self, context): def _execute(self, context):
import ifcopenshell.api.alignment
self.file = tool.Ifc.get() self.file = tool.Ifc.get()
start = time.time() start = time.time()
alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath) alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath)
@@ -60,8 +60,9 @@ class ClashDecorator:
unselected_elements_color = self.addon_prefs.decorator_color_unselected unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special special_elements_color = self.addon_prefs.decorator_color_special
text = context.scene.BIMClashProperties.active_clash_text props = tool.Clash.get_clash_props()
p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5) text = props.active_clash_text
p = props.p1.lerp(props.p2, 0.5)
font_id = 0 font_id = 0
blf.size(font_id, 12) blf.size(font_id, 12)
@@ -92,7 +93,8 @@ class ClashDecorator:
# general shader # general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") 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 = [] selected_edges = []
if selected_vertices[0] != selected_vertices[1]: if selected_vertices[0] != selected_vertices[1]:
selected_edges = [[0, 1]] selected_edges = [[0, 1]]
+46 -29
View File
@@ -69,9 +69,10 @@ class ImportClashSets(bpy.types.Operator):
def execute(self, context): def execute(self, context):
tool.Clash.load_clash_sets(self.filepath) 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(): 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.name = clash_set["name"]
new.mode = clash_set["mode"] new.mode = clash_set["mode"]
if new.mode == "intersection": if new.mode == "intersection":
@@ -106,7 +107,8 @@ class AddClashSet(bpy.types.Operator):
bl_description = "Add a clash set" bl_description = "Add a clash set"
def execute(self, context): 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" new.name = "New Clash Set"
return {"FINISHED"} return {"FINISHED"}
@@ -119,7 +121,8 @@ class RemoveClashSet(bpy.types.Operator):
index: bpy.props.IntProperty() index: bpy.props.IntProperty()
def execute(self, context): 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"} return {"FINISHED"}
@@ -131,7 +134,8 @@ class AddClashSource(bpy.types.Operator):
group: bpy.props.StringProperty() group: bpy.props.StringProperty()
def execute(self, context): 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() source = getattr(clash_set, self.group).add()
return {"FINISHED"} return {"FINISHED"}
@@ -145,7 +149,8 @@ class RemoveClashSource(bpy.types.Operator):
group: bpy.props.StringProperty() group: bpy.props.StringProperty()
def execute(self, context): 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) getattr(clash_set, self.group).remove(self.index)
return {"FINISHED"} return {"FINISHED"}
@@ -161,7 +166,8 @@ class SelectClashSource(bpy.types.Operator):
group: bpy.props.StringProperty() group: bpy.props.StringProperty()
def execute(self, context): 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 getattr(clash_set, self.group)[self.index].name = self.filepath
return {"FINISHED"} return {"FINISHED"}
@@ -178,7 +184,8 @@ class SelectClashResults(bpy.types.Operator):
filepath: bpy.props.StringProperty(subtype="FILE_PATH") filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context): 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"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context, event):
@@ -194,7 +201,8 @@ class SelectSmartGroupedClashesPath(bpy.types.Operator):
filepath: bpy.props.StringProperty(subtype="FILE_PATH") filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context): 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"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context, event):
@@ -218,7 +226,7 @@ class ExecuteIfcClash(bpy.types.Operator):
def execute(self, context): def execute(self, context):
from ifcclash import ifcclash from ifcclash import ifcclash
self.props = context.scene.BIMClashProperties self.props = tool.Clash.get_clash_props()
_, extension = os.path.splitext(self.filepath) _, extension = os.path.splitext(self.filepath)
if extension != ".bcf": if extension != ".bcf":
@@ -306,7 +314,9 @@ class SelectIfcClashResults(bpy.types.Operator):
self.filepath = bpy.path.ensure_ext(self.filepath, ".json") self.filepath = bpy.path.ensure_ext(self.filepath, ".json")
with open(self.filepath) as f: with open(self.filepath) as f:
clash_sets = json.load(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 = [] global_ids = []
for clash_set in clash_sets: for clash_set in clash_sets:
if clash_set["name"] != clash_set_name: if clash_set["name"] != clash_set_name:
@@ -358,7 +368,7 @@ class SelectClash(bpy.types.Operator):
index: bpy.props.IntProperty() index: bpy.props.IntProperty()
def execute(self, context): 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) clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name)
active_clash = self.props.active_clash active_clash = self.props.active_clash
clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id) 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 @classmethod
def poll(cls, context): 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): def execute(self, context):
from ifcclash import ifcclash from ifcclash import ifcclash
settings = ifcclash.ClashSettings() 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.output = self.filepath
settings.logger = logging.getLogger("Clash") settings.logger = logging.getLogger("Clash")
settings.logger.setLevel(logging.DEBUG) settings.logger.setLevel(logging.DEBUG)
@@ -408,19 +420,18 @@ class SmartClashGroup(bpy.types.Operator):
clash_sets = json.load(f) clash_sets = json.load(f)
# execute the smart grouping # execute the smart grouping
save_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json") save_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json")
smart_grouped_clashes = ifc_clasher.smart_group_clashes( smart_grouped_clashes = ifc_clasher.smart_group_clashes(clash_sets, props.smart_clash_grouping_max_distance)
clash_sets, context.scene.BIMClashProperties.smart_clash_grouping_max_distance
)
# save smart_groups to json # save smart_groups to json
with open(save_path, "w") as f: with open(save_path, "w") as f:
f.write(json.dumps(smart_grouped_clashes)) 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 # 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(): for clash_set, smart_groups in smart_grouped_clashes.items():
# Only select the clashes that correspond to the actively selected IFC Clash Set # Only select the clashes that correspond to the actively selected IFC Clash Set
@@ -428,7 +439,7 @@ class SmartClashGroup(bpy.types.Operator):
continue continue
else: else:
for smart_group, global_id_pairs in smart_groups[0].items(): 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}" new_group.number = f"{smart_group}"
for pair in global_id_pairs: for pair in global_id_pairs:
@@ -446,18 +457,21 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): 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): 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: with open(smart_groups_path) as f:
smart_grouped_clashes = json.load(f) smart_grouped_clashes = json.load(f)
# Reset the list of smart_clash_groups for the UI # 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(): for clash_set, smart_groups in smart_grouped_clashes.items():
# Only select the clashes that correspond to the actively selected IFC Clash Set # Only select the clashes that correspond to the actively selected IFC Clash Set
@@ -465,7 +479,7 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator):
continue continue
else: else:
for smart_group, global_id_pairs in smart_groups[0].items(): 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}" new_group.number = f"{smart_group}"
for pair in global_id_pairs: for pair in global_id_pairs:
for guid in pair: for guid in pair:
@@ -482,11 +496,14 @@ class SelectSmartGroup(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): 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): def execute(self, context):
selected_smart_group = context.scene.BIMClashProperties.active_smart_group props = tool.Clash.get_clash_props()
products = [] 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: for global_id in selected_smart_group.global_ids:
try: try:
products.append(tool.Ifc.get().by_guid(global_id.guid)) products.append(tool.Ifc.get().by_guid(global_id.guid))
+54 -11
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy import bpy
import bonsai.tool as tool
from bonsai.bim.prop import StrProperty, Attribute, BIMFilterGroup from bonsai.bim.prop import StrProperty, Attribute, BIMFilterGroup
from bpy.types import PropertyGroup from bpy.types import PropertyGroup
from bpy.props import ( from bpy.props import (
@@ -29,6 +30,8 @@ from bpy.props import (
FloatVectorProperty, FloatVectorProperty,
CollectionProperty, CollectionProperty,
) )
from mathutils import Vector
from typing import TYPE_CHECKING, Literal, Union
class ClashSource(PropertyGroup): class ClashSource(PropertyGroup):
@@ -43,6 +46,10 @@ class ClashSource(PropertyGroup):
name="Mode", name="Mode",
) )
if TYPE_CHECKING:
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
mode: Literal["a", "i", "e"]
class Clash(PropertyGroup): class Clash(PropertyGroup):
a_global_id: StringProperty(name="A") a_global_id: StringProperty(name="A")
@@ -51,9 +58,15 @@ class Clash(PropertyGroup):
b_name: StringProperty(name="B Name") b_name: StringProperty(name="B Name")
status: BoolProperty(name="Status", default=False) 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): class ClashSet(PropertyGroup):
name: StringProperty(name="Name")
mode: EnumProperty( mode: EnumProperty(
items=[ items=[
( (
@@ -76,11 +89,25 @@ class ClashSet(PropertyGroup):
b: CollectionProperty(name="Group B", type=ClashSource) b: CollectionProperty(name="Group B", type=ClashSource)
clashes: CollectionProperty(name="Clashes", type=Clash) 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): class SmartClashGroup(PropertyGroup):
number: StringProperty(name="Number") number: StringProperty(name="Number")
global_ids: CollectionProperty(name="GlobalIDs", type=StrProperty) global_ids: CollectionProperty(name="GlobalIDs", type=StrProperty)
if TYPE_CHECKING:
number: str
global_ids: bpy.types.bpy_prop_collection_idprop[StrProperty]
class BIMClashProperties(PropertyGroup): class BIMClashProperties(PropertyGroup):
blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty) blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty)
@@ -107,17 +134,33 @@ class BIMClashProperties(PropertyGroup):
subtype="FILE_PATH", subtype="FILE_PATH",
) )
@property if TYPE_CHECKING:
def active_clash_set(self): blender_clash_set_a: bpy.types.bpy_prop_collection_idprop[StrProperty]
if self.active_clash_set_index < len(self.clash_sets): blender_clash_set_b: bpy.types.bpy_prop_collection_idprop[StrProperty]
return self.clash_sets[self.active_clash_set_index] 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 @property
def active_smart_group(self): def active_clash_set(self) -> Union[ClashSet, None]:
if self.active_smart_group_index < len(self.smart_clash_groups): return tool.Blender.get_active_uilist_element(self.clash_sets, self.active_clash_set_index)
return self.smart_clash_groups[self.active_smart_group_index]
@property @property
def active_clash(self): def active_smart_group(self) -> Union[SmartClashGroup, None]:
if self.active_clash_index < len(self.active_clash_set.clashes): return tool.Blender.get_active_uilist_element(self.smart_clash_groups, self.active_smart_group_index)
return self.active_clash_set.clashes[self.active_clash_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)
+3 -4
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy import bpy
import bonsai.tool as tool
import bonsai.bim.helper import bonsai.bim.helper
from bpy.types import Panel from bpy.types import Panel
from bonsai.bim.module.clash.data import ClashData from bonsai.bim.module.clash.data import ClashData
@@ -36,9 +37,7 @@ class BIM_PT_ifcclash(Panel):
ClashData.load() ClashData.load()
layout = self.layout layout = self.layout
props = tool.Clash.get_clash_props()
scene = context.scene
props = scene.BIMClashProperties
row = layout.row(align=True) row = layout.row(align=True)
row.operator("bim.add_clash_set") row.operator("bim.add_clash_set")
@@ -157,7 +156,7 @@ class BIM_PT_smart_clash_manager(Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
props = context.scene.BIMClashProperties props = tool.Clash.get_clash_props()
row = layout.row() row = layout.row()
layout.label(text="Select clash results to group:") layout.label(text="Select clash results to group:")
+18 -13
View File
@@ -42,7 +42,8 @@ class AddCsvAttribute(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
attribute = context.scene.CsvProperties.csv_attributes.add() props = tool.Blender.get_csv_props()
props.csv_attributes.add()
return {"FINISHED"} return {"FINISHED"}
@@ -53,7 +54,8 @@ class RemoveCsvAttribute(bpy.types.Operator):
index: bpy.props.IntProperty() index: bpy.props.IntProperty()
def execute(self, context): 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"} return {"FINISHED"}
@@ -64,7 +66,8 @@ class RemoveAllCsvAttributes(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
context.scene.CsvProperties.csv_attributes.clear() props = tool.Blender.get_csv_props()
props.csv_attributes.clear()
return {"FINISHED"} return {"FINISHED"}
@@ -76,8 +79,9 @@ class ReorderCsvAttribute(bpy.types.Operator):
new_index: bpy.props.IntProperty() new_index: bpy.props.IntProperty()
def execute(self, context): def execute(self, context):
old = context.scene.CsvProperties.csv_attributes[self.old_index] props = tool.Blender.get_csv_props()
new = context.scene.CsvProperties.csv_attributes[self.new_index] old = props.csv_attributes[self.old_index]
new = props.csv_attributes[self.new_index]
props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"] props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"]
for prop in props: for prop in props:
value = getattr(new, prop) value = getattr(new, prop)
@@ -95,7 +99,7 @@ class ImportCsvAttributes(bpy.types.Operator):
filepath: bpy.props.StringProperty(subtype="FILE_PATH") filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context): def execute(self, context):
props = context.scene.CsvProperties props = tool.Blender.get_csv_props()
data = json.load(open(self.filepath)) data = json.load(open(self.filepath))
tool.Search.import_filter_query(data["query"], props.filter_groups) 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") filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context): def execute(self, context):
props = context.scene.CsvProperties props = tool.Blender.get_csv_props()
settings = {} settings = {}
for prop in [ for prop in [
@@ -191,14 +195,14 @@ class ExportIfcCsv(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): 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: 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.") cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
return False return False
return True return True
def invoke(self, context, event): def invoke(self, context, event):
props = context.scene.CsvProperties props = tool.Blender.get_csv_props()
if props.format == "web": if props.format == "web":
return self.execute(context) return self.execute(context)
self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}") 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): def execute(self, context):
import ifccsv import ifccsv
props = context.scene.CsvProperties props = tool.Blender.get_csv_props()
self.filepath = bpy.path.ensure_ext(self.filepath, f".{props.format}") self.filepath = bpy.path.ensure_ext(self.filepath, f".{props.format}")
if props.should_load_from_memory: if props.should_load_from_memory:
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
@@ -298,7 +302,7 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
@classmethod @classmethod
def poll(cls, context): 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: 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.") cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
return False return False
@@ -313,7 +317,7 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
import ifccsv import ifccsv
props = context.scene.CsvProperties props = tool.Blender.get_csv_props()
if props.should_load_from_memory: if props.should_load_from_memory:
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
else: else:
@@ -349,7 +353,8 @@ class SelectCsvIfcFile(bpy.types.Operator):
filepath: bpy.props.StringProperty(subtype="FILE_PATH") filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context): 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"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context, event):
+32
View File
@@ -29,6 +29,7 @@ from bpy.props import (
FloatVectorProperty, FloatVectorProperty,
CollectionProperty, CollectionProperty,
) )
from typing import TYPE_CHECKING, Literal
class CsvAttribute(PropertyGroup): class CsvAttribute(PropertyGroup):
@@ -59,6 +60,14 @@ class CsvAttribute(PropertyGroup):
) )
formatting: StringProperty(default="{{value}}", name="Formatting") 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): class CsvProperties(PropertyGroup):
csv_ifc_file: StringProperty(default="", name="IFC File") csv_ifc_file: StringProperty(default="", name="IFC File")
@@ -104,3 +113,26 @@ class CsvProperties(PropertyGroup):
name="Load from Memory", name="Load from Memory",
description="Use IFC file currently loaded in Bonsai", 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
+1 -3
View File
@@ -33,9 +33,7 @@ class BIM_PT_ifccsv(Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
props = tool.Blender.get_csv_props()
scene = context.scene
props = scene.CsvProperties
if tool.Ifc.get(): if tool.Ifc.get():
row = layout.row(align=True) row = layout.row(align=True)
+1 -1
View File
@@ -43,7 +43,7 @@ class DiffData:
@classmethod @classmethod
def diff_json(cls): def diff_json(cls):
props = bpy.context.scene.DiffProperties props = tool.Blender.get_diff_props()
if not props.diff_json_file: if not props.diff_json_file:
cls.diff = None cls.diff = None
return return
+11 -6
View File
@@ -36,7 +36,8 @@ class SelectDiffJsonFile(bpy.types.Operator):
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def execute(self, context): 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"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context, event):
@@ -51,7 +52,8 @@ class VisualiseDiff(bpy.types.Operator):
def execute(self, context): def execute(self, context):
ifc_file = tool.Ifc.get() 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) diff = json.load(file)
for obj in context.visible_objects: for obj in context.visible_objects:
obj.color = (1.0, 1.0, 1.0, 1.0) 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"}) filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
def execute(self, context): def execute(self, context):
context.scene.DiffProperties.old_file = self.filepath props = tool.Blender.get_diff_props()
props.old_file = self.filepath
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context, event):
@@ -128,7 +131,8 @@ class SelectDiffNewFile(bpy.types.Operator):
filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"})
def execute(self, context): def execute(self, context):
context.scene.DiffProperties.new_file = self.filepath props = tool.Blender.get_diff_props()
props.new_file = self.filepath
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context, event):
@@ -153,7 +157,7 @@ class ExecuteIfcDiff(bpy.types.Operator):
def execute(self, context): def execute(self, context):
import ifcdiff import ifcdiff
self.props = context.scene.DiffProperties self.props = tool.Blender.get_diff_props()
if tool.Ifc.get(): if tool.Ifc.get():
if self.props.active_file == "NONE": if self.props.active_file == "NONE":
@@ -242,7 +246,8 @@ class SelectDiffObjects(bpy.types.Operator):
def execute(self, context): def execute(self, context):
ifc_file = tool.Ifc.get() 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) diff = json.load(file)
for obj in context.visible_objects: for obj in context.visible_objects:
obj.select_set(False) obj.select_set(False)
+18 -2
View File
@@ -30,18 +30,25 @@ from bpy.props import (
FloatVectorProperty, FloatVectorProperty,
CollectionProperty, 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() DiffData.data["diff_json"] = DiffData.diff_json()
RelationshipType = Literal["type", "property", "container", "aggregate", "classification"]
class Relationships(PropertyGroup): class Relationships(PropertyGroup):
relationship: EnumProperty( relationship: EnumProperty(
name="Relationship", 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): class DiffProperties(PropertyGroup):
diff_json_file: StringProperty(default="", name="JSON Output", update=update_diff_json_file) diff_json_file: StringProperty(default="", name="JSON Output", update=update_diff_json_file)
@@ -59,3 +66,12 @@ class DiffProperties(PropertyGroup):
name="Active File", name="Active File",
default="NEW", 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"]
+1 -3
View File
@@ -37,9 +37,7 @@ class BIM_PT_diff(Panel):
layout = self.layout layout = self.layout
layout.use_property_split = True layout.use_property_split = True
props = tool.Blender.get_diff_props()
scene = context.scene
props = scene.DiffProperties
layout.label(text="IFC Diff Setup:") layout.label(text="IFC Diff Setup:")
@@ -155,7 +155,7 @@ class Search(Operator):
def execute(self, context): def execute(self, context):
if self.property_group == "CsvProperties": if self.property_group == "CsvProperties":
props = context.scene.CsvProperties props = tool.Blender.get_csv_props()
elif self.property_group == "BIMSearchProperties": elif self.property_group == "BIMSearchProperties":
props = tool.Search.get_search_props() props = tool.Search.get_search_props()
else: else:
+10
View File
@@ -43,6 +43,8 @@ from typing_extensions import assert_never
if TYPE_CHECKING: if TYPE_CHECKING:
from bonsai.bim.prop import BIMProperties, BIMObjectProperties 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") 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__())} dct = {cls.bl_idname: cls.ifc_element_type for cls in (BimTool.__subclasses__())}
return types.MappingProxyType(dct) 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 @classmethod
def get_bim_props(cls, scene: Optional[bpy.types.Scene] = None) -> BIMProperties: def get_bim_props(cls, scene: Optional[bpy.types.Scene] = None) -> BIMProperties:
if scene is None: if scene is None:
+29 -15
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import os import os
import bpy import bpy
import json import json
@@ -25,14 +26,23 @@ import bonsai.tool as tool
from contextlib import contextmanager from contextlib import contextmanager
from mathutils import Vector from mathutils import Vector
from ifcclash import ifcclash 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): class Clash(bonsai.core.tool.Clash):
@classmethod
def get_clash_props(cls) -> BIMClashProperties:
return bpy.context.scene.BIMClashProperties
@classmethod @classmethod
def export_clash_sets(cls) -> list[ifcclash.ClashSet]: def export_clash_sets(cls) -> list[ifcclash.ClashSet]:
clash_sets = [] clash_sets: list[ifcclash.ClashSet] = []
for clash_set in bpy.context.scene.BIMClashProperties.clash_sets: props = cls.get_clash_props()
for clash_set in props.clash_sets:
a = [] a = []
b = [] b = []
for ab in ["a", "b"]: for ab in ["a", "b"]:
@@ -46,7 +56,7 @@ class Clash(bonsai.core.tool.Clash):
a.append(clash_source) a.append(clash_source)
elif ab == "b": elif ab == "b":
b.append(clash_source) 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": if clash_set.mode == "intersection":
clash_set_data["tolerance"] = clash_set.tolerance clash_set_data["tolerance"] = clash_set.tolerance
clash_set_data["check_all"] = clash_set.check_all clash_set_data["check_all"] = clash_set.check_all
@@ -55,33 +65,37 @@ class Clash(bonsai.core.tool.Clash):
elif clash_set.mode == "clearance": elif clash_set.mode == "clearance":
clash_set_data["clearance"] = clash_set.clearance clash_set_data["clearance"] = clash_set.clearance
clash_set_data["check_all"] = clash_set.check_all 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 return clash_sets
@classmethod @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) clashes = clash_set.get("clashes", None)
if not clashes: if not clashes:
return return
return clashes.get(f"{a_global_id}-{b_global_id}", None) return clashes.get(f"{a_global_id}-{b_global_id}", None)
@classmethod @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: for clash_set in ClashStore.clash_sets:
if clash_set["name"] == name: if clash_set["name"] == name:
return clash_set return clash_set
@classmethod @classmethod
def get_clash_sets(cls): def get_clash_sets(cls) -> list[ifcclash.ClashSet]:
return ClashStore.clash_sets return ClashStore.clash_sets
@classmethod @classmethod
def import_active_clashes(cls): def import_active_clashes(cls) -> None:
clash_set = bpy.context.scene.BIMClashProperties.active_clash_set props = cls.get_clash_props()
clash_set = props.active_clash_set
if not clash_set: if not clash_set:
return return
clash_set.clashes.clear() clash_set.clashes.clear()
result = tool.Clash.get_clash_set(clash_set.name) 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"]): for clash in sorted(result.get("clashes", {}).values(), key=lambda x: x["distance"]):
blender_clash = clash_set.clashes.add() blender_clash = clash_set.clashes.add()
blender_clash.a_global_id = clash["a_global_id"] 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"] blender_clash.status = False if not "status" in clash.keys() else clash["status"]
@classmethod @classmethod
def load_clash_sets(cls, fn): def load_clash_sets(cls, fn: str) -> None:
with open(fn) as f: with open(fn) as f:
ClashStore.clash_sets = json.load(f) ClashStore.clash_sets = json.load(f)
@classmethod @classmethod
def look_at(cls, target, location): def look_at(cls, target: Vector, location: Vector) -> None:
camera_location = location camera_location = location
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") 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") 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") space = next(space for space in area.spaces if space.type == "VIEW_3D")
override = {"area": area, "region": region, "space_data": space} 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_location = target
space.region_3d.view_rotation = Vector((camera_location - target)).to_track_quat("Z", "Y") space.region_3d.view_rotation = Vector((camera_location - target)).to_track_quat("Z", "Y")
space.region_3d.view_distance = (camera_location - target).length space.region_3d.view_distance = (camera_location - target).length
@@ -109,10 +125,8 @@ class Clash(bonsai.core.tool.Clash):
class ClashStore: class ClashStore:
clash_sets = None clash_sets: list[ifcclash.ClashSet] = []
path = None
@staticmethod @staticmethod
def purge(): def purge():
ClashStore.clash_sets = None ClashStore.clash_sets = []
ClashStore.path = None
+5 -6
View File
@@ -43,22 +43,21 @@ class Search(bonsai.core.tool.Search):
return json.loads(group.Description)["query"] return json.loads(group.Description)["query"]
@classmethod @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": if module == "search":
return cls.get_search_props().filter_groups return cls.get_search_props().filter_groups
elif module == "csv": elif module == "csv":
return bpy.context.scene.CsvProperties.filter_groups return tool.Blender.get_csv_props().filter_groups
elif module == "diff": elif module == "diff":
return bpy.context.scene.DiffProperties.filter_groups return tool.Blender.get_diff_props().filter_groups
elif module == "drawing_include": elif module == "drawing_include":
return bpy.context.scene.camera.data.BIMCameraProperties.include_filter_groups return bpy.context.scene.camera.data.BIMCameraProperties.include_filter_groups
elif module == "drawing_exclude": elif module == "drawing_exclude":
return bpy.context.scene.camera.data.BIMCameraProperties.exclude_filter_groups return bpy.context.scene.camera.data.BIMCameraProperties.exclude_filter_groups
elif module.startswith("clash"): elif module.startswith("clash"):
_, clash_set_index, ab, clash_source_index = module.split("_") _, clash_set_index, ab, clash_source_index = module.split("_")
return getattr(bpy.context.scene.BIMClashProperties.clash_sets[int(clash_set_index)], ab)[ props = tool.Clash.get_clash_props()
int(clash_source_index) return getattr(props.clash_sets[int(clash_set_index)], ab)[int(clash_source_index)].filter_groups
].filter_groups
assert False, f"Unsupported module: {module}" assert False, f"Unsupported module: {module}"
@classmethod @classmethod
@@ -18,6 +18,7 @@
import ifcopenshell import ifcopenshell
import ifcopenshell.api.alignment import ifcopenshell.api.alignment
import ifcopenshell.api.geometry
from ifcopenshell import entity_instance from ifcopenshell import entity_instance
import math import math