From bbfd42cf1957b9621b2d1857826aeac3e3e4afc4 Mon Sep 17 00:00:00 2001 From: Massimo Fabbro Date: Mon, 2 Oct 2023 22:12:11 +0200 Subject: [PATCH] Refactor create_space operator and better UI for covering tool --- .../bim/module/covering/workspace.py | 12 +- .../blenderbim/bim/module/model/covering.py | 2 +- .../blenderbim/bim/module/model/prop.py | 1 + .../blenderbim/bim/module/model/space.py | 161 +-------------- src/blenderbim/blenderbim/core/spatial.py | 50 ++++- src/blenderbim/blenderbim/core/tool.py | 14 ++ src/blenderbim/blenderbim/tool/spatial.py | 184 +++++++++++++++++- 7 files changed, 251 insertions(+), 173 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/covering/workspace.py b/src/blenderbim/blenderbim/bim/module/covering/workspace.py index 9989d7caea..e41145d959 100644 --- a/src/blenderbim/blenderbim/bim/module/covering/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/covering/workspace.py @@ -42,13 +42,14 @@ class CoveringTool(WorkSpaceTool): bl_description = "Create and edit coverings" bl_icon = os.path.join(os.path.dirname(__file__), "ops.authoring.covering") bl_widget = None + ifc_element_type = "IfcCoveringType" bl_keymap = tool.Blender.get_default_selection_keypmap() + ( ("bim.covering_hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}), ) @classmethod def draw_settings(cls, context, layout, ws_tool): - CoveringToolUI.draw(context, layout, ifc_element_type="IfcCoveringType") + CoveringToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) def add_layout_hotkey(layout, text, hotkey, description): args = ["covering", layout, text, hotkey, description] @@ -71,7 +72,6 @@ class CoveringToolUI: elif AuthoringData.data["ifc_element_type"] != ifc_element_type: AuthoringData.load(ifc_element_type) - if context.region.type == "TOOL_HEADER": cls.draw_header_interface() elif context.region.type in ("UI", "WINDOW"): @@ -97,6 +97,7 @@ class CoveringToolUI: if AuthoringData.data["predefined_type"] == "FLOORING": if element and bpy.context.selected_objects and element.is_a("IfcWall"): op = row.operator("bim.add_instance_flooring_coverings_from_walls") +# PLEASE KEEP COMMENTS AS A REMINDER # elif element and bpy.context.selected_objects and element.is_a("IfcSpace"): # op. = row.operator("bim.add_istance_flooring_from_spaces"): else: @@ -183,8 +184,11 @@ class Hotkey(bpy.types.Operator, Operator): def hotkey_S_A(self): active_obj = bpy.context.active_object element = tool.Ifc.get_entity(active_obj) - if element and bpy.context.selected_objects and element.is_a("IfcWall"): - bpy.ops.bim.add_instance_flooring_coverings_from_walls() + if AuthoringData.data["predefined_type"] == "FLOORING": + if element and bpy.context.selected_objects and element.is_a("IfcWall"): + bpy.ops.bim.add_instance_flooring_coverings_from_walls() + else: + bpy.ops.bim.add_constr_type_instance() else: bpy.ops.bim.add_constr_type_instance() diff --git a/src/blenderbim/blenderbim/bim/module/model/covering.py b/src/blenderbim/blenderbim/bim/module/model/covering.py index 3f9f0f4b2d..a5d954aea0 100644 --- a/src/blenderbim/blenderbim/bim/module/model/covering.py +++ b/src/blenderbim/blenderbim/bim/module/model/covering.py @@ -25,7 +25,7 @@ import blenderbim.core.covering as core class AddInstanceFlooringCoveringsFromWalls(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_instance_flooring_coverings_from_walls" - bl_label = "Add Covering From Walls" + bl_label = "Add Flooring From Walls" bl_options = {"REGISTER", "UNDO"} bl_description = "Add instance flooring coverings from selected walls. The active object must be a wall and layered vertically" diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index 2723e7904e..2b167bd730 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -79,6 +79,7 @@ def update_type_class(self, context): def update_relating_type_id(self, context): AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id() AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail() + AuthoringData.data["predefined_type"] = AuthoringData.predefined_type() def update_type_page(self, context): diff --git a/src/blenderbim/blenderbim/bim/module/model/space.py b/src/blenderbim/blenderbim/bim/module/model/space.py index 2ae6135205..7a63a31604 100644 --- a/src/blenderbim/blenderbim/bim/module/model/space.py +++ b/src/blenderbim/blenderbim/bim/module/model/space.py @@ -50,13 +50,7 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): def msg(self, context): self.layout.label(text="NO ACTIVE STOREY") - props = context.scene.BIMModelProperties - relating_type_id = props.relating_type_id - relating_type = None - if relating_type_id: - relating_type = tool.Ifc.get().by_id(int(relating_type_id)) - if not relating_type.is_a("IfcSpaceType"): - relating_type = None +# props = context.scene.BIMModelProperties collection = context.view_layer.active_layer_collection.collection collection_obj = collection.BIMCollectionProperties.obj @@ -68,158 +62,7 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") return -# core.generate_space(tool.Ifc, tool.Spatial) - - - - - - - active_obj = bpy.context.active_object - element = None - if bpy.context.selected_objects and active_obj: - element = tool.Ifc.get_entity(active_obj) - mat = active_obj.matrix_world - local_bbox_center = 0.125 * sum((Vector(b) for b in active_obj.bound_box), Vector()) - global_bbox_center = mat @ local_bbox_center - x = global_bbox_center.x - y = global_bbox_center.y - z = (mat @ Vector(active_obj.bound_box[0])).z - - h = active_obj.dimensions.z - else: - x, y = context.scene.cursor.location.xy - z = collection_obj.matrix_world.translation.z - mat = Matrix() - mat.translation = (x, y, z) - h = 3 - - calculation_rl = context.scene.BIMModelProperties.rl3 - self.cut_point = collection_obj.matrix_world.translation.copy() + Vector((0, 0, calculation_rl)) - self.cut_normal = Vector((0, 0, 1)) - boundary_lines = [] - - gross_settings = ifcopenshell.geom.settings() - gross_settings.set(gross_settings.DISABLE_OPENING_SUBTRACTIONS, True) - - for obj in bpy.context.visible_objects: - visible_element = tool.Ifc.get_entity(obj) - - if ( - not visible_element - or obj.type != "MESH" - or not self.is_bounding_class(visible_element) - or not tool.Drawing.is_intersecting_plane(obj, self.cut_point, self.cut_normal) - ): - continue - - old_mesh = None - if visible_element.HasOpenings: - new_mesh = self.create_mesh(ifcopenshell.geom.create_shape(gross_settings, visible_element)) - old_mesh = obj.data - obj.data = new_mesh - - local_cut_point = obj.matrix_world.inverted() @ self.cut_point - local_cut_normal = obj.matrix_world.inverted().to_quaternion() @ self.cut_normal - verts, edges = tool.Drawing.bisect_mesh_with_plane(obj, local_cut_point, local_cut_normal) - - if old_mesh: - obj.data = old_mesh - bpy.data.meshes.remove(new_mesh) - - for edge in edges or []: - boundary_lines.append( - shapely.LineString( - [Vector((round(x, 3) for x in verts[edge[0]])), Vector((round(x, 3) for x in verts[edge[1]]))] - ) - ) - - unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines)) - closed_polygons = shapely.polygonize(unioned_boundaries.geoms) - - space_polygon = None - for polygon in closed_polygons.geoms: - if shapely.contains_xy(polygon, x, y): - space_polygon = shapely.force_3d(polygon) - - if not space_polygon: - return - - bm = bmesh.new() - bm.verts.index_update() - bm.edges.index_update() - - mat_invert = mat.inverted() - new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], z])) for v in space_polygon.exterior.coords[0:-1]] - [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)] - bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0])) - - for interior in space_polygon.interiors: - new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], z])) for v in interior.coords[0:-1]] - [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)] - bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0])) - - bm.verts.index_update() - bm.edges.index_update() - - bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5) - bmesh.ops.triangle_fill(bm, edges=bm.edges) - bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges) - - extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces) - extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)] - bmesh.ops.translate(bm, vec=[0.0, 0.0, h], verts=extruded_verts) - - mesh = bpy.data.meshes.new(name="Space") - bm.to_mesh(mesh) - bm.free() - - if element and element.is_a("IfcSpace"): - mesh.name = active_obj.data.name - mesh.BIMMeshProperties.ifc_definition_id = active_obj.data.BIMMeshProperties.ifc_definition_id - tool.Geometry.change_object_data(active_obj, mesh, is_global=True) - tool.Ifc.edit(active_obj) - else: - if relating_type: - name = tool.Model.generate_occurrence_name(relating_type, "IfcSpace") - else: - name = "Space" - obj = bpy.data.objects.new("Space", mesh) - obj.matrix_world = mat - collection.objects.link(obj) - bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace") - element = tool.Ifc.get_entity(obj) - if relating_type: - blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type) - - def is_bounding_class(self, element): - for ifc_class in ["IfcWall", "IfcColumn", "IfcMember", "IfcVirtualElement", "IfcPlate"]: - if element.is_a(ifc_class): - return True - return False - - def create_mesh(self, shape): - geometry = shape.geometry - mesh = bpy.data.meshes.new("tmp") - verts = geometry.verts - if geometry.faces: - num_vertices = len(verts) // 3 - total_faces = len(geometry.faces) - loop_start = range(0, total_faces, 3) - num_loops = total_faces // 3 - loop_total = [3] * num_loops - num_vertex_indices = len(geometry.faces) - - mesh.vertices.add(num_vertices) - mesh.vertices.foreach_set("co", verts) - mesh.loops.add(num_vertex_indices) - mesh.loops.foreach_set("vertex_index", geometry.faces) - mesh.polygons.add(num_loops) - mesh.polygons.foreach_set("loop_start", loop_start) - mesh.polygons.foreach_set("loop_total", loop_total) - mesh.polygons.foreach_set("use_smooth", [0] * total_faces) - mesh.update() - return mesh + core.generate_space(tool.Ifc, tool.Spatial, tool.Model) class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/blenderbim/blenderbim/core/spatial.py b/src/blenderbim/blenderbim/core/spatial.py index e5bbefa127..4ed8635878 100644 --- a/src/blenderbim/blenderbim/core/spatial.py +++ b/src/blenderbim/blenderbim/core/spatial.py @@ -16,8 +16,7 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . -import blenderbim.core - +import blenderbim.core.type def reference_structure(ifc, spatial, structure=None, element=None): if spatial.can_reference(structure, element): @@ -128,8 +127,51 @@ def select_decomposed_elements(spatial): spatial.select_products(spatial.get_decomposed_elements(container)) #HERE STARTS SPATIAL TOOL -def generate_space(ifc, spatial): - pass +def generate_space(ifc, spatial, model): + active_obj = spatial.get_active_obj() + selected_objects = spatial.get_selected_objects() + element = None + relating_type_id = spatial.get_relating_type_id() + + relating_type = None + if relating_type_id: + relating_type = ifc.get().by_id(int(relating_type_id)) + if not relating_type.is_a("IfcSpaceType"): + relating_type = None + + if selected_objects and active_obj: + x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_active_obj(active_obj) ##mat + element = ifc.get_entity(active_obj) + + else: + x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor() ##mat + + space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y) + + if not space_polygon: + return + + bm = spatial.get_bmesh_from_polygon(space_polygon, h=h) ##mat + + mesh = spatial.get_named_mesh_from_bmesh(name = "Space", bmesh = bm) + + if element and element.is_a("IfcSpace"): + spatial.edit_active_space_obj_from_mesh(mesh) + else: + if relating_type: + name = model.generate_occurrence_name(relating_type, "IfcSpace") + else: + name = "Space" + + obj = spatial.get_named_obj_from_mesh(name, mesh) ##mat + spatial.set_obj_origin_to_cursor_position(obj) + spatial.link_obj_to_active_collection(obj) + spatial.assign_ifcspace_class_to_obj(obj) + + element = ifc.get_entity(obj) + + if relating_type: + blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type) def generate_spaces_from_walls(ifc, spatial, collector): diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 272d50bddc..b21016b3b9 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -809,6 +809,13 @@ class Spatial: def set_relative_object_matrix(cls, target_obj, relative_to_obj, matrix): pass def show_scene_objects(cls): pass #HERE STARTS SPATIAL TOOL + def is_bounding_class(cls, visible_element): pass + def get_space_polygon_from_context_visible_objects(cls, x, y): pass + def get_boundary_lines_from_context_visible_objects(cls, visible_objects): pass + def get_gross_mesh_from_element(cls, visible_element): pass + def create_mesh_from_shape(cls, shape): pass + def get_x_y_z_h_mat_from_active_obj(cls, active_obj): pass + def get_x_y_z_h_mat_from_cursor(cls): pass def get_union_shape_from_selected_objects(cls): pass def get_boundary_elements(cls, selected_objects): pass def get_polygons(cls, boundary_elements): pass @@ -819,9 +826,16 @@ class Spatial: def get_buffered_poly_from_linear_ring(cls, linear_ring): pass def get_bmesh_from_polygon(cls, poly, h): pass def get_named_obj_from_bmesh(cls, name, bmesh): pass + def get_named_obj_from_mesh(cls, name, mesh): pass + def get_named_mesh_from_bmesh(cls, name, bmesh): pass + def edit_active_space_obj_from_mesh(cls, mesh): pass def set_obj_origin_to_bboxcenter(cls, obj): pass + def set_obj_origin_to_cursor_position(cls, obj): pass + def get_selected_objects(cls): pass + def get_active_obj(cls): pass def get_active_obj_z(cls): pass def get_active_obj_height(cls): pass + def get_relating_type_id(cls): pass def traslate_obj_to_z_location(cls, obj): pass def link_obj_to_active_collection(cls, obj): pass def get_2d_vertices_from_obj(cls, obj): pass diff --git a/src/blenderbim/blenderbim/tool/spatial.py b/src/blenderbim/blenderbim/tool/spatial.py index efa5d659a0..e8b9664e4e 100644 --- a/src/blenderbim/blenderbim/tool/spatial.py +++ b/src/blenderbim/blenderbim/tool/spatial.py @@ -270,6 +270,123 @@ class Spatial(blenderbim.core.tool.Spatial): #HERE STARTS SPATIAL TOOL + @classmethod + def is_bounding_class(cls, visible_element): + for ifc_class in ["IfcWall", "IfcColumn", "IfcMember", "IfcVirtualElement", "IfcPlate"]: + if visible_element.is_a(ifc_class): + return True + return False + + @classmethod + def get_space_polygon_from_context_visible_objects(cls, x, y): + boundary_lines = cls.get_boundary_lines_from_context_visible_objects() + unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines)) + closed_polygons = shapely.polygonize(unioned_boundaries.geoms) + space_polygon = None + for polygon in closed_polygons.geoms: + if shapely.contains_xy(polygon, x, y): + space_polygon = shapely.force_3d(polygon) + return space_polygon + + @classmethod + def get_boundary_lines_from_context_visible_objects(cls): + calculation_rl = bpy.context.scene.BIMModelProperties.rl3 + collection = bpy.context.view_layer.active_layer_collection.collection + collection_obj = collection.BIMCollectionProperties.obj + cut_point = collection_obj.matrix_world.translation.copy() + Vector((0, 0, calculation_rl)) + cut_normal = Vector((0, 0, 1)) + boundary_lines = [] + + for obj in bpy.context.visible_objects: + + + visible_element = tool.Ifc.get_entity(obj) + + if ( + not visible_element + or obj.type != "MESH" + or not cls.is_bounding_class(visible_element) + or not tool.Drawing.is_intersecting_plane(obj, cut_point, cut_normal) + ): + continue + + old_mesh = None + if visible_element.HasOpenings: + new_mesh = cls.get_gross_mesh_from_element(visible_element) + old_mesh = obj.data + obj.data = new_mesh + + local_cut_point = obj.matrix_world.inverted() @ cut_point + local_cut_normal = obj.matrix_world.inverted().to_quaternion() @ cut_normal + verts, edges = tool.Drawing.bisect_mesh_with_plane(obj, local_cut_point, local_cut_normal) + + if old_mesh: + obj.data = old_mesh + bpy.data.meshes.remove(new_mesh) + + for edge in edges or []: + boundary_lines.append( + shapely.LineString( + [Vector((round(x, 3) for x in verts[edge[0]])), Vector((round(x, 3) for x in verts[edge[1]]))] + ) + ) + + return boundary_lines + + @classmethod + def get_gross_mesh_from_element(cls, visible_element): + gross_settings = ifcopenshell.geom.settings() + gross_settings.set(gross_settings.DISABLE_OPENING_SUBTRACTIONS, True) + new_mesh = cls.create_mesh_from_shape(ifcopenshell.geom.create_shape(gross_settings, visible_element)) + return new_mesh + + @classmethod + def create_mesh_from_shape(cls, shape): + geometry = shape.geometry + mesh = bpy.data.meshes.new("tmp") + verts = geometry.verts + if geometry.faces: + num_vertices = len(verts) // 3 + total_faces = len(geometry.faces) + loop_start = range(0, total_faces, 3) + num_loops = total_faces // 3 + loop_total = [3] * num_loops + num_vertex_indices = len(geometry.faces) + + mesh.vertices.add(num_vertices) + mesh.vertices.foreach_set("co", verts) + mesh.loops.add(num_vertex_indices) + mesh.loops.foreach_set("vertex_index", geometry.faces) + mesh.polygons.add(num_loops) + mesh.polygons.foreach_set("loop_start", loop_start) + mesh.polygons.foreach_set("loop_total", loop_total) + mesh.polygons.foreach_set("use_smooth", [0] * total_faces) + mesh.update() + return mesh + + @classmethod + def get_x_y_z_h_mat_from_active_obj(cls, active_obj): + element = tool.Ifc.get_entity(active_obj) + mat = active_obj.matrix_world + local_bbox_center = 0.125 * sum((Vector(b) for b in active_obj.bound_box), Vector()) + global_bbox_center = mat @ local_bbox_center + x = global_bbox_center.x + y = global_bbox_center.y + z = (mat @ Vector(active_obj.bound_box[0])).z + + h = active_obj.dimensions.z + return x, y, z, h, mat + + @classmethod + def get_x_y_z_h_mat_from_cursor(cls): + collection = bpy.context.view_layer.active_layer_collection.collection + collection_obj = collection.BIMCollectionProperties.obj + x, y = bpy.context.scene.cursor.location.xy + z = collection_obj.matrix_world.translation.z + mat = Matrix() + h = 3 + return x, y, z, h, mat + @classmethod def get_union_shape_from_selected_objects(cls): selected_objects = bpy.context.selected_objects @@ -369,7 +486,11 @@ class Spatial(blenderbim.core.tool.Spatial): @classmethod def get_bmesh_from_polygon(cls, poly, h): - mat = bpy.context.active_object.matrix_world + if bpy.context.selected_objects and bpy.context.active_object: + mat = bpy.context.active_object.matrix_world + else: + mat = Matrix() + bm = bmesh.new() bm.verts.index_update() bm.edges.index_update() @@ -398,13 +519,32 @@ class Spatial(blenderbim.core.tool.Spatial): @classmethod def get_named_obj_from_bmesh(cls, name, bmesh): + mesh = cls.get_named_mesh_from_bmesh(name, bmesh) + obj = cls.get_named_obj_from_mesh(name, mesh) + return obj + + @classmethod + def get_named_obj_from_mesh(cls, name, mesh): + obj = bpy.data.objects.new(name, mesh) + if bpy.context.active_object: + mat = bpy.context.active_object.matrix_world + obj.matrix_world = mat + return obj + + @classmethod + def get_named_mesh_from_bmesh(cls, name, bmesh): mesh = bpy.data.meshes.new(name=name) bmesh.to_mesh(mesh) bmesh.free() - obj = bpy.data.objects.new(name, mesh) - mat = bpy.context.active_object.matrix_world - obj.matrix_world = mat - return obj + return mesh + + @classmethod + def edit_active_space_obj_from_mesh(cls, mesh): + active_obj = bpy.context.active_object + mesh.name = active_obj.data.name + mesh.BIMMeshProperties.ifc_definition_id = active_obj.data.BIMMeshProperties.ifc_definition_id + tool.Geometry.change_object_data(active_obj, mesh, is_global=True) + tool.Ifc.edit(active_obj) @classmethod def set_obj_origin_to_bboxcenter(cls, obj): @@ -422,6 +562,34 @@ class Spatial(blenderbim.core.tool.Spatial): vert.co = inverted @ aux_vector obj.location = newLoc + @classmethod + def set_obj_origin_to_cursor_position(cls, obj): + mat = obj.matrix_world + inverted = mat.inverted() + + + collection = bpy.context.view_layer.active_layer_collection.collection + collection_obj = collection.BIMCollectionProperties.obj + x, y = bpy.context.scene.cursor.location.xy + z = collection_obj.matrix_world.translation.z + + oldLoc = obj.location + newLoc = Vector((x,y,z)) + diff = newLoc - oldLoc + for vert in obj.data.vertices: + aux_vector = mat @ vert.co + aux_vector = aux_vector - diff + vert.co = inverted @ aux_vector + obj.location = newLoc + + @classmethod + def get_selected_objects(cls): + return bpy.context.selected_objects + + @classmethod + def get_active_obj(cls): + return bpy.context.active_object + @classmethod def get_active_obj_z(cls): x, y, z = bpy.context.active_object.matrix_world.translation.xyz @@ -432,6 +600,12 @@ class Spatial(blenderbim.core.tool.Spatial): height = bpy.context.active_object.dimensions.z return height + @classmethod + def get_relating_type_id(cls): + props = bpy.context.scene.BIMModelProperties + relating_type_id = props.relating_type_id + return relating_type_id + @classmethod def traslate_obj_to_z_location(cls, obj, z): if z != 0: