Skip to content

Commit

Permalink
Merge pull request #243 from gramaziokohler/butt_mill
Browse files Browse the repository at this point in the history
BTLx output for butt joints
  • Loading branch information
obucklin authored Jun 14, 2024
2 parents b8c59e1 + a28b04d commit 7606db8
Show file tree
Hide file tree
Showing 25 changed files with 554 additions and 158 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* Added `birdsmouth` parameter to `butt_joint` which applies a `btlx_double_cut` process to the part.
* Added `BTLxDoubleCut` BTLx Processing class
* Added BTLx support for `TButtJoint` and `LButtJoint`
* Added `BTLxLap` process class

### Changed

### Removed
Expand Down
1 change: 0 additions & 1 deletion data/PLACEHOLDER

This file was deleted.

Binary file added examples/Grasshopper/btlx.3dm
Binary file not shown.
Binary file added examples/Grasshopper/btlx.gh
Binary file not shown.
1 change: 1 addition & 0 deletions src/compas_timber/connections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"BeamJoinningError",
"TButtJoint",
"LButtJoint",
"TButtJoint",
"LMiterJoint",
"XHalfLapJoint",
"THalfLapJoint",
Expand Down
164 changes: 144 additions & 20 deletions src/compas_timber/connections/butt_joint.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
from compas.geometry import Plane
from compas.geometry import Point
from compas.geometry import Polyhedron
from compas.geometry import Transformation
from compas.geometry import Vector
from compas.geometry import angle_vectors
from compas.geometry import angle_vectors_signed
from compas.geometry import closest_point_on_line
from compas.geometry import distance_line_line
from compas.geometry import dot_vectors
from compas.geometry import intersection_plane_plane
from compas.geometry import intersection_plane_plane_plane
Expand Down Expand Up @@ -43,37 +47,42 @@ class ButtJoint(Joint):
@property
def __data__(self):
data_dict = {
"main_beam_key": self.main_beam_key,
"cross_beam_key": self.cross_beam_key,
"main_beam_guid": self.main_beam_guid,
"cross_beam_guid": self.cross_beam_guid,
"mill_depth": self.mill_depth,
"birds_mouth": self.birdsmouth,
}
data_dict.update(super(ButtJoint, 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"]
instance.main_beam_guid = value["main_beam_guid"]
instance.cross_beam_guid = value["cross_beam_guid"]
return instance

def __init__(self, main_beam=None, cross_beam=None, mill_depth=0, **kwargs):
def __init__(self, main_beam=None, cross_beam=None, mill_depth=0, birdsmouth=False, **kwargs):
super(ButtJoint, self).__init__(**kwargs)
self.main_beam = main_beam
self.cross_beam = cross_beam
self.main_beam_key = str(main_beam.guid) if main_beam else None
self.cross_beam_key = str(cross_beam.guid) if cross_beam else None
self.main_beam_guid = str(main_beam.guid) if main_beam else None
self.cross_beam_guid = str(cross_beam.guid) if cross_beam else None
self.mill_depth = mill_depth
self.birdsmouth = birdsmouth
self.btlx_params_main = {}
self.btlx_params_cross = {}
self.features = []
self.test = []

@property
def beams(self):
return [self.main_beam, self.cross_beam]

def restore_beams_from_keys(self, model):
"""After de-serialization, restors references to the main and cross beams saved in the model."""
self.main_beam = model.beam_by_guid(self.main_beam_key)
self.cross_beam = model.beam_by_guid(self.cross_beam_key)
self.main_beam = model.beam_by_guid(self.main_beam_guid)
self.cross_beam = model.beam_by_guid(self.cross_beam_guid)

def side_surfaces_cross(self):
assert self.main_beam and self.cross_beam
Expand All @@ -82,7 +91,8 @@ def side_surfaces_cross(self):
face_indices = face_dict.keys()
angles = face_dict.values()
angles, face_indices = zip(*sorted(zip(angles, face_indices)))
return self.cross_beam.faces[face_indices[1]], self.cross_beam.faces[face_indices[2]]

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
Expand All @@ -99,43 +109,75 @@ def back_surface_main(self):
return face_dict.values()[3]

def get_main_cutting_plane(self):
# TODO: this should be split into two functions. It's hard to read on the calling side.
assert self.main_beam and self.cross_beam
_, cfr = self.get_face_most_ortho_to_beam(self.main_beam, self.cross_beam, ignore_ends=True)
self.reference_side_index_cross, cfr = self.get_face_most_ortho_to_beam(
self.main_beam, self.cross_beam, ignore_ends=True
)

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):
dir_pts = []
"""Returns the volume to be subtracted from the cross beam.
# TODO: break this function into smaller more readable parts
# TODO: BTLx related code here should end up in a LapFeature..
"""
vertices = []
front_frame, back_frame = self.front_back_surface_main()
top_frame, bottom_frame = self.get_main_cutting_plane()
sides = self.side_surfaces_cross()
for i, side in enumerate(sides):
points = []
top_frame, bottom_frame = self.get_main_cutting_plane()
for frame in [top_frame, bottom_frame]:
for fr in self.front_back_surface_main():
for frame in [bottom_frame, top_frame]:
for fr in [front_frame, back_frame]:
points.append(
intersection_plane_plane_plane(
Plane.from_frame(side), Plane.from_frame(frame), Plane.from_frame(fr)
)
)
pv = [subtract_vectors(pt, self.cross_beam.centerline.start) for pt in points]
pv = [subtract_vectors(pt, self.cross_beam.blank_frame.point) for pt in points]
dots = [dot_vectors(v, self.cross_beam.centerline.direction) for v in pv]
dots, points = zip(*sorted(zip(dots, points)))
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)))
bottom_line = Line(*intersection_plane_plane(Plane.from_frame(side), Plane.from_frame(bottom_frame)))
top_min = Point(*closest_point_on_line(min_pt, top_line))
dir_pts.append(top_min)

