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

Max distance to joint rules #385

Merged
merged 13 commits into from
Feb 10, 2025
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased

### Added
* Added `distance_segment_segment` to `compas_timber.utils`

### Changed

Expand All @@ -27,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Fixed `restore_beams_from_keys` in `LMiterJoint` to use the correct variable names.
* Reworked `DoubleCut` to more reliably produce the feature and geometry with the `from_planes_and_element` class method.
* Renamed `intersection_box_line()` to `intersection_beam_line_param()`, which now take a beam input and outputs the intersecting ref_face_index.
* Added `max_distance` argument to `JointRule` subclasses and GH components so that max_distance can be set for each joint rule individually.

### Removed

Expand Down
106 changes: 85 additions & 21 deletions src/compas_timber/design/workflow.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from itertools import combinations

from compas_timber.connections import ConnectionSolver
from compas_timber.connections import JointTopology
from compas_timber.connections import LMiterJoint
from compas_timber.connections import TButtJoint
from compas_timber.connections import XLapJoint
from compas_timber.utils import distance_segment_segment
from compas_timber.utils import intersection_line_line_param


Expand Down Expand Up @@ -56,7 +59,7 @@ def get_topology_rules(rules, use_defaults=False):
}
for rule in rules: # separate category and topo and direct joint rules
if rule.__class__.__name__ == "TopologyRule":
topo_rules[rule.topology_type] = TopologyRule(rule.topology_type, rule.joint_type, **rule.kwargs) # overwrites, meaning last rule wins
topo_rules[rule.topology_type] = TopologyRule(rule.topology_type, rule.joint_type, rule.max_distance, **rule.kwargs) # overwrites, meaning last rule wins
return [rule for rule in topo_rules.values() if rule is not None]

@staticmethod
Expand All @@ -81,30 +84,31 @@ def joints_from_beams_and_rules(elements, rules, max_distance=1e-6):
elements = elements if isinstance(elements, list) else list(elements)
direct_rules = JointRule.get_direct_rules(rules)
solver = ConnectionSolver()

element_pairs = solver.find_intersecting_pairs(elements, rtree=True, max_distance=max_distance)
max_distances = [rule.max_distance for rule in rules if rule.max_distance]
max_rule_distance = max(max_distances) if max_distances else max_distance
element_pairs = solver.find_intersecting_pairs(elements, rtree=True, max_distance=max_rule_distance)
joint_defs = []
unmatched_pairs = []
for rule in direct_rules:
joint_defs.append(JointDefinition(rule.joint_type, rule.elements, **rule.kwargs))
while element_pairs:
pair = element_pairs.pop()
match_found = False
for rule in direct_rules: # see if pair is used in a direct rule
if rule.comply(pair):
match_found = True
break
for rule in direct_rules:
if rule.contains(pair): # see if pair is used in a direct rule
if rule.comply(pair, model_max_distance=max_distance): # see if pair complies with max distance
joint_defs.append(JointDefinition(rule.joint_type, rule.elements, **rule.kwargs))
match_found = True
break

if not match_found:
for rule in JointRule.get_category_rules(rules): # see if pair is used in a category rule
if rule.comply(pair, max_distance=max_distance):
if rule.comply(pair, model_max_distance=max_distance):
match_found = True
joint_defs.append(JointDefinition(rule.joint_type, rule.reorder(pair), **rule.kwargs))
break

if not match_found:
for rule in JointRule.get_topology_rules(rules): # see if pair is used in a topology rule
comply, ordered_pair = rule.comply(pair, max_distance=max_distance)
comply, ordered_pair = rule.comply(pair, model_max_distance=max_distance)
if comply:
match_found = True
joint_defs.append(JointDefinition(rule.joint_type, ordered_pair, **rule.kwargs))
Expand All @@ -115,11 +119,24 @@ def joints_from_beams_and_rules(elements, rules, max_distance=1e-6):


class DirectRule(JointRule):
"""Creates a Joint Rule that directly joins multiple elements."""
"""Creates a Joint Rule that directly joins multiple elements.

def __init__(self, joint_type, elements, **kwargs):
Parameters
----------
joint_type : cls(:class:`~compas_timber.connections.Joint`)
The joint type to be applied to the elements.
elements : list(:class:`~compas_timber.elements.TimberElement`)
The elements to be joined.
max_distance : float, optional
The maximum distance to consider two elements as intersecting.
kwargs : dict
The keyword arguments to be passed to the joint.
"""

