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

implemented half lap joint #118

Merged
merged 19 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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 @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* Added new joint type: Half-lap joint.

### Changed

* Beam transformed geometry with features is available using property `geometry`.
Expand Down
3 changes: 3 additions & 0 deletions src/compas_timber/connections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
TButtJoint
LButtJoint
LMiterJoint
XHalfLapJoint
JointTopology
ConnectionSolver

Expand Down Expand Up @@ -47,6 +48,7 @@
from .t_butt import TButtJoint
from .l_butt import LButtJoint
from .l_miter import LMiterJoint
from .x_halflap import XHalfLapJoint
from .solver import JointTopology
from .solver import ConnectionSolver
from .solver import find_neighboring_beams
Expand All @@ -59,6 +61,7 @@
"TButtJoint",
"LButtJoint",
"LMiterJoint",
"XHalfLapJoint",
"JointTopology",
"ConnectionSolver",
"find_neighboring_beams",
Expand Down
178 changes: 178 additions & 0 deletions src/compas_timber/connections/x_halflap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
from compas.geometry import Brep
from compas.geometry import Frame
from compas.geometry import Line
from compas.geometry import Plane
from compas.geometry import Point
from compas.geometry import Polyhedron
from compas.geometry import Vector
from compas.geometry import angle_vectors
from compas.geometry import intersection_line_plane
from compas.geometry import intersection_plane_plane
from compas.geometry import length_vector
from compas.geometry import midpoint_point_point

from compas_timber.parts import BeamBooleanSubtraction
from compas_timber.utils import intersection_line_line_3D

from .joint import Joint
from .solver import JointTopology


class XHalfLapJoint(Joint):
SUPPORTED_TOPOLOGY = JointTopology.TOPO_X

def __init__(self, beam_a=None, beam_b=None, cut_plane_choice=None, frame=None, key=None):
super(XHalfLapJoint, self).__init__(frame, key)
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.cut_plane_choice = cut_plane_choice # Decide if Direction of beam_a or beam_b
self.features = []

@property
def data(self):
data_dict = {
"beam_a": self.beam_a_key,
"beam_b": self.beam_b_key,
}
data_dict.update(Joint.data.fget(self))
return data_dict

@classmethod
def from_data(cls, value):
instance = cls(frame=Frame.from_data(value["frame"]), key=value["key"], cutoff=value["cut_plane_choice"])
instance.beam_a_key = value["beam_a"]
instance.beam_b_key = value["beam_b"]
instance.cut_plane_choice = value["cut_plane_choice"]
return instance

@property
def joint_type(self):
return "X-HalfLap"

@property
def beams(self):
return [self.beam_a, self.beam_b]

def _cutplane(self):
# Find the Point for the Cut Plane
centerline_a = self.beam_a.centerline
centerline_b = self.beam_b.centerline
max_distance = float("inf")
int_a, int_b = intersection_line_line_3D(centerline_a, centerline_b, max_distance)
int_a, _ = int_a
int_b, _ = int_b
point_cut = Point(*midpoint_point_point(int_a, int_b))

# Vector Cross Product
beam_a_start = self.beam_a.centerline_start
beam_b_start = self.beam_b.centerline_start
beam_a_end = self.beam_a.centerline_end
beam_b_end = self.beam_b.centerline_end
centerline_vec_a = Vector.from_start_end(beam_a_start, beam_a_end)
centerline_vec_b = Vector.from_start_end(beam_b_start, beam_b_end)
plane_cut = Plane.from_point_and_two_vectors(point_cut, centerline_vec_a, centerline_vec_b)

# Flip Cut Plane if its Normal Z-Coordinate is positive
if plane_cut[1][2] > 0:
plane_cut[1] = plane_cut[1] * -1

# Cutplane Normal Vector pointing from a and b to Cutplane Origin
cutplane_vector_a = Vector.from_start_end(int_a, point_cut)
cutplane_vector_b = Vector.from_start_end(int_b, point_cut)

# If Centerlines crossing, take the Cutplane Normal
if length_vector(cutplane_vector_a) < 1e-6:
cutplane_vector_a = plane_cut.normal
if length_vector(cutplane_vector_b) < 1e-6:
cutplane_vector_b = plane_cut.normal * -1

return plane_cut, cutplane_vector_a, cutplane_vector_b

@staticmethod
def _sort_beam_planes(beam, cutplane_vector):
# Sorts the Beam Face Planes according to the Cut Plane

frames = beam.faces[:4]
planes = []
planes_angles = []
for i in frames:
planes.append(Plane.from_frame(i))
planes_angles.append(angle_vectors(cutplane_vector, i.normal))
planes_angles, planes = zip(*sorted(zip(planes_angles, planes)))
return planes

@staticmethod
def _create_polyhedron(plane_a, plane_b, lines): # Hexahedron from 2 Planes and 4 Lines
# Step 1: Get 8 Intersection Points from 2 Planes and 4 Lines
int_points = []
for i in lines:
point_top = intersection_line_plane(i, plane_a)
point_bottom = intersection_line_plane(i, plane_b)
point_top = Point(*point_top)
point_bottom = Point(*point_bottom)
int_points.append(point_top)
int_points.append(point_bottom)

# Step 2: Check if int_points Order results in an inward facing Polyhedron
test_face_vector1 = Vector.from_start_end(int_points[0], int_points[2])
test_face_vector2 = Vector.from_start_end(int_points[0], int_points[6])
test_face_normal = Vector.cross(test_face_vector1, test_face_vector2)
check_vector = Vector.from_start_end(int_points[0], int_points[1])
# Flip int_points Order if needed
if angle_vectors(test_face_normal, check_vector) < 1:
a, b, c, d, e, f, g, h = int_points
int_points = b, a, d, c, f, e, h, g

