Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lbutt extend cross #213

Merged
merged 29 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
aa53de6
l-butt cleanup
chenkasirer Feb 6, 2024
b201f60
made beam_side_incident a staticmethod of Joint
chenkasirer Feb 7, 2024
52484d1
protecting extension calculation with try catch
chenkasirer Feb 7, 2024
786a19c
added extend_cross flag to l-butt
chenkasirer Feb 7, 2024
ffc47c8
updated example files
chenkasirer Feb 7, 2024
b1acb98
completed docstring
chenkasirer Feb 7, 2024
e28d56f
renamed `extend_cross` to `modify_cross`.
chenkasirer Feb 7, 2024
31b4b51
updated french_ridge_lap
chenkasirer Feb 7, 2024
bf7690d
linting and formatting
chenkasirer Feb 7, 2024
bbc4632
Merge branch 'main' into lbutt_extend_cross
chenkasirer Feb 7, 2024
628258c
sorted changelog entries
chenkasirer Feb 12, 2024
9d10aed
fixed docstring
chenkasirer Feb 12, 2024
6a3377d
added convinience methods to improve readability
chenkasirer Feb 12, 2024
b10f499
formatting
chenkasirer Feb 12, 2024
9dc66a7
updated component
chenkasirer Feb 12, 2024
63ab431
return from Beam.faces
chenkasirer Feb 12, 2024
cd4d2d8
updated GH example file
chenkasirer Feb 12, 2024
b68bf6b
Merge branch 'main' into lbutt_extend_cross
chenkasirer Feb 13, 2024
a06fef0
restored l-butt original behavior
chenkasirer Feb 13, 2024
41effa5
added show beam faces component
chenkasirer Feb 13, 2024
592be74
direct rule considers beam order
chenkasirer Feb 13, 2024
81b3fe8
updated rhino file
chenkasirer Feb 13, 2024
c6d0809
formatting
chenkasirer Feb 13, 2024
47c25b2
added flag `reject_i` to l-butt
chenkasirer Feb 14, 2024
a24118f
merged show beam frame into decompose beam
chenkasirer Feb 14, 2024
e192924
formatting
chenkasirer Feb 14, 2024
4218271
updated changelog
chenkasirer Feb 14, 2024
3bc433d
updated example files
chenkasirer Feb 14, 2024
00b72fb
Merge branch 'main' into lbutt_extend_cross
chenkasirer Feb 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* Added flag `modify_cross` to `L-Butt` joint.
* Added `debug_geometries` attribute to `BeamJoiningError`.

### Changed

* `BeamFromCurve` GH component accepts now referenced Rhino curves, referenced Rhino object IDs and internalized lines.
* `BeamFromCurve` GH component accepts now referenced Rhino curves, referenced Rhino object IDs and internalized lines.
* Fixed `FeatureError` when L-Butt applies the cutting plane.
* Fixed T-Butt doesn't get extended to cross beam's plane.
* `SimpleSequenceGenerator` updated to work with `compas.datastructures.assembly` and generates building plan acording to type.
* Changed GH Categories for joint rules
* Changed GH Categories for joint rules.
* Made `beam_side_incident` a `staticmethod` of `Joint` and reworked it.

### Removed

Expand Down
1 change: 0 additions & 1 deletion docs/api/compas_timber.connections.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ Functions
:nosignatures:

find_neighboring_beams
beam_side_incidence

Exceptions
==========
Expand Down
Binary file modified examples/Grasshopper/CT_NEW_UI_Example.3dm
Binary file not shown.
Binary file modified examples/Grasshopper/CT_NEW_UI_Example.gh
Binary file not shown.
2 changes: 0 additions & 2 deletions src/compas_timber/connections/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from .french_ridge_lap import FrenchRidgeLapJoint
from .joint import BeamJoinningError
from .joint import Joint
from .joint import beam_side_incidence
from .lap_joint import LapJoint
from .l_butt import LButtJoint
from .l_miter import LMiterJoint
Expand All @@ -15,7 +14,6 @@