top_max = Point(*closest_point_on_line(max_pt, top_line))

bottom_line = Line(*intersection_plane_plane(Plane.from_frame(side), Plane.from_frame(bottom_frame)))

bottom_min = Point(*closest_point_on_line(min_pt, bottom_line))
bottom_max = Point(*closest_point_on_line(max_pt, bottom_line))

vertices.extend([Point(*top_min), Point(*top_max), Point(*bottom_max), Point(*bottom_min)])

center = (vertices[0] + vertices[1] + vertices[2] + vertices[3]) * 0.25
top_front = Line(vertices[0], vertices[4])
top_back = Line(vertices[1], vertices[5])
_len = distance_line_line(top_front, top_back)

front_line = Line(*intersection_plane_plane(Plane.from_frame(front_frame), Plane.from_frame(top_frame)))

self.btlx_params_cross["depth"] = self.mill_depth

self.btlx_params_cross["width"] = (
self.cross_beam.height if self.reference_side_index_cross % 2 == 0 else self.cross_beam.width
)

self.btlx_params_cross["length"] = _len
if dot_vectors(top_frame.yaxis, front_line.direction) < 0:
front_line = Line(front_line.end, front_line.start)
self.btlx_params_cross["angle"] = abs(
angle_vectors_signed(top_frame.xaxis, front_line.direction, top_frame.zaxis, deg=True)
)

center = (vertices[0] + vertices[1] + vertices[2] + vertices[3]) * 0.25
angle = angle_vectors_signed(
subtract_vectors(vertices[0], center), subtract_vectors(vertices[1], center), sides[0].zaxis
)
Expand All @@ -149,3 +191,85 @@ def subtraction_volume(self):
)

return ph

