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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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:
max_distance = self.max_distance
else:
max_distance = model_max_distance

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
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