New feature to extend columns, beams, and members to a target

This commit is contained in:
Dion Moult
2021-08-07 17:01:42 +10:00
parent cdd46066fa
commit bcf82984ce
4 changed files with 150 additions and 5 deletions
@@ -12,6 +12,7 @@ classes = (
wall.SplitWall,
wall.AddWallOpening,
slab.AddSlabOpening,
profile.ExtendProfile,
prop.BIMModelProperties,
ui.BIM_PT_authoring,
ui.BIM_PT_authoring_architectural,
@@ -8,7 +8,7 @@ import ifcopenshell.util.element
import mathutils.geometry
import blenderbim.bim.handler
from blenderbim.bim.ifc import IfcStore
from math import pi, degrees
from math import pi, degrees, inf
from mathutils import Vector, Matrix
from ifcopenshell.api.pset.data import Data as PsetData
from ifcopenshell.api.material.data import Data as MaterialData
@@ -232,3 +232,136 @@ class DumbProfileRegenerator:
if not obj or obj not in IfcStore.edited_objs:
return
bpy.ops.bim.update_representation(obj=obj.name)
class ExtendProfile(bpy.types.Operator):
bl_idname = "bim.extend_profile"
bl_label = "Extend Profile"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
selected_objs = context.selected_objects
for obj in selected_objs:
bpy.ops.bim.dynamically_void_product(obj=obj.name)
if len(selected_objs) == 0:
return {"FINISHED"}
if not context.active_object:
return {"FINISHED"}
if len(selected_objs) == 1:
DumbProfileExtender(context.active_object, target_coordinate=context.scene.cursor.location).extend()
IfcStore.edited_objs.add(context.active_object)
return {"FINISHED"}
if len(selected_objs) < 2:
return {"FINISHED"}
for obj in selected_objs:
if obj == context.active_object:
continue
DumbProfileExtender(obj, context.active_object).extend()
IfcStore.edited_objs.add(obj)
return {"FINISHED"}
class DumbProfileExtender:
# A profile is a prismatic extrusion along its local Z axis.
def __init__(self, profile, target=None, target_coordinate=None):
self.profile = profile
self.target = target
self.target_coordinate = target_coordinate
# An extension of a profile is determined by casting a ray from the cardinal
# point of either extreme along the profiles local Z axis to a target
# object. The nearest result from this raycast will be the target extension
# point.
def extend(self):
extension_data = self.get_closest_extension_point()
if not extension_data:
return
if extension_data["end"] == "bottom":
self.profile.matrix_world.translation = extension_data["contact"]
if extension_data["direction"] == "up":
self.shift_top_faces(-extension_data["distance"])
elif extension_data["direction"] == "down":
self.shift_top_faces(extension_data["distance"])
if extension_data["end"] == "top":
if extension_data["direction"] == "up":
self.shift_top_faces(extension_data["distance"])
elif extension_data["direction"] == "down":
self.shift_top_faces(-extension_data["distance"])
def shift_top_faces(self, z):
vertices = []
for f in self.get_profile_top_faces():
vertices.extend(f.vertices)
for v in set(vertices):
self.profile.data.vertices[v].co.z += z
# An top face is defined as having at least one vertex on the max
# Z-axis, and a non-insignificant Z component of its face normal
def get_profile_top_faces(self):
faces = []
max_z = max([v[2] for v in self.profile.bound_box])
for f in self.profile.data.polygons:
if abs(f.normal.z) < 0.1:
continue
for v in f.vertices:
if self.profile.data.vertices[v].co.z == max_z:
faces.append(f)
break
return faces
def get_closest_extension_point(self):
top = self.profile.matrix_world @ Vector((0, 0, self.profile.dimensions[2]))
bottom = self.profile.matrix_world.translation
up = self.profile.matrix_world.to_quaternion() @ Vector((0, 0, 1))
down = self.profile.matrix_world.to_quaternion() @ Vector((0, 0, -1))
if self.target:
return self.get_closest_extension_point_from_target_obj(top, bottom, up, down)
elif self.target_coordinate:
return self.get_closest_extension_point_from_target_coordinate(top, bottom, up, down)
def get_closest_extension_point_from_target_coordinate(self, top, bottom, up, down):
point = mathutils.geometry.intersect_line_plane(
bottom, top, self.target_coordinate, self.profile.matrix_world.to_quaternion() @ Vector((0, 0, 1))
)
if not point:
return
top_distance = (point - top).length
bottom_distance = (point - bottom).length
if top_distance < bottom_distance:
intersection_point, signed_distance = mathutils.geometry.intersect_point_line(point, top, bottom)
if signed_distance > 0:
return {"contact": point, "distance": top_distance, "end": "top", "direction": "down"}
else:
return {"contact": point, "distance": top_distance, "end": "top", "direction": "up"}
else:
intersection_point, signed_distance = mathutils.geometry.intersect_point_line(point, bottom, top)
if signed_distance > 0:
return {"contact": point, "distance": bottom_distance, "end": "bottom", "direction": "up"}
else:
return {"contact": point, "distance": bottom_distance, "end": "bottom", "direction": "down"}
def get_closest_extension_point_from_target_obj(self, top, bottom, up, down):
t_top = self.target.matrix_world.inverted() @ top
t_bottom = self.target.matrix_world.inverted() @ bottom
t_up = self.target.matrix_world.inverted().to_quaternion() @ up
t_down = self.target.matrix_world.inverted().to_quaternion() @ down
results = []
results.append((self.target.ray_cast(t_top, t_up, distance=50), top, "top", "up"))
results.append((self.target.ray_cast(t_top, t_down, distance=50), top, "top", "down"))
results.append((self.target.ray_cast(t_bottom, t_up, distance=50), bottom, "bottom", "up"))
results.append((self.target.ray_cast(t_bottom, t_down, distance=50), bottom, "bottom", "down"))
min_distance = float(inf)
final = None
for result in results:
if result[0][0]:
contact = self.target.matrix_world @ result[0][1]
distance = (contact - result[1]).length
if distance < min_distance:
min_distance = distance
final = {"contact": contact, "distance": distance, "end": result[2], "direction": result[3]}
return final
@@ -609,6 +609,8 @@ class DumbWallJoiner:
min_x = min([v[0] for v in wall.bound_box])
max_x = max([v[0] for v in wall.bound_box])
for f in wall.data.polygons:
if abs(f.normal.x) < 0.1:
continue
end_face_index = self.get_wall_face_end(wall, f, min_x, max_x)
if end_face_index == 1:
min_faces.append(f)
@@ -619,9 +621,9 @@ class DumbWallJoiner:
# 1 is the leftmost (minimum local X axis) end, and 2 is the rightmost end
def get_wall_face_end(self, wall, face, min_x, max_x):
for v in face.vertices:
if wall.data.vertices[v].co.x == min_x and abs(face.normal.x) > 0.1:
if wall.data.vertices[v].co.x == min_x:
return 1
if wall.data.vertices[v].co.x == max_x and abs(face.normal.x) > 0.1:
if wall.data.vertices[v].co.x == max_x:
return 2
@@ -44,7 +44,7 @@ class BimTool(WorkSpaceTool):
row.label(text="Add Type Instance", icon="EVENT_A")
if props.ifc_class == "IfcWallType":
row = layout.row(align=True)
row = layout.row()
row.label(text="Join")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -56,7 +56,7 @@ class BimTool(WorkSpaceTool):
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Mitre", icon="EVENT_Y")
row = layout.row(align=True)
row = layout.row()
row.label(text="Wall Tools")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -73,6 +73,13 @@ class BimTool(WorkSpaceTool):
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Opening", icon="EVENT_O")
if props.ifc_class in ["IfcColumnType", "IfcBeamType", "IfcMemberType"]:
row = layout.row()
row.label(text="Join")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="Extend", icon="EVENT_E")
row = layout.row(align=True)
row.label(text="Align")
row = layout.row(align=True)
@@ -119,6 +126,8 @@ class Hotkey(bpy.types.Operator):
def hotkey_S_E(self):
if self.props.ifc_class == "IfcWallType":
bpy.ops.bim.join_wall(join_type="T")
elif self.props.ifc_class in ["IfcColumnType", "IfcBeamType", "IfcMemberType"]:
bpy.ops.bim.extend_profile()
def hotkey_S_V(self):
if self.props.ifc_class == "IfcWallType":