-
Notifications
You must be signed in to change notification settings - Fork 36
KButtJoint #731
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
Open
ericgozzi
wants to merge
8
commits into
KMiterJoint
Choose a base branch
from
KButtJoint
base: KMiterJoint
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
KButtJoint #731
Changes from 2 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
df0bd9b
KButtJoint
ericgozzi 01f207e
Merge branch 'KMiterJoint' into KButtJoint
ericgozzi c6eec03
Update k_miter.py
ericgozzi 0ed2520
Merge branch 'KButtJoint' of https://github.com/gramaziokohler/compas…
ericgozzi 070ee22
updating KBUtt
ericgozzi 8c8c5cb
Merge branch 'KMiterJoint' of https://github.com/gramaziokohler/compa…
ericgozzi 77ff458
remove extend_beam function in kbutt
ericgozzi af3691f
lint and format
ericgozzi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,320 @@ | ||
| from compas.geometry import Plane | ||
| from compas.geometry import Point | ||
| from compas.geometry import intersection_line_line | ||
| from compas.geometry import intersection_plane_plane | ||
|
|
||
| from compas_timber.elements import Beam | ||
| from compas_timber.fabrication import JackRafterCut | ||
| from compas_timber.fabrication import Lap | ||
| from compas_timber.fabrication import Pocket | ||
| from compas_timber.utils import polyhedron_from_box_planes | ||
|
|
||
| from .joint import Joint | ||
| from .joint import JointTopology | ||
| from .utilities import angle_and_dot_product_main_beam_and_cross_beam | ||
| from .utilities import are_beams_aligned_with_cross_vector | ||
| from .utilities import beam_ref_side_incidence | ||
| from .utilities import extend_beam_to_plane | ||
| from .utilities import parse_cross_beams_and_main_beams_from_cluster | ||
|
|
||
|
|
||
| class KButtJoint(Joint): | ||
| """Represents a K-Butt type joint which joins the ends of two beams along the length of another beam. | ||
|
|
||
| This joint type is compatible with beams in K topology. The two main beams are trimmed at their | ||
| ends to meet the cross beam. If the three beams are coplanar, the cross beam receives a | ||
| :class:`~compas_timber.fabrication.Pocket` feature; otherwise, the joint uses | ||
| :class:`~compas_timber.connections.t_butt.TButtJoint` features. | ||
|
ericgozzi marked this conversation as resolved.
Outdated
|
||
|
|
||
| Parameters | ||
| ---------- | ||
| cross_beam : :class:`~compas_timber.elements.Beam`, optional | ||
| The cross beam to be joined. This beam is connected along its length. | ||
| *main_beams : :class:`~compas_timber.elements.Beam` | ||
| The two main beams to be joined. These beams connect at their ends to the cross beam. | ||
| mill_depth : float, optional | ||
| The depth of the pocket to be milled in the cross beam, by default 0. | ||
| force_pocket : bool, optional | ||
| If True, forces the use of a `Pocket` BTLx processing instead of a `Lap` in the cross beam. Default is False. | ||
| conical_tool : bool, optional | ||
| If True, the pocket allows overhangs that require a conical tool to be milled. Default is False. | ||
| **kwargs : dict | ||
| Additional keyword arguments passed to the parent :class:`~compas_timber.connections.Joint` class. | ||
|
|
||
| Attributes | ||
| ---------- | ||
| cross_beam : :class:`~compas_timber.elements.Beam` | ||
| The cross beam to be joined. | ||
| main_beams : list of :class:`~compas_timber.elements.Beam` | ||
| The two main beams to be joined. | ||
| main_beam_a : :class:`~compas_timber.elements.Beam` | ||
| The first main beam. | ||
| main_beam_b : :class:`~compas_timber.elements.Beam` | ||
| The second main beam. | ||
| mill_depth : float | ||
| The depth of the pocket to be milled in the cross beam. | ||
| force_pocket : bool | ||
| If True, forces the use of a `Pocket` BTLx processing instead of a `Lap` in the cross beam. Default is False. | ||
| conical_tool : bool | ||
| If True, the pocket allows overhangs that require a conical tool to be milled. Default is False. | ||
| cross_beam_guid : str | ||
| The GUID of the cross beam. | ||
| main_beams_guids : list of str | ||
| The GUIDs of the main beams. | ||
| features : list | ||
| List of fabrication features added to the beams. | ||
|
|
||
| """ | ||
|
|
||
| SUPPORTED_TOPOLOGY = JointTopology.TOPO_K | ||
| MIN_ELEMENT_COUNT = 3 | ||
| MAX_ELEMENT_COUNT = 999 | ||
|
|
||
| @property | ||
| def __data__(self): | ||
| data = super().__data__ | ||
| data["cross_beam_guid"] = self.cross_beam_guid | ||
| data["main_beams_guids"] = self.main_beams_guids | ||
| data["mill_depth"] = self.mill_depth | ||
| data["force_pocket"] = self.force_pocket | ||
| data["conical_tool"] = self.conical_tool | ||
| return data | ||
|
|
||
| def __init__(self, cross_beam: Beam = None, *main_beams: Beam, mill_depth: float = 0, force_pocket=False, conical_tool=False, **kwargs): | ||
| super().__init__( | ||
| elements=(main_beams + (cross_beam,)), | ||
| **kwargs, | ||
| ) | ||
|
|
||
| self.cross_beam = cross_beam | ||
| self.main_beams = list(main_beams) | ||
| self.cross_beam_guid = kwargs.get("cross_beam_guid", None) or str(cross_beam.guid) | ||
| self.main_beams_guids = [str(beam.guid) for beam in main_beams] | ||
| self.mill_depth = mill_depth | ||
| self.force_pocket = force_pocket | ||
| self.conical_tool = conical_tool | ||
| self.features = [] | ||
|
|
||
| @property | ||
| def beams(self): | ||
| return [self.cross_beam] + self.main_beams | ||
|
|
||
| @property | ||
| def elements(self): | ||
| return self.beams | ||
|
|
||
|
ericgozzi marked this conversation as resolved.
|
||
| @property | ||
| def are_beams_coplanar(self): | ||
| if ( | ||
| are_beams_aligned_with_cross_vector(self.elements[0], self.elements[1]) | ||
| and are_beams_aligned_with_cross_vector(self.elements[1], self.elements[2]) | ||
| and are_beams_aligned_with_cross_vector(self.elements[0], self.elements[2]) | ||
| ): | ||
| return True | ||
| return False | ||
|
|
||
| @property | ||
| def location(self): | ||
| return Point(*(intersection_line_line(self.main_beams[0].centerline, self.main_beams[-1].centerline)[0])) | ||
|
|
||
| @classmethod | ||
| def promote_cluster(cls, model, cluster, reordered_elements=None, **kwargs): | ||
| """Create an instance of this joint from a cluster of elements. | ||
| Automatically parse the elements in the cluster to identify the cross beam and the two main beams. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| model : :class:`~compas_timber.model.Model` | ||
| The model to which the joint will be added. | ||
| cluster : :class:`~compas_timber.clusters.BeamCluster` | ||
| The cluster of beams to be used to create the joint. | ||
| reordered_elements : list of :class:`~compas_timber.elements.Beam`, optional | ||
| The elements in the order required by the joint. If not provided, the elements in the cluster will be used. | ||
| **kwargs : dict | ||
| Additional keyword arguments passed to the joint constructor. | ||
|
|
||
| Returns | ||
| ------- | ||
| :class:`~compas_timber.connections.KButtJoint` | ||
| The created joint instance. | ||
| """ | ||
| cross_beams, main_beams = parse_cross_beams_and_main_beams_from_cluster(cluster) | ||
| elements = list(cross_beams) + list(main_beams) | ||
| return cls.create(model, *elements, **kwargs) | ||
|
|
||
|
ericgozzi marked this conversation as resolved.
|
||
| def cross_beam_ref_side_index(self, beam): | ||
| ref_side_dict = beam_ref_side_incidence(beam, self.cross_beam, ignore_ends=True) | ||
| ref_side_index = min(ref_side_dict, key=ref_side_dict.get) | ||
| return ref_side_index | ||
|
|
||
| def main_beam_ref_side_index(self, beam): | ||
| ref_side_dict = beam_ref_side_incidence(self.cross_beam, beam, ignore_ends=True) | ||
| ref_side_index = min(ref_side_dict, key=ref_side_dict.get) | ||
| return ref_side_index | ||
|
|
||
| def beam_relative_side_to_beam(self, beam, ref_beam): | ||
| ref_side_dict = beam_ref_side_incidence(ref_beam, beam, ignore_ends=True) | ||
| ref_side_index = min(ref_side_dict, key=ref_side_dict.get) | ||
| return ref_side_index | ||
|
|
||
| def add_extensions(self): | ||
| """ | ||
| Calculates and adds the necessary extensions to the main beams. | ||
| It accounts for the mill depth in the cross beam. | ||
|
|
||
| This method is automatically called when the joint is created by the call to `Joint.create()`. | ||
|
|
||
| Raises | ||
| ------ | ||
| BeamJoiningError | ||
| If the extension could not be calculated. | ||
| """ | ||
| assert self.main_beams and self.cross_beam | ||
| for beam in self.main_beams: | ||
| ref_side_dict = beam_ref_side_incidence(beam, self.cross_beam, ignore_ends=True) | ||
| cross_beam_ref_side_index = min(ref_side_dict, key=ref_side_dict.get) | ||
| cutting_plane = self.cross_beam.ref_sides[cross_beam_ref_side_index] | ||
| if self.mill_depth: | ||
| cutting_plane.translate(-cutting_plane.normal * self.mill_depth) | ||
| extend_beam_to_plane(beam, cutting_plane) | ||
|
|
||
| def add_features(self): | ||
| """ | ||
| Adds the required extension and trimming features to the three beams. | ||
|
|
||
| This method is called by `model.process_joinery()`. | ||
| """ | ||
| assert self.main_beams and self.cross_beam | ||
|
|
||
| # self.main_beams is sorted based on their dot product with the cross beam direction | ||
| sorted_angles, sorted_dots = self._sort_main_beams() | ||
|
|
||
| # processings on cross_beam | ||
| if self.force_pocket and self.mill_depth: | ||
| milling_volume = self._pocket_milling_volume() | ||
| pocket = Pocket.from_volume_and_element( | ||
| milling_volume, self.cross_beam, allow_undercut=self.conical_tool, ref_side_index=self.cross_beam_ref_side_index(self.main_beams[0]) | ||
| ) | ||
| self.cross_beam.add_feature(pocket) | ||
| self.features.append(pocket) | ||
|
|
||
| elif self.mill_depth: | ||
| milling_volume = self._lap_milling_volume() | ||
| lap = Lap.from_volume_and_beam(milling_volume, self.cross_beam) | ||
| self.cross_beam.add_feature(lap) | ||
| self.features.append(lap) | ||
|
|
||
| # processings on beams | ||
| cross_plane = Plane.from_frame(self.cross_beam.ref_sides[self.beam_relative_side_to_beam(self.cross_beam, self.main_beams[0])]) | ||
| if self.mill_depth: | ||
| cross_plane.translate(-cross_plane.normal * self.mill_depth) | ||
| cross_plane.normal *= -1 # flip normal to point towards the main beams | ||
|
|
||
| for i, beam in enumerate(self.main_beams): | ||
| jrc1 = JackRafterCut.from_plane_and_beam(cross_plane, beam) | ||
| beam.add_feature(jrc1) | ||
| self.features.append(jrc1) | ||
|
|
||
| if i > 0: | ||
| prev_beam = self.main_beams[i - 1] | ||
| beam_plane = Plane.from_frame(prev_beam.ref_sides[self.beam_relative_side_to_beam(prev_beam, beam)]) | ||
| beam_plane.normal *= -1 # flip normal to point towards the cross beam | ||
| jrc2 = JackRafterCut.from_plane_and_beam(beam_plane, beam) | ||
| beam.add_feature(jrc2) | ||
| self.features.append(jrc2) | ||
|
|
||
| def _sort_main_beams(self): | ||
| angles = [] | ||
| dots = [] | ||
| for main_beam in self.main_beams: | ||
| angle, dot = angle_and_dot_product_main_beam_and_cross_beam(main_beam, self.cross_beam, self) | ||
| angles.append(angle) | ||
| dots.append(dot) | ||
| # Sort main_beams based on dots (ascending order) | ||
| sorted_indices = sorted(range(len(dots)), key=lambda i: dots[i]) | ||
| sorted_beams = [self.main_beams[i] for i in sorted_indices] | ||
| sorted_angles = [angles[i] for i in sorted_indices] | ||
| sorted_dots = [dots[i] for i in sorted_indices] | ||
| self.main_beams = sorted_beams | ||
| return sorted_angles, sorted_dots | ||
|
|
||
| def _pocket_milling_volume(self): | ||
| first_main_beam = self.main_beams[0] | ||
| last_main_beam = self.main_beams[-1] | ||
|
|
||
| # first beam and last beam have to be on the same side of the cross beam | ||
| ref_side_dict = beam_ref_side_incidence(first_main_beam, self.cross_beam, ignore_ends=True) | ||
| cross_beam_ref_side_index = min(ref_side_dict, key=ref_side_dict.get) | ||
| cross_plane = self.cross_beam.ref_sides[cross_beam_ref_side_index] | ||
| cross_plane_next = Plane.from_frame(self.cross_beam.ref_sides[(cross_beam_ref_side_index + 1) % 4]) | ||
| cross_plane_prev = Plane.from_frame(self.cross_beam.ref_sides[(cross_beam_ref_side_index - 1) % 4]) | ||
|
|
||
| # first_plane | ||
| ref_side_dict = beam_ref_side_incidence(self.cross_beam, first_main_beam, ignore_ends=True) | ||
| first_main_beam_ref_side_index = (min(ref_side_dict, key=ref_side_dict.get) + 2) % 4 | ||
| first_plane = Plane.from_frame(first_main_beam.ref_sides[first_main_beam_ref_side_index]) | ||
|
|
||
| # last_plane | ||
| ref_side_dict = beam_ref_side_incidence(self.cross_beam, last_main_beam) | ||
| last_main_beam_ref_side_index = min(ref_side_dict, key=ref_side_dict.get) | ||
| last_plane = Plane.from_frame(last_main_beam.ref_sides[last_main_beam_ref_side_index]) | ||
|
|
||
| # adjust mill depth | ||
| if self.mill_depth: | ||
| cutting_plane = Plane.from_frame(cross_plane.translated(-cross_plane.normal * self.mill_depth)) | ||
| else: | ||
| cutting_plane = Plane.from_frame(cross_plane) | ||
|
|
||
| # make a plane | ||
| cross_plane = Plane.from_frame(cross_plane) | ||
|
|
||
| milling_volume = polyhedron_from_box_planes(cross_plane, cutting_plane, first_plane, last_plane, cross_plane_next, cross_plane_prev) | ||
| return milling_volume | ||
|
|
||
| def _lap_milling_volume(self): | ||
| first_main_beam = self.main_beams[0] | ||
| last_main_beam = self.main_beams[-1] | ||
|
|
||
| # first beam and last beam have to be on the same side of the cross beam | ||
| ref_side_dict = beam_ref_side_incidence(first_main_beam, self.cross_beam, ignore_ends=True) | ||
| cross_beam_ref_side_index = min(ref_side_dict, key=ref_side_dict.get) | ||
| cross_frame = self.cross_beam.ref_sides[cross_beam_ref_side_index] | ||
| cross_plane = Plane.from_frame(cross_frame) | ||
| cross_plane_prev = Plane.from_frame(self.cross_beam.ref_sides[(cross_beam_ref_side_index - 1) % 4]) | ||
| cross_plane_next = Plane.from_frame(self.cross_beam.ref_sides[(cross_beam_ref_side_index + 1) % 4]) | ||
|
|
||
| # adjust mill depth | ||
| if self.mill_depth: | ||
| cutting_plane = Plane.from_frame(cross_frame.translated(-cross_frame.normal * self.mill_depth)) | ||
| else: | ||
| cutting_plane = Plane.from_frame(cross_frame) | ||
|
|
||
| # first_plane | ||
| ref_side_dict = beam_ref_side_incidence(self.cross_beam, first_main_beam, ignore_ends=True) | ||
| first_main_beam_ref_side_index = (min(ref_side_dict, key=ref_side_dict.get) + 2) % 4 | ||
| first_plane = Plane.from_frame(first_main_beam.ref_sides[first_main_beam_ref_side_index]) | ||
| exterior_first_plane = first_plane.copy() | ||
| exterior_first_plane.point = Point(*intersection_plane_plane(cross_plane, first_plane)[0]) | ||
| exterior_first_plane.normal = self.cross_beam.centerline.direction | ||
| interior_first_plane = first_plane.copy() | ||
| interior_first_plane.point = Point(*intersection_plane_plane(cutting_plane, first_plane)[0]) | ||
| interior_first_plane.normal = self.cross_beam.centerline.direction | ||
|
|
||
| # last_plane | ||
| ref_side_dict = beam_ref_side_incidence(self.cross_beam, last_main_beam) | ||
| last_main_beam_ref_side_index = min(ref_side_dict, key=ref_side_dict.get) | ||
| last_plane = Plane.from_frame(last_main_beam.ref_sides[last_main_beam_ref_side_index]) | ||
| exterior_last_plane = last_plane.copy() | ||
| exterior_last_plane.point = Point(*intersection_plane_plane(cross_plane, last_plane)[0]) | ||
| exterior_last_plane.normal = self.cross_beam.centerline.direction | ||
| interior_last_plane = last_plane.copy() | ||
| interior_last_plane.point = Point(*intersection_plane_plane(cutting_plane, last_plane)[0]) | ||
| interior_last_plane.normal = self.cross_beam.centerline.direction | ||
|
|
||
| planes = [exterior_first_plane, interior_first_plane, exterior_last_plane, interior_last_plane] | ||
| planes = sorted(planes, key=lambda plane: plane.point.distance_to_point(self.cross_beam.centerline.start)) | ||
| first_plane = planes[0] | ||
| last_plane = planes[-1] | ||
|
|
||
| milling_volume = polyhedron_from_box_planes(cross_plane, cutting_plane, first_plane, last_plane, cross_plane_next, cross_plane_prev) | ||
| return milling_volume | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.