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.
This commit is contained in:
Dion Moult
2023-07-24 23:22:12 +10:00
parent f3df78784a
commit 4a5769c322
3 changed files with 89 additions and 49 deletions
@@ -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(
@@ -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"}
@@ -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")