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

Ball_node_fastener #318

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d43f79f
added generic fastener
obucklin Oct 28, 2024
078808e
format, lint, Changelog
obucklin Oct 28, 2024
8887db3
Pub branch
obucklin Oct 28, 2024
2fc9355
added unit test
obucklin Oct 28, 2024
8de3802
moment
obucklin Oct 28, 2024
6060706
passed tests
obucklin Oct 28, 2024
874eb52
changelog
obucklin Oct 28, 2024
ab7bca3
lint format clean
obucklin Oct 28, 2024
025d8ce
Merge branch 'main' into fastener_element
obucklin Oct 28, 2024
0d9b218
Merge branch 'fastener_element' into ball_node_fastener
obucklin Oct 29, 2024
c75fcae
no more errors
obucklin Oct 29, 2024
37463ab
started BallNodeJoint
obucklin Oct 30, 2024
ee313ec
try ball node joint
obucklin Oct 30, 2024
76ac44e
move to generic solver
obucklin Oct 30, 2024
c514b8b
new workflow
obucklin Oct 31, 2024
c88197f
Merge branch 'main' into new_solver_workflow
obucklin Oct 31, 2024
efb12c1
Changelog
obucklin Oct 31, 2024
1fc2ad1
fixed topo beam order
obucklin Nov 4, 2024
7eee105
Merge branch 'new_solver_workflow' into ball_node_fastener
obucklin Nov 4, 2024
7324374
STARTED
obucklin Nov 4, 2024
f006405
is that all?
obucklin Nov 5, 2024
00cf465
Merge branch 'many_beam_joints' into ball_node_fastener
obucklin Nov 5, 2024
eeffef8
fixed GH
obucklin Nov 5, 2024
80f07b8
Merge branch 'many_beam_joints' into ball_node_fastener
obucklin Nov 5, 2024
6a5d2ec
before looking at interactions
obucklin Nov 6, 2024
87c1b86
joints create interactions
obucklin Nov 6, 2024
e966853
Merge branch 'many_beam_joints' into ball_node_fastener
obucklin Nov 6, 2024
8be1066
almost working
obucklin Nov 6, 2024
cee6ae4
maybe owrking...
obucklin Nov 6, 2024
83e624d
checked for unnecessary changes to core functions
obucklin Nov 6, 2024
e693c54
needs testing and unit tests
obucklin Nov 7, 2024
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Added new `utilities` module in `connections` package.
* Added new `compas_timber._fabrication.DoubleCut`.
* Added new `compas_timber.connections.TBirdsmouthJoint`.
* Added `JointRule.joints_from_beams_and_rules()` static method
* Added `Element.reset()` method.

* Added new `fasteners.py` module with new `Fastener` element type.
* Added unit tests for `fasteners.py` module.

### Changed

* Changed incorrect import of `compas.geometry.intersection_line_plane()` to `compas_timber.utils.intersection_line_plane()`
* Renamed `intersection_line_plane` to `intersection_line_plane_param`.
* Renamed `intersection_line_line_3D` to `intersection_line_line_param`.
* Reworked the model generation pipeline.
* Reworked `comply` methods for `JointRule`s.

### Removed

* Removed module `compas_timber.utils.compas_extra`.
* Removed a bunch of spaghetti from `CT_model` GH component.

## [0.11.0] 2024-09-17

Expand Down
2 changes: 2 additions & 0 deletions src/compas_timber/connections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .t_halflap import THalfLapJoint
from .x_halflap import XHalfLapJoint
from .t_dovetail import TDovetailJoint
from .ball_node import BallNodeJoint

__all__ = [
"Joint",
Expand All @@ -37,4 +38,5 @@
"ConnectionSolver",
"find_neighboring_beams",
"TDovetailJoint",
"BallNodeJoint"
]
186 changes: 186 additions & 0 deletions src/compas_timber/connections/ball_node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
from compas.geometry import Frame

