diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index 370c99793e..f5505e68c5 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -70,10 +70,10 @@ classes = ( wall.ChangeExtrusionXAngle, wall.ChangeLayerLength, wall.FlipWall, - wall.JoinWall, wall.MergeWall, wall.RecalculateWall, wall.SplitWall, + wall.UnjoinWalls, opening.AddBoolean, opening.AddFilledOpening, opening.AddPotentialHalfSpaceSolid, diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py index 45a5b723bc..726f35143a 100644 --- a/src/blenderbim/blenderbim/bim/module/model/wall.py +++ b/src/blenderbim/blenderbim/bim/module/model/wall.py @@ -41,74 +41,17 @@ from blenderbim.bim.module.model.opening import FilledOpeningGenerator from typing import Optional -class JoinWall(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.join_wall" - bl_label = "Join Wall" +class UnjoinWalls(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.unjoin_walls" + bl_label = "Unjoin Walls" bl_options = {"REGISTER", "UNDO"} - bl_description = """ Trim/Extend the selected walls to the last selected wall: - 'T' mode: Trim/Extend to a selected wall, slab, or 3D target - 'L' mode: Butt join two selected walls - 'V' mode: Mitre join two selected wall - '' (empty) mode: Unjoin selected walls - """ - join_type: bpy.props.StringProperty() @classmethod def poll(cls, context): return context.selected_objects def _execute(self, context): - selected_objs = [o for o in context.selected_objects if o.BIMObjectProperties.ifc_definition_id] - joiner = DumbWallJoiner() - if not self.join_type: - for obj in selected_objs: - joiner.unjoin(obj) - return {"FINISHED"} - - if not context.active_object or not context.active_object.BIMObjectProperties.ifc_definition_id: - self.report({"ERROR"}, f"No active object selected") - return {"CANCELLED"} - - for obj in selected_objs: - tool.Geometry.clear_scale(obj) - - if not selected_objs: - self.report({"ERROR"}, f"No IFC objects selected") - return {"CANCELLED"} - - if len(selected_objs) == 1: - joiner.join_E(context.active_object, context.scene.cursor.location) - return {"FINISHED"} - - if self.join_type in ("L", "V"): - try: - core.join_wall_LV(tool.Blender, joiner, join_type=self.join_type) - except core.RequireTwoObjectsError: - join_type_name = {"L": "butt", "V": "mitre"}[self.join_type] - self.report({"ERROR"}, f"Please select 2 objects to do a {join_type_name} joint") - return {"CANCELLED"} - return {"FINISHED"} - - if self.join_type == "T": - elements = [tool.Ifc.get_entity(o) for o in context.selected_objects] - layer2_elements = [] - layer3_elements = [] - for element in elements: - usage = tool.Model.get_usage_type(element) - if usage == "LAYER2": - layer2_elements.append(element) - elif usage == "LAYER3": - layer3_elements.append(element) - if layer3_elements: - target = tool.Ifc.get_object(layer3_elements[0]) - for element in layer2_elements: - joiner.join_Z(tool.Ifc.get_object(element), target) - else: - for obj in selected_objs: - if obj == context.active_object: - continue - joiner.join_T(obj, context.active_object) - return {"FINISHED"} + core.unjoin_walls(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model) class AlignWall(bpy.types.Operator): @@ -1049,8 +992,6 @@ class DumbWallJoiner: def join_E(self, wall1, target): element1 = tool.Ifc.get_entity(wall1) - if not element1: - return axis1 = tool.Model.get_wall_axis(wall1) intersect, connection = mathutils.geometry.intersect_point_line(target.to_2d(), *axis1["reference"]) diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 7b993df474..8a13b2e1f4 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -18,10 +18,8 @@ import os import bpy -import ifcopenshell -import ifcopenshell.util.unit import blenderbim.tool as tool -import blenderbim.core.model +import blenderbim.core.model as core from blenderbim.bim.module.model.wall import DumbWallJoiner from blenderbim.bim.helper import prop_with_search from bpy.types import WorkSpaceTool @@ -350,7 +348,7 @@ class BimToolUI: add_layout_hotkey_operator(cls.layout, "Split", "S_K", bpy.ops.bim.split_wall.__doc__) add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__) add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__) - row.operator("bim.join_wall", icon="X", text="").join_type = "" + row.operator("bim.unjoin_walls", icon="X", text="") elif AuthoringData.data["active_material_usage"] == "LAYER3": if len(context.selected_objects) == 1: @@ -680,7 +678,14 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.bim.enable_editing_extrusion_profile() elif self.active_material_usage == "LAYER2": # Extend LAYER2 to cursor - bpy.ops.bim.join_wall(join_type="T") + core.extend_walls( + tool.Ifc, + tool.Blender, + tool.Geometry, + DumbWallJoiner(), + tool.Model, + bpy.context.scene.cursor.location, + ) elif self.active_material_usage == "PROFILE": # Extend PROFILE to cursor bpy.ops.bim.extend_profile(join_type="T") @@ -698,13 +703,19 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): # Extend LAYER2s to LAYER3 [o.select_set(False) for o in selected_usages.get("PROFILE", [])] [o.select_set(False) for o in selected_usages.get("LAYER3", []) if o != bpy.context.active_object] - bpy.ops.bim.join_wall(join_type="T") + try: + core.join_walls_TZ(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model) + except core.RequireAtLeastTwoLayeredElements as e: + self.report({"ERROR"}, str(e)) elif self.active_material_usage == "LAYER2": # Extend LAYER2s to LAYER2 [o.select_set(False) for o in selected_usages.get("LAYER3", [])] [o.select_set(False) for o in selected_usages.get("PROFILE", [])] - bpy.ops.bim.join_wall(join_type="T") + try: + core.join_walls_TZ(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model) + except core.RequireAtLeastTwoLayeredElements as e: + self.report({"ERROR"}, str(e)) elif self.active_material_usage == "PROFILE": # Extend PROFILEs to PROFILE @@ -722,7 +733,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): elif self.active_material_usage == "PROFILE": bpy.ops.bim.flip_object(flip_local_axes="XZ") - def hotkey_S_G(self): obj = bpy.context.active_object element = tool.Ifc.get_entity(obj) @@ -755,7 +765,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): else: if len(bpy.context.selected_objects) == 1: self.report( - {"INFO"}, + {"ERROR"}, "At least two objects must be selected: an object to be mirrored, and a mirror axis as the active object.", ) else: @@ -782,10 +792,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): return if self.active_material_usage == "LAYER2": try: - blenderbim.core.model.join_wall_LV(tool.Blender, DumbWallJoiner(), join_type="L") - except blenderbim.core.model.RequireTwoObjectsError: - self.report({"ERROR"}, "Please select 2 objects to do a butt joint") - return {"CANCELLED"} + core.join_walls_LV(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model, join_type="L") + except core.RequireTwoWallsError as e: + self.report({"ERROR"}, str(e)) elif self.active_material_usage == "PROFILE": bpy.ops.bim.extend_profile(join_type="L") @@ -811,16 +820,14 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): return if self.active_material_usage == "LAYER2": try: - blenderbim.core.model.join_wall_LV(tool.Blender, DumbWallJoiner(), join_type="V") - except blenderbim.core.model.RequireTwoObjectsError: - self.report({"ERROR"}, "Please select 2 objects to do a mitre joint") - return {"CANCELLED"} + core.join_walls_LV(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model, join_type="V") + except core.RequireTwoWallsError as e: + self.report({"ERROR"}, str(e)) elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"): bpy.ops.bim.fit_flow_segments() elif self.active_material_usage == "PROFILE": bpy.ops.bim.extend_profile(join_type="V") - def hotkey_S_B(self): bpy.ops.bim.add_boundary() @@ -828,7 +835,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): if len(bpy.context.selected_objects) == 2: bpy.ops.bim.add_opening() else: - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) bpy.ops.bim.add_potential_opening(x=self.x, y=self.y, z=self.z) self.props.x = self.x self.props.y = self.y diff --git a/src/blenderbim/blenderbim/core/model.py b/src/blenderbim/blenderbim/core/model.py index 7749f8c22a..f609d0cfce 100644 --- a/src/blenderbim/blenderbim/core/model.py +++ b/src/blenderbim/blenderbim/core/model.py @@ -20,17 +20,91 @@ import blenderbim.core.tool as tool from typing import Literal -def join_wall_LV(blender: tool.Blender, joiner, join_type: Literal["L", "V"] = "L") -> None: - if len(selected_objs := blender.get_selected_objects()) != 2: - raise RequireTwoObjectsError() +def unjoin_walls(ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner, model: tool.Model) -> None: + for obj in blender.get_selected_objects(): + if not (element := ifc.get_entity(obj)) or model.get_usage_type(element) != "LAYER2": + continue + geometry.clear_scale(obj) + joiner.unjoin(obj) + + +def extend_walls( + ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner, model: tool.Model, target +) -> None: + for obj in blender.get_selected_objects(): + if not (element := ifc.get_entity(obj)) or model.get_usage_type(element) != "LAYER2": + continue + geometry.clear_scale(obj) + joiner.join_E(obj, target) + + +def join_walls_LV( + ifc: tool.Ifc, + blender: tool.Blender, + geometry: tool.Geometry, + joiner, + model: tool.Model, + join_type: Literal["L", "V"] = "L", +) -> None: + selected_objs = [ + o for o in blender.get_selected_objects() if (e := ifc.get_entity(o)) and model.get_usage_type(e) == "LAYER2" + ] + if len(selected_objs) != 2: + raise RequireTwoWallsError("Two vertically layered elements must be selected to connect their paths together") + + if active_obj := blender.get_active_object(): + another_selected_object = next(o for o in selected_objs if o != active_obj) + else: + active_obj, another_selected_object = selected_objs + + for obj in selected_objs: + geometry.clear_scale(obj) - active_obj = blender.get_active_object() - another_selected_object = next(o for o in selected_objs if o != active_obj) if join_type == "L": joiner.join_L(another_selected_object, active_obj) elif join_type == "V": joiner.join_V(another_selected_object, active_obj) -class RequireTwoObjectsError(Exception): +def join_walls_TZ(ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner, model: tool.Model) -> None: + selected_objs = [ + o + for o in blender.get_selected_objects() + if (e := ifc.get_entity(o)) and model.get_usage_type(e) in ("LAYER2", "LAYER3") + ] + if len(selected_objs) != 2: + raise RequireAtLeastTwoLayeredElements( + "Two or more vertically or horizontally layered elements must be selected to connect their paths together" + ) + + for obj in selected_objs: + geometry.clear_scale(obj) + + elements = [ifc.get_entity(o) for o in blender.get_selected_objects()] + layer2_elements = [] + layer3_elements = [] + for element in elements: + usage = model.get_usage_type(element) + if usage == "LAYER2": + layer2_elements.append(element) + elif usage == "LAYER3": + layer3_elements.append(element) + if layer3_elements: + target = ifc.get_object(layer3_elements[0]) + for element in layer2_elements: + joiner.join_Z(ifc.get_object(element), target) + else: + if not (active_obj := blender.get_active_object()): + active_obj = selected_objs[0] + for obj in selected_objs: + if obj == active_obj: + continue + joiner.join_T(obj, active_obj) + + +class RequireTwoWallsError(Exception): + pass + + +class RequireAtLeastTwoLayeredElements(Exception): pass diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 15dee26972..7c8582faa0 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -537,6 +537,7 @@ class Model: def import_rectangle(cls, obj, position, profile): pass def load_openings(cls, openings): pass def clear_scene_openings(cls): pass + def get_usage_type(cls, element): pass def get_material_layer_parameters(cls, element): pass def get_manual_booleans(cls, element): pass def get_wall_axis(cls, obj, layers=None): pass diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index 9b7d0a6522..4f79e13a16 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -41,7 +41,7 @@ from blenderbim.bim.module.geometry.helper import Helper from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData from blenderbim.bim.module.model.opening import FilledOpeningGenerator from ifcopenshell.util.shape_builder import V, ShapeBuilder -from typing import Optional, Union, TypeVar, Any, Iterable +from typing import Optional, Union, TypeVar, Any, Iterable, Literal T = TypeVar("T") @@ -570,7 +570,9 @@ class Model(blenderbim.core.tool.Model): return material.MaterialProfiles[0].Profile @classmethod - def get_usage_type(cls, element): + def get_usage_type( + cls, element: ifcopenshell.entity_instance + ) -> Optional[Literal["LAYER1", "LAYER2", "LAYER3", "PROFILE"]]: material = ifcopenshell.util.element.get_material(element, should_inherit=False) if material: if material.is_a("IfcMaterialLayerSetUsage"):