# Step 3: Create a Hexahedron with 6 Faces from the 8 Points
return Polyhedron(
int_points,
[
[1, 7, 5, 3], # top
[0, 2, 4, 6], # bottom
[1, 3, 2, 0], # left
[3, 5, 4, 2], # back
[5, 7, 6, 4], # right
[7, 1, 0, 6], # front
],
)

def _create_negative_volumes(self):
# Get Cut Plane
plane_cut, plane_cut_vector_a, plane_cut_vector_b = self._cutplane()

# Get Beam Faces (Planes) in right order
planes_a = self._sort_beam_planes(self.beam_a, plane_cut_vector_a)
plane_a0, plane_a1, plane_a2, plane_a3 = planes_a

planes_b = self._sort_beam_planes(self.beam_b, plane_cut_vector_b)
plane_b0, plane_b1, plane_b2, plane_b3 = planes_b

# Lines as Frame Intersections
lines = []
x = intersection_plane_plane(plane_a1, plane_b1)
lines.append(Line(x[0], x[1]))
x = intersection_plane_plane(plane_a1, plane_b2)
lines.append(Line(x[0], x[1]))
x = intersection_plane_plane(plane_a2, plane_b2)
lines.append(Line(x[0], x[1]))
x = intersection_plane_plane(plane_a2, plane_b1)
lines.append(Line(x[0], x[1]))

# Create Polyhedrons
negative_polyhedron_beam_a = self._create_polyhedron(plane_a0, plane_cut, lines)
negative_polyhedron_beam_b = self._create_polyhedron(plane_b0, plane_cut, lines)

# Create BREP
return Brep.from_mesh(negative_polyhedron_beam_a), Brep.from_mesh(negative_polyhedron_beam_b)

def restore_beams_from_keys(self, assemly):
"""After de-serialization, resotres references to the main and cross beams saved in the assembly."""
self.beam_a = assemly.find_by_key(self.beam_a_key)
self.beam_b = assemly.find_by_key(self.beam_b_key)

def add_features(self):
negative_brep_beam_a, negative_brep_beam_b = self._create_negative_volumes()
self.beam_a.add_feature(BeamBooleanSubtraction(negative_brep_beam_a))
self.beam_b.add_feature(BeamBooleanSubtraction(negative_brep_beam_b))
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
from compas_timber.connections import LButtJoint
from compas_timber.connections import LMiterJoint
from compas_timber.connections import TButtJoint
from compas_timber.connections import XHalfLapJoint
from compas_timber.ghpython import CategoryRule


class JointCategoryRule(component):
# TODO: auto fill with subclasses of Joint
MAP = {"T-Butt": TButtJoint, "L-Miter": LMiterJoint, "L-Butt": LButtJoint}
MAP = {"T-Butt": TButtJoint, "L-Miter": LMiterJoint, "L-Butt": LButtJoint, "X-HalfLap": XHalfLapJoint}

def RunScript(self, JointType, CatA, CatB):
if JointType and CatA and CatB:
Expand Down
39 changes: 39 additions & 0 deletions src/compas_timber/ghpython/components/CT_JointDef_XHalfLap/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from ghpythonlib.componentbase import executingcomponent as component
from Grasshopper.Kernel.GH_RuntimeMessageLevel import Error
from Grasshopper.Kernel.GH_RuntimeMessageLevel import Warning

from compas_timber.connections import ConnectionSolver
from compas_timber.connections import XHalfLapJoint
from compas_timber.connections import JointTopology
from compas_timber.ghpython import JointDefinition


class XHalfLapDefinition(component):
def RunScript(self, MainBeam, CrossBeam):
if not MainBeam:
self.AddRuntimeMessage(Warning, "Input parameter MainBeams failed to collect data.")
if not CrossBeam:
self.AddRuntimeMessage(Warning, "Input parameter CrossBeams failed to collect data.")
if not (MainBeam and CrossBeam):
return
if not isinstance(MainBeam, list):
MainBeam = [MainBeam]
if not isinstance(CrossBeam, list):
CrossBeam = [CrossBeam]
if len(MainBeam) != len(CrossBeam):
self.AddRuntimeMessage(Error, "Number of items in MainBeams and CrossBeams must match!")
return
Joint = []
for main, cross in zip(MainBeam, CrossBeam):
max_distance_from_beams = max([main.width, main.height, cross.width, cross.height])
topology, _, _ = ConnectionSolver().find_topology(main, cross, max_distance=max_distance_from_beams)
if topology != XHalfLapJoint.SUPPORTED_TOPOLOGY:
self.AddRuntimeMessage(
Warning,
"Beams meet with topology: {} which does not agree with joint of type: {}".format(
JointTopology.get_name(topology), XHalfLapJoint.__name__
),
)
continue
Joint.append(JointDefinition(XHalfLapJoint, [main, cross]))
return Joint
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "JointDef: XHalfLap",
"nickname": "JointDef: XHalfLap",
"category": "COMPAS Timber",
"subcategory": "Joints",
"description": "Defines an X-HalfLap Joint for the given two Beams.",
"exposure": 4,
"ghpython": {
"isAdvancedMode": true,
"iconDisplay": 0,
"inputParameters": [
{
"name": "BeamA",
"description": "First Beam.",
"typeHintID": "none",
"scriptParamAccess": 1
},
{
"name": "BeamB",
"description": "Second Beam.",
"typeHintID": "none",
"scriptParamAccess": 1
}
],
"outputParameters": [
{
"name": "Joint",
"description": "XHalfLap JointDefinition."
}
]
}
}
Binary file not shown.
Loading