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

X/T/L HalfLap rework & Lap Volume viz adjustment #340

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4ab8a83
init
papachap Nov 27, 2024
bded00a
wip
papachap Nov 29, 2024
d6f3588
adjusted volume from params
papachap Nov 29, 2024
8f140ff
wip
papachap Dec 3, 2024
07ad0f3
planar halflaps
papachap Dec 3, 2024
f0874c1
viz issues lap
papachap Dec 4, 2024
28fa6af
working planar halflaps
papachap Dec 4, 2024
35ac110
wip l_halflap
papachap Dec 4, 2024
be23543
adjust cutting plane definition
papachap Dec 4, 2024
f898fb2
l_half lap impl
papachap Dec 5, 2024
c26ad5a
t/x/l halflap
papachap Dec 5, 2024
de5934d
lint & format
papachap Dec 5, 2024
1184f0f
frame to plane adjustment
papachap Dec 5, 2024
0be1d7b
Merge branch 'main' of https://github.com/gramaziokohler/compas_timbe…
papachap Dec 5, 2024
1eae1fa
changelog
papachap Dec 5, 2024
3127da6
unittest adjustment due to attribute refactoring
papachap Dec 5, 2024
c49c995
lap volume is now mesh
papachap Dec 5, 2024
ab4e132
minor t_butt debug
papachap Dec 6, 2024
1a81b8f
update lap unittest
papachap Dec 6, 2024
f039ede
docstring update
papachap Dec 6, 2024
4c4dea4
self.beams => self.elements
papachap Dec 10, 2024
4c581eb
Merge branch 'main' of https://github.com/gramaziokohler/compas_timbe…
papachap Dec 10, 2024
5a03245
check_geometry => check_elements_compatibility
papachap Dec 10, 2024
d407df4
Merge branch 'main' of https://github.com/gramaziokohler/compas_timbe…
papachap Jan 6, 2025
53f9f78
calculate cutting planes once
papachap Jan 6, 2025
a4bdf03
relative imports
papachap Jan 6, 2025
c7ffd96
new Tolerance istance for modifying its properties
papachap Jan 6, 2025
8c1882b
use frame normals directly for compatibility check
papachap Jan 6, 2025
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Fixed `JointRuleFromList` GH component.
* Changed `TButtJoint` to take an optional `PlateFastener`.
* Moved `FeatureApplicationError`, `BeamJoinningError`, and `FastenerApplicationError` to `errors.__init__.py`.
* Changed `compas_timber._fabrication.Lap` so that the volume is generated fully from the relevant BTLx params.
* Changed `THalfLapJoint`, `LHalfLapJoint`, `XHalfLapJoint` from `compas_timber.connections` so that they use the Lap process.

### Removed

Expand Down
7,535 changes: 7,535 additions & 0 deletions examples/Grasshopper/tests/test_halflap.ghx

Large diffs are not rendered by default.

3,570 changes: 3,116 additions & 454 deletions examples/Grasshopper/tests/test_lap.ghx

Large diffs are not rendered by default.

17 changes: 16 additions & 1 deletion src/compas_timber/_fabrication/btlx_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,20 +165,33 @@ class MachiningLimits(object):
Limit the front face.
face_limited_back : bool
Limit the back face.
face_limited_top : bool
Limit the top face.
face_limited_bottom : bool
Limit the bottom face.

Properties
----------
limits : dict
The limits dictionary with values as a boolean.
"""

EXPECTED_KEYS = ["FaceLimitedStart", "FaceLimitedEnd", "FaceLimitedFront", "FaceLimitedBack"]
EXPECTED_KEYS = [
"FaceLimitedStart",
"FaceLimitedEnd",
"FaceLimitedFront",
"FaceLimitedBack",
"FaceLimitedTop",
"FaceLimitedBottom",
]

def __init__(self):
self.face_limited_start = True
self.face_limited_end = True
self.face_limited_front = True
self.face_limited_back = True
self.face_limited_top = True
self.face_limited_bottom = True

@property
def limits(self):
Expand All @@ -188,6 +201,8 @@ def limits(self):
"FaceLimitedEnd": self.face_limited_end,
"FaceLimitedFront": self.face_limited_front,
"FaceLimitedBack": self.face_limited_back,
"FaceLimitedTop": self.face_limited_top,
"FaceLimitedBottom": self.face_limited_bottom,
}


Expand Down
250 changes: 187 additions & 63 deletions src/compas_timber/_fabrication/lap.py

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/compas_timber/connections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from .t_dovetail import TDovetailJoint
from .t_tenon_mortise import TenonMortiseJoint
from .ball_node import BallNodeJoint
from .utilities import beam_ref_side_incidence
from .utilities import beam_ref_side_incidence_with_vector

__all__ = [
"Joint",
Expand All @@ -39,4 +41,6 @@
"TDovetailJoint",
"BallNodeJoint",
"TenonMortiseJoint",
"beam_ref_side_incidence",
"beam_ref_side_incidence_with_vector",
]
7 changes: 6 additions & 1 deletion src/compas_timber/connections/l_butt.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,12 @@ def add_features(self):
cross_cutting_plane = self.main_beam.ref_sides[self.main_beam_ref_side_index]
lap_width = self.main_beam.height if self.main_beam_ref_side_index % 2 == 0 else self.main_beam.width
cross_feature = Lap.from_plane_and_beam(
cross_cutting_plane, self.cross_beam, lap_width, self.mill_depth, self.cross_beam_ref_side_index
cross_cutting_plane,
self.cross_beam,
lap_width,
self.mill_depth,
is_pocket=True,
ref_side_index=self.cross_beam_ref_side_index,
)
self.cross_beam.add_features(cross_feature)
self.features.append(cross_feature)
Expand Down
216 changes: 161 additions & 55 deletions src/compas_timber/connections/l_halflap.py
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.errors import BeamJoinningError

from .lap_joint import LapJoint
from .joint import Joint
from .solver import JointTopology
from .utilities import beam_ref_side_incidence
from .utilities import beam_ref_side_incidence_with_vector


class LHalfLapJoint(LapJoint):
class LHalfLapJoint(Joint):
"""Represents a L-Lap type joint which joins the ends of two beams,
Copy link
Contributor

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?

Copy link
Contributor Author

@papachap papachap Jan 6, 2025

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 the XHalfLapJoint, LHalfLapJoint, and THalfLapJoint into one class—maybe call it HalfLapJoint or just LapJoint (same thing that we are doing for the TenonMortiseJoint). Since SUPPORTED_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?

trimming the main beam.

Expand All @@ -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
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

these look very similar to the ones above, are they the same.
if we don't want these in a parent class, maybe they could be moved into a new higher level function(s) in our joinery toolkit modules where they return what's needed (index of surface) instead of the dictionary.

Then they can be called once in add_features this will be more readable, will have less duplication code and will potentially be more performant as some of these properties end up being called multiple times.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 Lap Processing of each beam, and the second pair is for calculating the planes needed for the alternative constructor of the Lap class. Even though each pair uses a different function to figure out the incidence, the two properties in each pair use the same function, so there’s some code repetition.

I’m not really a fan of generalizing this into something like get_ref_side_index() or get_cutting_plane() since each joint has its own way of calculating these values. It could make things less flexible and harder to adapt for different joint types.

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.
Expand All @@ -62,43 +110,101 @@ 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
cutting_plane_a = self.cutting_plane_a.to_plane()
cutting_plane_b = self.cutting_plane_b.to_plane()
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(cutting_plane_a)
start_b, end_b = self.beam_b.extension_to_plane(cutting_plane_b)
except AttributeError as ae:
# I want here just the plane that caused the error
geometries = [cutting_plane_b] if start_a is not None else [cutting_plane_a]
raise BeamJoinningError(self.elements, self, debug_info=str(ae), debug_geometries=geometries)
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.
"""
normal_a = self.beam_a.frame.normal.unitized()
normal_b = self.beam_b.frame.normal.unitized()
# calculate the dot product of the two normals
dot = abs(normal_a.dot(normal_b))
if not (TOL.is_zero(dot) or TOL.is_close(dot, 1)):
raise BeamJoinningError(
self.elements,
self,
debug_info="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)
8 changes: 5 additions & 3 deletions src/compas_timber/connections/t_butt.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,15 @@ def add_features(self):
# apply the pocket on the cross beam
if self.mill_depth:
cross_cutting_plane = self.main_beam.ref_sides[self.main_beam_ref_side_index]
lap_width = self.main_beam.height if self.main_beam_ref_side_index % 2 == 0 else self.main_beam.width
lap_length = self.main_beam.height if self.main_beam_ref_side_index % 2 == 0 else self.main_beam.width

cross_feature = Lap.from_plane_and_beam(
cross_cutting_plane,
self.cross_beam,
lap_width,
lap_length,
self.mill_depth,
self.cross_beam_ref_side_index,
is_pocket=True,
ref_side_index=self.cross_beam_ref_side_index,
)
self.cross_beam.add_features(cross_feature)
self.features.append(cross_feature)
Expand Down
Loading
Loading