def __init__(self, joint_type, elements, max_distance=None, **kwargs):
self.elements = elements
self.joint_type = joint_type
self.max_distance = max_distance
self.kwargs = kwargs

def ToString(self):
Expand All @@ -129,21 +146,55 @@ def ToString(self):
def __repr__(self):
return "{}({}, {})".format(DirectRule, self.elements, self.joint_type)

def comply(self, elements):
def contains(self, elements):
"""Returns True if the given elements are defined within this DirectRule."""
try:
return set(elements).issubset(set(self.elements))
except TypeError:
raise UserWarning("unable to comply direct joint element sets")

def comply(self, elements, model_max_distance=1e-6):
"""Returns True if the given elements comply with this DirectRule.
only checks if the distance between the centerlines of the elements is less than the max_distance.
allows joint topology overrides.
"""
if self.max_distance:
obucklin marked this conversation as resolved.
Show resolved Hide resolved
max_distance = self.max_distance
else:
max_distance = model_max_distance
Copy link
Contributor

Choose a reason for hiding this comment

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

use compas.tolerance.TOL here instead:

    def comply(self, elements, model_max_distance=None):       
        model_max_distance = model_max_distance or TOL.absolute
        if self.max_distance is not None:
            max_distance = self.max_distance
        else:
            max_distance = model_max_distance

Copy link
Contributor

Choose a reason for hiding this comment

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

To control the max_distance through Joint rules definitely makes sense to me, because until now we had to set it globally, and this was a bit too inflexible. Did you do some tests with a few Structures in Grasshopper?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To control the max_distance through Joint rules definitely makes sense to me, because until now we had to set it globally, and this was a bit too inflexible. Did you do some tests with a few Structures in Grasshopper?

I did some testing in GH, but the structures were not very complex. I added a bunch of unit tests, however.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@chenkasirer @jonashaldemann I sent an invite for testing on Monday. I hope you guys are available. Maybe we can have a look at this then and then hopefully merge it for the big release.


try:
for pair in combinations(list(elements), 2):
return distance_segment_segment(pair[0].centerline, pair[1].centerline) <= max_distance
except TypeError:
raise UserWarning("unable to comply direct joint element sets")


class CategoryRule(JointRule):
"""Based on the category attribute attached to the elements, this rule assigns"""
"""Based on the category attribute attached to the elements, this rule assigns

def __init__(self, joint_type, category_a, category_b, topos=None, **kwargs):
Parameters
----------
joint_type : cls(:class:`~compas_timber.connections.Joint`)
The joint type to be applied to the elements.
category_a : str
The category of the first element.
category_b : str
The category of the second element.
topos : list(:class:`~compas_timber.connections.JointTopology`), optional
The topologies that are supported by this rule.
max_distance : float, optional
The maximum distance to consider two elements as intersecting.
kwargs : dict
The keyword arguments to be passed to the joint.
"""

def __init__(self, joint_type, category_a, category_b, topos=None, max_distance=None, **kwargs):
self.joint_type = joint_type
self.category_a = category_a
self.category_b = category_b
self.topos = topos or []
self.max_distance = max_distance
self.kwargs = kwargs

def ToString(self):
Expand All @@ -153,7 +204,11 @@ def ToString(self):
def __repr__(self):
return "{}({}, {}, {}, {})".format(CategoryRule.__name__, self.joint_type.__name__, self.category_a, self.category_b, self.topos)

