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 bonsai.bim.ifc import IfcStore
from bonsai.bim.module.clash.decorator import ClashDecorator
from typing import TYPE_CHECKING
class ExportClashSets(bpy.types.Operator, ExportHelper):
@@ -127,7 +128,8 @@ class AddClashSource(bpy.types.Operator):
def execute(self, context):
props = tool.Clash.get_clash_props()
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"}
@@ -142,7 +144,8 @@ class RemoveClashSource(bpy.types.Operator):
def execute(self, context):
props = tool.Clash.get_clash_props()
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"}
@@ -159,7 +162,9 @@ class SelectClashSource(bpy.types.Operator, ImportHelper):
def execute(self, context):
props = tool.Clash.get_clash_props()
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"}
@@ -191,9 +196,21 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
bl_idname = "bim.execute_ifc_clash"
bl_label = "Execute IFC Clash"
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")])
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
filter_glob: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
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
def filename_ext(self) -> str:
@@ -225,11 +242,14 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
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")
if not camera:
camera = bpy.data.objects.new("IFC Clash Camera", bpy.data.cameras.new("IFC Clash Camera"))
context.scene.collection.objects.link(camera)
assert isinstance(camera.data, bpy.types.Camera)
bcf_camera = viewpoint.visualization_info.perspective_camera
p = bcf_camera.camera_view_point
@@ -238,6 +258,7 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
y = bcf_camera.camera_up_vector
y = Vector([y.x, y.y, y.z])
x = y.cross(z)
assert isinstance(x, Vector)
mat = Matrix(
[
@@ -251,9 +272,9 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
camera.matrix_world = mat
context.scene.camera = camera
camera.data.angle = radians(60)
area = next(area for area in context.screen.areas if area.type == "VIEW_3D")
area.spaces[0].region_3d.view_perspective = "CAMERA"
area.spaces[0].shading.show_xray = True
assert (space := tool.Blender.get_view3d_space()) and space.region_3d
space.region_3d.view_perspective = "CAMERA"
space.shading.show_xray = True
context.scene.render.resolution_x = 480
context.scene.render.resolution_y = 270
context.scene.render.image_settings.file_format = "PNG"
@@ -271,7 +292,7 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
if extension == ".json":
tool.Clash.load_clash_sets(self.filepath)
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"}
@@ -348,8 +369,10 @@ class SelectClash(bpy.types.Operator):
def execute(self, context):
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
assert (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)
if not clash:
+22 -4
View File
@@ -35,9 +35,12 @@ from typing import TYPE_CHECKING, Literal, Union
class ClashSource(PropertyGroup):
name: StringProperty(name="File")
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
mode: EnumProperty(
name: StringProperty( # pyright: ignore[reportRedeclaration]
name="File",
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=[
("a", "All Elements", "All elements will be used for clashing"),
("i", "Include", "Only the selected elements are included for clashing"),
@@ -47,6 +50,7 @@ class ClashSource(PropertyGroup):
)
if TYPE_CHECKING:
name: str
filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
mode: Literal["a", "i", "e"]
@@ -56,7 +60,11 @@ class Clash(PropertyGroup):
b_global_id: StringProperty(name="B")
a_name: StringProperty(name="A 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:
a_global_id: str
@@ -99,6 +107,16 @@ class ClashSet(PropertyGroup):
b: bpy.types.bpy_prop_collection_idprop[ClashSource]
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):
number: StringProperty(name="Number")
+37 -5
View File
@@ -16,11 +16,16 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
import bonsai.tool as tool
import bonsai.bim.helper
from bpy.types import Panel
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):
@@ -36,6 +41,7 @@ class BIM_PT_ifcclash(Panel):
if not ClashData.is_loaded:
ClashData.load()
assert self.layout
layout = self.layout
props = tool.Clash.get_clash_props()
@@ -155,6 +161,7 @@ class BIM_PT_smart_clash_manager(Panel):
bl_parent_id = "BIM_PT_clash_manager"
def draw(self, context):
assert self.layout
layout = self.layout
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):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
ob = data
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMClashProperties,
item: ClashSet,
icon,
active_data,
active_propname,
) -> None:
if item:
layout.prop(item, "name", text="", emboss=False)
else:
@@ -197,8 +212,16 @@ class BIM_UL_clash_sets(bpy.types.UIList):
class BIM_UL_smart_groups(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
ob = data
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMClashProperties,
item: SmartClashGroup,
icon,
active_data,
active_propname,
) -> None:
if item:
layout.label(text=str(item.number), translate=False, icon="NONE", icon_value=0)
else:
@@ -206,7 +229,16 @@ class BIM_UL_smart_groups(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:
row = layout.row(align=True)
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 mathutils import Vector
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:
from bonsai.bim.module.clash.prop import BIMClashProperties
@@ -38,16 +39,19 @@ class Clash(bonsai.core.tool.Clash):
def get_clash_props(cls) -> BIMClashProperties:
return bpy.context.scene.BIMClashProperties
ClashSourceGroup = Literal["a", "b"]
CLASH_SOURCE_GROUP_LITERALS = ("a", "b")
@classmethod
def export_clash_sets(cls) -> list[ifcclash.ClashSet]:
clash_sets: list[ifcclash.ClashSet] = []
props = cls.get_clash_props()
for clash_set in props.clash_sets:
a = []
b = []
for ab in ["a", "b"]:
for data in getattr(clash_set, ab):
clash_source = {"file": data.name}
a: list[ClashSource] = []
b: list[ClashSource] = []
for ab, ab_data in clash_set.get_clash_sources().items():
for data in ab_data:
clash_source: ClashSource = {"file": data.name}
query = tool.Search.export_filter_query(data.filter_groups)
if query and data.mode != "a":
clash_source["selector"] = query
@@ -96,13 +100,15 @@ class Clash(bonsai.core.tool.Clash):
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"]):
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.a_global_id = clash["a_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.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
def load_clash_sets(cls, fn: str) -> None:
+4 -3
View File
@@ -28,7 +28,7 @@ import ifcopenshell
import ifcopenshell.geom
import ifcopenshell.util.selector
from logging import Logger
from typing import Literal, TypedDict
from typing import Literal, TypedDict, Union
from typing_extensions import NotRequired
@@ -206,6 +206,7 @@ class Clasher:
start = time.time()
def export(self) -> None:
"""Save clash results to ``settings.output``."""
if len(self.settings.output) > 4 and self.settings.output[-4:] == ".bcf":
return self.export_bcfxml()
self.export_json()
@@ -230,12 +231,12 @@ class Clasher:
suffix = f".{i}" if i else ""
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.
# Should return a tuple of (filename, bytes).
return None
def export_json(self) -> None:
"""Saved clash results as ``list[ClashSet]``."""
clash_sets = self.clash_sets.copy()
for clash_set in clash_sets:
for source in clash_set["a"]: