From 4a5769c32233cf808ce69eb854f047172e12cec5 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 24 Jul 2023 23:22:12 +1000 Subject: [PATCH] Fix #3391 #3491 #3493. New space generation that uses a cutting plane across all eligible objects instead of a simplified "straight wall" analysis. This makes generating spaces significantly more robust, makes it easier to add / remove "space separating objects" (so we now have support for virtual elements), and only acts on visible objects. --- .../blenderbim/bim/module/model/prop.py | 2 + .../blenderbim/bim/module/model/space.py | 132 +++++++++++------- .../bim/module/spatial/workspace.py | 4 +- 3 files changed, 89 insertions(+), 49 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index afd90a767c..231df15cd8 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -135,6 +135,8 @@ class BIMModelProperties(PropertyGroup): rl1: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for walls") # Used for things like windows, other hosted furniture rl2: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for windows") + # Used for plan calculation points such as in room generation + rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation") x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE", min=-pi / 180 * 89, max=pi / 180 * 89) type_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page) type_template: bpy.props.EnumProperty( diff --git a/src/blenderbim/blenderbim/bim/module/model/space.py b/src/blenderbim/blenderbim/bim/module/model/space.py index b827b09f1f..fb52923279 100644 --- a/src/blenderbim/blenderbim/bim/module/model/space.py +++ b/src/blenderbim/blenderbim/bim/module/model/space.py @@ -42,11 +42,9 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): return tool.Ifc.get_entity(collection_obj) def _execute(self, context): - # This only works based on a 2D plan only considering the standard - # layered walls (i.e. prismatic) in the currently active storey (though - # we can easily extend it to include other "bounding" elements). For - # now we generate rooms that exclude walls (i.e. not to wall midpoint - # or exterior / interior edge). + # This works as a 2.5 extruded polygon based on a cutting plane. Note + # that rooms exclude walls (i.e. not to wall midpoint or exterior / + # exterior edge. def msg(self, context): self.layout.label(text="NO ACTIVE STOREY") @@ -88,21 +86,45 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): mat.translation = (x, y, z) h = 3 - boundary_elements = [] - for subelement in ifcopenshell.util.element.get_decomposition(spatial_element): - if subelement.is_a("IfcWall") and tool.Model.get_usage_type(subelement) == "LAYER2": - boundary_elements.append(subelement) - + 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 = [] - for boundary_element in boundary_elements: - obj = tool.Ifc.get_object(boundary_element) - if not obj: + + gross_settings = ifcopenshell.geom.settings() + gross_settings.set(gross_settings.DISABLE_OPENING_SUBTRACTIONS, True) + + for obj in bpy.context.visible_objects: + element = tool.Ifc.get_entity(obj) + + if ( + not element + or obj.type != "MESH" + or not self.is_bounding_class(element) + or not tool.Drawing.is_intersecting_plane(obj, self.cut_point, self.cut_normal) + ): continue - axis = self.get_wall_axis(obj) - for ypos in ["miny", "maxy"]: - start, end = axis[ypos] - offset = (end - start).normalized() * 0.3 # Offset wall lines by 300mm - boundary_lines.append(shapely.LineString([start - offset, end + offset])) + + old_mesh = None + if element.HasOpenings: + new_mesh = self.create_mesh(ifcopenshell.geom.create_shape(gross_settings, 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) @@ -162,23 +184,34 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator): if relating_type: blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type) - def get_wall_axis(self, obj): - x_values = [v[0] for v in obj.bound_box] - y_values = [v[1] for v in obj.bound_box] - min_x = min(x_values) - max_x = max(x_values) - min_y = min(y_values) - max_y = max(y_values) - return { - "miny": [ - (obj.matrix_world @ Vector((min_x, min_y, 0.0))).to_2d(), - (obj.matrix_world @ Vector((max_x, min_y, 0.0))).to_2d(), - ], - "maxy": [ - (obj.matrix_world @ Vector((min_x, max_y, 0.0))).to_2d(), - (obj.matrix_world @ Vector((max_x, max_y, 0.0))).to_2d(), - ], - } + 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 class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator): @@ -231,7 +264,7 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator): union = shapely.ops.unary_union(polys).buffer(converted_tolerance, cap_style=2, join_style=2) - union = self.get_purged_inner_holes_poly(union_geom = union, min_area = self.get_converted_tolerance(tolerance = 3)) + union = self.get_purged_inner_holes_poly(union_geom=union, min_area=self.get_converted_tolerance(tolerance=3)) for i, linear_ring in enumerate(union.interiors): poly = Polygon(linear_ring) @@ -250,7 +283,7 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator): self.set_obj_origin_to_bboxcenter(obj) if z != 0: - obj.location = obj.location + Vector((0,0,z)) + obj.location = obj.location + Vector((0, 0, z)) context.view_layer.active_layer_collection.collection.objects.link(obj) bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace") @@ -311,13 +344,17 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator): if union_geom.geom_type == "MultiPolygon": for poly in union_geom.geoms: - interiors_list = self.get_poly_valid_interior_list(poly = poly, min_area = min_area, interiors_list = interiors_list) + interiors_list = self.get_poly_valid_interior_list( + poly=poly, min_area=min_area, interiors_list=interiors_list + ) - new_poly = Polygon(poly.exterior.coords, holes = interiors_list) + new_poly = Polygon(poly.exterior.coords, holes=interiors_list) if union_geom.geom_type == "Polygon": - interiors_list = self.get_poly_valid_interior_list(poly = union_geom, min_area = min_area, interiors_list = interiors_list) - new_poly = Polygon(union_geom.exterior.coords, holes = interiors_list) + interiors_list = self.get_poly_valid_interior_list( + poly=union_geom, min_area=min_area, interiors_list=interiors_list + ) + new_poly = Polygon(union_geom.exterior.coords, holes=interiors_list) return new_poly @@ -328,7 +365,6 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator): interiors_list.append(interior) return interiors_list - def get_bmesh_from_polygon(self, poly, mat, h): bm = bmesh.new() bm.verts.index_update() @@ -370,6 +406,7 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator): vert.co = inverted @ aux_vector obj.location = newLoc + class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.toggle_space_visibility" bl_label = "Toggle Space Visibility" @@ -379,7 +416,7 @@ class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator): def execute(cls, context): model = tool.Ifc.get() - spaces = model.by_type('IfcSpace') + spaces = model.by_type("IfcSpace") if not spaces: print(spaces) @@ -387,17 +424,16 @@ class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator): first_obj = tool.Ifc.get_object(spaces[0]) - if bpy.data.objects[first_obj.name].display_type == 'TEXTURED': + if bpy.data.objects[first_obj.name].display_type == "TEXTURED": for space in spaces: obj = tool.Ifc.get_object(space) bpy.data.objects[obj.name].show_wire = True - bpy.data.objects[obj.name].display_type = 'WIRE' + bpy.data.objects[obj.name].display_type = "WIRE" return {"FINISHED"} - elif bpy.data.objects[first_obj.name].display_type == 'WIRE': + elif bpy.data.objects[first_obj.name].display_type == "WIRE": for space in spaces: obj = tool.Ifc.get_object(space) bpy.data.objects[obj.name].show_wire = False - bpy.data.objects[obj.name].display_type = 'TEXTURED' + bpy.data.objects[obj.name].display_type = "TEXTURED" return {"FINISHED"} - diff --git a/src/blenderbim/blenderbim/bim/module/spatial/workspace.py b/src/blenderbim/blenderbim/bim/module/spatial/workspace.py index b52232a61f..04b54e1993 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/workspace.py @@ -83,7 +83,7 @@ class SpatialToolUI: # SpatialData.load() if not AuthoringData.is_loaded: - AuthoringData.load() + AuthoringData.load(None) cls.draw_type_selection_interface(context) cls.draw_default_interface(context) @@ -93,6 +93,8 @@ class SpatialToolUI: @classmethod def draw_default_interface(cls, context): + row = cls.layout.row(align=True) + row.prop(data=cls.model_props, property="rl3", text="RL") row = cls.layout.row(align=True) row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_A")