def comply(self, elements, max_distance=1e-6):
def comply(self, elements, model_max_distance=1e-6):
if self.max_distance:
max_distance = self.max_distance
else:
max_distance = model_max_distance
obucklin marked this conversation as resolved.
Show resolved Hide resolved
try:
element_cats = set([e.attributes["category"] for e in elements])
comply = False
Expand Down Expand Up @@ -207,19 +262,28 @@ class TopologyRule(JointRule):
The keyword arguments to be passed to the joint.
"""

def __init__(self, topology_type, joint_type, **kwargs):
def __init__(self, topology_type, joint_type, max_distance=None, **kwargs):
self.topology_type = topology_type
self.joint_type = joint_type
self.max_distance = max_distance
self.kwargs = kwargs

def ToString(self):
# GH doesn't know
return repr(self)

def __repr__(self):
return "{}({}, {})".format(TopologyRule, self.topology_type, self.joint_type)

def comply(self, elements, max_distance=1e-3):
return "{}({}, {})".format(
TopologyRule,
self.topology_type,
self.joint_type,
)

def comply(self, elements, model_max_distance=1e-6):
if self.max_distance:
max_distance = self.max_distance
else:
max_distance = model_max_distance
try:
elements = list(elements)
solver = ConnectionSolver()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def arg_names(self):
names = inspect.getargspec(self.joint_type.__init__)[0][1:]
for i in range(2):
names[i] += " category"
return [name for name in names if (name != "key") and (name != "frame")]
return [name for name in names if (name != "key") and (name != "frame")] + ["max_distance"]

def AppendAdditionalMenuItems(self, menu):
for name in self.classes.keys():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def RunScript(self, *args):
return Rules

def arg_names(self):
return inspect.getargspec(self.joint_type.__init__)[0][1:]
return inspect.getargspec(self.joint_type.__init__)[0][1:] + ["max_distance"]

def AppendAdditionalMenuItems(self, menu):
for name in self.classes.keys():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def arg_start_index(self):

@property
def arg_names(self):
return inspect.getargspec(self.joint_type.__init__)[0][self.arg_start_index :]
return inspect.getargspec(self.joint_type.__init__)[0][self.arg_start_index :] + ["max_distance"]

def AppendAdditionalMenuItems(self, menu):
for name in self.classes.keys():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def RunScript(self, *args):

def arg_names(self):
names = inspect.getargspec(self.joint_type.__init__)[0][3:]
return [name for name in names if (name != "key") and (name != "frame")]
return [name for name in names if (name != "key") and (name != "frame")] + ["max_distance"]

def AppendAdditionalMenuItems(self, menu):
for name in self.classes.keys():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def RunScript(self, *args):

def arg_names(self):
names = inspect.getargspec(self.joint_type.__init__)[0][3:]
return [name for name in names if (name != "key") and (name != "frame")]
return [name for name in names if (name != "key") and (name != "frame")] + ["max_distance"]

def AppendAdditionalMenuItems(self, menu):
for name in self.classes.keys():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def RunScript(self, *args):

def arg_names(self):
names = inspect.getargspec(self.joint_type.__init__)[0][3:]
return [name for name in names if (name != "key") and (name != "frame")]
return [name for name in names if (name != "key") and (name != "frame")] + ["max_distance"]

def AppendAdditionalMenuItems(self, menu):
for name in self.classes.keys():
Expand Down
26 changes: 25 additions & 1 deletion src/compas_timber/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from compas.geometry import Frame
from compas.geometry import Transformation
from compas.geometry import intersection_line_plane
from compas.geometry import closest_point_on_segment
from compas.geometry import intersection_line_line


def intersection_line_line_param(line1, line2, max_distance=1e-6, limit_to_segments=True, tol=1e-6):
Expand Down Expand Up @@ -171,4 +173,26 @@ def intersection_line_beam_param(line, beam, ignore_ends=False):
return [Point(*coords) for coords in pts], ref_side_indices


__all__ = ["intersection_line_line_param", "intersection_line_plane_param", "intersection_line_beam_param"]
def distance_segment_segment(segment_a, segment_b):
"""Computes the distance between two segments.

Parameters
----------
segment_a : tuple(tuple(float, float, float), tuple(float, float, float))
The first segment, defined by two points.
segment_b : tuple(tuple(float, float, float), tuple(float, float, float))
The second segment, defined by two points.

Returns
-------
float
The distance between the two segments.

"""
pta, ptb = intersection_line_line(segment_a, segment_b)
pt_seg_a = closest_point_on_segment(pta, segment_a)
pt_seg_b = closest_point_on_segment(ptb, segment_b)
return distance_point_point(pt_seg_a, pt_seg_b)


__all__ = ["intersection_line_line_param", "intersection_line_plane_param", "intersection_line_beam_param", "distance_segment_segment"]
Loading