diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index 5227400c96..985e4e8baf 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -12,6 +12,7 @@ classes = ( wall.SplitWall, wall.AddWallOpening, slab.AddSlabOpening, + profile.ExtendProfile, prop.BIMModelProperties, ui.BIM_PT_authoring, ui.BIM_PT_authoring_architectural, diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py index e3e3d8ce06..6de8a6a0c1 100644 --- a/src/blenderbim/blenderbim/bim/module/model/profile.py +++ b/src/blenderbim/blenderbim/bim/module/model/profile.py @@ -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 diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py index c658e46609..c132a72339 100644 --- a/src/blenderbim/blenderbim/bim/module/model/wall.py +++ b/src/blenderbim/blenderbim/bim/module/model/wall.py @@ -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 diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 0d449baefe..4b29e5092f 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -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":