def calc_params_birdsmouth(self):
"""
Calculate the parameters for a birdsmouth joint.
# TODO: break this function into smaller more readable parts
# TODO: this is BTLx only code, it should move some where else
# TODO: eventually, there should be a DoubleCut Feature that holds these parameters
# TODO: there's no birds mouth in L-Butt, if there shouldn't be then at least move to T-Butt
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 = 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
frame2 = self.cross_beam.faces[face_keys[1]]

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]

ref_frame.point = self.main_beam.blank_frame.point
if ref_frame_id % 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)

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]

dot_frame1 = plane1.normal.dot(ref_frame.yaxis)
if dot_frame1 > 0:
plane1, plane2 = plane2, plane1

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)))

if self.ends[str(self.main_beam.guid)] == "start":
reference_vector = ref_frame.xaxis
else:
reference_vector = -ref_frame.xaxis

if intersect_vec1.dot(ref_frame.yaxis) < 0:
intersect_vec1 = -intersect_vec1
if intersect_vec2.dot(ref_frame.yaxis) < 0:
intersect_vec2 = -intersect_vec2

Angle1 = angle_vectors(intersect_vec1, reference_vector, deg=True)
Angle2 = angle_vectors(intersect_vec2, reference_vector, deg=True)

Inclination1 = angle_vectors(ref_frame.zaxis, plane1.normal, deg=True)
Inclination2 = angle_vectors(ref_frame.zaxis, plane2.normal, deg=True)

self.btlx_params_main = {
"Orientation": self.ends[str(self.main_beam.guid)],
"StartX": StartX,
"StartY": StartY,
"Angle1": Angle1,
"Inclination1": Inclination1,
"Angle2": Angle2,
"Inclination2": Inclination2,
"ReferencePlaneID": ref_frame_id,
}
16 changes: 8 additions & 8 deletions src/compas_timber/connections/french_ridge_lap.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,25 +40,25 @@ def __init__(self, beam_a=None, beam_b=None, **kwargs):
super(FrenchRidgeLapJoint, self).__init__(beams=(beam_a, beam_b), **kwargs)
self.beam_a = beam_a
self.beam_b = beam_b
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.beam_a_guid = str(beam_a.guid) if beam_a else None
self.beam_b_guid = str(beam_b.guid) if beam_b else None
self.reference_face_indices = {}
self.check_geometry()

@property
def __data__(self):
data_dict = {
"beam_a_key": self.beam_a_key,
"beam_b_key": self.beam_b_key,
"beam_a_guid": self.beam_a_guid,
"beam_b_guid": self.beam_b_guid,
}
data_dict.update(super(FrenchRidgeLapJoint, self).__data__)
return data_dict

@classmethod
def __from_data__(cls, value):
instance = cls(frame=Frame.__from_data__(value["frame"]), key=value["key"])
instance.beam_a_key = value["beam_a_key"]
instance.beam_b_key = value["beam_b_key"]
instance.beam_a_guid = value["beam_a_guid"]
instance.beam_b_guid = value["beam_b_guid"]
return instance

@property
Expand All @@ -74,8 +74,8 @@ def cutting_plane_bottom(self):

def restore_beams_from_keys(self, assemly):
"""After de-serialization, restores references to the top and bottom beams saved in the model."""
self.beam_a = assemly.find_by_key(self.beam_a_key)
self.beam_b = assemly.find_by_key(self.beam_b_key)
self.beam_a = assemly.find_by_key(self.beam_a_guid)
self.beam_b = assemly.find_by_key(self.beam_b_guid)
self._beams = (self.beam_a, self.beam_b)

def check_geometry(self):
Expand Down
23 changes: 7 additions & 16 deletions src/compas_timber/connections/joint.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from compas.geometry import Point
from compas.geometry import angle_vectors
from compas.geometry import distance_point_line
from compas.geometry import intersection_line_line
from compas_model.interactions import Interaction

Expand Down Expand Up @@ -128,26 +129,16 @@ def create(cls, model, *beams, **kwargs):

@property
def ends(self):
"""Returns a map of ehich end of each beam is joined by this joint."""
"""Returns a map of which end of each beam is joined by this joint."""

self._ends = {}
for index, beam in enumerate(self.beams):
start_distance = min(
[
beam.centerline.start.distance_to_point(self.beams[index - 1].centerline.start),
beam.centerline.start.distance_to_point(self.beams[index - 1].centerline.end),
]
)
end_distance = min(
[
beam.centerline.end.distance_to_point(self.beams[index - 1].centerline.start),
beam.centerline.end.distance_to_point(self.beams[index - 1].centerline.end),
]
)
if start_distance < end_distance:
self._ends[str(beam.key)] = "start"
if distance_point_line(beam.centerline.start, self.beams[index - 1].centerline) < distance_point_line(
beam.centerline.end, self.beams[index - 1].centerline
):
self._ends[str(beam.guid)] = "start"
else:
self._ends[str(beam.key)] = "end"
self._ends[str(beam.guid)] = "end"

return self._ends

Expand Down
12 changes: 6 additions & 6 deletions src/compas_timber/connections/l_miter.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,17 @@ class LMiterJoint(Joint):
@property
def __data__(self):
data = super(LMiterJoint, self).__data__
data["beam_a"] = self.beam_a_key
data["beam_b"] = self.beam_b_key
data["beam_a"] = self.beam_a_guid
data["beam_b"] = self.beam_b_guid
data["cutoff"] = self.cutoff
return data

def __init__(self, beam_a=None, beam_b=None, cutoff=None):
super(LMiterJoint, self).__init__()
self.beam_a = beam_a
self.beam_b = beam_b
self.beam_a_key = str(beam_a.guid) if beam_a else None
self.beam_b_key = str(beam_b.guid) if beam_b else None
self.beam_a_guid = str(beam_a.guid) if beam_a else None
self.beam_b_guid = str(beam_b.guid) if beam_b else None
self.cutoff = cutoff # for very acute angles, limit the extension of the tip/beak of the joint
self.features = []

Expand Down Expand Up @@ -131,5 +131,5 @@ def add_features(self):

def restore_beams_from_keys(self, model):
"""After de-serialization, restores references to the main and cross beams saved in the model."""
self.beam_a = model.elementdict[self.beam_a_key]
self.beam_b = model.elementdict[self.beam_b_key]
self.beam_a = model.elementdict[self.beam_a_guid]
self.beam_b = model.elementdict[self.beam_b_guid]
Loading

0 comments on commit 7606db8

Please sign in to comment.