mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-21 14:23:53 +00:00
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:
@@ -135,6 +135,8 @@ class BIMModelProperties(PropertyGroup):
|
|||||||
rl1: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for walls")
|
rl1: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for walls")
|
||||||
# Used for things like windows, other hosted furniture
|
# Used for things like windows, other hosted furniture
|
||||||
rl2: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for windows")
|
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)
|
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_page: bpy.props.IntProperty(name="Type Page", default=1, update=update_type_page)
|
||||||
type_template: bpy.props.EnumProperty(
|
type_template: bpy.props.EnumProperty(
|
||||||
|
|||||||
@@ -42,11 +42,9 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
return tool.Ifc.get_entity(collection_obj)
|
return tool.Ifc.get_entity(collection_obj)
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
# This only works based on a 2D plan only considering the standard
|
# This works as a 2.5 extruded polygon based on a cutting plane. Note
|
||||||
# layered walls (i.e. prismatic) in the currently active storey (though
|
# that rooms exclude walls (i.e. not to wall midpoint or exterior /
|
||||||
# we can easily extend it to include other "bounding" elements). For
|
# exterior edge.
|
||||||
# now we generate rooms that exclude walls (i.e. not to wall midpoint
|
|
||||||
# or exterior / interior edge).
|
|
||||||
|
|
||||||
def msg(self, context):
|
def msg(self, context):
|
||||||
self.layout.label(text="NO ACTIVE STOREY")
|
self.layout.label(text="NO ACTIVE STOREY")
|
||||||
@@ -88,21 +86,45 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
mat.translation = (x, y, z)
|
mat.translation = (x, y, z)
|
||||||
h = 3
|
h = 3
|
||||||
|
|
||||||
boundary_elements = []
|
calculation_rl = context.scene.BIMModelProperties.rl3
|
||||||
for subelement in ifcopenshell.util.element.get_decomposition(spatial_element):
|
self.cut_point = collection_obj.matrix_world.translation.copy() + Vector((0, 0, calculation_rl))
|
||||||
if subelement.is_a("IfcWall") and tool.Model.get_usage_type(subelement) == "LAYER2":
|
self.cut_normal = Vector((0, 0, 1))
|
||||||
boundary_elements.append(subelement)
|
|
||||||
|
|
||||||
boundary_lines = []
|
boundary_lines = []
|
||||||
for boundary_element in boundary_elements:
|
|
||||||
obj = tool.Ifc.get_object(boundary_element)
|
gross_settings = ifcopenshell.geom.settings()
|
||||||
if not obj:
|
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
|
continue
|
||||||
axis = self.get_wall_axis(obj)
|
|
||||||
for ypos in ["miny", "maxy"]:
|
old_mesh = None
|
||||||
start, end = axis[ypos]
|
if element.HasOpenings:
|
||||||
offset = (end - start).normalized() * 0.3 # Offset wall lines by 300mm
|
new_mesh = self.create_mesh(ifcopenshell.geom.create_shape(gross_settings, element))
|
||||||
boundary_lines.append(shapely.LineString([start - offset, end + offset]))
|
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))
|
unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines))
|
||||||
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
|
closed_polygons = shapely.polygonize(unioned_boundaries.geoms)
|
||||||
@@ -162,23 +184,34 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if relating_type:
|
if relating_type:
|
||||||
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
|
blenderbim.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type)
|
||||||
|
|
||||||
def get_wall_axis(self, obj):
|
def is_bounding_class(self, element):
|
||||||
x_values = [v[0] for v in obj.bound_box]
|
for ifc_class in ["IfcWall", "IfcColumn", "IfcMember", "IfcVirtualElement", "IfcPlate"]:
|
||||||
y_values = [v[1] for v in obj.bound_box]
|
if element.is_a(ifc_class):
|
||||||
min_x = min(x_values)
|
return True
|
||||||
max_x = max(x_values)
|
return False
|
||||||
min_y = min(y_values)
|
|
||||||
max_y = max(y_values)
|
def create_mesh(self, shape):
|
||||||
return {
|
geometry = shape.geometry
|
||||||
"miny": [
|
mesh = bpy.data.meshes.new("tmp")
|
||||||
(obj.matrix_world @ Vector((min_x, min_y, 0.0))).to_2d(),
|
verts = geometry.verts
|
||||||
(obj.matrix_world @ Vector((max_x, min_y, 0.0))).to_2d(),
|
if geometry.faces:
|
||||||
],
|
num_vertices = len(verts) // 3
|
||||||
"maxy": [
|
total_faces = len(geometry.faces)
|
||||||
(obj.matrix_world @ Vector((min_x, max_y, 0.0))).to_2d(),
|
loop_start = range(0, total_faces, 3)
|
||||||
(obj.matrix_world @ Vector((max_x, max_y, 0.0))).to_2d(),
|
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):
|
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 = 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):
|
for i, linear_ring in enumerate(union.interiors):
|
||||||
poly = Polygon(linear_ring)
|
poly = Polygon(linear_ring)
|
||||||
@@ -250,7 +283,7 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
self.set_obj_origin_to_bboxcenter(obj)
|
self.set_obj_origin_to_bboxcenter(obj)
|
||||||
|
|
||||||
if z != 0:
|
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)
|
context.view_layer.active_layer_collection.collection.objects.link(obj)
|
||||||
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
|
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":
|
if union_geom.geom_type == "MultiPolygon":
|
||||||
for poly in union_geom.geoms:
|
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":
|
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)
|
interiors_list = self.get_poly_valid_interior_list(
|
||||||
new_poly = Polygon(union_geom.exterior.coords, holes = interiors_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
|
return new_poly
|
||||||
|
|
||||||
@@ -328,7 +365,6 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
interiors_list.append(interior)
|
interiors_list.append(interior)
|
||||||
return interiors_list
|
return interiors_list
|
||||||
|
|
||||||
|
|
||||||
def get_bmesh_from_polygon(self, poly, mat, h):
|
def get_bmesh_from_polygon(self, poly, mat, h):
|
||||||
bm = bmesh.new()
|
bm = bmesh.new()
|
||||||
bm.verts.index_update()
|
bm.verts.index_update()
|
||||||
@@ -370,6 +406,7 @@ class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
vert.co = inverted @ aux_vector
|
vert.co = inverted @ aux_vector
|
||||||
obj.location = newLoc
|
obj.location = newLoc
|
||||||
|
|
||||||
|
|
||||||
class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator):
|
class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.toggle_space_visibility"
|
bl_idname = "bim.toggle_space_visibility"
|
||||||
bl_label = "Toggle Space Visibility"
|
bl_label = "Toggle Space Visibility"
|
||||||
@@ -379,7 +416,7 @@ class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
def execute(cls, context):
|
def execute(cls, context):
|
||||||
model = tool.Ifc.get()
|
model = tool.Ifc.get()
|
||||||
|
|
||||||
spaces = model.by_type('IfcSpace')
|
spaces = model.by_type("IfcSpace")
|
||||||
|
|
||||||
if not spaces:
|
if not spaces:
|
||||||
print(spaces)
|
print(spaces)
|
||||||
@@ -387,17 +424,16 @@ class ToggleSpaceVisibility(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
|
|
||||||
first_obj = tool.Ifc.get_object(spaces[0])
|
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:
|
for space in spaces:
|
||||||
obj = tool.Ifc.get_object(space)
|
obj = tool.Ifc.get_object(space)
|
||||||
bpy.data.objects[obj.name].show_wire = True
|
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"}
|
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:
|
for space in spaces:
|
||||||
obj = tool.Ifc.get_object(space)
|
obj = tool.Ifc.get_object(space)
|
||||||
bpy.data.objects[obj.name].show_wire = False
|
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"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class SpatialToolUI:
|
|||||||
# SpatialData.load()
|
# SpatialData.load()
|
||||||
|
|
||||||
if not AuthoringData.is_loaded:
|
if not AuthoringData.is_loaded:
|
||||||
AuthoringData.load()
|
AuthoringData.load(None)
|
||||||
|
|
||||||
cls.draw_type_selection_interface(context)
|
cls.draw_type_selection_interface(context)
|
||||||
cls.draw_default_interface(context)
|
cls.draw_default_interface(context)
|
||||||
@@ -93,6 +93,8 @@ class SpatialToolUI:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def draw_default_interface(cls, context):
|
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 = cls.layout.row(align=True)
|
||||||
row.label(text="", icon="EVENT_SHIFT")
|
row.label(text="", icon="EVENT_SHIFT")
|
||||||
row.label(text="", icon="EVENT_A")
|
row.label(text="", icon="EVENT_A")
|
||||||
|
|||||||
Reference in New Issue
Block a user