Skip to content

Commit

Permalink
Merge pull request #385 from gramaziokohler/MAX_DISTANCE_to_joint_rules
Browse files Browse the repository at this point in the history
Max distance to joint rules
  • Loading branch information
obucklin authored Feb 10, 2025
2 parents aea6c47 + db63077 commit e3844dd
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 28 deletions.
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`
* Added `BTLxFromGeometryDefinition` class to replace the depricated `FeatureDefinition`. This allows deferred calculation of BTLx processings.
* Added `from_shapes_and_element` class method to `Drilling`, `JackRafterCut`, and `DoubleCut` as a wrapper for their geometry based constructors for use with `BTLxFromGeometryDefinition`.
* Added `YButtJoint` which joins the ends of three joints where the `cross_beams` get a miter cut and the `main_beam` gets a double cut.
Expand All @@ -30,6 +31,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.
* Changed referenced to `beam` in `Drilling` to `element`.
* Changed `Drill Hole` and `Trim Feature` GH components to generate the relevant `BTLxProcessing` type rather than the deprecated `FeatureDefinition` type.

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

from compas.tolerance import TOL

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,11 +61,11 @@ 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
def joints_from_beams_and_rules(elements, rules, max_distance=1e-6):
def joints_from_beams_and_rules(elements, rules, max_distance=TOL.absolute):
"""processes joint rules into joint definitions.
Parameters
Expand All @@ -81,30 +86,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 +121,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 +148,69 @@ 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=TOL.absolute):
"""Returns True if the given elements comply with this DirectRule.
Checks if the distance between the centerlines of the elements is less than the max_distance.
Does not check for JointTopology compliance.
Parameters
----------
elements : tuple(:class:`~compas_timber.elements.TimberElement`, :class:`~compas_timber.elements.TimberElement`)
A tuple containing two elements to check.
model_max_distance : float, optional
The maximum distance to consider two elements as intersecting. Defaults to TOL.absolute.
This is only used if the rule does not already have a max_distance set.
Returns
-------
bool
True if the elements comply with the rule, False otherwise.
"""
if self.max_distance is not None:
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 +220,32 @@ 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=TOL.absolute):
"""Checks if the given elements comply with this CategoryRule.
It checks:
that the elements have the expected category attribute,
that the max_distance is not exceeded,
that the joint supports the topology of the elements.
Parameters
----------
elements : tuple(:class:`~compas_timber.elements.TimberElement`, :class:`~compas_timber.elements.TimberElement`)
A tuple containing two elements to check.
model_max_distance : float, optional
The maximum distance to consider two elements as intersecting. Defaults to TOL.absolute.
This is only used if the rule does not already have a max_distance set.
Returns
-------
bool
True if the elements comply with the rule, False otherwise.
"""
if self.max_distance is not None:
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 @@ -203,23 +295,55 @@ class TopologyRule(JointRule):
The topology type to which the rule is applied.
joint_type : cls(:class:`compas_timber.connections.Joint`)
The joint type to be applied to this topology.
max_distance : float, optional
The maximum distance to consider two elements as intersecting.
This will override a global max_distance if set.
kwargs : dict
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)
return "{}({}, {})".format(
TopologyRule,
self.topology_type,
self.joint_type,
)

def comply(self, elements, model_max_distance=TOL.absolute):
"""Checks if the given elements comply with this TopologyRule.
It checks that the max_distance is not exceeded and that the topology of the elements matches the rule.
If the elements are not in the correct order, they are reversed.
Parameters
----------
elements : tuple(:class:`~compas_timber.elements.TimberElement`, :class:`~compas_timber.elements.TimberElement`)
A tuple containing two elements to check.
model_max_distance : float, optional
The maximum distance to consider two elements as intersecting. Defaults to TOL.absolute.
This is only used if the rule does not already have a max_distance set.
def comply(self, elements, max_distance=1e-3):
Returns
-------
bool
True if the elements comply with the rule, False otherwise.
list(:class:`~compas_timber.elements.TimberElement`)
The elements in the correct order.
"""
if self.max_distance is not None:
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

0 comments on commit e3844dd

Please sign in to comment.