from compas_timber.elements import BallNodeFastener
from compas_timber.elements import CutFeature
from compas_timber.elements import MillVolume
from compas_timber.elements import DrillFeature
from compas_timber.elements import BrepSubtraction
from compas_timber.elements.fasteners.fastener import Fastener
from compas_timber.utils import intersection_line_line_param
from compas.geometry import Brep
from compas.geometry import Sphere
from compas.geometry import Cylinder
from compas.geometry import Box
from compas.geometry import Plane
from compas.geometry import Line
from compas.geometry import intersection_sphere_line


from .joint import BeamJoinningError
from .joint import Joint
from .solver import JointTopology


class BallNodeJoint(Joint):
"""Represents a ball node type joint which joins the ends of multiple beams,
trimming the main beam.

Please use `BallNodeJoint.create()` to properly create an instance of this class and associate it with an model.

Parameters
----------
beams : list(:class:`~compas_timber.parts.Beam`)
The beams to be joined.

Attributes
----------
beams : list(:class:`~compas_timber.parts.Beam`)
The beams joined by this joint.
beam_keys : list(str)
The keys of the beams.
features : list(:class:`~compas_timber.parts.Feature`)
The features created by this joint.
joint_type : str
A string representation of this joint's type.

"""
SUPPORTED_TOPOLOGY = JointTopology.TOPO_UNKNOWN
GH_ARGS = {"beams": None, "thickness": 10, "holes": 6, "strut_length": 100, "ball_diameter": 50}

def __init__(self, beams, thickness=10, holes=6, strut_length=100, ball_diameter=50, **kwargs):
super(BallNodeJoint, self).__init__( **kwargs)
self._elements = beams
self.thickness = thickness
self.plate_holes = holes
self.strut_length = strut_length
self.ball_diameter = ball_diameter
self.beam_keys = [str(beam.guid) for beam in self.beams]
self.features = []
self.joint_type = "BallNode"
self.fastener = BallNodeFastener()

@property
def elements(self):
return self._elements

@property
def interactions(self):
for beam in self.beams:
yield (beam, self.fastener, self)

@property
def element_parameter_count(self):
return 1

@classmethod
def create(cls, model, beams, **kwargs):
"""Creates an instance of this joint and creates the new connection in `model`.

`beams` are expected to have been added to `model` before calling this method.

This code does not verify that the given beams are adjacent and/or lie in a topology which allows connecting
them. This is the responsibility of the calling code.

A `ValueError` is raised if `beams` contains less than two `Beam` objects.

Parameters
----------
model : :class:`~compas_timber.model.TimberModel`
The model to which the beams and this joing belong.
beams : list(:class:`~compas_timber.parts.Beam`)
A list containing two beams that whould be joined together

Returns
-------
:class:`compas_timber.connections.Joint`
The instance of the created joint.

"""

joint = cls(beams, **kwargs)
model.add_element(joint.fastener)
for interaction in joint.interactions:
_ = model.add_interaction(*interaction)
return joint

def add_element(self, element):
self._elements.append(element)

def add_extensions(self):
"""Calculates and adds the necessary extensions to the beams.

This method is automatically called when joint is created by the call to `Joint.create()`.

Raises
------
BeamJoinningError
If the extension could not be calculated.

"""
return

def add_features(self):
ends = []
beams = list(self.beams)
points = intersection_line_line_param(beams[0].centerline, beams[1].centerline)
cpt = None
if points[0][0] is not None:
cpt = (points[0][0])
if points[0][1] > 0.5:
ends.append("end")
else:
ends.append("start")

for beam in list(beams)[1::]:
points = intersection_line_line_param(beams[0].centerline, beam.centerline)
if points[0][0] is not None and points[1][0] is not None:
cpt = cpt + points[1][0]
if points[1][1] > 0.5:
ends.append("end")
else:
ends.append("start")
cpt = cpt*(1.0/len(beams))

