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_label = "Extend Profile"
bl_options = {"REGISTER", "UNDO"}
join_type: bpy.props.StringProperty()
def execute(self, context):
selected_objs = context.selected_objects
for obj in selected_objs:
bpy.ops.bim.dynamically_void_product(obj=obj.name)
joiner = DumbProfileJoiner()
if len(selected_objs) == 0:
return {"FINISHED"}
if not context.active_object:
return {"FINISHED"}
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"}
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:
return {"FINISHED"}
for obj in selected_objs:
if obj == context.active_object:
continue
DumbProfileExtender(None).join_T(obj, context.active_object)
if self.join_type == "T":
for obj in selected_objs:
if obj == context.active_object:
continue
joiner.join_T(obj, context.active_object)
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
class DumbProfileJoiner:
def __init__(self):
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.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)
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):
if axis is None or body is None:
axis = body = self.get_profile_axis(obj)
@@ -426,9 +452,9 @@ class DumbProfileExtender:
# Work out body extents
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":
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()))
if xy_angle >= -135 and xy_angle <= -45:
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())
self.body[1] = intersect
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())
max_dim = self.get_max_bound_box_dimension(profile1)
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())
self.body[0] = intersect
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())
max_dim = self.get_max_bound_box_dimension(profile1)
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)
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
# Here's an example of looking at the end of an I-Beam profile with a +Z extrusion out of the screen
# top
@@ -491,26 +521,42 @@ class DumbProfileExtender:
max_y = max([v[1] for v in obj.bound_box])
p = obj.matrix_world @ Vector((0, max_y, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
y_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
if z_inwards:
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":
min_y = min([v[1] for v in obj.bound_box])
p = obj.matrix_world @ Vector((0, min_y, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
y_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
if z_inwards:
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":
max_x = max([v[0] for v in obj.bound_box])
p = obj.matrix_world @ Vector((max_x, 0, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
if z_inwards:
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":
min_x = min([v[0] for v in obj.bound_box])
p = obj.matrix_world @ Vector((min_x, 0, 0))
x_axis = obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, -1, 0))
z_axis = obj.matrix_world.to_quaternion() @ Vector((1, 0, 0))
if z_inwards:
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)
def create_matrix(self, p, x, y, z):
@@ -544,7 +590,6 @@ class DumbProfileExtender:
break
return height
# 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
@@ -89,31 +89,28 @@ class JoinWall(bpy.types.Operator):
def execute(self, context):
selected_objs = [o for o in context.selected_objects if o.BIMObjectProperties.ifc_definition_id]
joiner = DumbWallJoiner()
#for obj in selected_objs:
# bpy.ops.bim.dynamically_void_product(obj=obj.name)
if not self.join_type:
for obj in selected_objs:
DumbWallJoiner().unjoin(obj)
joiner.unjoin(obj)
return {"FINISHED"}
if not context.active_object:
return {"FINISHED"}
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)
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:
return {"FINISHED"}
joiner = DumbWallJoiner()
for obj in selected_objs:
if obj == context.active_object:
continue
if self.join_type == "T":
if self.join_type == "T":
for obj in selected_objs:
if obj == context.active_object:
continue
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"}
@@ -41,7 +41,7 @@ class BimTool(WorkSpaceTool):
# ("mesh.add_wall", {"type": 'LEFTMOUSE', "value": 'PRESS'}, {"properties": []}),
("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.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.merge_wall", {"type": "M", "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":
bpy.ops.bim.join_wall(join_type="T")
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):
if self.has_ifc_class and self.props.ifc_class == "IfcWallType":