Skip to content

Commit

Permalink
Merge pull request #118 from gramaziokohler/feature/lap_joint
Browse files Browse the repository at this point in the history
implemented half lap joint
  • Loading branch information
jonashaldemann authored Sep 18, 2023
2 parents 89102a3 + 63ae7b7 commit 70f493e
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 1 deletion.
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.

0 comments on commit 70f493e

Please sign in to comment.