geometry = Brep.from_sphere(Sphere(self.ball_diameter/2, point= cpt))
cut_sphere = Sphere(self.strut_length, point= cpt)
feat_dict = {}
for beam, end in zip(beams, ends):
feat_dict[beam.key] = []
cut_pts = intersection_sphere_line([cut_sphere.base, cut_sphere.radius], beam.centerline)
if cut_pts:
""" trim beam ends"""
cut_pt = cut_pts[0] if beam.midpoint.distance_to_point(cut_pts[0])<beam.midpoint.distance_to_point(cut_pts[1]) else cut_pts[1]
cut_plane = Plane(cut_pt, beam.centerline.direction) if end == "end" else Plane(cut_pt, -beam.centerline.direction)
beam.add_feature(CutFeature(cut_plane))
feat_dict[beam.key].append((cut_plane))

""" add strut to connect beam to ball node"""
cylinder = Cylinder(self.thickness, self.strut_length, Frame.from_plane(cut_plane))
cylinder.translate(cylinder.axis.direction * (self.strut_length / 2.0))
geometry += Brep.from_cylinder(cylinder)

""" add plate to connect to beam"""
plate_frame = Frame(cut_pt, beam.frame.xaxis, beam.frame.zaxis) if end == "start" else Frame(cut_pt, -beam.frame.xaxis, beam.frame.zaxis)
plate = Box(beam.height*self.plate_holes/4.0, beam.height, self.thickness, plate_frame)
plate.translate(plate_frame.xaxis * (beam.height*self.plate_holes/8.0))
plate = Brep.from_box(plate)

""" add drill holes to plate and beam"""
y_offset = beam.height/6.0
for _ in range(2):
drill_start = plate_frame.point + (plate_frame.zaxis * (-beam.width/2.0)) + (plate_frame.yaxis * y_offset)
for _ in range(self.plate_holes/2):
drill_start += (plate_frame.xaxis * (beam.height/3.0))
drill_line = Line.from_point_direction_length(drill_start, plate_frame.zaxis, beam.width)
drill = DrillFeature(drill_line, 10, beam.width)
beam.add_feature(drill)

mill = BrepSubtraction(plate)
beam.add_feature(mill)
drillinder = Brep.from_cylinder(Cylinder.from_line_and_radius(drill_line, 5))
feat_dict[beam.key].append((drillinder))
plate -= drillinder
y_offset = -beam.height/6.0
geometry += plate
# self.test.append(feat_dict)
self.fastener.geometry = geometry
34 changes: 30 additions & 4 deletions src/compas_timber/connections/joint.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,25 @@ def __init__(self, **kwargs):

@property
def beams(self):
raise NotImplementedError
for element in self.elements:
if getattr(element, "is_beam", False):
yield element

@property
def plates(self):
for element in self.elements:
if getattr(element, "is_plate", False):
yield element

@property
def fasteners(self):
for element in self.elements:
if getattr(element, "is_fastener", False):
yield element

@property
def element_parameter_count(self):
return 2

def add_features(self):
"""Adds the features defined by this joint to affected beam(s).
Expand Down Expand Up @@ -112,7 +130,7 @@ def restore_beams_from_keys(self, model):
raise NotImplementedError

@classmethod
def create(cls, model, *beams, **kwargs):
def create(cls, model, beams, **kwargs):
"""Creates an instance of this joint and creates the new connection in `model`.

`beams` are expected to have been added to `model` before calling this method.
Expand All @@ -135,11 +153,10 @@ def create(cls, model, *beams, **kwargs):
The instance of the created joint.

"""

if len(beams) < 2:
raise ValueError("Expected at least 2 beams. Got instead: {}".format(len(beams)))
joint = cls(*beams, **kwargs)
model.add_joint(joint, beams)
model.add_joint(joint)
return joint

@property
Expand All @@ -157,6 +174,15 @@ def ends(self):

return self._ends

@property
def interactions(self):
"""Returns interactions consisting of all possible pairs of beams that are connected by this joint and the joint itself."""
interactions = []
for i in range(len(self.beams)):
for j in range(i + 1, len(self.beams)):
interactions.append((self.beams[i], self.beams[j], self))
return interactions

@staticmethod
def get_face_most_towards_beam(beam_a, beam_b, ignore_ends=True):
"""Of all the faces of `beam_b`, returns the one whose normal most faces `beam_a`.
Expand Down
20 changes: 10 additions & 10 deletions src/compas_timber/connections/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ def find_intersecting_pairs(cls, beams, rtree=False, max_distance=0.0):
"""
return find_neighboring_beams(beams, inflate_by=max_distance) if rtree else itertools.combinations(beams, 2)

def find_topology(self, beam_a, beam_b, tol=TOLERANCE, max_distance=None):
@staticmethod
def find_topology(beam_a, beam_b, tol=TOLERANCE, max_distance=None):
"""If `beam_a` and `beam_b` intersect within the given `max_distance`, return the topology type of the intersection.

If the topology is role-sensitive, the method outputs the beams in a consistent specific order
Expand All @@ -133,9 +134,8 @@ def find_topology(self, beam_a, beam_b, tol=TOLERANCE, max_distance=None):

"""

tol = self.TOLERANCE # TODO: change to a unit-sensitive value
tol = ConnectionSolver.TOLERANCE # TODO: change to a unit-sensitive value
angtol = 1e-3

a1, a2 = beam_a.centerline
b1, b2 = beam_b.centerline
va = subtract_vectors(a2, a1)
Expand All @@ -151,12 +151,12 @@ def find_topology(self, beam_a, beam_b, tol=TOLERANCE, max_distance=None):
if parallel:
pa = a1
pb = closest_point_on_line(a1, [b1, b2])
if self._exceed_max_distance(pa, pb, max_distance, tol):
if ConnectionSolver._exceed_max_distance(pa, pb, max_distance, tol):
return JointTopology.TOPO_UNKNOWN, None, None

# check if any ends meet
comb = [[0, 0], [0, 1], [1, 0], [1, 1]]
meet = [not self._exceed_max_distance([a1, a2][ia], [b1, b2][ib], max_distance, tol) for ia, ib in comb]
meet = [not ConnectionSolver._exceed_max_distance([a1, a2][ia], [b1, b2][ib], max_distance, tol) for ia, ib in comb]
if sum(meet) != 1:
return JointTopology.TOPO_UNKNOWN, None, None

Expand All @@ -181,9 +181,9 @@ def find_topology(self, beam_a, beam_b, tol=TOLERANCE, max_distance=None):
vna = cross_vectors(va, vn)
vnb = cross_vectors(vb, vn)

ta = self._calc_t([a1, a2], [b1, vnb])
ta = ConnectionSolver._calc_t([a1, a2], [b1, vnb])
pa = Point(*add_vectors(a1, scale_vector(va, ta)))
tb = self._calc_t([b1, b2], [a1, vna])
tb = ConnectionSolver._calc_t([b1, b2], [a1, vna])
pb = Point(*add_vectors(b1, scale_vector(vb, tb)))

# for max_distance calculations, limit intersection point to line segment
Expand All @@ -196,12 +196,12 @@ def find_topology(self, beam_a, beam_b, tol=TOLERANCE, max_distance=None):
if tb > 1:
pb = b2

if self._exceed_max_distance(pa, pb, max_distance, tol):
if ConnectionSolver._exceed_max_distance(pa, pb, max_distance, tol):
return JointTopology.TOPO_UNKNOWN, None, None

# topologies:
xa = self._is_near_end(ta, beam_a.centerline.length, max_distance or 0, tol)
xb = self._is_near_end(tb, beam_b.centerline.length, max_distance or 0, tol)
xa = ConnectionSolver._is_near_end(ta, beam_a.centerline.length, max_distance or 0, tol)
xb = ConnectionSolver._is_near_end(tb, beam_b.centerline.length, max_distance or 0, tol)

# L-joint (both meeting at ends)
if xa and xb:
Expand Down
Loading
Loading