-
Notifications
You must be signed in to change notification settings - Fork 26
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
X/T/L HalfLap rework & Lap Volume viz adjustment #340
base: main
Are you sure you want to change the base?
Changes from 23 commits
4ab8a83
bded00a
d6f3588
8f140ff
07ad0f3
f0874c1
28fa6af
35ac110
be23543
f898fb2
c26ad5a
de5934d
1184f0f
0be1d7b
1eae1fa
3127da6
c49c995
ab4e132
1a81b8f
f039ede
4c4dea4
4c581eb
5a03245
d407df4
53f9f78
a4bdf03
c7ffd96
8c1882b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,16 @@ | ||
from compas.geometry import Frame | ||
from compas.tolerance import TOL | ||
|
||
from compas_timber.elements import CutFeature | ||
from compas_timber.elements import MillVolume | ||
from compas_timber._fabrication import JackRafterCut | ||
from compas_timber._fabrication import Lap | ||
from compas_timber.connections.utilities import beam_ref_side_incidence | ||
from compas_timber.connections.utilities import beam_ref_side_incidence_with_vector | ||
|
||
from .joint import BeamJoinningError | ||
from .lap_joint import LapJoint | ||
from .joint import Joint | ||
from .solver import JointTopology | ||
|
||
|
||
class LHalfLapJoint(LapJoint): | ||
class LHalfLapJoint(Joint): | ||
"""Represents a L-Lap type joint which joins the ends of two beams, | ||
trimming the main beam. | ||
|
||
|
@@ -18,10 +20,10 @@ class LHalfLapJoint(LapJoint): | |
|
||
Parameters | ||
---------- | ||
main_beam : :class:`~compas_timber.parts.Beam` | ||
The main beam to be joined. | ||
cross_beam : :class:`~compas_timber.parts.Beam` | ||
The cross beam to be joined. | ||
beam_a : :class:`~compas_timber.parts.Beam` | ||
The first beam to be joined. | ||
beam_b : :class:`~compas_timber.parts.Beam` | ||
The second beam to be joined. | ||
flip_lap_side : bool | ||
If True, the lap is flipped to the other side of the beams. | ||
cut_plane_bias : float | ||
|
@@ -31,25 +33,71 @@ class LHalfLapJoint(LapJoint): | |
---------- | ||
beams : list(:class:`~compas_timber.parts.Beam`) | ||
The beams joined by this joint. | ||
main_beam : :class:`~compas_timber.parts.Beam` | ||
The main beam to be joined. | ||
cross_beam : :class:`~compas_timber.parts.Beam` | ||
The cross beam to be joined. | ||
main_beam_key : str | ||
The key of the main beam. | ||
cross_beam_key : str | ||
The key of the cross beam. | ||
features : list(:class:`~compas_timber.parts.Feature`) | ||
The features created by this joint. | ||
joint_type : str | ||
A string representation of this joint's type. | ||
|
||
beam_a : :class:`~compas_timber.parts.Beam` | ||
The first beam to be joined. | ||
beam_b : :class:`~compas_timber.parts.Beam` | ||
The second beam to be joined. | ||
flip_lap_side : bool | ||
If True, the lap is flipped to the other side of the beams. | ||
cut_plane_bias : float | ||
Allows lap to be shifted deeper into one beam or the other. Value should be between 0 and 1.0 without completely cutting through either beam. Default is 0.5. | ||
""" | ||
|
||
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) | ||
@property | ||
def __data__(self): | ||
data = super(LHalfLapJoint, self).__data__ | ||
data["beam_a"] = self.beam_a_guid | ||
data["beam_b"] = self.beam_b_guid | ||
data["flip_lap_side"] = self.flip_lap_side | ||
data["cut_plane_bias"] = self.cut_plane_bias | ||
return data | ||
|
||
def __init__(self, beam_a=None, beam_b=None, flip_lap_side=None, cut_plane_bias=None, **kwargs): | ||
super(LHalfLapJoint, self).__init__(**kwargs) | ||
self.beam_a = beam_a | ||
self.beam_b = beam_b | ||
self.beam_a_guid = kwargs.get("beam_a_guid", None) or str(beam_a.guid) | ||
self.beam_b_guid = kwargs.get("beam_b_guid", None) or str(beam_b.guid) | ||
|
||
self.flip_lap_side = flip_lap_side | ||
self.cut_plane_bias = 0.5 if cut_plane_bias is None else cut_plane_bias | ||
self.features = [] | ||
|
||
@property | ||
def elements(self): | ||
return [self.beam_a, self.beam_b] | ||
|
||
@property | ||
def beam_a_ref_side_index(self): | ||
cross_vector = self.beam_a.centerline.direction.cross(self.beam_b.centerline.direction) | ||
ref_side_dict = beam_ref_side_incidence_with_vector(self.beam_a, cross_vector, ignore_ends=True) | ||
if self.flip_lap_side: | ||
return max(ref_side_dict, key=ref_side_dict.get) | ||
return min(ref_side_dict, key=ref_side_dict.get) | ||
|
||
@property | ||
def beam_b_ref_side_index(self): | ||
cross_vector = self.beam_a.centerline.direction.cross(self.beam_b.centerline.direction) | ||
ref_side_dict = beam_ref_side_incidence_with_vector(self.beam_b, cross_vector, ignore_ends=True) | ||
if self.flip_lap_side: | ||
return min(ref_side_dict, key=ref_side_dict.get) | ||
return max(ref_side_dict, key=ref_side_dict.get) | ||
|
||
@property | ||
def cutting_plane_a(self): | ||
# the plane that cuts beam_a as a planar surface | ||
ref_side_dict = beam_ref_side_incidence(self.beam_a, self.beam_b, ignore_ends=True) | ||
ref_side_index = max(ref_side_dict, key=ref_side_dict.get) | ||
return self.beam_b.side_as_surface(ref_side_index) | ||
|
||
@property | ||
def cutting_plane_b(self): | ||
# the plane that cuts beam_b as a planar surface | ||
ref_side_dict = beam_ref_side_incidence(self.beam_b, self.beam_a, ignore_ends=True) | ||
ref_side_index = max(ref_side_dict, key=ref_side_dict.get) | ||
return self.beam_a.side_as_surface(ref_side_index) | ||
|
||
Comment on lines
+72
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these look very similar to the ones above, are they the same. Then they can be called once in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, there’s definitely some redundancy here. The first pair of properties is for getting the ref_side for the I’m not really a fan of generalizing this into something like What I’d suggest is converting these properties into lists and iterating through the beams to calculate the planes for each one. That would clean up the code, cut down on duplication, and make it easier to read. what do you think? |
||
def add_extensions(self): | ||
"""Calculates and adds the necessary extensions to the beams. | ||
|
@@ -62,43 +110,100 @@ def add_extensions(self): | |
If the extension could not be calculated. | ||
|
||
""" | ||
assert self.main_beam and self.cross_beam | ||
assert self.beam_a and self.beam_b | ||
start_a, start_b = None, None | ||
try: | ||
main_cutting_frame = self.get_main_cutting_frame() | ||
cross_cutting_frame = self.get_cross_cutting_frame() | ||
start_a, end_a = self.beam_a.extension_to_plane(self.cutting_plane_a.to_plane()) | ||
start_b, end_b = self.beam_b.extension_to_plane(self.cutting_plane_b.to_plane()) | ||
except AttributeError as ae: | ||
# I want here just the plane that caused the error | ||
geometries = [self.cutting_plane_b] if start_a is not None else [self.cutting_plane_a] | ||
raise BeamJoinningError(self.elements, self, debug_info=str(ae), debug_geometries=geometries) | ||
papachap marked this conversation as resolved.
Show resolved
Hide resolved
|
||
except Exception as ex: | ||
raise BeamJoinningError(beams=self.elements, 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.guid) | ||
self.cross_beam.add_blank_extension( | ||
start_cross + extension_tolerance, end_cross + extension_tolerance, self.guid | ||
) | ||
raise BeamJoinningError(self.elements, self, debug_info=str(ex)) | ||
self.beam_a.add_blank_extension(start_a, end_a, self.beam_a_guid) | ||
self.beam_b.add_blank_extension(start_b, end_b, self.beam_b_guid) | ||
|
||
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() | ||
negative_brep_main_beam, negative_brep_cross_beam = self._create_negative_volumes() | ||
except Exception as ex: | ||
raise BeamJoinningError(beams=self.elements, joint=self, debug_info=str(ex)) | ||
"""Adds the required joint features to both beams. | ||
|
||
main_volume = MillVolume(negative_brep_main_beam) | ||
cross_volume = MillVolume(negative_brep_cross_beam) | ||
This method is automatically called when joint is created by the call to `Joint.create()`. | ||
|
||
self.main_beam.add_features(main_volume) | ||
self.cross_beam.add_features(cross_volume) | ||
""" | ||
assert self.beam_a and self.beam_b | ||
|
||
if self.features: | ||
self.beam_a.remove_features(self.features) | ||
self.beam_b.remove_features(self.features) | ||
|
||
# calculate the lap length and depth for each beam | ||
beam_a_lap_length, beam_b_lap_length = self._get_lap_lengths() | ||
beam_a_lap_depth, beam_b_lap_depth = self._get_lap_depths() | ||
|
||
## beam_a | ||
# lap feature on beam_a | ||
lap_feature_a = Lap.from_plane_and_beam( | ||
self.cutting_plane_a.to_plane(), | ||
self.beam_a, | ||
beam_a_lap_length, | ||
beam_a_lap_depth, | ||
ref_side_index=self.beam_a_ref_side_index, | ||
) | ||
# cutoff feature for beam_a | ||
cutoff_feature_a = JackRafterCut.from_plane_and_beam( | ||
self.cutting_plane_a.to_plane(), self.beam_a, self.beam_a_ref_side_index | ||
) | ||
beam_a_features = [lap_feature_a, cutoff_feature_a] | ||
self.beam_a.add_features(beam_a_features) | ||
self.features.extend(beam_a_features) | ||
|
||
## beam_b | ||
# lap feature on beam_b | ||
lap_feature_b = Lap.from_plane_and_beam( | ||
self.cutting_plane_b.to_plane(), | ||
self.beam_b, | ||
beam_b_lap_length, | ||
beam_b_lap_depth, | ||
ref_side_index=self.beam_b_ref_side_index, | ||
) | ||
# cutoff feature for beam_b | ||
cutoff_feature_b = JackRafterCut.from_plane_and_beam( | ||
self.cutting_plane_b.to_plane(), self.beam_b, self.beam_b_ref_side_index | ||
) | ||
beam_b_features = [lap_feature_b, cutoff_feature_b] | ||
self.beam_b.add_features(beam_b_features) | ||
self.features.extend(beam_b_features) | ||
|
||
f_cross = CutFeature(cross_cutting_frame) | ||
self.cross_beam.add_features(f_cross) | ||
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.element_by_guid(self.beam_a_guid) | ||
self.beam_b = model.element_by_guid(self.beam_b_guid) | ||
|
||
trim_frame = Frame(main_cutting_frame.point, main_cutting_frame.xaxis, -main_cutting_frame.yaxis) | ||
f_main = CutFeature(trim_frame) | ||
self.main_beam.add_features(f_main) | ||
def check_elements_compatibility(self): | ||
"""Checks if the elements are compatible for the creation of the joint. | ||
|
||
self.features = [main_volume, cross_volume, f_main, f_cross] | ||
Raises | ||
------ | ||
BeamJoinningError | ||
If the elements are not compatible for the creation of the joint. | ||
""" | ||
# check if the beams are aligned | ||
for beam in self.elements: | ||
cross_vector = self.beam_a.centerline.direction.cross(self.beam_b.centerline.direction) | ||
papachap marked this conversation as resolved.
Show resolved
Hide resolved
|
||
beam_normal = beam.frame.normal.unitized() | ||
dot = abs(beam_normal.dot(cross_vector.unitized())) | ||
if not (TOL.is_zero(dot) or TOL.is_close(dot, 1)): | ||
raise BeamJoinningError( | ||
self.elements, | ||
self, | ||
debug_info="The the two beams are not aligned to create a Half Lap joint.", | ||
) | ||
|
||
def _get_lap_lengths(self): | ||
lap_a_length = self.beam_b.side_as_surface(self.beam_b_ref_side_index).ysize | ||
lap_b_length = self.beam_a.side_as_surface(self.beam_a_ref_side_index).ysize | ||
return lap_a_length, lap_b_length | ||
|
||
def _get_lap_depths(self): | ||
lap_depth = (self.cutting_plane_a.ysize + self.cutting_plane_b.ysize) / 2 | ||
return lap_depth * self.cut_plane_bias, lap_depth * (1 - self.cut_plane_bias) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so are we sure we want to get rid of the
LapJoint
class? discussed with @obucklin as well?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think we can drop the current
LapJoint
class since it’s no longer relevant or needed. To keep things cleaner and avoid redundancy, we should combine theXHalfLapJoint
,LHalfLapJoint
, andTHalfLapJoint
into one class—maybe call it HalfLapJoint or just LapJoint (same thing that we are doing for theTenonMortiseJoint
). SinceSUPPORTED_TOPOLOGY
is already a list, with some adjustments we can handle all the topologies (X, L, T) in the same class and avoid repeating the same logic in three places.@obucklin @chenkasirer what do you guys think?