__all__ = [
"Joint",
"beam_side_incidence",
"LapJoint",
"BeamJoinningError",
"TButtJoint",
Expand Down
7 changes: 2 additions & 5 deletions src/compas_timber/connections/french_ridge_lap.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from .joint import BeamJoinningError
from .joint import Joint
from .joint import beam_side_incidence
from .solver import JointTopology


Expand Down Expand Up @@ -77,15 +76,13 @@ def joint_type(self):

@property
def cutting_plane_top(self):
angles_faces = beam_side_incidence(self.beam_a, self.beam_b)
cfr = max(angles_faces, key=lambda x: x[0])[1]
_, cfr = self.get_face_most_towards_beam(self.beam_a, self.beam_b, ignore_ends=True)
cfr = Frame(cfr.point, cfr.xaxis, cfr.yaxis * -1.0) # flip normal
return cfr

@property
def cutting_plane_bottom(self):
angles_faces = beam_side_incidence(self.beam_b, self.beam_a)
cfr = max(angles_faces, key=lambda x: x[0])[1]
_, cfr = self.get_face_most_towards_beam(self.beam_b, self.beam_b, ignore_ends=True)
return cfr

def restore_beams_from_keys(self, assemly):
Expand Down
151 changes: 117 additions & 34 deletions src/compas_timber/connections/joint.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,52 +7,31 @@
from .solver import JointTopology


def beam_side_incidence(beam1, beam2):
"""Returns a map of faces of beam2 and the angle of their normal with beam1's centerline.

This is used to find a cutting plane when joining the two beams.

Parameters
----------
beam1 : :class:`~compas_timber.parts.Beam`
The beam that attaches with one of its ends to the side of Beam2.
beam2 : :class:`~compas_timber.parts.Beam`
The other beam.

Returns
-------
list(tuple(float, :class:`~compas.geometry.Frame`))

"""

# find the orientation of beam1's centerline so that it's pointing outward of the joint
# find the closest end
p1x, p2x = intersection_line_line(beam1.centerline, beam2.centerline)
which, _ = beam1.endpoint_closest_to_point(Point(*p1x))

if which == "start":
centerline_vec = beam1.centerline.vector
else:
centerline_vec = beam1.centerline.vector * -1

# map faces to their angle with centerline, choose smallest
angle_face = [(angle_vectors(side.normal, centerline_vec), side) for side in beam2.faces[:4]]
return angle_face


class BeamJoinningError(Exception):
"""Indicates that an error has occurred while trying to join two or more beams.

This error should indicate that an error has occurred while calculating the features which
should be applied by this joint.

Attributes
----------
beams : list(:class:`~compas_timber.parts.Beam`)
The beams that were supposed to be joined.
debug_geometries : list(:class:`~compas.geometry.Geometry`)
A list of geometries that can be used to visualize the error.
debug_info : str
A string containing debug information about the error.
joint : :class:`~compas_timber.connections.Joint`
The joint that was supposed to join the beams.

"""

def __init__(self, beams, joint, debug_info=None):
def __init__(self, beams, joint, debug_info=None, debug_geometries=None):
super(BeamJoinningError, self).__init__()
self.beams = beams
self.joint = joint
self.debug_info = debug_info
self.debug_geometries = debug_geometries or []


class Joint(Data):
Expand All @@ -76,6 +55,7 @@ def __init__(self, frame=None, key=None):
super(Joint, self).__init__()
self.frame = frame or Frame.worldXY()
self.key = key
self.attributes = {}

@property
def __data__(self):
Expand Down Expand Up @@ -170,3 +150,106 @@ def ends(self):
self._ends[str(beam.key)] = "end"

return self._ends

@staticmethod
def get_face_most_towards_beam(beam_a, beam_b, ignore_ends=True):
"""Of all the faces of `beam_b`, returns the one whose normal most faces `beam_a`.

This is done by calculating the inner-product of `beam_a`'s centerline which each of the face normals of `beam_b`.
The face with the result closest to 1 is chosen.

Parameters
----------
beam_a : :class:`~compas_timber.parts.Beam`
The beam that attaches with one of its ends to `beam_b`.
beam_b : :class:`~compas_timber.parts.Beam`
The other beam.
ignore_ends : bool, optional
If True, the faces at each end of `beam_b` are ignored.

Returns
-------
tuple(face_index, :class:`~compas.geometry.Frame`)
Tuple containing the index of the chosen face and a frame at the center of if.

"""
face_dict = Joint._beam_side_incidence(beam_a, beam_b, ignore_ends)
face_index = max(face_dict, key=face_dict.get) # type: ignore
return face_index, beam_b.faces[face_index]

@staticmethod
def get_face_most_ortho_to_beam(beam_a, beam_b, ignore_ends=True):
"""Of all the faces of `beam_b`, returns the one whose normal is most orthogonal to `beam_a`.

This is done by calculating the inner-product of `beam_a`'s centerline which each of the face normals of `beam_b`.
The face with the result closest to 0 is chosen.

Parameters
----------
beam_a : :class:`~compas_timber.parts.Beam`
The beam that attaches with one of its ends to `beam_b`.
beam_b : :class:`~compas_timber.parts.Beam`
The other beam.
ignore_ends : bool, optional
If True, the faces at each end of `beam_b` are ignored.

Returns
-------
tuple(face_index, :class:`~compas.geometry.Frame`)
Tuple containing the index of the chosen face and a frame at the center of if.

"""
face_dict = Joint._beam_side_incidence(beam_a, beam_b, ignore_ends)
face_index = min(face_dict, key=face_dict.get) # type: ignore
return face_index, beam_b.faces[face_index]

@staticmethod
def _beam_side_incidence(beam_a, beam_b, ignore_ends=True):
"""Returns a map of face indices of beam_b and the angle of their normal with beam_a's centerline.

This is used to find a cutting plane when joining the two beams.

Parameters
----------
beam_a : :class:`~compas_timber.parts.Beam`
The beam that attaches with one of its ends to the side of beam_b.
beam_b : :class:`~compas_timber.parts.Beam`
The other beam.
ignore_ends : bool, optional
If True, only the first four faces of `beam_b` are considered. Otherwise all faces are considered.

Examples
--------
>>> face_angles = Joint.beam_side_incidence(beam_a, beam_b)
>>> closest_face_index = min(face_angles, key=face_angles.get)
>>> cutting_plane = beam_b.faces[closest_face_index]

Returns
-------
dict(int, float)
A map of face indices of beam_b and their respective angle with beam_a's centerline.

"""
# find the orientation of beam_a's centerline so that it's pointing outward of the joint
# find the closest end
p1x, _ = intersection_line_line(beam_a.centerline, beam_b.centerline)
if p1x is None:
raise AssertionError("No intersection found")

end, _ = beam_a.endpoint_closest_to_point(Point(*p1x))

if end == "start":
centerline_vec = beam_a.centerline.vector
else:
centerline_vec = beam_a.centerline.vector * -1

if ignore_ends:
beam_b_faces = beam_b.faces[:4]
else:
beam_b_faces = beam_b.faces

face_angles = {}
for face_index, face in enumerate(beam_b_faces):
face_angles[face_index] = angle_vectors(face.normal, centerline_vec)

return face_angles
66 changes: 41 additions & 25 deletions src/compas_timber/connections/l_butt.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

from .joint import BeamJoinningError
from .joint import Joint
from .joint import beam_side_incidence
from .solver import JointTopology


Expand All @@ -22,50 +21,56 @@ class LButtJoint(Joint):
The main beam to be joined.
cross_beam : :class:`~compas_timber.parts.Beam`
The cross beam to be joined.
small_beam_butts : bool, default False
If True, the beam with the smaller cross-section will be trimmed. Otherwise, the main beam will be trimmed.
modify_cross : bool, default True
If True, the cross beam will be extended to the opposite face of the main beam and cut with the same plane.

Attributes
----------
beams : list(:class:`~compas_timber.parts.Beam`)
The beams joined by this joint.
cutting_plane_main : :class:`~compas.geometry.Frame`
The frame by which the main beam is trimmed.
cutting_plane_cross : :class:`~compas.geometry.Frame`
The frame by which the cross beam is trimmed.
joint_type : str
A string representation of this joint's type.


"""

SUPPORTED_TOPOLOGY = JointTopology.TOPO_L

def __init__(self, main_beam=None, cross_beam=None, gap=0.0, frame=None, key=None, small_beam_butts=False):
super(LButtJoint, self).__init__(frame=frame, key=key)
def __init__(self, main_beam=None, cross_beam=None, small_beam_butts=False, modify_cross=True, **kwargs):
super(LButtJoint, self).__init__(**kwargs)

if small_beam_butts:
if small_beam_butts and main_beam and cross_beam:
if main_beam.width * main_beam.height > cross_beam.width * cross_beam.height:
main_beam, cross_beam = cross_beam, main_beam

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.gap = gap # float, additional gap, e.g. for glue
self.modify_cross = modify_cross
self.small_beam_butts = small_beam_butts
self.features = []

@property
def __data__(self):
data_dict = {
"main_beam_key": self.main_beam_key,
"cross_beam_key": self.cross_beam_key,
"gap": self.gap,
"small_beam_butts": self.small_beam_butts,
"modify_cross": self.modify_cross,
}
data_dict.update(super(LButtJoint, self).__data__)
return data_dict

@classmethod
def __from_data__(cls, value):
instance = cls(frame=Frame.__from_data__(value["frame"]), key=value["key"], gap=value["gap"])
instance = cls(
frame=Frame.__from_data__(value["frame"]),
key=value["key"],
small_beam_butts=value["small_beam_butts"],
modify_cross=value["modify_cross"],
)
instance.main_beam_key = value["main_beam_key"]
instance.cross_beam_key = value["cross_beam_key"]
return instance
Expand All @@ -79,14 +84,18 @@ def joint_type(self):
return "L-Butt"

def get_main_cutting_plane(self):
angles_faces = beam_side_incidence(self.main_beam, self.cross_beam)
cfr = min(angles_faces, key=lambda x: x[0])[1]
assert self.main_beam and self.cross_beam

index, cfr = self.get_face_most_ortho_to_beam(self.main_beam, self.cross_beam, ignore_ends=False)
if index in [5, 6]: # end faces
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my concern is, what happens if someone wants to make a butt joint with beam incidence angle of < 45deg? In this case, the joint will have one of the ends as cfr or index will be 5 or 6, even though the user wants to join in the old way with just the 4 main faces.

Should we add this as a kwarg in the GH component? it could be a cutoff angle with default of 45.

Copy link
Contributor Author

@chenkasirer chenkasirer Feb 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a sketch here would help.

but if I get it right though, what you're suggesting sounds like an l-miter, no?
otherwise, when considering only the 4 main faces like before this rework you would get something like this:
image

Basically L-Miter is the only joint we currently have that supports an I topology (knows how to handle end to end joining)

image

raise BeamJoinningError(beams=self.beams, joint=self, debug_info="Can't join to end faces")

cfr = Frame(cfr.point, cfr.xaxis, cfr.yaxis * -1.0) # flip normal
return cfr

def get_cross_cutting_plane(self):
angles_faces = beam_side_incidence(self.cross_beam, self.main_beam)
cfr = max(angles_faces, key=lambda x: x[0])[1]
assert self.main_beam and self.cross_beam
_, cfr = self.get_face_most_towards_beam(self.cross_beam, self.main_beam)
return cfr

def restore_beams_from_keys(self, assemly):
Expand All @@ -104,25 +113,32 @@ def add_features(self):

if self.features:
self.main_beam.remove_features(self.features)
start_main, start_cross = None, None

try:
main_cutting_plane = self.get_main_cutting_plane()
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 AttributeError as ae:
# I want here just the plane that caused the error
geometries = [cross_cutting_plane] if start_main is not None else [main_cutting_plane]
raise BeamJoinningError(beams=self.beams, joint=self, debug_info=str(ae), debug_geometries=geometries)
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
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)

if self.modify_cross:
self.cross_beam.add_blank_extension(
start_cross + extension_tolerance, end_cross + extension_tolerance, self.key
)
f_cross = CutFeature(self.get_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)
self.cross_beam.add_blank_extension(
start_cross + extension_tolerance, end_cross + extension_tolerance, self.key
)

f_main = CutFeature(main_cutting_plane)
self.main_beam.add_features(f_main)
self.features.append(f_main)

f_cross = CutFeature(self.get_cross_cutting_plane())
self.cross_beam.add_features(f_cross)
self.features.append(f_cross)
Loading
Loading