You can now have connection aware L joints for profiled elements

This commit is contained in:
Dion Moult
2022-09-23 00:09:56 +10:00
parent 748aaa671d
commit 28022581a7
3 changed files with 89 additions and 39 deletions
@@ -253,33 +253,34 @@ class ExtendProfile(bpy.types.Operator):
bl_idname = "bim.extend_profile" bl_idname = "bim.extend_profile"
bl_label = "Extend Profile" bl_label = "Extend Profile"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
join_type: bpy.props.StringProperty()
def execute(self, context): def execute(self, context):
selected_objs = context.selected_objects selected_objs = context.selected_objects
for obj in selected_objs: for obj in selected_objs:
bpy.ops.bim.dynamically_void_product(obj=obj.name) bpy.ops.bim.dynamically_void_product(obj=obj.name)
joiner = DumbProfileJoiner()
if len(selected_objs) == 0: if len(selected_objs) == 0:
return {"FINISHED"} return {"FINISHED"}
if not context.active_object: if not context.active_object:
return {"FINISHED"} return {"FINISHED"}
if len(selected_objs) == 1: if len(selected_objs) == 1:
DumbProfileExtender(None).join_E(context.active_object, context.scene.cursor.location) joiner.join_E(context.active_object, context.scene.cursor.location)
return {"FINISHED"} return {"FINISHED"}
if len(selected_objs) == 2 and self.join_type == "L":
joiner.join_L([o for o in selected_objs if o != context.active_object][0], context.active_object)
if len(selected_objs) < 2: if len(selected_objs) < 2:
return {"FINISHED"} return {"FINISHED"}
for obj in selected_objs: if self.join_type == "T":
if obj == context.active_object: for obj in selected_objs:
continue if obj == context.active_object:
DumbProfileExtender(None).join_T(obj, context.active_object) continue
joiner.join_T(obj, context.active_object)
return {"FINISHED"} return {"FINISHED"}
class DumbProfileExtender: class DumbProfileJoiner:
# A profile is a prismatic extrusion along its local Z axis. def __init__(self):
def __init__(self, profile, target=None, target_coordinate=None):
self.profile = profile
self.target = target
self.target_coordinate = target_coordinate
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Axis", "GRAPH_VIEW") self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Axis", "GRAPH_VIEW")
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
@@ -323,6 +324,31 @@ class DumbProfileExtender:
self.recreate_profile(element1, profile1, axis1, axis1) self.recreate_profile(element1, profile1, axis1, axis1)
def join_L(self, profile1, profile2):
element1 = tool.Ifc.get_entity(profile1)
element2 = tool.Ifc.get_entity(profile2)
axis1 = self.get_profile_axis(profile1)
axis2 = self.get_profile_axis(profile2)
intersect = tool.Cad.intersect_edges(axis1, axis2)
if intersect:
intersect, _ = intersect
else:
return
profile1_end = "ATEND" if tool.Cad.edge_percent(intersect, axis1) > 0.5 else "ATSTART"
profile2_end = "ATEND" if tool.Cad.edge_percent(intersect, axis2) > 0.5 else "ATSTART"
ifcopenshell.api.run(
"geometry.connect_path",
tool.Ifc.get(),
relating_element=element1,
related_element=element2,
relating_connection=profile1_end,
related_connection=profile2_end,
)
self.recreate_profile(element1, profile1, axis1, axis1)
self.recreate_profile(element2, profile2, axis2, axis2)
def recreate_profile(self, element, obj, axis=None, body=None): def recreate_profile(self, element, obj, axis=None, body=None):
if axis is None or body is None: if axis is None or body is None:
axis = body = self.get_profile_axis(obj) axis = body = self.get_profile_axis(obj)
@@ -426,9 +452,9 @@ class DumbProfileExtender:
# Work out body extents # Work out body extents
if connection == "ATEND": if connection == "ATEND":
axisl = ((profile2.matrix_world.inverted() @ axis1[1]) - (profile2.matrix_world.inverted() @ axis1[0])) axisl = (profile2.matrix_world.inverted() @ axis1[1]) - (profile2.matrix_world.inverted() @ axis1[0])
elif connection == "ATSTART": elif connection == "ATSTART":
axisl = ((profile2.matrix_world.inverted() @ axis1[0]) - (profile2.matrix_world.inverted() @ axis1[1])) axisl = (profile2.matrix_world.inverted() @ axis1[0]) - (profile2.matrix_world.inverted() @ axis1[1])
xy_angle = math.degrees(Vector((1, 0)).angle_signed(axisl.normalized().to_2d())) xy_angle = math.degrees(Vector((1, 0)).angle_signed(axisl.normalized().to_2d()))
if xy_angle >= -135 and xy_angle <= -45: if xy_angle >= -135 and xy_angle <= -45:
closest_plane = "bottom" closest_plane = "bottom"
@@ -451,7 +477,9 @@ class DumbProfileExtender:
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()) intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.col[3].to_3d(), plane.col[2].to_3d())
self.body[1] = intersect self.body[1] = intersect
else: else:
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane) plane = self.get_profile_plane(
profile2, furthest_plane if is_relating else closest_plane, z_inwards=False if is_relating else True
)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()) intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.col[3].to_3d(), plane.col[2].to_3d())
max_dim = self.get_max_bound_box_dimension(profile1) max_dim = self.get_max_bound_box_dimension(profile1)
self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim)) self.body[1] = intersect + profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
@@ -462,7 +490,9 @@ class DumbProfileExtender:
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()) intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.col[3].to_3d(), plane.col[2].to_3d())
self.body[0] = intersect self.body[0] = intersect
else: else:
plane = self.get_profile_plane(profile2, furthest_plane if is_relating else closest_plane) plane = self.get_profile_plane(
profile2, furthest_plane if is_relating else closest_plane, z_inwards=False if is_relating else True
)
intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.col[3].to_3d(), plane.col[2].to_3d()) intersect = mathutils.geometry.intersect_line_plane(*axis1, plane.col[3].to_3d(), plane.col[2].to_3d())
max_dim = self.get_max_bound_box_dimension(profile1) max_dim = self.get_max_bound_box_dimension(profile1)
self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim)) self.body[0] = intersect - profile1.matrix_world.to_quaternion() @ Vector((0, 0, max_dim))
@@ -475,7 +505,7 @@ class DumbProfileExtender:
y_dim = max(y) - min(y) y_dim = max(y) - min(y)
return x_dim if x_dim > y_dim else y_dim return x_dim if x_dim > y_dim else y_dim
def get_profile_plane(self, obj, plane): def get_profile_plane(self, obj, plane, z_inwards=True):
# Get a matrix for one of the bounding planes of the profile to be used as cutting plane # Get a matrix for one of the bounding planes of the profile to be used as cutting plane
# Here's an example of looking at the end of an I-Beam profile with a +Z extrusion out of the screen # Here's an example of looking at the end of an I-Beam profile with a +Z extrusion out of the screen
# top # top
@@ -491,26 +521,42 @@ class DumbProfileExtender:
max_y = max([v[1] for v in obj.bound_box]) max_y = max([v[1] for v in obj.bound_box])
p = obj.matrix_world @ Vector((0, max_y, 0)) p = obj.matrix_world @ Vector((0, max_y, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1)) x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
y_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0)) if z_inwards:
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0)) y_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
else:
y_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
elif plane == "bottom": elif plane == "bottom":
min_y = min([v[1] for v in obj.bound_box]) min_y = min([v[1] for v in obj.bound_box])
p = obj.matrix_world @ Vector((0, min_y, 0)) p = obj.matrix_world @ Vector((0, min_y, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1)) x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
y_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0)) if z_inwards:
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0)) y_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
else:
y_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
elif plane == "right": elif plane == "right":
max_x = max([v[0] for v in obj.bound_box]) max_x = max([v[0] for v in obj.bound_box])
p = obj.matrix_world @ Vector((max_x, 0, 0)) p = obj.matrix_world @ Vector((max_x, 0, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1)) x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0)) if z_inwards:
z_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0)) y_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
else:
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
elif plane == "left": elif plane == "left":
min_x = min([v[0] for v in obj.bound_box]) min_x = min([v[0] for v in obj.bound_box])
p = obj.matrix_world @ Vector((min_x, 0, 0)) p = obj.matrix_world @ Vector((min_x, 0, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1)) x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0)) if z_inwards:
z_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0)) y_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
else:
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
return self.create_matrix(p, x_axis, y_axis, z_axis) return self.create_matrix(p, x_axis, y_axis, z_axis)
def create_matrix(self, p, x, y, z): def create_matrix(self, p, x, y, z):
@@ -544,7 +590,6 @@ class DumbProfileExtender:
break break
return height return height
# An extension of a profile is determined by casting a ray from the cardinal # 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 # 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 # object. The nearest result from this raycast will be the target extension
@@ -89,31 +89,28 @@ class JoinWall(bpy.types.Operator):
def execute(self, context): def execute(self, context):
selected_objs = [o for o in context.selected_objects if o.BIMObjectProperties.ifc_definition_id] selected_objs = [o for o in context.selected_objects if o.BIMObjectProperties.ifc_definition_id]
joiner = DumbWallJoiner()
#for obj in selected_objs: #for obj in selected_objs:
# bpy.ops.bim.dynamically_void_product(obj=obj.name) # bpy.ops.bim.dynamically_void_product(obj=obj.name)
if not self.join_type: if not self.join_type:
for obj in selected_objs: for obj in selected_objs:
DumbWallJoiner().unjoin(obj) joiner.unjoin(obj)
return {"FINISHED"} return {"FINISHED"}
if not context.active_object: if not context.active_object:
return {"FINISHED"} return {"FINISHED"}
if len(selected_objs) == 1: if len(selected_objs) == 1:
DumbWallJoiner().join_E(context.active_object, context.scene.cursor.location) joiner.join_E(context.active_object, context.scene.cursor.location)
#IfcStore.edited_objs.add(context.active_object) #IfcStore.edited_objs.add(context.active_object)
return {"FINISHED"} return {"FINISHED"}
if len(selected_objs) == 2 and self.join_type == "L":
joiner.join_L([o for o in selected_objs if o != context.active_object][0], context.active_object)
if len(selected_objs) < 2: if len(selected_objs) < 2:
return {"FINISHED"} return {"FINISHED"}
joiner = DumbWallJoiner() if self.join_type == "T":
for obj in selected_objs: for obj in selected_objs:
if obj == context.active_object: if obj == context.active_object:
continue continue
if self.join_type == "T":
joiner.join_T(obj, context.active_object) joiner.join_T(obj, context.active_object)
elif self.join_type == "L":
joiner.join_L(obj, context.active_object)
IfcStore.edited_objs.add(obj)
#if self.join_type != "T":
# IfcStore.edited_objs.add(context.active_object)
return {"FINISHED"} return {"FINISHED"}
@@ -41,7 +41,7 @@ class BimTool(WorkSpaceTool):
# ("mesh.add_wall", {"type": 'LEFTMOUSE', "value": 'PRESS'}, {"properties": []}), # ("mesh.add_wall", {"type": 'LEFTMOUSE', "value": 'PRESS'}, {"properties": []}),
("bim.hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}), ("bim.hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
("bim.hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}), ("bim.hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
("bim.join_wall", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("join_type", "L")]}), ("bim.hotkey", {"type": "T", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_T")]}),
("bim.recalculate_wall", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": []}), ("bim.recalculate_wall", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": []}),
("bim.merge_wall", {"type": "M", "value": "PRESS", "shift": True}, {"properties": []}), ("bim.merge_wall", {"type": "M", "value": "PRESS", "shift": True}, {"properties": []}),
("bim.flip_wall", {"type": "F", "value": "PRESS", "shift": True}, {"properties": []}), ("bim.flip_wall", {"type": "F", "value": "PRESS", "shift": True}, {"properties": []}),
@@ -247,7 +247,15 @@ class Hotkey(bpy.types.Operator):
if self.props.ifc_class == "IfcWallType": if self.props.ifc_class == "IfcWallType":
bpy.ops.bim.join_wall(join_type="T") bpy.ops.bim.join_wall(join_type="T")
elif self.props.ifc_class in ["IfcColumnType", "IfcBeamType", "IfcMemberType"]: elif self.props.ifc_class in ["IfcColumnType", "IfcBeamType", "IfcMemberType"]:
bpy.ops.bim.extend_profile() bpy.ops.bim.extend_profile(join_type="T")
def hotkey_S_T(self):
if not self.has_ifc_class:
return
if self.props.ifc_class == "IfcWallType":
bpy.ops.bim.join_wall(join_type="L")
elif self.props.ifc_class in ["IfcColumnType", "IfcBeamType", "IfcMemberType"]:
bpy.ops.bim.extend_profile(join_type="L")
def hotkey_S_V(self): def hotkey_S_V(self):
if self.has_ifc_class and self.props.ifc_class == "IfcWallType": if self.has_ifc_class and self.props.ifc_class == "IfcWallType":