clash - typing fixes and some refactor

This commit is contained in:
Andrej730
2025-05-21 14:36:32 +05:00
parent 84b0abfb60
commit cc73143ac1
5 changed files with 113 additions and 33 deletions
+36 -13
View File
@@ -29,6 +29,7 @@ from math import radians
from mathutils import Matrix, Vector from mathutils import Matrix, Vector
from bonsai.bim.ifc import IfcStore from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.clash.decorator import ClashDecorator from bonsai.bim.module.clash.decorator import ClashDecorator
from typing import TYPE_CHECKING
class ExportClashSets(bpy.types.Operator, ExportHelper): class ExportClashSets(bpy.types.Operator, ExportHelper):
@@ -127,7 +128,8 @@ class AddClashSource(bpy.types.Operator):
def execute(self, context): def execute(self, context):
props = tool.Clash.get_clash_props() props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set clash_set = props.active_clash_set
source = getattr(clash_set, self.group).add() assert clash_set
clash_set.get_clash_sources_group(self.group).add()
return {"FINISHED"} return {"FINISHED"}
@@ -142,7 +144,8 @@ class RemoveClashSource(bpy.types.Operator):
def execute(self, context): def execute(self, context):
props = tool.Clash.get_clash_props() props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set clash_set = props.active_clash_set
getattr(clash_set, self.group).remove(self.index) assert clash_set
clash_set.get_clash_sources_group(self.group).remove(self.index)
return {"FINISHED"} return {"FINISHED"}
@@ -159,7 +162,9 @@ class SelectClashSource(bpy.types.Operator, ImportHelper):
def execute(self, context): def execute(self, context):
props = tool.Clash.get_clash_props() props = tool.Clash.get_clash_props()
clash_set = props.active_clash_set clash_set = props.active_clash_set
getattr(clash_set, self.group)[self.index].name = self.filepath assert clash_set
clash_source = clash_set.get_clash_sources_group(self.group)[self.index]
clash_source.name = self.filepath
return {"FINISHED"} return {"FINISHED"}
@@ -191,9 +196,21 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
bl_idname = "bim.execute_ifc_clash" bl_idname = "bim.execute_ifc_clash"
bl_label = "Execute IFC Clash" bl_label = "Execute IFC Clash"
bl_description = "Execute clash detection and save the information to a .bcf or .json file" bl_description = "Execute clash detection and save the information to a .bcf or .json file"
filter_glob: bpy.props.StringProperty(default="*.bcf;*.json", options={"HIDDEN"})
format: bpy.props.EnumProperty(name="Format", items=[(i, i, "") for i in ("bcf", "json")]) filter_glob: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"}) default="*.bcf;*.json", options={"HIDDEN"}
)
format: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
name="Format", items=[(i, i, "") for i in ("bcf", "json")]
)
filepath: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
subtype="FILE_PATH", options={"SKIP_SAVE"}
)
if TYPE_CHECKING:
filter_glob: str
format: str
filepath: str
@property @property
def filename_ext(self) -> str: def filename_ext(self) -> str:
@@ -225,11 +242,14 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
if self.props.should_create_clash_snapshots: if self.props.should_create_clash_snapshots:
def get_viewpoint_snapshot(viewpoint): def get_viewpoint_snapshot(viewpoint) -> tuple[str, bytes]:
assert context.scene
camera = bpy.data.objects.get("IFC Clash Camera") camera = bpy.data.objects.get("IFC Clash Camera")
if not camera: if not camera:
camera = bpy.data.objects.new("IFC Clash Camera", bpy.data.cameras.new("IFC Clash Camera")) camera = bpy.data.objects.new("IFC Clash Camera", bpy.data.cameras.new("IFC Clash Camera"))
context.scene.collection.objects.link(camera) context.scene.collection.objects.link(camera)
assert isinstance(camera.data, bpy.types.Camera)
bcf_camera = viewpoint.visualization_info.perspective_camera bcf_camera = viewpoint.visualization_info.perspective_camera
p = bcf_camera.camera_view_point p = bcf_camera.camera_view_point
@@ -238,6 +258,7 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
y = bcf_camera.camera_up_vector y = bcf_camera.camera_up_vector
y = Vector([y.x, y.y, y.z]) y = Vector([y.x, y.y, y.z])
x = y.cross(z) x = y.cross(z)
assert isinstance(x, Vector)
mat = Matrix( mat = Matrix(
[ [
@@ -251,9 +272,9 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
camera.matrix_world = mat camera.matrix_world = mat
context.scene.camera = camera context.scene.camera = camera
camera.data.angle = radians(60) camera.data.angle = radians(60)
area = next(area for area in context.screen.areas if area.type == "VIEW_3D") assert (space := tool.Blender.get_view3d_space()) and space.region_3d
area.spaces[0].region_3d.view_perspective = "CAMERA" space.region_3d.view_perspective = "CAMERA"
area.spaces[0].shading.show_xray = True space.shading.show_xray = True
context.scene.render.resolution_x = 480 context.scene.render.resolution_x = 480
context.scene.render.resolution_y = 270 context.scene.render.resolution_y = 270
context.scene.render.image_settings.file_format = "PNG" context.scene.render.image_settings.file_format = "PNG"
@@ -271,7 +292,7 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
if extension == ".json": if extension == ".json":
tool.Clash.load_clash_sets(self.filepath) tool.Clash.load_clash_sets(self.filepath)
tool.Clash.import_active_clashes() tool.Clash.import_active_clashes()
self.report({"INFO"}, "Finished IFC clash.") self.report({"INFO"}, f"IFC Clash results are saved to '{Path(self.filepath).name}'.")
return {"FINISHED"} return {"FINISHED"}
@@ -348,8 +369,10 @@ class SelectClash(bpy.types.Operator):
def execute(self, context): def execute(self, context):
self.props = tool.Clash.get_clash_props() self.props = tool.Clash.get_clash_props()
clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name) assert (active_clash := self.props.active_clash)
active_clash = self.props.active_clash assert (active_clash_set := self.props.active_clash_set)
clash_set = tool.Clash.get_clash_set(active_clash_set.name)
assert clash_set
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)
if not clash: if not clash:
+22 -4
View File
@@ -35,9 +35,12 @@ from typing import TYPE_CHECKING, Literal, Union
class ClashSource(PropertyGroup): class ClashSource(PropertyGroup):
name: StringProperty(name="File") name: StringProperty( # pyright: ignore[reportRedeclaration]
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") name="File",
mode: EnumProperty( description="Absolute filepath to existing .ifc file to use as a clash source.",
)
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") # pyright: ignore[reportRedeclaration]
mode: EnumProperty( # pyright: ignore[reportRedeclaration]
items=[ items=[
("a", "All Elements", "All elements will be used for clashing"), ("a", "All Elements", "All elements will be used for clashing"),
("i", "Include", "Only the selected elements are included for clashing"), ("i", "Include", "Only the selected elements are included for clashing"),
@@ -47,6 +50,7 @@ class ClashSource(PropertyGroup):
) )
if TYPE_CHECKING: if TYPE_CHECKING:
name: str
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup] filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
mode: Literal["a", "i", "e"] mode: Literal["a", "i", "e"]
@@ -56,7 +60,11 @@ class Clash(PropertyGroup):
b_global_id: StringProperty(name="B") b_global_id: StringProperty(name="B")
a_name: StringProperty(name="A Name") a_name: StringProperty(name="A Name")
b_name: StringProperty(name="B Name") b_name: StringProperty(name="B Name")
status: BoolProperty(name="Status", default=False) status: BoolProperty(
name="Status",
description="Clash status, not stored anywhere - currently just displayed in UI for convenience.",
default=False,
)
if TYPE_CHECKING: if TYPE_CHECKING:
a_global_id: str a_global_id: str
@@ -99,6 +107,16 @@ class ClashSet(PropertyGroup):
b: bpy.types.bpy_prop_collection_idprop[ClashSource] b: bpy.types.bpy_prop_collection_idprop[ClashSource]
clashes: bpy.types.bpy_prop_collection_idprop[Clash] clashes: bpy.types.bpy_prop_collection_idprop[Clash]
def get_clash_sources_group(
self, group: tool.Clash.ClashSourceGroup
) -> "bpy.types.bpy_prop_collection_idprop[ClashSource]":
return getattr(self, group)
def get_clash_sources(
self,
) -> "dict[tool.Clash.ClashSourceGroup, bpy.types.bpy_prop_collection_idprop[ClashSource]]":
return {g: self.get_clash_sources_group(g) for g in tool.Clash.CLASH_SOURCE_GROUP_LITERALS}
class SmartClashGroup(PropertyGroup): class SmartClashGroup(PropertyGroup):
number: StringProperty(name="Number") number: StringProperty(name="Number")
+37 -5
View File
@@ -16,11 +16,16 @@
# 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 bpy import bpy
import bonsai.tool as tool 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
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.clash.prop import BIMClashProperties, ClashSet, SmartClashGroup, Clash
class BIM_PT_ifcclash(Panel): class BIM_PT_ifcclash(Panel):
@@ -36,6 +41,7 @@ class BIM_PT_ifcclash(Panel):
if not ClashData.is_loaded: if not ClashData.is_loaded:
ClashData.load() ClashData.load()
assert self.layout
layout = self.layout layout = self.layout
props = tool.Clash.get_clash_props() props = tool.Clash.get_clash_props()
@@ -155,6 +161,7 @@ class BIM_PT_smart_clash_manager(Panel):
bl_parent_id = "BIM_PT_clash_manager" bl_parent_id = "BIM_PT_clash_manager"
def draw(self, context): def draw(self, context):
assert self.layout
layout = self.layout layout = self.layout
props = tool.Clash.get_clash_props() props = tool.Clash.get_clash_props()
@@ -188,8 +195,16 @@ class BIM_PT_smart_clash_manager(Panel):
class BIM_UL_clash_sets(bpy.types.UIList): class BIM_UL_clash_sets(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
ob = data self,
context,
layout: bpy.types.UILayout,
data: BIMClashProperties,
item: ClashSet,
icon,
active_data,
active_propname,
) -> None:
if item: if item:
layout.prop(item, "name", text="", emboss=False) layout.prop(item, "name", text="", emboss=False)
else: else:
@@ -197,8 +212,16 @@ class BIM_UL_clash_sets(bpy.types.UIList):
class BIM_UL_smart_groups(bpy.types.UIList): class BIM_UL_smart_groups(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
ob = data self,
context,
layout: bpy.types.UILayout,
data: BIMClashProperties,
item: SmartClashGroup,
icon,
active_data,
active_propname,
) -> None:
if item: if item:
layout.label(text=str(item.number), translate=False, icon="NONE", icon_value=0) layout.label(text=str(item.number), translate=False, icon="NONE", icon_value=0)
else: else:
@@ -206,7 +229,16 @@ class BIM_UL_smart_groups(bpy.types.UIList):
class BIM_UL_clashes(bpy.types.UIList): class BIM_UL_clashes(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMClashProperties,
item: Clash,
icon,
active_data,
active_propname,
) -> None:
if item: if item:
row = layout.row(align=True) row = layout.row(align=True)
row.label(text=str(item.a_name), translate=False, icon="NONE", icon_value=0) row.label(text=str(item.a_name), translate=False, icon="NONE", icon_value=0)
+14 -8
View File
@@ -26,7 +26,8 @@ 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 from ifcclash.ifcclash import ClashSource
from typing import TYPE_CHECKING, Union, Literal, get_args
if TYPE_CHECKING: if TYPE_CHECKING:
from bonsai.bim.module.clash.prop import BIMClashProperties from bonsai.bim.module.clash.prop import BIMClashProperties
@@ -38,16 +39,19 @@ class Clash(bonsai.core.tool.Clash):
def get_clash_props(cls) -> BIMClashProperties: def get_clash_props(cls) -> BIMClashProperties:
return bpy.context.scene.BIMClashProperties return bpy.context.scene.BIMClashProperties
ClashSourceGroup = Literal["a", "b"]
CLASH_SOURCE_GROUP_LITERALS = ("a", "b")
@classmethod @classmethod
def export_clash_sets(cls) -> list[ifcclash.ClashSet]: def export_clash_sets(cls) -> list[ifcclash.ClashSet]:
clash_sets: list[ifcclash.ClashSet] = [] clash_sets: list[ifcclash.ClashSet] = []
props = cls.get_clash_props() props = cls.get_clash_props()
for clash_set in props.clash_sets: for clash_set in props.clash_sets:
a = [] a: list[ClashSource] = []
b = [] b: list[ClashSource] = []
for ab in ["a", "b"]: for ab, ab_data in clash_set.get_clash_sources().items():
for data in getattr(clash_set, ab): for data in ab_data:
clash_source = {"file": data.name} clash_source: ClashSource = {"file": data.name}
query = tool.Search.export_filter_query(data.filter_groups) query = tool.Search.export_filter_query(data.filter_groups)
if query and data.mode != "a": if query and data.mode != "a":
clash_source["selector"] = query clash_source["selector"] = query
@@ -96,13 +100,15 @@ class Clash(bonsai.core.tool.Clash):
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 assert result is not None
for clash in sorted(result.get("clashes", {}).values(), key=lambda x: x["distance"]): if "clashes" not in result:
return
for clash in sorted(result["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"]
blender_clash.b_global_id = clash["b_global_id"] blender_clash.b_global_id = clash["b_global_id"]
blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"]) blender_clash.a_name = "{}/{}".format(clash["a_ifc_class"], clash["a_name"])
blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"]) blender_clash.b_name = "{}/{}".format(clash["b_ifc_class"], clash["b_name"])
blender_clash.status = False if not "status" in clash.keys() else clash["status"] blender_clash.status = False if not "status" in clash else clash["status"]
@classmethod @classmethod
def load_clash_sets(cls, fn: str) -> None: def load_clash_sets(cls, fn: str) -> None:
+4 -3
View File
@@ -28,7 +28,7 @@ import ifcopenshell
import ifcopenshell.geom import ifcopenshell.geom
import ifcopenshell.util.selector import ifcopenshell.util.selector
from logging import Logger from logging import Logger
from typing import Literal, TypedDict from typing import Literal, TypedDict, Union
from typing_extensions import NotRequired from typing_extensions import NotRequired
@@ -206,6 +206,7 @@ class Clasher:
start = time.time() start = time.time()
def export(self) -> None: def export(self) -> None:
"""Save clash results to ``settings.output``."""
if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf": if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf":
return self.export_bcfxml() return self.export_bcfxml()
self.export_json() self.export_json()
@@ -230,12 +231,12 @@ class Clasher:
suffix = f".{i}" if i else "" suffix = f".{i}" if i else ""
bcfxml.save(f"{self.settings.output}{suffix}") bcfxml.save(f"{self.settings.output}{suffix}")
def get_viewpoint_snapshot(self, viewpoint) -> None: def get_viewpoint_snapshot(self, viewpoint) -> Union[None, tuple[str, bytes]]:
# Possible to overload this function in a GUI application if used as a library. # Possible to overload this function in a GUI application if used as a library.
# Should return a tuple of (filename, bytes).
return None return None
def export_json(self) -> None: def export_json(self) -> None:
"""Saved clash results as ``list[ClashSet]``."""
clash_sets = self.clash_sets.copy() clash_sets = self.clash_sets.copy()
for clash_set in clash_sets: for clash_set in clash_sets:
for source in clash_set["a"]: for source in clash_set["a"]: