diff --git a/src/compas_timber/connections/__init__.py b/src/compas_timber/connections/__init__.py index 9454bc3b8b..6d427512fc 100644 --- a/src/compas_timber/connections/__init__.py +++ b/src/compas_timber/connections/__init__.py @@ -9,6 +9,7 @@ from .x_halflap import XHalfLapJoint from .t_halflap import THalfLapJoint from .l_halflap import LHalfLapJoint +from .t_stirnversatz import TStirnversatzJoint from .solver import ConnectionSolver from .solver import JointTopology from .solver import find_neighboring_beams @@ -25,6 +26,7 @@ "LMiterJoint", "XHalfLapJoint", "THalfLapJoint", + "TStirnversatzJoint", "LHalfLapJoint", "NullJoint", "FrenchRidgeLapJoint", diff --git a/src/compas_timber/connections/butt_joint.py b/src/compas_timber/connections/butt_joint.py index f0c1b24635..a3c0f1e53f 100644 --- a/src/compas_timber/connections/butt_joint.py +++ b/src/compas_timber/connections/butt_joint.py @@ -5,15 +5,23 @@ from compas.geometry import closest_point_on_line from compas.geometry import distance_line_line from compas.geometry import intersection_plane_plane +from compas.geometry import intersection_line_plane +from compas.geometry import intersection_line_line from compas.geometry import Plane from compas.geometry import Line from compas.geometry import Polyhedron from compas.geometry import Point from compas.geometry import Vector from compas.geometry import Transformation +from compas.geometry import Polyline +from compas.geometry import Curve from compas.geometry import angle_vectors_signed from compas.geometry import angle_vectors +from compas.geometry import cross_vectors +from compas.geometry import Brep +from compas.geometry import Scale from .joint import Joint +import math class ButtJoint(Joint): @@ -45,16 +53,21 @@ class ButtJoint(Joint): """ - def __init__(self, main_beam=None, cross_beam=None, mill_depth=0, birdsmouth=False, **kwargs): + def __init__(self, main_beam=None, cross_beam=None, mill_depth=0, drill_diameter=0.0, birdsmouth=False, stepjoint=False, **kwargs): super(ButtJoint, self).__init__(**kwargs) self.main_beam = main_beam self.cross_beam = cross_beam self.main_beam_key = main_beam.key if main_beam else None self.cross_beam_key = cross_beam.key if cross_beam else None self.mill_depth = mill_depth + self.drill_diameter = float(drill_diameter) self.birdsmouth = birdsmouth + self.stepjoint = stepjoint self.btlx_params_main = {} self.btlx_params_cross = {} + self.btlx_drilling_params_cross = {} + self.btlx_stepjoint_params_main = {} + self.btlx_params_stepjoint_cross = {} self.features = [] self.test = [] @@ -92,7 +105,7 @@ def side_surfaces_cross(self): angles = face_dict.values() angles, face_indices = zip(*sorted(zip(angles, face_indices))) - return self.cross_beam.faces[(face_indices[0] + 1) % 4], self.cross_beam.faces[(face_indices[0] + 3) % 4] + return [self.cross_beam.faces[(face_indices[0] + 1) % 4], self.cross_beam.faces[(face_indices[0] + 3) % 4]] def front_back_surface_main(self): assert self.main_beam and self.cross_beam @@ -117,14 +130,15 @@ def get_main_cutting_plane(self): cross_mating_frame = cfr.copy() cfr = Frame(cfr.point, cfr.xaxis, cfr.yaxis * -1.0) # flip normal cfr.point = cfr.point + cfr.zaxis * self.mill_depth + return cfr, cross_mating_frame def subtraction_volume(self): """Returns the volume to be subtracted from the cross beam.""" vertices = [] - front_frame, back_frame = self.front_back_surface_main() - top_frame, bottom_frame = self.get_main_cutting_plane() - sides = self.side_surfaces_cross() + front_frame, back_frame = self.front_back_surface_main() #main_beam + top_frame, bottom_frame = self.get_main_cutting_plane() #cross_beam -- cutting/offsetted_cutting plane + sides = self.side_surfaces_cross() #cross_beam -- side faces for i, side in enumerate(sides): points = [] for frame in [bottom_frame, top_frame]: @@ -140,7 +154,6 @@ def subtraction_volume(self): min_pt, max_pt = points[0], points[-1] if i == 1: self.btlx_params_cross["start_x"] = abs(dots[0]) - top_line = Line(*intersection_plane_plane(Plane.from_frame(side), Plane.from_frame(top_frame))) top_min = Point(*closest_point_on_line(min_pt, top_line)) top_max = Point(*closest_point_on_line(max_pt, top_line)) @@ -186,79 +199,6 @@ def subtraction_volume(self): return ph - # @staticmethod - # def calc_params_birdsmouth(joint, main_part, cross_part): - # """ - # Calculate the parameters for a birdsmouth joint. - - # Parameters: - # ---------- - # joint (object): The joint object. - # main_part (object): The main part object. - # cross_part (object): The cross part object. - - # Returns: - # ---------- - # dict: A dictionary containing the calculated parameters for the birdsmouth joint - - # """ - # face_dict = joint._beam_side_incidence(main_part.beam, cross_part.beam, ignore_ends=True) - # face_dict = sorted(face_dict, key=face_dict.get) - - # # frame1 = joint.get_main_cutting_plane()[0] - # frame1 = joint.get_main_cutting_plane()[0] - # frame2 = cross_part.beam.faces[face_dict[1]] - - # plane1, plane2 = Plane.from_frame(frame1), Plane.from_frame(frame2) - # intersect_vec = Vector.from_start_end(*intersection_plane_plane(plane2, plane1)) - - # angles_dict = {} - # for i, face in enumerate(main_part.beam.faces): - # angles_dict[i] = (face.normal.angle(intersect_vec)) - # ref_frame_id = min(angles_dict, key=angles_dict.get) - # ref_frame = main_part.reference_surface_planes(ref_frame_id+1) - - # dot_frame1 = plane1.normal.dot(ref_frame.yaxis) - # if dot_frame1 > 0: - # plane1, plane2 = plane2, plane1 - - # start_point = Point(*intersection_plane_plane_plane(plane1, plane2, Plane.from_frame(ref_frame))) - # start_point.transform(Transformation.from_frame_to_frame(ref_frame, Frame.worldXY())) - # StartX, StartY = start_point[0], start_point[1] - - # intersect_vec1 = Vector.from_start_end(*intersection_plane_plane(plane1, Plane.from_frame(ref_frame))) - # intersect_vec2 = Vector.from_start_end(*intersection_plane_plane(plane2, Plane.from_frame(ref_frame))) - - # dot_2 = math.degrees(intersect_vec1.dot(ref_frame.yaxis)) - # if dot_2 < 0: - # intersect_vec1 = -intersect_vec1 - - # dot_1 = math.degrees(intersect_vec2.dot(ref_frame.yaxis)) - # if dot_1 < 0: - # intersect_vec2 = -intersect_vec2 - - # if joint.ends[str(main_part.key)] == "start": - # reference_frame = ref_frame.xaxis - # else: - # reference_frame = -ref_frame.xaxis - - # Angle1 = math.degrees(intersect_vec1.angle(reference_frame)) - # Angle2 = math.degrees(intersect_vec2.angle(reference_frame)) - - # Inclination1 = math.degrees(plane1.normal.angle(ref_frame.zaxis)) - # Inclination2 = math.degrees(plane2.normal.angle(ref_frame.zaxis)) - - # return { - # "Orientation": joint.ends[str(main_part.key)], - # "StartX": StartX, - # "StartY": StartY, - # "Angle1": Angle1, - # "Inclination1": Inclination1, - # "Angle2": Angle2, - # "Inclination2": Inclination2, - # "ReferencePlaneID": ref_frame_id - # } - def calc_params_birdsmouth(self): """ Calculate the parameters for a birdsmouth joint. @@ -271,36 +211,63 @@ def calc_params_birdsmouth(self): Returns: ---------- - dict: A dictionary containing the calculated parameters for the birdsmouth joint + bool: True if the joint creation is successful, False otherwise. """ face_dict = self._beam_side_incidence(self.main_beam, self.cross_beam, ignore_ends=True) face_keys = sorted([key for key in face_dict.keys()], key=face_dict.get) - frame1 = self.get_main_cutting_plane()[0] # offset pocket mill plane + frame1, og_frame = self.get_main_cutting_plane() # offset pocket mill plane frame2 = self.cross_beam.faces[face_keys[1]] + self.test.append(og_frame) + plane1, plane2 = Plane(frame1.point, -frame1.zaxis), Plane.from_frame(frame2) intersect_vec = Vector.from_start_end(*intersection_plane_plane(plane2, plane1)) angles_dict = {} for i, face in enumerate(self.main_beam.faces[0:4]): angles_dict[i] = face.normal.angle(intersect_vec) - ref_frame_id = min(angles_dict.keys(), key=angles_dict.get) - ref_frame = self.main_beam.faces[ref_frame_id] + self.main_face_index = min(angles_dict.keys(), key=angles_dict.get) + ref_frame = self.main_beam.faces[self.main_face_index] + + if angle_vectors(og_frame.zaxis, self.main_beam.centerline.direction, deg = True) < 1: + self.birdsmouth = False + return False ref_frame.point = self.main_beam.blank_frame.point - if ref_frame_id % 2 == 0: + if self.main_face_index % 2 == 0: ref_frame.point = ref_frame.point - ref_frame.yaxis * self.main_beam.height * 0.5 ref_frame.point = ref_frame.point + ref_frame.zaxis * self.main_beam.width * 0.5 else: ref_frame.point = ref_frame.point - ref_frame.yaxis * self.main_beam.width * 0.5 ref_frame.point = ref_frame.point + ref_frame.zaxis * self.main_beam.height * 0.5 - self.test.append(ref_frame) + + + cross_ref_main = cross_vectors(og_frame.zaxis, self.main_beam.centerline.direction) + cross_centerlines = cross_vectors(self.main_beam.centerline.direction, self.cross_beam.centerline.direction) + self.test.append(Line(og_frame.point, og_frame.point + cross_ref_main * 100)) + angle = angle_vectors(cross_ref_main, og_frame.yaxis, deg=True) + angle2 = angle_vectors(cross_centerlines, self.main_beam.frame.zaxis, deg=True) + angle2 = round(angle2, 1) - 180 + threshold_angle = 3.0 + # if angle < 1.0 or angle > 179.0: + # self.birdsmouth = False + # return False + + if abs(angle2)%90 <= threshold_angle or abs((abs(angle2)-90)%90) <= threshold_angle: + self.birdsmouth = False + return False start_point = Point(*intersection_plane_plane_plane(plane1, plane2, Plane.from_frame(ref_frame))) - start_point.transform(Transformation.from_frame_to_frame(ref_frame, Frame.worldXY())) - StartX, StartY = start_point[0], start_point[1] + coord_point = start_point.transformed(Transformation.from_frame_to_frame(ref_frame, Frame.worldXY())) + StartX, StartY = coord_point[0], coord_point[1] + + self.bm_sub_volume = Brep.from_box(self.cross_beam.blank) + self.bm_sub_volume.translate(Vector.from_start_end(og_frame.point, frame1.point)) + s = Scale.from_factors([10.0, 10.0, 10.0], Frame(start_point, ref_frame.xaxis, ref_frame.yaxis)) + self.bm_sub_volume.transform(s) + dot_frame1 = plane1.normal.dot(ref_frame.yaxis) if dot_frame1 > 0: @@ -333,5 +300,331 @@ def calc_params_birdsmouth(self): "Inclination1": Inclination1, "Angle2": Angle2, "Inclination2": Inclination2, - "ReferencePlaneID": ref_frame_id, + "ReferencePlaneID": self.main_face_index, + } + + return True + + + def calc_params_drilling(self): + """ + Calculate the parameters for a drilling joint. + + Parameters: + ---------- + joint (object): The joint object. + main_part (object): The main part object. + cross_part (object): The cross part object. + + Returns: + ---------- + dict: A dictionary containing the calculated parameters for the drilling joint + + """ + + _cut_plane, cutting_frame = self.get_main_cutting_plane() + ref_plane = Plane.from_frame(cutting_frame) + + angles_dict = {} + for i, face in enumerate(self.cross_beam.faces[0:4]): + angles_dict[i] = face.normal.angle(cutting_frame.normal) + cross_face_index = min(angles_dict.keys(), key=angles_dict.get) + ref_frame = self.cross_beam.faces[cross_face_index] + + ref_frame.point = self.cross_beam.blank_frame.point + if cross_face_index % 2 == 0: + ref_frame.point = ref_frame.point - ref_frame.yaxis * self.cross_beam.height * 0.5 + ref_frame.point = ref_frame.point + ref_frame.zaxis * self.cross_beam.width * 0.5 + else: + ref_frame.point = ref_frame.point - ref_frame.yaxis * self.cross_beam.width * 0.5 + ref_frame.point = ref_frame.point + ref_frame.zaxis * self.cross_beam.height * 0.5 + + point_xyz = (intersection_line_plane(self.main_beam.centerline, ref_plane)) + start_point = Point(*point_xyz) + ref_point = start_point.transformed(Transformation.from_frame_to_frame(ref_frame, Frame.worldXY())) + StartX, StartY = ref_point[0], ref_point[1] + + param_point_on_line = self.main_beam.centerline.closest_point(start_point, True)[1] + if param_point_on_line > 0.5: + line_point = self.main_beam.centerline.end + else: + line_point = self.main_beam.centerline.start + projected_point = ref_plane.projected_point(line_point) + + center_line_vec = Vector.from_start_end(start_point, line_point) + projected_vec = Vector.from_start_end(start_point, projected_point) + Angle = 180 - math.degrees(ref_frame.xaxis.angle_signed(projected_vec, ref_frame.zaxis)) + inclination = projected_vec.angle(center_line_vec, True) + if inclination == 0: + Inclination = 90.0 + else: + Inclination = inclination + + self.btlx_drilling_params_cross = { + "ReferencePlaneID": cross_face_index, + "StartX": StartX, + "StartY": StartY, + "Angle": Angle, + "Inclination": float(Inclination), + "Diameter": self.drill_diameter, + "DepthLimited": "no", + "Depth": 0.0 + + } + + # Rhino geometry visualization + line = Line(start_point, line_point) + line.start.translate(-line.vector) + normal_centerline_angle = 180-math.degrees(ref_frame.zaxis.angle(self.main_beam.centerline.direction)) + length = abs(self.cross_beam.width/(math.cos(math.radians(normal_centerline_angle)))) + return line, self.drill_diameter, length*3 + + def calc_params_stepjoint(self): + """ + Calculate the parameters for a step joint based on a Double Cut BTLx process. + + Parameters: + ---------- + joint (object): The joint object. + main_part (object): The main part object. + cross_part (object): The cross part object. + StepDepth (float): The depth of the step joint. + + Returns: + ---------- + dict: A dictionary containing the calculated parameters for the step joint (double cut process) + + """ + #check if beams are coplanar + cross_product_centerlines = self.main_beam.centerline.direction.cross(self.cross_beam.centerline.direction).unitized() + dot_product_cp_crossbnormal = float(abs(cross_product_centerlines.dot(self.cross_beam.frame.normal))) + dot_product_centerline = float(abs(self.main_beam.centerline.direction.dot(self.cross_beam.centerline.direction))) + if 0.999 < dot_product_cp_crossbnormal or dot_product_cp_crossbnormal < 0.001: + self.mill_depth = 0.0 + else: + self.stepjoint = False + return False + + #######ACTIVATE THIS IF YOU DONT WANT STEPJOINT WHEN PERPENDICULAR + # if 0.999 < dot_product_centerline or dot_product_centerline < 0.001: + # self.stepjoint = False + # return False + # else: + # self.mill_depth = 0.0 + + + face_dict = self._beam_side_incidence(self.cross_beam, self.main_beam, ignore_ends=True) + face_keys = sorted([key for key in face_dict.keys()], key=face_dict.get) + + if self.main_beam.centerline.end.on_line(self.cross_beam.centerline): + centerline_vec = self.main_beam.centerline.direction + else: + centerline_vec = -self.main_beam.centerline.direction + + # finding the inclination of the strut based on the two centerlines + StrutInclination = math.degrees(self.cross_beam.centerline.direction.angle(centerline_vec)) + + inter_centerlines = intersection_line_line(self.cross_beam.centerline, self.main_beam.centerline) + inter_param = self.cross_beam.centerline.closest_point(Point(*inter_centerlines[0]), True)[1] + + angles_dict = {} + for i, face in enumerate(self.main_beam.faces[0:4]): + angles_dict[i] = face.normal.angle_signed(self.main_beam.faces[face_keys[0]].normal, centerline_vec) + faces_ordered = sorted(angles_dict.keys(), key=angles_dict.get) + if (inter_param > 0.5 and StrutInclination < 90) or (inter_param < 0.5 and StrutInclination > 90): + self.ref_face_id = faces_ordered[2] + else: + self.ref_face_id = faces_ordered[0] + + ref_face = self.main_beam.faces[self.ref_face_id] + + ref_face.point = self.main_beam.blank_frame.point + if self.ref_face_id % 2 == 0: + ref_face.point = ref_face.point - ref_face.yaxis * self.main_beam.height * 0.5 + ref_face.point = ref_face.point + ref_face.zaxis * self.main_beam.width * 0.5 + else: + ref_face.point = ref_face.point - ref_face.yaxis * self.main_beam.width * 0.5 + ref_face.point = ref_face.point + ref_face.zaxis * self.main_beam.height * 0.5 + + if StrutInclination < 90: + angle1 = (180 - StrutInclination)/2 + strut_inclination = StrutInclination + else: + angle1 = StrutInclination/2 + strut_inclination = 180 - StrutInclination + + buried_depth = math.sin(math.radians(90-strut_inclination))*self.main_beam.width/2 + blank_vert_depth = self.cross_beam.width/2 - buried_depth + blank_edge_depth = abs(blank_vert_depth)/math.sin(math.radians(strut_inclination)) + startx = blank_edge_depth/2 + starty = self.main_beam.width/4 + + outside_length = self.main_beam.width/math.tan(math.radians(strut_inclination)) + x_main_cutting_face = outside_length + blank_edge_depth + + vec_angle2 = Vector.from_start_end(Point(startx, self.cross_beam.width - starty), Point(x_main_cutting_face, 0)) + vec_xaxis = Vector.from_start_end(Point(startx, self.cross_beam.width - starty), Point(0, self.cross_beam.width - starty)) + angle2 = vec_xaxis.angle(vec_angle2, True) + + if self.ends[str(self.main_beam.key)] == "start": + StartX = startx + StartY = starty + Angle1 = 180-angle1 + Angle2 = 180-angle2 + else: + StartX = self.main_beam.blank_length - startx + StartY = self.main_beam.width - starty + Angle1 = angle2 + Angle2 = angle1 + + if StrutInclination == 90.0: + startx_90deg = self.main_beam.width/4 + starty_90deg = self.main_beam.width/2 + angle_90deg = math.degrees(math.atan(startx_90deg/starty_90deg)) + if self.ends[str(self.main_beam.key)] == "start": + StartX = startx_90deg + StartY = starty_90deg + Angle1 = 90+angle_90deg + Angle2 = 90-angle_90deg + else: + StartX = self.main_beam.blank_length - startx_90deg + StartY = starty_90deg + Angle1 = 90+angle_90deg + Angle2 = 90-angle_90deg + + Inclination1 = 90.0 + Inclination2 = 90.0 + self.btlx_params_stepjoint_main = { + "Orientation": self.ends[str(self.main_beam.key)], + "StartX": float(StartX), + "StartY": float(StartY), + "Angle1": float(Angle1), + "Inclination1": float(Inclination1), + "Angle2": Angle2, + "Inclination2": Inclination2, + "ReferencePlaneID": self.ref_face_id, + } + + #find params lap cross beam + angles_dict_cross = {} + for i, face in enumerate(self.cross_beam.faces[0:4]): + angles_dict_cross[i] = face.normal.dot(ref_face.normal) + self.cross_face_id = max(angles_dict_cross.keys(), key=angles_dict_cross.get) + cross_face = self.cross_beam.faces[self.cross_face_id] + + cross_face.point = self.cross_beam.blank_frame.point + if self.cross_face_id % 2 == 0: + cross_face.point = cross_face.point - cross_face.yaxis * self.cross_beam.height * 0.5 + cross_face.point = cross_face.point + cross_face.zaxis * self.cross_beam.width * 0.5 + else: + cross_face.point = cross_face.point - cross_face.yaxis * self.cross_beam.width * 0.5 + cross_face.point = cross_face.point + cross_face.zaxis * self.cross_beam.height * 0.5 + + main_xypoint = Point(StartX, StartY, 0) + worldxy_xypoint = main_xypoint.transformed(Transformation.from_frame_to_frame(Frame.worldXY(), ref_face)) + cross_xy_point = worldxy_xypoint.transformed(Transformation.from_frame_to_frame(cross_face, Frame.worldXY())) + + StartX_cross = cross_xy_point[0] + StartY_cross = cross_xy_point[1] + + if (inter_param > 0.5 and StrutInclination < 90) or (inter_param < 0.5 and StrutInclination > 90): + orientation = self.ends[str(self.cross_beam.key)] + if self.ends[str(self.cross_beam.key)] == "start": + self.cross_face_id = min(angles_dict_cross.keys(), key=angles_dict_cross.get) + cross_face = self.cross_beam.faces[self.cross_face_id] + StartY_cross = self.cross_beam.width - StartY_cross + if self.ends[str(self.main_beam.key)] == "start": + Angle_cross = 180 - Angle1 + LeadAngle = 180 - (Angle1 - Angle2) + else: + Angle_cross = Angle2 + LeadAngle = 180 - (Angle1 - Angle2) + else: + if self.ends[str(self.main_beam.key)] == "start": + Angle_cross = 180 - Angle1 + LeadAngle = 180 - (Angle1 - Angle2) + else: + Angle_cross = Angle2 + LeadAngle = 180 - (Angle1 - Angle2) + elif StrutInclination == 90.0: + orientation = self.ends[str(self.cross_beam.key)] + Angle_cross = angle_90deg + LeadAngle = 180-angle_90deg*2 + if self.ends[str(self.cross_beam.key)] == "end": + self.cross_face_id = min(angles_dict_cross.keys(), key=angles_dict_cross.get) + cross_face = self.cross_beam.faces[self.cross_face_id] + StartY_cross = self.cross_beam.width - StartY_cross + else: + if self.ends[str(self.cross_beam.key)] == "start": + if self.ends[str(self.main_beam.key)] == "start": + orientation = "end" + Angle_cross = 180 - Angle1 + LeadAngle = 180 - (Angle1 - Angle2) + else: + orientation = "end" + Angle_cross = Angle2 + LeadAngle = 180 - (Angle1 - Angle2) + else: + self.cross_face_id = min(angles_dict_cross.keys(), key=angles_dict_cross.get) + cross_face = self.cross_beam.faces[self.cross_face_id] + StartY_cross = self.cross_beam.width - StartY_cross + if self.ends[str(self.main_beam.key)] == "start": + orientation = "start" + Angle_cross = 180 - Angle1 + LeadAngle = 180 - (Angle1 - Angle2) + else: + orientation = "start" + Angle_cross = Angle2 + LeadAngle = (180 - Angle1) + Angle2 + + + main_most_towards = self.get_face_most_towards_beam(self.cross_beam, self.main_beam, ignore_ends=True)[1] + cross_most_ortho = self.get_face_most_ortho_to_beam(self.main_beam, self.cross_beam, ignore_ends=True)[1] + + main_most_ortho = self.get_face_most_ortho_to_beam(self.cross_beam, self.main_beam, ignore_ends=True)[1] + + intersection_pt = Point(*intersection_plane_plane_plane(Plane.from_frame(main_most_towards), Plane.from_frame(cross_most_ortho), Plane.from_frame(ref_face))) + intersection_pt2 = Point(*intersection_plane_plane_plane(Plane.from_frame(main_most_ortho), Plane.from_frame(cross_most_ortho), Plane.from_frame(ref_face))) + + self.btlx_params_stepjoint_cross = { + "orientation": orientation, + "start_x": StartX_cross, + "start_y": StartY_cross, + "angle": Angle_cross, + "depth": 60.0, + "lead_angle_parallel": "no", + "lead_angle": LeadAngle, + "ReferencePlaneID": self.cross_face_id, } + + + #brep for main beam sub volume + if (inter_param > 0.5 and StrutInclination < 90) or (inter_param < 0.5 and StrutInclination > 90): + self.sj_main_sub_volume0 = Brep.from_box(self.cross_beam.blank) + self.sj_main_sub_volume0.rotate(math.radians(180+Angle_cross+LeadAngle), ref_face.normal, intersection_pt2) + self.sj_main_sub_volume1 = Brep.from_box(self.cross_beam.blank) + self.sj_main_sub_volume1.rotate(math.radians(Angle_cross), ref_face.normal, intersection_pt) + elif StrutInclination == 90.0: + self.sj_main_sub_volume0 = Brep.from_box(self.cross_beam.blank) + self.sj_main_sub_volume0.rotate(math.radians(angle_90deg), ref_face.normal, intersection_pt2) + self.sj_main_sub_volume1 = Brep.from_box(self.cross_beam.blank) + self.sj_main_sub_volume1.rotate(math.radians(-angle_90deg), ref_face.normal, intersection_pt) + else: + self.sj_main_sub_volume0 = Brep.from_box(self.cross_beam.blank) + self.sj_main_sub_volume0.rotate(math.radians(Angle_cross), ref_face.normal, intersection_pt2) + self.sj_main_sub_volume1 = Brep.from_box(self.cross_beam.blank) + self.sj_main_sub_volume1.rotate(math.radians(180+Angle_cross+LeadAngle), ref_face.normal, intersection_pt) + + + #brep for cross beam sub volume + pts_ph = [worldxy_xypoint, intersection_pt, intersection_pt2] + vertices_ph_sj_cross = pts_ph + vertices_ph_sj_cross.extend([pt.translated(-ref_face.normal*60) for pt in pts_ph]) + if (inter_param > 0.5 and StrutInclination < 90) or (inter_param < 0.5 and StrutInclination > 90): + self.ph_sj_cross = Polyhedron(vertices_ph_sj_cross, [[0, 1, 2], [3, 5, 4], [0, 3, 4, 1], [1, 4, 5, 2], [0, 2, 5, 3]]) + else: + self.ph_sj_cross = Polyhedron(vertices_ph_sj_cross, [[0, 2, 1], [3, 4, 5], [0, 1, 4, 3], [1, 2, 5, 4], [0, 3, 5, 2]]) + self.brep_sj_cross = Brep.from_mesh(self.ph_sj_cross) + + + return True diff --git a/src/compas_timber/connections/french_ridge_lap.py b/src/compas_timber/connections/french_ridge_lap.py index d4d332c703..b359e035e7 100644 --- a/src/compas_timber/connections/french_ridge_lap.py +++ b/src/compas_timber/connections/french_ridge_lap.py @@ -38,10 +38,11 @@ class FrenchRidgeLapJoint(Joint): SUPPORTED_TOPOLOGY = JointTopology.TOPO_L - def __init__(self, beam_a=None, beam_b=None, **kwargs): + def __init__(self, beam_a=None, beam_b=None, drill_diameter=0.0, **kwargs): super(FrenchRidgeLapJoint, self).__init__(beams=(beam_a, beam_b), **kwargs) self.beam_a = beam_a self.beam_b = beam_b + self.drill_diameter = float(drill_diameter) self.beam_a_key = beam_a.key if beam_a else None self.beam_b_key = beam_b.key if beam_b else None self.reference_face_indices = {} @@ -71,7 +72,7 @@ def cutting_plane_top(self): @property def cutting_plane_bottom(self): - _, cfr = self.get_face_most_towards_beam(self.beam_b, self.beam_b, ignore_ends=True) + _, cfr = self.get_face_most_towards_beam(self.beam_b, self.beam_a, ignore_ends=True) return cfr def restore_beams_from_keys(self, assemly): @@ -80,15 +81,22 @@ def restore_beams_from_keys(self, assemly): self.beam_b = assemly.find_by_key(self.beam_b_key) self._beams = (self.beam_a, self.beam_b) + def add_extensions(self): + self.beam_a.add_blank_extension(*self.beam_a.extension_to_plane(self.cutting_plane_top), joint_key=self.key) + self.beam_b.add_blank_extension(*self.beam_b.extension_to_plane(self.cutting_plane_bottom), joint_key=self.key) + + def add_features(self): + self.features = [] + def check_geometry(self): """ This method checks whether the parts are aligned as necessary to create French Ridge Lap and determines which face is used as reference face for machining. """ if not (self.beam_a and self.beam_b): - raise (BeamJoinningError("French Ridge Lap requires 2 beams")) + raise (BeamJoinningError(beams=self.beams, joint=self, debug_info="beams not set")) if not (self.beam_a.width == self.beam_b.width and self.beam_a.height == self.beam_b.height): - raise (BeamJoinningError("widths and heights for both beams must match for the French Ridge Lap")) + raise (BeamJoinningError(beams=self.beams, joint=self, debug_info="beams are not of same size")) normal = cross_vectors(self.beam_a.frame.xaxis, self.beam_b.frame.xaxis) @@ -103,7 +111,13 @@ def check_geometry(self): elif angle_vectors(normal, -self.beam_a.frame.zaxis) < 0.001: indices.append(2) else: - raise (BeamJoinningError("part not aligned with corner normal, no French Ridge Lap possible")) + raise ( + BeamJoinningError( + beams=self.beams, + joint=self, + debug_info="part not aligned with corner normal, no French Ridge Lap possible", + ) + ) if abs(angle_vectors(normal, self.beam_b.frame.yaxis) - math.pi) < 0.001: indices.append(3) @@ -114,5 +128,11 @@ def check_geometry(self): elif abs(angle_vectors(normal, -self.beam_b.frame.zaxis) - math.pi) < 0.001: indices.append(2) else: - raise (BeamJoinningError("part not aligned with corner normal, no French Ridge Lap possible")) + raise ( + BeamJoinningError( + beams=self.beams, + joint=self, + debug_info="part not aligned with corner normal, no French Ridge Lap possible", + ) + ) self.reference_face_indices = {str(self.beam_a.key): indices[0], str(self.beam_b.key): indices[1]} diff --git a/src/compas_timber/connections/joint.py b/src/compas_timber/connections/joint.py index 0a9a246f12..52d411d539 100644 --- a/src/compas_timber/connections/joint.py +++ b/src/compas_timber/connections/joint.py @@ -76,6 +76,17 @@ def __data__(self): def beams(self): return self._beams + def add_extensions(self): + """Adds the features defined by this joint to affected beam(s). + + Raises + ------ + :class:`~compas_timber.connections.BeamJoinningError` + Should be raised whenever the joint was not able to calculate the features to be applied to the beams. + + """ + raise NotImplementedError + def add_features(self): """Adds the features defined by this joint to affected beam(s). @@ -134,7 +145,7 @@ def create(cls, assembly, *beams, **kwargs): raise ValueError("Expected at least 2 beams. Got instead: {}".format(len(beams))) joint = cls(*beams, **kwargs) assembly.add_joint(joint, beams) - joint.add_features() + joint.add_extensions() return joint @property @@ -238,7 +249,6 @@ def _beam_side_incidence(beam_a, beam_b, ignore_ends=True): raise AssertionError("No intersection found") end, _ = beam_a.endpoint_closest_to_point(Point(*p1x)) - if end == "start": centerline_vec = beam_a.centerline.vector else: diff --git a/src/compas_timber/connections/l_butt.py b/src/compas_timber/connections/l_butt.py index acc961a6ce..35b515322d 100644 --- a/src/compas_timber/connections/l_butt.py +++ b/src/compas_timber/connections/l_butt.py @@ -1,5 +1,6 @@ from compas_timber.parts import CutFeature from compas_timber.parts import MillVolume +from compas_timber.parts import BrepSubtraction from .joint import BeamJoinningError from .solver import JointTopology @@ -49,6 +50,7 @@ def __init__( main_beam=None, cross_beam=None, mill_depth=0, + birdsmouth=False, small_beam_butts=False, modify_cross=True, reject_i=False, @@ -58,7 +60,7 @@ def __init__( if main_beam.width * main_beam.height > cross_beam.width * cross_beam.height: main_beam, cross_beam = cross_beam, main_beam - super(LButtJoint, self).__init__(main_beam, cross_beam, mill_depth, **kwargs) + super(LButtJoint, self).__init__(main_beam, cross_beam, mill_depth, birdsmouth, **kwargs) self.modify_cross = modify_cross self.small_beam_butts = small_beam_butts self.reject_i = reject_i @@ -88,6 +90,25 @@ def get_main_cutting_plane(self): ) return super(LButtJoint, self).get_main_cutting_plane() + def add_extensions(self): + """Adds the required extensions to both beams. + + This method is automatically called when joint is created by the call to `Joint.create()`. + + """ + assert self.main_beam and self.cross_beam + extension_tolerance = 0.01 # TODO: this should be proportional to the unit used + if self.birdsmouth: + extension_plane_main = self.get_face_most_towards_beam(self.main_beam, self.cross_beam, ignore_ends=True)[1] + else: + extension_plane_main = self.get_face_most_ortho_to_beam(self.main_beam, self.cross_beam, ignore_ends=True)[1] + start_main, end_main = self.main_beam.extension_to_plane(extension_plane_main) + self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.key) + + extension_plane_cross = self.get_face_most_towards_beam(self.cross_beam, self.main_beam, ignore_ends=True)[1] + start_cross, end_cross = self.cross_beam.extension_to_plane(extension_plane_cross) + self.cross_beam.add_blank_extension(start_cross + extension_tolerance, end_cross + extension_tolerance, self.key) + def add_features(self): """Adds the required extension and trimming features to both beams. @@ -102,8 +123,7 @@ def add_features(self): try: main_cutting_plane = self.get_main_cutting_plane()[0] cross_cutting_plane = self.get_cross_cutting_plane() - start_main, end_main = self.main_beam.extension_to_plane(main_cutting_plane) - start_cross, end_cross = self.cross_beam.extension_to_plane(cross_cutting_plane) + except BeamJoinningError as be: raise be except AttributeError as ae: @@ -113,20 +133,26 @@ def add_features(self): except Exception as ex: raise BeamJoinningError(beams=self.beams, joint=self, debug_info=str(ex)) - extension_tolerance = 0.01 # TODO: this should be proportional to the unit used - if self.modify_cross: - self.cross_beam.add_blank_extension( - start_cross + extension_tolerance, end_cross + extension_tolerance, self.key - ) + f_cross = CutFeature(cross_cutting_plane) self.cross_beam.add_features(f_cross) self.features.append(f_cross) - self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.key) - - f_main = CutFeature(main_cutting_plane) if self.mill_depth: self.cross_beam.add_features(MillVolume(self.subtraction_volume())) - self.main_beam.add_features(f_main) - self.features.append(f_main) + self.features.append(MillVolume(self.subtraction_volume())) + + do_jack = False + if self.birdsmouth: + if self.calc_params_birdsmouth(): + self.main_beam.add_features(BrepSubtraction(self.bm_sub_volume)) + self.features.append(BrepSubtraction(self.bm_sub_volume)) + + else: + do_jack = True + if do_jack: + f_main = CutFeature(main_cutting_plane) + self.main_beam.add_features(f_main) + self.features.append(f_main) + diff --git a/src/compas_timber/connections/l_halflap.py b/src/compas_timber/connections/l_halflap.py index 86d3d5734e..bcb2f2fa71 100644 --- a/src/compas_timber/connections/l_halflap.py +++ b/src/compas_timber/connections/l_halflap.py @@ -1,6 +1,14 @@ +from math import e from compas.geometry import Frame +from compas.geometry import Line +from compas.geometry import Point +from compas.geometry import Vector +from compas.geometry import intersection_line_line +from compas.geometry import angle_vectors +from compas.geometry import cross_vectors from compas_timber.parts import CutFeature from compas_timber.parts import MillVolume +from compas_timber.parts import DrillFeature from .joint import BeamJoinningError from .solver import JointTopology @@ -47,27 +55,91 @@ class LHalfLapJoint(LapJoint): SUPPORTED_TOPOLOGY = JointTopology.TOPO_L - def __init__(self, main_beam=None, cross_beam=None, flip_lap_side=False, cut_plane_bias=0.5, **kwargs): - super(LHalfLapJoint, self).__init__(main_beam, cross_beam, flip_lap_side, cut_plane_bias, **kwargs) + def __init__( + self, main_beam=None, cross_beam=None, flip_lap_side=False, cut_plane_bias=0.5, drill_diameter=0.0, **kwargs + ): + super(LHalfLapJoint, self).__init__(**kwargs) + + self.main_beam = main_beam + self.cross_beam = cross_beam + self.main_beam_key = main_beam.key if main_beam else None + self.cross_beam_key = cross_beam.key if cross_beam else None + self.flip_lap_side = flip_lap_side + self.cut_plane_bias = cut_plane_bias + self.drill_diameter = float(drill_diameter) + self.btlx_params_main = {} + self.btlx_params_cross = {} + self.btlx_drilling_params_main = {} + self.features = [] + self.test = [] + self.top_plane, self.bottom_plane = self.get_world_top_bottom_faces(self.cross_beam) + + @property + def __data__(self): + data_dict = { + "main_beam_key": self.main_beam_key, + "cross_beam_key": self.cross_beam_key, + } + data_dict.update(super(LHalfLapJoint, self).__data__) + return data_dict + + @classmethod + def __from_data__(cls, value): + instance = cls(**value) + instance.main_beam_key = value["main_beam_key"] + instance.cross_beam_key = value["cross_beam_key"] + return instance + + @property + def beams(self): + return [self.main_beam, self.cross_beam] + + def restore_beams_from_keys(self, assemly): + """After de-serialization, resotres references to the main and cross beams saved in the assembly.""" + self.main_beam = assemly.find_by_key(self.main_beam_key) + self.cross_beam = assemly.find_by_key(self.cross_beam_key) + + def add_extensions(self): + """Adds the extensions to the main beam and cross beam. + + This method is automatically called when joint is created by the call to `Joint.create()`. + + """ + + assert self.main_beam and self.cross_beam + extension_tolerance = 0.01 # TODO: this should be proportional to the unit used + + extension_plane_main = self.get_face_most_towards_beam(self.main_beam, self.cross_beam, ignore_ends=True)[1] + extension_plane_main = self.get_face_most_towards_beam(self.main_beam, self.cross_beam, ignore_ends=True)[1] + start_main, end_main = self.main_beam.extension_to_plane(extension_plane_main) + self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.key) + + extension_plane_cross = self.get_face_most_towards_beam(self.cross_beam, self.main_beam, ignore_ends=True)[1] + start_cross, end_cross = self.cross_beam.extension_to_plane(extension_plane_cross) + self.cross_beam.add_blank_extension( + start_cross + extension_tolerance, end_cross + extension_tolerance, self.key + ) + def add_features(self): assert self.main_beam and self.cross_beam try: - main_cutting_frame = self.get_main_cutting_frame() - cross_cutting_frame = self.get_cross_cutting_frame() + if self.main_beam.length < self.cross_beam.length: + main_cutting_frame = self.main_beam.faces[self.top_plane] + cross_cutting_frame = self.cross_beam.faces[self.bottom_plane] + + else: + main_cutting_frame = self.main_beam.faces[self.bottom_plane] + cross_cutting_frame = self.cross_beam.faces[self.top_plane] + negative_brep_main_beam, negative_brep_cross_beam = self._create_negative_volumes() except Exception as ex: raise BeamJoinningError(beams=self.beams, joint=self, debug_info=str(ex)) - start_main, end_main = self.main_beam.extension_to_plane(main_cutting_frame) - start_cross, end_cross = self.cross_beam.extension_to_plane(cross_cutting_frame) - - extension_tolerance = 0.01 # TODO: this should be proportional to the unit used - self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.key) - self.cross_beam.add_blank_extension( - start_cross + extension_tolerance, end_cross + extension_tolerance, self.key - ) + # call functions to calculate the parameters + self.calc_params_cross() + self.calc_params_main() main_volume = MillVolume(negative_brep_main_beam) cross_volume = MillVolume(negative_brep_cross_beam) @@ -82,4 +154,94 @@ def add_features(self): f_main = CutFeature(trim_frame) self.main_beam.add_features(f_main) + if self.drill_diameter > 0: + self.cross_beam.add_features(DrillFeature(*self.calc_params_drilling_main())) + self.features.append(DrillFeature(*self.calc_params_drilling_main())) + self.features = [main_volume, cross_volume, f_main, f_cross] + + def get_world_top_bottom_faces(self, beam): + faces = beam.faces + face_normals = [face.zaxis for face in faces] + angles = [angle_vectors(face_normal, [0, 0, 1]) for face_normal in face_normals] + + top_face_index = angles.index(min(angles)) + bottom_face_index = angles.index(max(angles)) + return top_face_index, bottom_face_index + + def calc_params_main(self): + if self.ends[str(self.main_beam.key)] == "start": + start_x = 0.0 + else: + start_x = self.main_beam.blank_length + self.btlx_params_main["ReferencePlaneID"] = str(self.bottom_plane) + self.btlx_params_main["orientation"] = self.ends[str(self.main_beam.key)] + self.btlx_params_main["start_x"] = start_x + self.btlx_params_main["start_y"] = 0.0 + self.btlx_params_main["length"] = 60.0 + self.btlx_params_main["width"] = 30.0 + self.btlx_params_main["depth"] = 60.0 + + self.btlx_params_main["machining_limits"] = { + "FaceLimitedFront": "no", + "FaceLimitedBack": "no", + } + + def calc_params_cross(self): + if self.ends[str(self.cross_beam.key)] == "start": + start_x = 0.0 + else: + start_x = self.cross_beam.blank_length + self.btlx_params_cross["ReferencePlaneID"] = str(self.top_plane) + self.btlx_params_cross["orientation"] = self.ends[str(self.cross_beam.key)] + self.btlx_params_cross["start_x"] = start_x + self.btlx_params_cross["start_y"] = 0.0 + self.btlx_params_cross["length"] = 60.0 + self.btlx_params_cross["width"] = 30.0 + self.btlx_params_cross["depth"] = 60.0 + self.btlx_params_cross["machining_limits"] = { + "FaceLimitedFront": "no", + "FaceLimitedBack": "no", + } + + def calc_params_drilling_main(self): + """ + Calculate the parameters for a drilling joint. + + Parameters: + ---------- + joint (object): The joint object. + main_part (object): The main part object. + + Returns: + ---------- + dict: A dictionary containing the calculated parameters for the drilling joint + + """ + if self.ends[str(self.main_beam.key)] == "start": + start_x = 30.0 + else: + start_x = self.main_beam.blank_length - 30.0 + + self.btlx_drilling_params_main = { + "ReferencePlaneID": str(self.bottom_plane+1), + "StartX": start_x, + "StartY": 30.0, + "Angle": 0.0, + "Inclination": 90.0, + "Diameter": self.drill_diameter, + "DepthLimited": "no", + "Depth": 0.0, + } + + # Rhino geometry visualization + point_xyz = intersection_line_line(self.cross_beam.centerline, self.main_beam.centerline)[1] + cross_product = cross_vectors(self.cross_beam.centerline.direction, self.main_beam.centerline.direction) + cross_vect = Vector(*cross_product)*(self.main_beam.width) + + mid_point = Point(*point_xyz) + start_point = mid_point.translated(cross_vect) + end_point = mid_point.translated(-cross_vect) + + line = Line(start_point, end_point) + return line, self.drill_diameter, line.length diff --git a/src/compas_timber/connections/l_miter.py b/src/compas_timber/connections/l_miter.py index c931777c8d..39173323cb 100644 --- a/src/compas_timber/connections/l_miter.py +++ b/src/compas_timber/connections/l_miter.py @@ -100,6 +100,26 @@ def get_cutting_planes(self): plnB = Frame.from_plane(plnB) return plnA, plnB + def add_extensions(self): + """Adds the extensions to the main beam and cross beam. + + This method is automatically called when joint is created by the call to `Joint.create()`. + + """ + + assert self.main_beam and self.cross_beam + extension_tolerance = 0.01 # TODO: this should be proportional to the unit used + + extension_plane_main = self.get_face_most_ortho_to_beam(self.main_beam, self.cross_beam, ignore_ends=True)[1] + start_main, end_main = self.main_beam.extension_to_plane(extension_plane_main) + self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.key) + + extension_plane_cross = self.get_face_most_ortho_to_beam(self.cross_beam, self.main_beam, ignore_ends=True)[1] + start_cross, end_cross = self.cross_beam.extension_to_plane(extension_plane_cross) + self.cross_beam.add_blank_extension( + start_cross + extension_tolerance, end_cross + extension_tolerance, self.key + ) + def add_features(self): """Adds the required extension and trimming features to both beams. @@ -115,8 +135,6 @@ def add_features(self): start_a, start_b = None, None try: plane_a, plane_b = self.get_cutting_planes() - start_a, end_a = self.beam_a.extension_to_plane(plane_a) - start_b, end_b = self.beam_b.extension_to_plane(plane_b) except AttributeError as ae: # I want here just the plane that caused the error geometries = [plane_b] if start_a is not None else [plane_a] @@ -124,9 +142,6 @@ def add_features(self): except Exception as ex: raise BeamJoinningError(self.beams, self, debug_info=str(ex)) - self.beam_a.add_blank_extension(start_a, end_a, self.key) - self.beam_b.add_blank_extension(start_b, end_b, self.key) - f1, f2 = CutFeature(plane_a), CutFeature(plane_b) self.beam_a.add_features(f1) self.beam_b.add_features(f2) diff --git a/src/compas_timber/connections/solver.py b/src/compas_timber/connections/solver.py index bb15130fcc..45057e2720 100644 --- a/src/compas_timber/connections/solver.py +++ b/src/compas_timber/connections/solver.py @@ -10,6 +10,7 @@ from compas.geometry import dot_vectors from compas.geometry import scale_vector from compas.geometry import subtract_vectors +from compas.geometry import intersection_line_line from compas.plugins import pluggable @@ -89,7 +90,7 @@ class ConnectionSolver(object): @classmethod def find_intersecting_pairs(cls, beams, rtree=False, max_distance=None): - """Finds pairs of intersecting beams in the given list of beams. + """Finds pairs of intersecting beams in the given list of beams and stores the intersection parameters for each specific beam. Parameters ---------- @@ -107,7 +108,25 @@ def find_intersecting_pairs(cls, beams, rtree=False, max_distance=None): List containing sets or neightboring pairs beams. """ - return find_neighboring_beams(beams, inflate_by=max_distance) if rtree else itertools.combinations(beams, 2) + + neighboring_pairs = find_neighboring_beams(beams, inflate_by=max_distance) if rtree else itertools.combinations(beams, 2) + return neighboring_pairs + + @classmethod + def find_intersection_parameters(cls, beams, tol=None): + generic_pair_indeces = itertools.combinations(range(len(beams)), 2) + for pair in generic_pair_indeces: + pair_centerlines = [beams[p].centerline for p in pair] + intersection_points = intersection_line_line(*pair_centerlines, tol=10.0) + if None in intersection_points: + continue + if all(abs(p1 - p2) < cls.TOLERANCE for p1, p2 in zip(intersection_points[0], intersection_points[1])): + intersection_point = Point(*intersection_points[0]) + intersection_params = [cls._parameter_on_line(intersection_point, beams[p].centerline) for p in pair] + for i, p in enumerate(pair): + # Check if the parameter is within [0, 1], if not, adjust it + intersection_params[i]= max(0, min(1,(intersection_params[i]))) #//TODO: this is just a temporal solution + beams[p].intersections.append(intersection_params[i]) def find_topology(self, beam_a, beam_b, tol=TOLERANCE, max_distance=None): """If `beam_a` and `beam_b` intersect within the given `max_distance`, return the topology type of the intersection. @@ -218,6 +237,14 @@ def find_topology(self, beam_a, beam_b, tol=TOLERANCE, max_distance=None): # X-joint (both meeting somewhere along the line) return JointTopology.TOPO_X, beam_a, beam_b + @staticmethod + def _parameter_on_line(point, line): + # Vector from the start of the line segment to the given point + point_vector = [(point.x - line.start.x), (point.y - line.start.y), (point.z - line.start.z)] + # Calculate the parameter (t) using dot product + t = dot_vectors(point_vector, line.vector) / dot_vectors(line.vector, line.vector) #denomenator is the length^2 of the vector + return t + @staticmethod def _calc_t(line, plane): a, b = line diff --git a/src/compas_timber/connections/t_butt.py b/src/compas_timber/connections/t_butt.py index 9b72305015..e472f9c7a7 100644 --- a/src/compas_timber/connections/t_butt.py +++ b/src/compas_timber/connections/t_butt.py @@ -1,7 +1,9 @@ from compas_timber.connections.butt_joint import ButtJoint from compas_timber.parts import CutFeature +from compas_timber.parts import BrepSubtraction from compas_timber.parts import MillVolume +from compas_timber.parts import DrillFeature from .joint import BeamJoinningError from .solver import JointTopology @@ -33,8 +35,39 @@ class TButtJoint(ButtJoint): SUPPORTED_TOPOLOGY = JointTopology.TOPO_T - def __init__(self, main_beam=None, cross_beam=None, mill_depth=0, birdsmouth=False, **kwargs): - super(TButtJoint, self).__init__(main_beam, cross_beam, mill_depth, birdsmouth, **kwargs) + def __init__(self, main_beam=None, cross_beam=None, mill_depth=0, drill_diameter=0, birdsmouth=False, stepjoint=False, **kwargs): + super(TButtJoint, self).__init__(main_beam, cross_beam, mill_depth, drill_diameter, birdsmouth, stepjoint, **kwargs) + + + def add_extensions(self): + """Adds the extensions to the main beam and cross beam. + + This method is automatically called when joint is created by the call to `Joint.create()`. + + """ + assert self.main_beam and self.cross_beam + extension_tolerance = 0.01 # TODO: this should be proportional to the unit used + if self.birdsmouth: + extension_plane_main = self.get_face_most_towards_beam(self.main_beam, self.cross_beam, ignore_ends=True)[1] + else: + extension_plane_main = self.get_face_most_ortho_to_beam(self.main_beam, self.cross_beam, ignore_ends=True)[1] + start_main, end_main = self.main_beam.extension_to_plane(extension_plane_main) + self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.key) + + def add_extensions(self): + """Adds the extensions to the main beam and cross beam. + + This method is automatically called when joint is created by the call to `Joint.create()`. + + """ + assert self.main_beam and self.cross_beam + extension_tolerance = 0.01 # TODO: this should be proportional to the unit used + if self.birdsmouth: + extension_plane_main = self.get_face_most_towards_beam(self.main_beam, self.cross_beam, ignore_ends=True)[1] + else: + extension_plane_main = self.get_face_most_ortho_to_beam(self.main_beam, self.cross_beam, ignore_ends=True)[1] + start_main, end_main = self.main_beam.extension_to_plane(extension_plane_main) + self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.key) def add_features(self): """Adds the trimming plane to the main beam (no features for the cross beam). @@ -43,23 +76,37 @@ def add_features(self): """ assert self.main_beam and self.cross_beam # should never happen - if self.features: self.main_beam.remove_features(self.features) cutting_plane = None try: cutting_plane = self.get_main_cutting_plane()[0] - start_main, end_main = self.main_beam.extension_to_plane(cutting_plane) except AttributeError as ae: raise BeamJoinningError(beams=self.beams, joint=self, debug_info=str(ae), debug_geometries=[cutting_plane]) except Exception as ex: raise BeamJoinningError(beams=self.beams, joint=self, debug_info=str(ex)) - extension_tolerance = 0.01 # TODO: this should be proportional to the unit used - self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.key) - - trim_feature = CutFeature(cutting_plane) - if self.mill_depth: + if self.stepjoint: + if self.calc_params_stepjoint(): + self.main_beam.add_features(BrepSubtraction(self.sj_main_sub_volume0)) + self.features.append(BrepSubtraction(self.sj_main_sub_volume0)) + self.main_beam.add_features(BrepSubtraction(self.sj_main_sub_volume1)) + self.features.append(BrepSubtraction(self.sj_main_sub_volume1)) + self.cross_beam.add_features(BrepSubtraction(self.brep_sj_cross)) + self.features.append(BrepSubtraction(self.brep_sj_cross)) + if self.mill_depth > 0: self.cross_beam.add_features(MillVolume(self.subtraction_volume())) - self.main_beam.add_features(trim_feature) - self.features = [trim_feature] + self.features.append(MillVolume(self.subtraction_volume())) + do_jack = False + if self.birdsmouth: + if self.calc_params_birdsmouth(): + self.main_beam.add_features(BrepSubtraction(self.bm_sub_volume)) + self.features.append(BrepSubtraction(self.bm_sub_volume)) + else: + do_jack = True + if do_jack: + self.main_beam.add_features(CutFeature(cutting_plane)) + self.features.append(cutting_plane) + if self.drill_diameter > 0: + self.cross_beam.add_features(DrillFeature(*self.calc_params_drilling())) + self.features.append(DrillFeature(*self.calc_params_drilling())) diff --git a/src/compas_timber/connections/t_halflap.py b/src/compas_timber/connections/t_halflap.py index 65cf24e278..9d21426a3b 100644 --- a/src/compas_timber/connections/t_halflap.py +++ b/src/compas_timber/connections/t_halflap.py @@ -34,6 +34,20 @@ class THalfLapJoint(LapJoint): def __init__(self, main_beam=None, cross_beam=None, flip_lap_side=False, cut_plane_bias=0.5, **kwargs): super(THalfLapJoint, self).__init__(main_beam, cross_beam, flip_lap_side, cut_plane_bias, **kwargs) + def add_extensions(self): + """Adds the extensions to the main beam and cross beam. + + This method is automatically called when joint is created by the call to `Joint.create()`. + + """ + + assert self.main_beam and self.cross_beam + extension_tolerance = 0.01 # TODO: this should be proportional to the unit used + + extension_plane_main = self.get_face_most_ortho_to_beam(self.main_beam, self.cross_beam, ignore_ends=True)[1] + start_main, end_main = self.main_beam.extension_to_plane(extension_plane_main) + self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.key) + def add_features(self): assert self.main_beam and self.cross_beam # should never happen @@ -41,7 +55,6 @@ def add_features(self): try: main_cutting_frame = self.get_main_cutting_frame() negative_brep_main_beam, negative_brep_cross_beam = self._create_negative_volumes() - start_main, end_main = self.main_beam.extension_to_plane(main_cutting_frame) except AttributeError as ae: raise BeamJoinningError( beams=self.beams, joint=self, debug_info=str(ae), debug_geometries=[main_cutting_frame] @@ -49,9 +62,6 @@ def add_features(self): except Exception as ex: raise BeamJoinningError(beams=self.beams, joint=self, debug_info=str(ex)) - extension_tolerance = 0.01 # TODO: this should be proportional to the unit used - self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.key) - main_volume = MillVolume(negative_brep_main_beam) cross_volume = MillVolume(negative_brep_cross_beam) self.main_beam.add_features(main_volume) diff --git a/src/compas_timber/connections/t_stirnversatz.py b/src/compas_timber/connections/t_stirnversatz.py new file mode 100644 index 0000000000..30ef490aad --- /dev/null +++ b/src/compas_timber/connections/t_stirnversatz.py @@ -0,0 +1,189 @@ +from .joint import Joint +from .solver import JointTopology +from .joint import BeamJoinningError +from compas_timber.parts import CutFeature +from compas_timber.parts import MillVolume +from compas.geometry import Plane, Polyhedron, Vector, Frame, Point +from compas.geometry import Rotation +from compas.geometry import intersection_plane_plane +from compas.geometry import intersection_plane_plane_plane +from compas.geometry import intersection_line_plane +from compas.geometry import angle_vectors +from compas.geometry import distance_point_point +from compas.geometry import midpoint_line +from compas.geometry import project_point_plane +from compas.geometry import translate_points +from compas.geometry import cross_vectors +import math + + +class TStirnversatzJoint(Joint): + + SUPPORTED_TOPOLOGY = JointTopology.TOPO_T + + def __init__( + self, cross_beam=None, main_beam=None, cut_depth=0.25, extend_cut=True + ): # TODO Why main & cross swapped??? + super(TStirnversatzJoint, self).__init__(main_beam, cross_beam, cut_depth) + self.main_beam = main_beam + self.cross_beam = cross_beam + self.main_beam_key = None + self.cross_beam_key = None + self.cut_depth = cut_depth + self.extend_cut = extend_cut + self.features = [] + self.cross_cutting_plane_1 = None + self.cross_cutting_plane_2 = None + self.planetogh = [] # TODO Remove + self.linetogh = [] # TODO Remove + self.pointtogh = [] # TODO Remove + self.polyhedrontogh = [] # TODO Remove + + @property + def data(self): + data_dict = { + "cross_beam": self.cross_beam_key, + "main_beam": self.main_beam_key, + } + data_dict.update(Joint.data.fget(self)) + return data_dict + + # @data.setter + # def data(self, value): + # Joint.data.fset(self, value) + # self.cross_beam_key = value["cross_beam"] + # self.main_beam_key = value["main_beam"] + + @property + def joint_type(self): + return "Stirnversatz" + + @property + def beams(self): + return [self.main_beam, self.cross_beam] + + @staticmethod + def _bisector_plane(plane1, plane2, angle_factor): + bisector = plane1.normal + plane2.normal * angle_factor + intersection = intersection_plane_plane(plane1, plane2) + rotation_axis = Vector.from_start_end(*intersection) + origin = intersection[0] + R = Rotation.from_axis_and_angle(rotation_axis, math.radians(90)) + bisector.transform(R) + plane = Plane(origin, bisector) + return plane + + # find the Face on cross_beam where main_beam intersects + # TODO simplify with Chen! + def get_main_intersection_frame(self): + diagonal = math.sqrt(self.main_beam.width**2 + self.main_beam.height**2) + main_frames = self.main_beam.faces[:4] + cross_centerline = self.cross_beam.centerline + cross_centerpoint = midpoint_line(self.cross_beam.centerline) + projectionplane = self.main_beam.faces[5] + frames, distances = [], [] + for i in main_frames: + int_centerline_frame = intersection_line_plane(cross_centerline, Plane.from_frame(i)) + if int_centerline_frame == None: + pass + else: + projected_int = project_point_plane(int_centerline_frame, Plane.from_frame(projectionplane)) + distance = distance_point_point(projected_int, projectionplane.point) + if distance > diagonal / 2: + pass + else: + distance = distance_point_point(cross_centerpoint, int_centerline_frame) + distances.append(distance) + frames.append(i) + distances, frames = zip(*sorted(zip(distances, frames))) + return frames[0] + + @staticmethod + def _sort_frames_according_normals(frames, checkvector): + angles = [] + for i in frames: + angles.append(angle_vectors(checkvector, i.normal)) + angles, frames = zip(*sorted(zip(angles, frames))) + return frames + + @staticmethod + def _flip_plane_according_vector(plane, vector): + if angle_vectors(plane.normal, vector, True) > 90: + plane = Plane(plane.point, plane.normal * -1) + return plane + + def get_cross_cutting_planes(self): + main_int_frame = self.get_main_intersection_frame() + main_int_plane = Plane.from_frame(main_int_frame) + cross_faces = self.cross_beam.faces[:4] + cross_faces_sorted = self._sort_frames_according_normals(cross_faces, main_int_frame.zaxis) + cross_face = Plane.from_frame(cross_faces_sorted[0]) + cutplane_1 = self._bisector_plane(main_int_plane, cross_face, 0.5) + cut_depth_point = project_point_plane(self.main_beam.frame.point, main_int_plane) + cut_depth = distance_point_point(self.main_beam.frame.point, cut_depth_point) * self.cut_depth * 2 + split_plane = Plane(main_int_frame.point, main_int_frame.yaxis) + p1 = intersection_plane_plane_plane(main_int_plane, Plane.from_frame(cross_faces_sorted[3]), split_plane) + origin = translate_points([p1], main_int_frame.zaxis * -cut_depth)[0] + cut_depth_plane = Plane(origin, main_int_frame.zaxis) + p2 = intersection_plane_plane_plane(cut_depth_plane, cutplane_1, split_plane) + cutplane_2 = Plane.from_frame(Frame(p1, Vector.from_start_end(p1, p2), split_plane.normal)) + cutplane_2 = self._flip_plane_according_vector(cutplane_2, main_int_frame.zaxis * -1) + + self.cross_cutting_plane_1 = cutplane_1 + self.cross_cutting_plane_2 = cutplane_2 + return self.cross_cutting_plane_1, self.cross_cutting_plane_2 + + def get_main_cutting_volume(self): + main_int_frame = self.get_main_intersection_frame() + main_int_plane = Plane.from_frame(main_int_frame) + l1 = intersection_plane_plane(main_int_plane, self.cross_cutting_plane_1) + l2 = intersection_plane_plane(main_int_plane, self.cross_cutting_plane_2) + l3 = intersection_plane_plane(self.cross_cutting_plane_1, self.cross_cutting_plane_2) + main_frames_sorted = self._sort_frames_according_normals(self.main_beam.faces[:4], main_int_frame.zaxis) + cut_frames_sorted = self._sort_frames_according_normals(self.cross_beam.faces[:4], main_int_frame.zaxis) + + # Extend Cut True or False + if self.extend_cut == True: + plane_side = [Plane.from_frame(main_frames_sorted[1]), Plane.from_frame(main_frames_sorted[2])] + else: + plane_side = [Plane.from_frame(cut_frames_sorted[1]), Plane.from_frame(cut_frames_sorted[2])] + + crossvector = cross_vectors(self.cross_cutting_plane_2.normal, self.cross_cutting_plane_1.normal) + plane_side = self._sort_frames_according_normals(plane_side, crossvector) + + lines = [l1, l2, l3] + points = [] + for i in lines: + points.append(intersection_line_plane(i, plane_side[0])) + points.append(intersection_line_plane(i, plane_side[1])) + + # TODO fix with Chen: Polyhedron.from_planes not working because numpy missing ????? + main_cutting_volume = Polyhedron( + points, + [ + [0, 2, 4], # front + [1, 5, 3], # back + [0, 1, 3, 2], # first + [2, 3, 5, 4], # second + [4, 5, 1, 0], # third + ], + ) + + return main_cutting_volume + + def add_extensions(self): + pass + + def add_features(self): + + assert self.main_beam and self.cross_beam # should never happen + + cross_cutting_plane1, cross_cutting_plane2 = self.get_cross_cutting_planes() + main_cutting_vol = self.get_main_cutting_volume() + + trim_feature = CutFeature(cross_cutting_plane1) + self.cross_beam.add_features(trim_feature) + trim_feature = CutFeature(cross_cutting_plane2) + self.cross_beam.add_features(trim_feature) + volume = MillVolume(main_cutting_vol) + self.main_beam.add_features(volume) diff --git a/src/compas_timber/connections/x_halflap.py b/src/compas_timber/connections/x_halflap.py index 67553a2355..2ba9cb257f 100644 --- a/src/compas_timber/connections/x_halflap.py +++ b/src/compas_timber/connections/x_halflap.py @@ -32,6 +32,9 @@ class XHalfLapJoint(LapJoint): def __init__(self, main_beam=None, cross_beam=None, flip_lap_side=False, cut_plane_bias=0.5, **kwargs): super(XHalfLapJoint, self).__init__(main_beam, cross_beam, flip_lap_side, cut_plane_bias, **kwargs) + def add_extensions(self): + pass + def add_features(self): assert self.main_beam and self.cross_beam # should never happen diff --git a/src/compas_timber/fabrication/__init__.py b/src/compas_timber/fabrication/__init__.py index 0f680f849c..f723eeed29 100644 --- a/src/compas_timber/fabrication/__init__.py +++ b/src/compas_timber/fabrication/__init__.py @@ -3,19 +3,29 @@ from .btlx_processes.btlx_french_ridge_lap import BTLxFrenchRidgeLap from .btlx_processes.btlx_jack_cut import BTLxJackCut from .btlx_processes.btlx_lap import BTLxLap +from .btlx_processes.btlx_text import BTLxText +from .btlx_processes.btlx_double_cut import BTLxDoubleCut +from .btlx_processes.btlx_drilling import BTLxDrilling from .joint_factories.french_ridge_factory import FrenchRidgeFactory from .joint_factories.l_butt_factory import LButtFactory from .joint_factories.l_miter_factory import LMiterFactory +from .joint_factories.l_halflap_factory import LHalfLapFactory from .joint_factories.t_butt_factory import TButtFactory +from .joint_factories.text_factory import TextFactory __all__ = [ "BTLx", "BTLxProcess", "BTLxJackCut", "BTLxLap", + "BTLxText", + "BTLxDoubleCut", + "BTLxDrilling", "BTLxFrenchRidgeLap", - "LButtFactory", + "LButtFactory" + "LHalfLapFactory", "TButtFactory", "LMiterFactory", "FrenchRidgeFactory", + "TextFactory", ] diff --git a/src/compas_timber/fabrication/btlx.py b/src/compas_timber/fabrication/btlx.py index e194c4a767..7b2632d9c7 100644 --- a/src/compas_timber/fabrication/btlx.py +++ b/src/compas_timber/fabrication/btlx.py @@ -1,4 +1,5 @@ import os +from re import split import uuid import xml.dom.minidom as MD import xml.etree.ElementTree as ET @@ -11,7 +12,6 @@ from compas.geometry import angle_vectors from compas.geometry import Transformation - class BTLx(object): """Class representing a BTLx object. @@ -38,6 +38,7 @@ class BTLx(object): POINT_PRECISION = 3 ANGLE_PRECISION = 3 REGISTERED_JOINTS = {} + REGISTERED_FEATURES = {} FILE_ATTRIBUTES = OrderedDict( [ ("xmlns", "https://www.design2machine.com"), @@ -73,6 +74,37 @@ def history(self): "Comment": "", } + def get_split_strings(self, beam_key, split_into_two = True): + + if split_into_two: + split_lists = [[1,2,6],[3,4,5]] + else: + split_lists = [[1],[2],[3],[4,5,6]] + + part = self.parts[str(beam_key)] + + start_ref_plane = part.get_start_end_ref_plane() + print("start_ref_plane", start_ref_plane) + for i, list in enumerate(split_lists): + if start_ref_plane in list: + last_list = split_lists.pop(i) + split_lists.append(last_list) + break + print("split_lists", split_lists) + ET_element = ET.Element("BTLx", BTLx.FILE_ATTRIBUTES) + ET_element.append(self.file_history) + project_element = ET.SubElement(ET_element, "Project", Name="testProject") + parts_element = ET.SubElement(project_element, "Parts") + + for i, ref_plane_list in enumerate(split_lists): + part.element_number = str(i+1) + parts_element.append(part.et_element(ref_plane_list)) + + + return MD.parseString(ET.tostring(ET_element)).toprettyxml(indent=" ") + + + def btlx_string(self): """Returns a pretty XML string for visualization in GH, Terminal, etc.""" self.ET_element = ET.Element("BTLx", BTLx.FILE_ATTRIBUTES) @@ -81,7 +113,7 @@ def btlx_string(self): self.parts_element = ET.SubElement(self.project_element, "Parts") for part in self.parts.values(): - self.parts_element.append(part.et_element) + self.parts_element.append(part.et_element()) return MD.parseString(ET.tostring(self.ET_element)).toprettyxml(indent=" ") def process_assembly(self): @@ -91,6 +123,10 @@ def process_assembly(self): for joint in self.joints: factory_type = self.REGISTERED_JOINTS.get(str(type(joint))) factory_type.apply_processings(joint, self.parts) + for part in self.parts.values(): + if part.ID: + factory_type = self.REGISTERED_FEATURES.get("TextID") + factory_type.apply_processings(part) @classmethod def register_joint(cls, joint_type, joint_factory): @@ -110,6 +146,24 @@ def register_joint(cls, joint_type, joint_factory): """ cls.REGISTERED_JOINTS[str(joint_type)] = joint_factory + @classmethod + def register_feature(cls, feature_type, feature_factory): + """Registers a feature type and its corresponding factory. + + Parameters + ---------- + feature_type : type + The type of the feature. + feature_factory : : class:`~compas_timber.fabrication.feature_factories.feature_factory.FeatureFactory` + The factory for creating the feature. + + Returns + ------- + None + + """ + cls.REGISTERED_FEATURES[str(feature_type)] = feature_factory + @property def file_history(self): """Returns the file history element.""" @@ -148,6 +202,8 @@ class BTLxPart(object): The frame of the blank. blank_length : float The blank length of the beam. + intersections : list + A list of the intersection parameters on the beam processings : list A list of the processings applied to the beam. et_element : :class:`~xml.etree.ElementTree.Element` @@ -159,17 +215,20 @@ def __init__(self, beam): self.beam = beam self.key = beam.key self.length = beam.blank_length - self.width = beam.height - self.height = beam.width + self.width = beam.width + self.height = beam.height self.frame = Frame( self.beam.long_edges[2].closest_point(self.beam.blank_frame.point), beam.frame.xaxis, beam.frame.yaxis, ) # I used long_edge[2] because it is in Y and Z negative. Using that as reference puts the beam entirely in positive coordinates. self.blank_length = beam.blank_length + self.ID = beam.attributes["ID"] + self.element_number = 0 + self.intersections = beam.intersections self._reference_surfaces = [] self.processings = [] - self._et_element = None + def reference_surface_from_beam_face(self, beam_face): """Finds the reference surface with normal that matches the normal of the beam face argument @@ -251,7 +310,7 @@ def attr(self): "Weight": "0", "ProcessingQuality": "automatic", "StoreyType": "", - "ElementNumber": "00", + "ElementNumber": str(self.element_number), "Layer": "0", "ModuleNumber": "", } @@ -286,19 +345,33 @@ def et_point_vals(self, point): "Z": "{:.{prec}f}".format(point.z, prec=BTLx.POINT_PRECISION), } - @property - def et_element(self): - if not self._et_element: - self._et_element = ET.Element("Part", self.attr) - self._shape_strings = None - self._et_element.append(self.et_transformations) - self._et_element.append(ET.Element("GrainDirection", X="1", Y="0", Z="0", Align="no")) - self._et_element.append(ET.Element("ReferenceSide", Side="1", Align="no")) - processings_et = ET.Element("Processings") - for process in self.processings: + def get_start_end_ref_plane(self): + result = 0 + for process in self.processings: + name = process.header_attributes.get("Name") + if name.split(" ")[0] == "start": + result = process.header_attributes.get("ReferencePlaneID") + return int(result) + + + def et_element(self, ref_sides = [1,2,3,4,5,6]): + + self._et_element = ET.Element("Part", self.attr) + self._shape_strings = None + self._et_element.append(self.et_transformations) + self._et_element.append(ET.Element("GrainDirection", X="1", Y="0", Z="0", Align="no")) + self._et_element.append(ET.Element("ReferenceSide", Side="1", Align="no")) + processings_et = ET.Element("Processings") + + for process in self.processings: + print(process.header_attributes.get("ReferencePlaneID")) + print(ref_sides) + if int(process.header_attributes.get("ReferencePlaneID")) in ref_sides: + print("in ref sides") processings_et.append(process.et_element) - self._et_element.append(processings_et) - self._et_element.append(self.et_shape) + + self._et_element.append(processings_et) + self._et_element.append(self.et_shape) return self._et_element @property diff --git a/src/compas_timber/fabrication/btlx_processes/btlx_double_cut.py b/src/compas_timber/fabrication/btlx_processes/btlx_double_cut.py index bfe30f7b3c..26b129c697 100644 --- a/src/compas_timber/fabrication/btlx_processes/btlx_double_cut.py +++ b/src/compas_timber/fabrication/btlx_processes/btlx_double_cut.py @@ -22,7 +22,7 @@ class BTLxDoubleCut(object): def __init__(self, param_dict, joint_name=None, **kwargs): self.apply_process = True - self.reference_plane_id = param_dict["ReferencePlaneID"] + self.reference_plane_id = str(param_dict["ReferencePlaneID"]) self.orientation = param_dict["Orientation"] self.start_x = param_dict["StartX"] self.start_y = param_dict["StartY"] @@ -40,7 +40,7 @@ def __init__(self, param_dict, joint_name=None, **kwargs): if joint_name: self.name = joint_name else: - self.name = "lap" + self.name = "double_cut" @property def header_attributes(self): @@ -58,7 +58,7 @@ def process_params(self): """This property is required for all process types. It returns a dict with the geometric parameters to fabricate the joint.""" if self.apply_process: - """the following attributes are specific to Lap""" + """the following attributes are specific to a Double Cut process.""" od = OrderedDict( [ ("Orientation", str(self.orientation)), @@ -76,6 +76,6 @@ def process_params(self): @classmethod def create_process(cls, param_dict, joint_name=None, **kwargs): - """Creates a lap process from a dictionary of parameters.""" - lap = BTLxDoubleCut(param_dict, joint_name, **kwargs) - return BTLxProcess(BTLxDoubleCut.PROCESS_TYPE, lap.header_attributes, lap.process_params) + """Creates a double cut process from a dictionary of parameters.""" + double_cut = BTLxDoubleCut(param_dict, joint_name, **kwargs) + return BTLxProcess(BTLxDoubleCut.PROCESS_TYPE, double_cut.header_attributes, double_cut.process_params) diff --git a/src/compas_timber/fabrication/btlx_processes/btlx_dovetail.py b/src/compas_timber/fabrication/btlx_processes/btlx_dovetail.py new file mode 100644 index 0000000000..c373b70b5b --- /dev/null +++ b/src/compas_timber/fabrication/btlx_processes/btlx_dovetail.py @@ -0,0 +1,202 @@ +from collections import OrderedDict +from compas_timber.fabrication import BTLx +from compas_timber.fabrication import BTLxProcess + +FLANK_ANGLE = 15.0 +SHAPE_RADIUS = 30 ##CHECK +LENGTH_LIMITED_BOTTOM = True + + +class BTLxDoveTailTenon(object): + """ + Represents a dovetail_tenon process for timber fabrication. + + Parameters + ---------- + param_dict : dict + A dictionary containing the parameters for the BTLx lap process. + joint_name : str + The name of the joint. If not provided, the default name is "lap". + kwargs : dict + Additional keyword arguments to be added to the object. + + """ + + PROCESS_TYPE = "DoveTail_Tenon" + + def __init__(self, param_dict, joint_name=None, **kwargs): + self.apply_process = True + self.reference_plane_id = param_dict["ReferencePlaneID"] + self.orientation = param_dict["Orientation"] + self.start_x = param_dict["StartX"] + self.start_y = param_dict["StartY"] + self.start_depth = param_dict["StartDepth"] + self.angle = param_dict["Angle"] + self.inclination = 90.0 + self.rotation = 90.0 + self.length_limited_top = bool(False) + self.length_limited_bottom = bool(LENGTH_LIMITED_BOTTOM) + self.length = param_dict["Length"] + self.width = param_dict["Width"] + self.height = param_dict["Height"] + self.cone_angle = param_dict["ConeAngle"] + self.use_flank_angle = bool(True) + self.flank_angle = FLANK_ANGLE ##check + self.shape = str("automatic") + self.shape_radius = SHAPE_RADIUS # check + + for key, value in param_dict.items(): + setattr(self, key, value) + + for key, value in kwargs.items(): + setattr(self, key, value) + + if joint_name: + self.name = joint_name + else: + self.name = "dovetail_tenon" + + @property + def header_attributes(self): + """the following attributes are required for all processes, but the keys and values of header_attributes are process specific.""" + return { + "Name": self.name, + "Process": "yes", + "Priority": "0", + "ProcessID": "0", + "ReferencePlaneID": str(self.reference_plane_id + 1), + } + + @property + def process_params(self): + """This property is required for all process types. It returns a dict with the geometric parameters to fabricate the joint.""" + + if self.apply_process: + """the following attributes are specific to Dovetail_Tenon""" + od = OrderedDict( + [ + ("Orientation", str(self.orientation)), + ("StartX", "{:.{prec}f}".format(self.start_x, prec=BTLx.POINT_PRECISION)), + ("StartY", "{:.{prec}f}".format(self.start_y, prec=BTLx.POINT_PRECISION)), + ("StartDepth", "{:.{prec}f}".format(self.start_depth, prec=BTLx.POINT_PRECISION)), + ("Angle", "{:.{prec}f}".format(self.angle, prec=BTLx.ANGLE_PRECISION)), + ("Inclination", "{:.{prec}f}".format(self.inclination, prec=BTLx.ANGLE_PRECISION)), + ("Rotation", "{:.{prec}f}".format(self.rotation, prec=BTLx.ANGLE_PRECISION)), + ("LengthLimitedTop", bool(self.length_limited_top)), + ("LengthLimitedBottom", bool(self.length_limited_bottom)), + ("Length", "{:.{prec}f}".format(self.length, prec=BTLx.POINT_PRECISION)), + ("Width", "{:.{prec}f}".format(self.width, prec=BTLx.POINT_PRECISION)), + ("Height", "{:.{prec}f}".format(self.height, prec=BTLx.POINT_PRECISION)), + ("ConeAngle", "{:.{prec}f}".format(self.cone_angle, prec=BTLx.POINT_PRECISION)), + ("UseFlankAngle", bool(self.use_flank_angle)), + ("FlankAngle", "{:.{prec}f}".format(self.flank_angle, prec=BTLx.POINT_PRECISION)), + ("Shape", str(self.shape)), + ("ShapeRadius", "{:.{prec}f}".format(self.shape_radius, prec=BTLx.POINT_PRECISION)), + ] + ) + return od + else: + return None + + @classmethod + def create_process(cls, param_dict, joint_name=None, **kwargs): + """Creates a dovetail_tenon process from a dictionary of parameters.""" + dovetail_t = BTLxDoveTailTenon(param_dict, joint_name, **kwargs) + return BTLxProcess(BTLxDoveTailTenon.PROCESS_TYPE, dovetail_t.header_attributes, dovetail_t.process_params) + + +class BTLxDoveTailMortise(object): + """ + Represents a dovetail_mortise process for timber fabrication. + + Parameters + ---------- + param_dict : dict + A dictionary containing the parameters for the BTLx lap process. + joint_name : str + The name of the joint. If not provided, the default name is "lap". + kwargs : dict + Additional keyword arguments to be added to the object. + + """ + + PROCESS_TYPE = "DoveTail_Mortise" + + def __init__(self, param_dict, joint_name=None, **kwargs): + self.apply_process = True + self.reference_plane_id = param_dict["ReferencePlaneID"] + + self.start_x = param_dict["StartX"] + self.start_y = param_dict["StartY"] + self.start_depth = param_dict["StartDepth"] + self.angle = param_dict["Angle"] + self.slope = 90.0 + self.inclination = 90.0 + self.limitation_top = str("unlimited") + self.length_limited_bottom = bool(LENGTH_LIMITED_BOTTOM) + self.length = param_dict["Length"] + self.width = param_dict["Width"] + self.depth = param_dict["Depth"] + self.cone_angle = param_dict["ConeAngle"] + self.use_flank_angle = bool(True) + self.flank_angle = FLANK_ANGLE ##check + self.shape = str("automatic") + self.shape_radius = SHAPE_RADIUS # check + + for key, value in param_dict.items(): + setattr(self, key, value) + + for key, value in kwargs.items(): + setattr(self, key, value) + + if joint_name: + self.name = joint_name + else: + self.name = "dovetail_mortise" + + @property + def header_attributes(self): + """the following attributes are required for all processes, but the keys and values of header_attributes are process specific.""" + return { + "Name": self.name, + "Process": "yes", + "Priority": "0", + "ProcessID": "0", + "ReferencePlaneID": str(self.reference_plane_id + 1), + } + + @property + def process_params(self): + """This property is required for all process types. It returns a dict with the geometric parameters to fabricate the joint.""" + + if self.apply_process: + """the following attributes are specific to Dovetail_Mortise""" + od = OrderedDict( + [ + ("StartX", "{:.{prec}f}".format(self.start_x, prec=BTLx.POINT_PRECISION)), + ("StartY", "{:.{prec}f}".format(self.start_y, prec=BTLx.POINT_PRECISION)), + ("StartDepth", "{:.{prec}f}".format(self.start_depth, prec=BTLx.POINT_PRECISION)), + ("Angle", "{:.{prec}f}".format(self.angle, prec=BTLx.ANGLE_PRECISION)), + ("Slope", "{:.{prec}f}".format(self.slope, prec=BTLx.ANGLE_PRECISION)), + ("Inclination", "{:.{prec}f}".format(self.inclination, prec=BTLx.ANGLE_PRECISION)), + ("LimitationTop", bool(self.limitation_top)), + ("LengthLimitedBottom", bool(self.length_limited_bottom)), + ("Length", "{:.{prec}f}".format(self.length, prec=BTLx.POINT_PRECISION)), + ("Width", "{:.{prec}f}".format(self.width, prec=BTLx.POINT_PRECISION)), + ("Depth", "{:.{prec}f}".format(self.depth, prec=BTLx.POINT_PRECISION)), + ("ConeAngle", "{:.{prec}f}".format(self.cone_angle, prec=BTLx.POINT_PRECISION)), + ("UseFlankAngle", bool(self.use_flank_angle)), + ("FlankAngle", "{:.{prec}f}".format(self.flank_angle, prec=BTLx.POINT_PRECISION)), + ("Shape", str(self.shape)), + ("ShapeRadius", "{:.{prec}f}".format(self.shape_radius, prec=BTLx.POINT_PRECISION)), + ] + ) + return od + else: + return None + + @classmethod + def create_process(cls, param_dict, joint_name=None, **kwargs): + """Creates a dovetail_mortise process from a dictionary of parameters.""" + dovetail_m = BTLxDoveTailMortise(param_dict, joint_name, **kwargs) + return BTLxProcess(BTLxDoveTailMortise.PROCESS_TYPE, dovetail_m.header_attributes, dovetail_m.process_params) diff --git a/src/compas_timber/fabrication/btlx_processes/btlx_drilling.py b/src/compas_timber/fabrication/btlx_processes/btlx_drilling.py new file mode 100644 index 0000000000..a32e3e72a4 --- /dev/null +++ b/src/compas_timber/fabrication/btlx_processes/btlx_drilling.py @@ -0,0 +1,81 @@ +from collections import OrderedDict +from compas_timber.fabrication import BTLx +from compas_timber.fabrication import BTLxProcess + + +class BTLxDrilling(object): + """ + Represents a drilling process for timber fabrication. + + Parameters + ---------- + param_dict : dict + A dictionary containing the parameters for the BTLx lap process. + joint_name : str + The name of the joint. If not provided, the default name is "lap". + kwargs : dict + Additional keyword arguments to be added to the object. + + """ + + PROCESS_TYPE = "Drilling" + + def __init__(self, param_dict, joint_name=None, **kwargs): # joint_name replace by "feature_name"? + self.apply_process = True + self.reference_plane_id = param_dict["ReferencePlaneID"] + self.start_x = float(param_dict["StartX"]) + self.start_y = float(param_dict["StartY"]) + self.angle = float(param_dict["Angle"]) + self.inclination = float(param_dict["Inclination"]) + self.depth_limited = param_dict["DepthLimited"] + self.depth = float(param_dict["Depth"]) + self.diameter = float(param_dict["Diameter"]) + + for key, value in param_dict.items(): + setattr(self, key, value) + + for key, value in kwargs.items(): + setattr(self, key, value) + + if joint_name: # to delete since no joint? + self.name = joint_name + else: + self.name = "drilling" # what instead? + + @property + def header_attributes(self): + """the following attributes are required for all processes, but the keys and values of header_attributes are process specific.""" + return { + "Name": self.name, + "Process": "yes", + "Priority": "0", + "ProcessID": "0", + "ReferencePlaneID": self.reference_plane_id, + } + + @property + def process_params(self): + """This property is required for all process types. It returns a dict with the geometric parameters to fabricate the joint.""" + + if self.apply_process: + """the following attributes are specific to Drilling""" + od = OrderedDict( + [ + ("StartX", "{:.{prec}f}".format(self.start_x, prec=BTLx.POINT_PRECISION)), + ("StartY", "{:.{prec}f}".format(self.start_y, prec=BTLx.POINT_PRECISION)), + ("Angle", "{:.{prec}f}".format(self.angle, prec=BTLx.ANGLE_PRECISION)), + ("Inclination", "{:.{prec}f}".format(self.inclination, prec=BTLx.ANGLE_PRECISION)), + ("DepthLimited", str(self.depth_limited)), + ("Depth", "{:.{prec}f}".format(self.depth, prec=BTLx.POINT_PRECISION)), + ("Diameter", "{:.{prec}f}".format(self.diameter, prec=BTLx.POINT_PRECISION)), + ] + ) + return od + else: + return None + + @classmethod + def create_process(cls, param_dict, joint_name=None, **kwargs): + """Creates a drilling process from a dictionary of parameters.""" + drilling = BTLxDrilling(param_dict, joint_name, **kwargs) + return BTLxProcess(BTLxDrilling.PROCESS_TYPE, drilling.header_attributes, drilling.process_params) diff --git a/src/compas_timber/fabrication/btlx_processes/btlx_french_ridge_lap.py b/src/compas_timber/fabrication/btlx_processes/btlx_french_ridge_lap.py index d22ae6fb7d..71f895ce14 100644 --- a/src/compas_timber/fabrication/btlx_processes/btlx_french_ridge_lap.py +++ b/src/compas_timber/fabrication/btlx_processes/btlx_french_ridge_lap.py @@ -1,7 +1,7 @@ import math from collections import OrderedDict -from compas.geometry import angle_vectors_signed +from compas.geometry import angle_vectors_signed, angle_vectors from compas_timber.fabrication import BTLx from compas_timber.fabrication import BTLxProcess @@ -51,7 +51,7 @@ class BTLxFrenchRidgeLap(object): PROCESS_TYPE = "FrenchRidgeLap" - def __init__(self, part, joint, is_top): + def __init__(self, part, joint, is_top, drill_diameter=0.0): for beam in joint.beams: if beam.key == part.key: self.beam = beam @@ -62,11 +62,11 @@ def __init__(self, part, joint, is_top): self.is_top = is_top self.orientation = joint.ends[str(part.key)] self._ref_edge = True - self._drill_hole = True - self.drill_hole_diameter = 10.0 + self._drill_hole = True if drill_diameter > 0 else False + self.drill_hole_diameter = float(drill_diameter) self.ref_face_index = self.joint.reference_face_indices[str(self.beam.key)] - self.ref_face = self.part.faces[self.ref_face_index] + self.ref_face = self.part.reference_surface_planes(str(self.ref_face_index)) """ the following attributes are required for all processes, but the keys and values of header_attributes are process specific. @@ -127,23 +127,30 @@ def get_params(self): other_vector = -other_vector self.angle_rad = angle_vectors_signed(self.ref_face.xaxis, other_vector, self.ref_face.normal) + self.angle_lines = angle_vectors(self.ref_face.xaxis, other_vector) if self.orientation == "start": - if self.angle_rad < math.pi / 2 and self.angle_rad > -math.pi / 2: - raise Exception("french ridge lap joint beams must join at 90-180 degrees") - elif self.angle_rad < -math.pi / 2: + # if self.angle_rad < math.pi / 3 and self.angle_rad > -math.pi / 2: + # raise Exception("french ridge lap joint beams must join at 90-180 degrees") + if self.angle_rad < 0: self._ref_edge = False self.angle_rad = abs(self.angle_rad) + self.startX = abs(self.beam.width / math.tan(self.angle_rad)) + # print(self.angle_lines, "angle_lines") + # print(self.startX) + if self.angle_lines < math.pi / 2: + self.startX = 0.0 + else: - if self.angle_rad < -math.pi / 2 or self.angle_rad > math.pi / 2: - raise Exception("french ridge lap joint beams must join at 90-180 degrees") - elif self.angle_rad < 0: + # if self.angle_rad < -math.pi / 2 or self.angle_rad > math.pi / 2: + # raise Exception("french ridge lap joint beams must join at 90-180 degrees") + if self.angle_rad < 0: self.angle_rad = abs(self.angle_rad) self._ref_edge = False - self.angle_rad = math.pi - self.angle_rad - self.startX = self.beam.width / abs(math.tan(self.angle_rad)) + self.angle_rad = math.pi - self.angle_rad + self.startX = abs(self.beam.width / math.tan(self.angle_rad)) if self.orientation == "end": if self._ref_edge: @@ -151,9 +158,19 @@ def get_params(self): else: self.startX = self.beam.blank_length + self.startX + # print("orientation: ", self.orientation, " angle: ", self.angle_rad, " start: ", self.startX) + # print( + # "ref_edge: ", + # self.ref_edge, + # " drill_hole: ", + # self.drill_hole, + # " drill_hole_diameter: ", + # self.drill_hole_diameter, + # ) + @classmethod - def create_process(cls, part, joint, is_top): - frl_process = BTLxFrenchRidgeLap(part, joint, is_top) + def create_process(cls, part, joint, is_top, drill_diameter): + frl_process = BTLxFrenchRidgeLap(part, joint, is_top, drill_diameter) return BTLxProcess( BTLxFrenchRidgeLap.PROCESS_TYPE, frl_process.header_attributes, frl_process.process_parameters ) diff --git a/src/compas_timber/fabrication/btlx_processes/btlx_lap.py b/src/compas_timber/fabrication/btlx_processes/btlx_lap.py index bb243b4c67..f36e62760b 100644 --- a/src/compas_timber/fabrication/btlx_processes/btlx_lap.py +++ b/src/compas_timber/fabrication/btlx_processes/btlx_lap.py @@ -22,7 +22,7 @@ class BTLxLap(object): def __init__(self, param_dict, joint_name=None, **kwargs): self.apply_process = True - self.reference_plane_id = 0 + self.reference_plane_id = param_dict["ReferencePlaneID"] self.orientation = "start" self.start_x = 0.0 self.start_y = 0.0 @@ -52,6 +52,7 @@ def __init__(self, param_dict, joint_name=None, **kwargs): @property def header_attributes(self): """the following attributes are required for all processes, but the keys and values of header_attributes are process specific.""" + return { "Name": self.name, "Process": "yes", @@ -77,7 +78,7 @@ def process_params(self): ("Length", "{:.{prec}f}".format(self.length, prec=BTLx.POINT_PRECISION)), ("Width", "{:.{prec}f}".format(self.width, prec=BTLx.POINT_PRECISION)), ("Depth", "{:.{prec}f}".format(float(self.depth), prec=BTLx.POINT_PRECISION)), - ("LeadAngleParallel", "yes"), + ("LeadAngleParallel", str(self.lead_angle_parallel)), ("LeadAngle", "{:.{prec}f}".format(self.lead_angle, prec=BTLx.ANGLE_PRECISION)), ("LeadInclinationParallel", "yes"), ("LeadInclination", "{:.{prec}f}".format(self.lead_inclination, prec=BTLx.ANGLE_PRECISION)), diff --git a/src/compas_timber/fabrication/btlx_processes/btlx_stepjoint.py b/src/compas_timber/fabrication/btlx_processes/btlx_stepjoint.py new file mode 100644 index 0000000000..f19ced3542 --- /dev/null +++ b/src/compas_timber/fabrication/btlx_processes/btlx_stepjoint.py @@ -0,0 +1,86 @@ +from collections import OrderedDict +from compas_timber.fabrication import BTLx +from compas_timber.fabrication import BTLxProcess + + +class BTLxStepJoint(object): + """ + Represents a step joint process for timber fabrication. + + Parameters + ---------- + param_dict : dict + A dictionary containing the parameters for the BTLx Step Joint process. + joint_name : str + The name of the joint. If not provided, the default name is "step joint". + kwargs : dict + Additional keyword arguments to be added to the object. + + """ + + PROCESS_TYPE = "StepJoint" + + def __init__(self, param_dict, joint_name=None, **kwargs): + self.apply_process = True + self.reference_plane_id = param_dict["ReferencePlaneID"] + self.orientation = param_dict["Orientation"] + self.start_x = param_dict["StartX"] + self.strut_inclination = param_dict["StrutInclination"] + self.step_depth = param_dict["StepDepth"] + self.hell_depth = param_dict["HeelDepth"] + self.step_shape = param_dict["StepShape"] + self.tenon = param_dict["Tenon"] + self.tenon_width = param_dict["TenonWidth"] + self.tenon_height = param_dict["TenonHeight"] + + for key, value in param_dict.items(): + setattr(self, key, value) + + for key, value in kwargs.items(): + setattr(self, key, value) + + if joint_name: + self.name = joint_name + else: + self.name = "step joint" + + @property + def header_attributes(self): + """the following attributes are required for all processes, but the keys and values of header_attributes are process specific.""" + return { + "Name": self.name, + "Process": "yes", + "Priority": "0", + "ProcessID": "0", + "ReferencePlaneID": self.reference_plane_id, + } + + @property + def process_params(self): + """This property is required for all process types. It returns a dict with the geometric parameters to fabricate the joint.""" + + if self.apply_process: + """the following attributes are specific to Step Joint""" + od = OrderedDict( + [ + ("Orientation", str(self.orientation)), + ("StartX", "{:.{prec}f}".format(self.start_x, prec=BTLx.POINT_PRECISION)), + ("StrutInclination", "{:.{prec}f}".format(self.strut_inclination, prec=BTLx.ANGLE_PRECISION)), + ("StepDepth", "{:.{prec}f}".format(self.step_depth, prec=BTLx.POINT_PRECISION)), + ("HeelDepth", "{:.{prec}f}".format(self.hell_depth, prec=BTLx.POINT_PRECISION)), + ("StepShape", str(self.step_shape)), + ("Tenon", str(self.tenon)), + ("TenonWidth", "{:.{prec}f}".format(self.tenon_width, prec=BTLx.POINT_PRECISION)), + ("TenonHeight", "{:.{prec}f}".format(self.tenon_height, prec=BTLx.POINT_PRECISION)), + ] + ) + print("param dict", od) + return od + else: + return None + + @classmethod + def create_process(cls, param_dict, joint_name=None, **kwargs): + """Creates a Step Joint process from a dictionary of parameters.""" + step_joint = BTLxStepJoint(param_dict, joint_name, **kwargs) + return BTLxProcess(BTLxStepJoint.PROCESS_TYPE, step_joint.header_attributes, step_joint.process_params) diff --git a/src/compas_timber/fabrication/btlx_processes/btlx_text.py b/src/compas_timber/fabrication/btlx_processes/btlx_text.py new file mode 100644 index 0000000000..eada1ceabb --- /dev/null +++ b/src/compas_timber/fabrication/btlx_processes/btlx_text.py @@ -0,0 +1,83 @@ +from collections import OrderedDict +from compas_timber.fabrication import BTLx +from compas_timber.fabrication import BTLxProcess + +class BTLxText(object): + """ + Represents an engraving process of a text for timber fabrication. + + Parameters + ---------- + param_dict : dict + A dictionary containing the parameters for the BTLx lap process. + joint_name : str + The name of the joint. If not provided, the default name is "lap". + kwargs : dict + Additional keyword arguments to be added to the object. + + """ + + PROCESS_TYPE = "Text" + + def __init__(self, param_dict, joint_name=None, **kwargs): + self.apply_process = True + self.reference_plane_id = param_dict["ReferencePlaneID"] + self.start_x = param_dict["StartX"] + self.start_y = param_dict["StartY"] + self.angle = param_dict["Angle"] + self.alignment_vertical = param_dict["AlignmentVertical"] + self.alignment_horizontal = param_dict["AlignmentHorizontal"] + self.alignment_multiline = param_dict["AlignmentMultiline"] + self.text_height = param_dict["TextHeight"] + self.text = param_dict["Text"] + + for key, value in param_dict.items(): + setattr(self, key, value) + + for key, value in kwargs.items(): + setattr(self, key, value) + + if joint_name: + self.name = joint_name + else: + self.name = "text_engraving" + + @property + def header_attributes(self): + """the following attributes are required for all processes, but the keys and values of header_attributes are process specific.""" + return { + "Name": self.name, + "Process": "yes", + "Priority": "0", + "ProcessID": "0", + "ReferencePlaneID": self.reference_plane_id, + } + + @property + def process_params(self): + """This property is required for all process types. It returns a dict with the geometric parameters to fabricate the joint.""" + + if self.apply_process: + """the following attributes are specific to a text engraving""" + od = OrderedDict( + [ + ("StartX", "{:.{prec}f}".format(self.start_x, prec=BTLx.POINT_PRECISION)), + ("StartY", "{:.{prec}f}".format(self.start_y, prec=BTLx.POINT_PRECISION)), + ("Angle", "{:.{prec}f}".format(self.angle, prec=BTLx.ANGLE_PRECISION)), + ("AlignmentVertical", str(self.alignment_vertical)), + ("AlignmentHorizontal", str(self.alignment_horizontal)), + ("AlignmentMultiline", str(self.alignment_multiline)), + ("TextHeight", "{:.{prec}f}".format(self.text_height, prec=BTLx.POINT_PRECISION)), + ("Text", str(self.text)), + + ] + ) + return od + else: + return None + + @classmethod + def create_process(cls, param_dict, joint_name=None, **kwargs): + """Creates a text process from a dictionary of parameters.""" + text = BTLxText(param_dict, joint_name, **kwargs) + return BTLxProcess(BTLxText.PROCESS_TYPE, text.header_attributes, text.process_params) diff --git a/src/compas_timber/fabrication/joint_factories/french_ridge_factory.py b/src/compas_timber/fabrication/joint_factories/french_ridge_factory.py index f4ca677c31..c28535faa0 100644 --- a/src/compas_timber/fabrication/joint_factories/french_ridge_factory.py +++ b/src/compas_timber/fabrication/joint_factories/french_ridge_factory.py @@ -1,6 +1,7 @@ from compas_timber.connections import FrenchRidgeLapJoint from compas_timber.fabrication import BTLx from compas_timber.fabrication import BTLxFrenchRidgeLap +from compas_timber.fabrication.btlx_processes.btlx_drilling import BTLxDrilling class FrenchRidgeFactory(object): @@ -31,11 +32,11 @@ def apply_processings(cls, joint, parts): top_key = joint.beams[0].key top_part = parts[str(top_key)] - top_part.processings.append(BTLxFrenchRidgeLap.create_process(top_part, joint, True)) + top_part.processings.append(BTLxFrenchRidgeLap.create_process(top_part, joint, True, joint.drill_diameter)) bottom_key = joint.beams[1].key bottom_part = parts[str(bottom_key)] - bottom_part.processings.append(BTLxFrenchRidgeLap.create_process(bottom_part, joint, False)) + bottom_part.processings.append(BTLxFrenchRidgeLap.create_process(bottom_part, joint, False, 0.0)) BTLx.register_joint(FrenchRidgeLapJoint, FrenchRidgeFactory) diff --git a/src/compas_timber/fabrication/joint_factories/l_butt_factory.py b/src/compas_timber/fabrication/joint_factories/l_butt_factory.py index 33aec8c14f..639a580241 100644 --- a/src/compas_timber/fabrication/joint_factories/l_butt_factory.py +++ b/src/compas_timber/fabrication/joint_factories/l_butt_factory.py @@ -2,6 +2,7 @@ from compas_timber.fabrication import BTLx from compas_timber.fabrication import BTLxJackCut from compas_timber.fabrication import BTLxLap +from compas_timber.fabrication import BTLxDoubleCut class LButtFactory(object): @@ -31,14 +32,12 @@ def apply_processings(cls, joint, parts): main_part = parts[str(joint.main_beam.key)] cross_part = parts[str(joint.cross_beam.key)] - main_part.processings.append( - BTLxJackCut.create_process(main_part, joint.get_main_cutting_plane()[0], "L-Butt Joint") - ) - cross_part.processings.append( - BTLxJackCut.create_process(cross_part, joint.get_cross_cutting_plane(), "L-Butt Joint") - ) + main_cut_plane, ref_plane = joint.get_main_cutting_plane() + cross_cut_plane = joint.get_cross_cutting_plane() + cross_part.processings.append(BTLxJackCut.create_process(cross_part, cross_cut_plane, "L-Butt Joint")) + if joint.mill_depth > 0: - if joint.ends[1] == "start": + if joint.ends[str(cross_part.key)] == "start": joint.btlx_params_cross["machining_limits"] = { "FaceLimitedStart": "no", "FaceLimitedFront": "no", @@ -50,7 +49,16 @@ def apply_processings(cls, joint, parts): "FaceLimitedFront": "no", "FaceLimitedBack": "no", } - cross_part.processings.append(BTLxLap.create_process(joint.btlx_params_cross, "L-Butt Joint")) + + joint.btlx_params_cross["ReferencePlaneID"] = str(cross_part.reference_surface_from_beam_face(ref_plane)) + cross_part.processings.append(BTLxLap.create_process(joint.btlx_params_cross, joint.ends[str(cross_part.key)] + " L-Butt Joint")) + + if joint.birdsmouth: + ref_face = main_part.beam.faces[joint.main_face_index] + joint.btlx_params_main["ReferencePlaneID"] = str(main_part.reference_surface_from_beam_face(ref_face)) + main_part.processings.append(BTLxDoubleCut.create_process(joint.btlx_params_main, joint.ends[str(main_part.key)] + " L-Butt Joint")) + else: + main_part.processings.append(BTLxJackCut.create_process(main_part, main_cut_plane, joint.ends[str(main_part.key)] + " L-Butt Joint")) BTLx.register_joint(LButtJoint, LButtFactory) diff --git a/src/compas_timber/fabrication/joint_factories/l_halflap_factory.py b/src/compas_timber/fabrication/joint_factories/l_halflap_factory.py new file mode 100644 index 0000000000..34f7270563 --- /dev/null +++ b/src/compas_timber/fabrication/joint_factories/l_halflap_factory.py @@ -0,0 +1,42 @@ +from compas_timber.connections import LHalfLapJoint +from compas_timber.fabrication import BTLx +from compas_timber.fabrication import BTLxLap +from compas_timber.fabrication import BTLxDrilling + + +class LHalfLapFactory(object): + """ + Factory class for creating L-Butt joints. + """ + + def __init__(self): + pass + + + @classmethod + def apply_processings(cls, joint, parts): + """Apply processings to the joint and its associated parts. + + Parameters + ---------- + joint : :class:`~compas_timber.connections.joint.Joint` + The joint object. + parts : dict + A dictionary of the BTLxParts connected by this joint, with part keys as the dictionary keys. + + Returns + ------- + None + + """ + + main_part = parts[str(joint.main_beam.key)] + cross_part = parts[str(joint.cross_beam.key)] + + cross_part.processings.append(BTLxLap.create_process(joint.btlx_params_cross, "L-HalfLap Joint")) + main_part.processings.append(BTLxLap.create_process(joint.btlx_params_main, "L-HalfLap Joint")) + + if joint.drill_diameter > 0: + main_part.processings.append(BTLxDrilling.create_process(joint.btlx_drilling_params_main, "L-HalfLap Joint")) + +BTLx.register_joint(LHalfLapJoint, LHalfLapFactory) diff --git a/src/compas_timber/fabrication/joint_factories/l_miter_factory.py b/src/compas_timber/fabrication/joint_factories/l_miter_factory.py index f6627ecd4d..6554e94310 100644 --- a/src/compas_timber/fabrication/joint_factories/l_miter_factory.py +++ b/src/compas_timber/fabrication/joint_factories/l_miter_factory.py @@ -28,12 +28,12 @@ def apply_processings(cls, joint, parts): None """ - - parts[str(joint.beams[0].key)].processings.append( - BTLxJackCut.create_process(parts[str(joint.beams[0].key)], joint.get_cutting_planes()[0], "L-Miter Joint") + beams = [joint.beam_a, joint.beam_b] + parts[str(beams[0].key)].processings.append( + BTLxJackCut.create_process(parts[str(beams[0].key)], joint.get_cutting_planes()[0], joint.ends[str(beams[0].key)] + " L-Miter Joint") ) - parts[str(joint.beams[1].key)].processings.append( - BTLxJackCut.create_process(parts[str(joint.beams[1].key)], joint.get_cutting_planes()[1], "L-Miter Joint") + parts[str(beams[1].key)].processings.append( + BTLxJackCut.create_process(parts[str(beams[1].key)], joint.get_cutting_planes()[1], joint.ends[str(beams[1].key)] + " L-Miter Joint") ) diff --git a/src/compas_timber/fabrication/joint_factories/t_butt_factory.py b/src/compas_timber/fabrication/joint_factories/t_butt_factory.py index 288387ad23..181add18b2 100644 --- a/src/compas_timber/fabrication/joint_factories/t_butt_factory.py +++ b/src/compas_timber/fabrication/joint_factories/t_butt_factory.py @@ -1,6 +1,7 @@ from compas_timber.connections import TButtJoint from compas_timber.fabrication import BTLx from compas_timber.fabrication import BTLxJackCut +from compas_timber.fabrication.btlx_processes.btlx_drilling import BTLxDrilling from compas_timber.fabrication.btlx_processes.btlx_lap import BTLxLap from compas_timber.fabrication.btlx_processes.btlx_double_cut import BTLxDoubleCut @@ -34,17 +35,29 @@ def apply_processings(cls, joint, parts): cut_plane, ref_plane = joint.get_main_cutting_plane() if joint.birdsmouth: - joint.calc_params_birdsmouth() - ref_face = main_part.beam.faces[joint.btlx_params_main["ReferencePlaneID"]] + ref_face = main_part.beam.faces[joint.main_face_index] joint.btlx_params_main["ReferencePlaneID"] = str(main_part.reference_surface_from_beam_face(ref_face)) - main_part.processings.append(BTLxDoubleCut.create_process(joint.btlx_params_main, "T-Butt Joint")) + main_part.processings.append(BTLxDoubleCut.create_process(joint.btlx_params_main, joint.ends[str(main_part.key)] + " T-Butt Joint")) + elif joint.stepjoint: + ref_face = main_part.beam.faces[joint.ref_face_id] + joint.btlx_params_stepjoint_main["ReferencePlaneID"] = str(main_part.reference_surface_from_beam_face(ref_face)) + main_part.processings.append(BTLxDoubleCut.create_process(joint.btlx_params_stepjoint_main, "T-Butt Joint")) + ref_face_cross = cross_part.beam.faces[joint.cross_face_id] + joint.btlx_params_stepjoint_cross["ReferencePlaneID"] = str(cross_part.reference_surface_from_beam_face(ref_face_cross)) + cross_part.processings.append(BTLxLap.create_process(joint.btlx_params_stepjoint_cross, "T-Butt Joint pocket")) else: - main_part.processings.append(BTLxJackCut.create_process(main_part, cut_plane, "T-Butt Joint")) + main_part.processings.append(BTLxJackCut.create_process(main_part, cut_plane, joint.ends[str(main_part.key)] + " T-Butt Joint")) - joint.btlx_params_cross["reference_plane_id"] = cross_part.reference_surface_from_beam_face(ref_plane) + joint.btlx_params_cross["reference_plane_id"] = str(cross_part.reference_surface_from_beam_face(ref_plane)) if joint.mill_depth > 0: + if joint.btlx_params_cross["length"] <= 61: + joint.btlx_params_cross["length"] = 61.5 joint.btlx_params_cross["machining_limits"] = {"FaceLimitedFront": "no", "FaceLimitedBack": "no"} + joint.btlx_params_cross["ReferencePlaneID"] = str(cross_part.reference_surface_from_beam_face(ref_plane)) cross_part.processings.append(BTLxLap.create_process(joint.btlx_params_cross, "T-Butt Joint")) + if joint.drill_diameter > 0: + joint.btlx_drilling_params_cross["ReferencePlaneID"] = str(cross_part.reference_surface_from_beam_face(ref_plane)) + cross_part.processings.append(BTLxDrilling.create_process(joint.btlx_drilling_params_cross, "T-Butt Joint")) BTLx.register_joint(TButtJoint, TButtFactory) diff --git a/src/compas_timber/fabrication/joint_factories/text_factory.py b/src/compas_timber/fabrication/joint_factories/text_factory.py new file mode 100644 index 0000000000..787fc4c89a --- /dev/null +++ b/src/compas_timber/fabrication/joint_factories/text_factory.py @@ -0,0 +1,100 @@ +from compas_timber.parts import BrepSubtraction + +from compas_timber.fabrication import BTLx +from compas_timber.fabrication import BTLxText + +class TextFactory(object): + """ + Factory class for creating Text engraving. + """ + + def __init__(self): + pass + + @staticmethod + def get_engraving_position(part): + """Finds the optimal parameter on the line for the text engraving process.""" + intersections = set(part.intersections) + intersections.update({0, 1}) # Ensure 0 and 1 are included + all_intersections = sorted(intersections) + + max_length = 0 + optimal_parameter = 0.5 + for i in range(len(all_intersections) - 1): + seg_length = all_intersections[i+1] - all_intersections[i] + if seg_length > max_length: + max_length = seg_length + optimal_parameter = (all_intersections[i] + all_intersections[i+1]) / 2 + + optimal_parameter = 0.5 if optimal_parameter in {0, 1} else optimal_parameter + optimal_position = optimal_parameter * part.length + return optimal_position + + @staticmethod + def get_text_engraving_params(part): + """Returns the text engraving parameters for the BTLx part.""" + return { + "ReferencePlaneID": 1, #default face + "StartX": TextFactory.get_engraving_position(part) - (7*20.0)/2, #7=number of characters in the ID + "StartY": part.width/2 - 10., #manually center it since text is not centered in easybeam + "Angle": 0.0, + "AlignmentVertical": "bottom", #default(bottom) in easybeam + "AlignmentHorizontal": "left", #default(left) in easybeam + "AlignmentMultiline": "left", #default(left) in easybeam + "TextHeight": 15.0, + "Text": part.ID + } + + def add_features(self): + """Adds the trimming plane to the main beam (no features for the cross beam). + + This method is automatically called when joint is created by the call to `Joint.create()`. + + """ + pass + # assert self.main_beam and self.cross_beam # should never happen + # if self.features: + # self.main_beam.remove_features(self.features) + # cutting_plane = None + # try: + # cutting_plane = self.get_main_cutting_plane()[0] + # except AttributeError as ae: + # raise BeamJoinningError(beams=self.beams, joint=self, debug_info=str(ae), debug_geometries=[cutting_plane]) + # except Exception as ex: + # raise BeamJoinningError(beams=self.beams, joint=self, debug_info=str(ex)) + + # self.features = [] + # if self.birdsmouth: + # if self.calc_params_birdsmouth(): + # self.main_beam.add_features(BrepSubtraction(self.bm_sub_volume)) + # self.features.append(BrepSubtraction(self.bm_sub_volume)) + + + @classmethod + def apply_processings(cls, part): + """ + Apply processings to the joint and parts. + + Parameters + ---------- + joint : :class:`~compas_timber.connections.joint.Joint` + The joint object. + parts : dict + A dictionary of the BTLxParts connected by this joint, with part keys as the dictionary keys. + + Returns + ------- + None + + """ + + if part.processings: + ref_plane_id = part.processings[0].header_attributes.get("ReferencePlaneID", 1) + else: + ref_plane_id = "1" + params_dict = TextFactory.get_text_engraving_params(part) + params_dict["ReferencePlaneID"] = ref_plane_id + part.processings.append(BTLxText.create_process(params_dict, "Text")) + +BTLx.register_feature("TextID", TextFactory) + diff --git a/src/compas_timber/ghpython/components/CT_Assembly/code.py b/src/compas_timber/ghpython/components/CT_Assembly/code.py index d07a92742b..17871e716d 100644 --- a/src/compas_timber/ghpython/components/CT_Assembly/code.py +++ b/src/compas_timber/ghpython/components/CT_Assembly/code.py @@ -151,6 +151,7 @@ def RunScript(self, Beams, JointRules, Features, MaxDistance, CreateGeometry): self._beam_map[id(beam)] = c_beam beams = Assembly.beams + solver.find_intersection_parameters(Assembly.beams) joints = self.get_joints_from_rules(beams, JointRules, topologies) if joints: @@ -168,7 +169,8 @@ def RunScript(self, Beams, JointRules, Features, MaxDistance, CreateGeometry): debug_info.add_joint_error(bje) else: handled_beams.append(beam_pair_ids) - + for joint in Assembly.joints: + joint.add_features() if Features: features = [f for f in Features if f is not None] for f_def in features: diff --git a/src/compas_timber/ghpython/components/CT_BTLx/code.py b/src/compas_timber/ghpython/components/CT_BTLx/code.py index 8ec8dcf43e..a2d01ebe46 100644 --- a/src/compas_timber/ghpython/components/CT_BTLx/code.py +++ b/src/compas_timber/ghpython/components/CT_BTLx/code.py @@ -23,4 +23,4 @@ def RunScript(self, assembly, path, write): path += ".btlx" with open(path, "w") as f: f.write(btlx.btlx_string()) - return btlx.btlx_string() + return btlx, btlx.btlx_string() diff --git a/src/compas_timber/ghpython/components/CT_BTLx/metadata.json b/src/compas_timber/ghpython/components/CT_BTLx/metadata.json index 8dcbacdf25..84c1254492 100644 --- a/src/compas_timber/ghpython/components/CT_BTLx/metadata.json +++ b/src/compas_timber/ghpython/components/CT_BTLx/metadata.json @@ -31,7 +31,11 @@ ], "outputParameters": [ { - "name": "BTLx", + "name": "BTLx Object", + "description": "BTLx object to pass to other components." + }, + { + "name": "BTLx String", "description": "Pretty BTLx string" } ] diff --git a/src/compas_timber/ghpython/components/CT_BTLx_Flip_Beam/code.py b/src/compas_timber/ghpython/components/CT_BTLx_Flip_Beam/code.py new file mode 100644 index 0000000000..edc3a0b210 --- /dev/null +++ b/src/compas_timber/ghpython/components/CT_BTLx_Flip_Beam/code.py @@ -0,0 +1,24 @@ +import Rhino +from ghpythonlib.componentbase import executingcomponent as component +from Grasshopper.Kernel.GH_RuntimeMessageLevel import Warning + +from compas_timber.fabrication import BTLx + + +class WriteBTLx(component): + def RunScript(self, btlx, path, write, beam_key, split_into_two): + if not btlx: + self.AddRuntimeMessage(Warning, "Input parameter btlx failed to collect data") + return + btlx.history["FileName"] = Rhino.RhinoDoc.ActiveDoc.Name + + if write: + if not path: + self.AddRuntimeMessage(Warning, "Input parameter Path failed to collect data") + return + path = path.split(".")[0] if "." in path else path + path_end = "_" + str(beam_key) + "_SPLIT" + ".btlx" + path += path_end + with open(path, "w") as f: + f.write(btlx.get_split_strings(beam_key, split_into_two)) + return btlx.get_split_strings(beam_key, split_into_two) diff --git a/src/compas_timber/ghpython/components/CT_BTLx_Flip_Beam/icon.png b/src/compas_timber/ghpython/components/CT_BTLx_Flip_Beam/icon.png new file mode 100644 index 0000000000..b4ae4794c2 Binary files /dev/null and b/src/compas_timber/ghpython/components/CT_BTLx_Flip_Beam/icon.png differ diff --git a/src/compas_timber/ghpython/components/CT_BTLx_Flip_Beam/metadata.json b/src/compas_timber/ghpython/components/CT_BTLx_Flip_Beam/metadata.json new file mode 100644 index 0000000000..54bbda8760 --- /dev/null +++ b/src/compas_timber/ghpython/components/CT_BTLx_Flip_Beam/metadata.json @@ -0,0 +1,50 @@ +{ + "name": "WriteBTLx", + "nickname": "WriteBTLx", + "category": "COMPAS Timber", + "subcategory": "Fabrication", + "description": "Writes a BTLx file for machining of assembly parts.", + "exposure": 4, + "ghpython": { + "isAdvancedMode": true, + "iconDisplay": 0, + "inputParameters": [ + { + "name": "Assembly", + "description": "Assembly object.", + "typeHintID": "none", + "scriptParamAccess": 0 + }, + { + "name": "Path", + "description": "Filepath for new BTLx.", + "typeHintID": "none", + "scriptParamAccess": 0 + }, + { + "name": "Write", + "description": "Write to file.", + "typeHintID": "bool", + "scriptParamAccess": 0 + }, + { + "name": "Beam Key", + "description": "beam key of beam to split", + "typeHintID": "none", + "scriptParamAccess": 0 + }, + { + "name": "flip once", + "description": "splits BTLx file into 2. if false, splits into 4.", + "typeHintID": "bool", + "scriptParamAccess": 0 + } + ], + "outputParameters": [ + { + "name": "BTLx", + "description": "BTLx with two parts representing the two sides of the beam." + } + ] + } +} diff --git a/src/compas_timber/ghpython/components/CT_Beam_fromCurve/code.py b/src/compas_timber/ghpython/components/CT_Beam_fromCurve/code.py index 0852fe09fd..49dd919d17 100644 --- a/src/compas_timber/ghpython/components/CT_Beam_fromCurve/code.py +++ b/src/compas_timber/ghpython/components/CT_Beam_fromCurve/code.py @@ -73,7 +73,6 @@ def RunScript(self, centerline, z_vector, width, height, category, updateRefObj) beam = CTBeam.from_centerline(centerline=line, width=w, height=h, z_vector=z) beam.attributes["rhino_guid"] = str(guid) if guid else None beam.attributes["category"] = c - print(guid) if updateRefObj and guid: update_rhobj_attributes_name(guid, "width", str(w)) update_rhobj_attributes_name(guid, "height", str(h)) diff --git a/src/compas_timber/ghpython/components/CT_Joint_Rule_Category/code.py b/src/compas_timber/ghpython/components/CT_Joint_Rule_Category/code.py index 7e5a337d0a..f9f537c236 100644 --- a/src/compas_timber/ghpython/components/CT_Joint_Rule_Category/code.py +++ b/src/compas_timber/ghpython/components/CT_Joint_Rule_Category/code.py @@ -44,7 +44,6 @@ def RunScript(self, *args): for i, val in enumerate(args[2:]): if val is not None: kwargs[self.arg_names()[i + 2]] = val - print(kwargs) if not cat_a: self.AddRuntimeMessage( Warning, "Input parameter {} failed to collect data.".format(self.arg_names()[0]) diff --git a/src/compas_timber/parts/beam.py b/src/compas_timber/parts/beam.py index 1440767c71..e2ad2375b5 100644 --- a/src/compas_timber/parts/beam.py +++ b/src/compas_timber/parts/beam.py @@ -79,7 +79,12 @@ class Beam(Part): A list containing the 4 lines along the long axis of this beam. midpoint : :class:`~compas.geometry.Point` The point at the middle of the centerline of this beam. - + airModule_no : string + The air module number the assembly module of the beam is part of. + assemblyModule_no : string + The assembly module number the beam is part of. + beam_no : string + The beam number. (irrelevant of the assembly sequence) """ def __init__(self, frame, length, width, height, **kwargs): @@ -89,6 +94,10 @@ def __init__(self, frame, length, width, height, **kwargs): self.length = length self.features = [] self._blank_extensions = {} + self.intersections = [] + self.attributes["ID"] = [] ### TODO names to be defined + self.attributes["assembly_no"] = [] ### TODO names to be defined + self.attributes["beam_no"] = [] ### TODO names to be defined @property def __data__(self):