From bfffbea401bf5417af5d431fa1fec57c3be5e1a8 Mon Sep 17 00:00:00 2001 From: obucklin Date: Mon, 20 Jan 2025 15:34:39 +0100 Subject: [PATCH 01/10] added `max_distance` to joint rules --- src/compas_timber/design/workflow.py | 57 ++++++++++++++----- .../components/CT_Joint_Rule_Category/code.py | 2 +- .../components/CT_Joint_Rule_Direct/code.py | 2 +- .../CT_Joint_Rule_From_List/code.py | 2 +- .../CT_Joint_Rule_Topology_L/code.py | 2 +- .../CT_Joint_Rule_Topology_T/code.py | 2 +- .../CT_Joint_Rule_Topology_X/code.py | 2 +- 7 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/compas_timber/design/workflow.py b/src/compas_timber/design/workflow.py index 01d938ab0b..c8c7f4a287 100644 --- a/src/compas_timber/design/workflow.py +++ b/src/compas_timber/design/workflow.py @@ -1,8 +1,12 @@ +from itertools import combinations +from compas.geometry import distance_line_line + 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 XHalfLapJoint +from compas_timber.elements import beam from compas_timber.utils import intersection_line_line_param @@ -56,7 +60,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 @@ -81,17 +85,20 @@ 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_rule_distance = max([rule.max_distance for rule in rules if rule.max_distance]) + print("max_rule_distance", max_rule_distance) + element_pairs = solver.find_intersecting_pairs(elements, rtree=True, max_distance=max_rule_distance) + print("element_pairs", [[beam.key for beam in pair]for pair in element_pairs]) joint_defs = [] unmatched_pairs = [] for rule in direct_rules: - joint_defs.append(JointDefinition(rule.joint_type, rule.elements, **rule.kwargs)) + if rule.comply(element_pairs, max_distance=max_distance): + 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): + if rule.contains(pair): match_found = True break @@ -104,6 +111,8 @@ def joints_from_beams_and_rules(elements, rules, max_distance=1e-6): if not match_found: for rule in JointRule.get_topology_rules(rules): # see if pair is used in a topology rule + print("topo rule", rule, rule.max_distance) + print([beam.key for beam in pair]) comply, ordered_pair = rule.comply(pair, max_distance=max_distance) if comply: match_found = True @@ -117,9 +126,10 @@ def joints_from_beams_and_rules(elements, rules, max_distance=1e-6): class DirectRule(JointRule): """Creates a Joint Rule that directly joins multiple elements.""" - def __init__(self, joint_type, elements, **kwargs): + 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): @@ -129,21 +139,33 @@ 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, max_distance=1e-6): + if not self.max_distance: + self.max_distance = max_distance + try: + for pair in combinations(elements, 2): + if distance_line_line(pair[0].centerline, pair[1].centerline) > self.max_distance: + return False + 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""" - def __init__(self, joint_type, category_a, category_b, topos=None, **kwargs): + 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): @@ -154,13 +176,15 @@ 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): + if not self.max_distance: + self.max_distance = max_distance try: element_cats = set([e.attributes["category"] for e in elements]) comply = False elements = list(elements) if element_cats == set([self.category_a, self.category_b]): solver = ConnectionSolver() - found_topology = solver.find_topology(elements[0], elements[1], max_distance=max_distance)[0] + found_topology = solver.find_topology(elements[0], elements[1], max_distance=self.max_distance)[0] supported_topo = self.joint_type.SUPPORTED_TOPOLOGY if not isinstance(supported_topo, list): supported_topo = [supported_topo] @@ -207,9 +231,12 @@ 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 + print("topo __init__ max_distance", self.max_distance) + print(kwargs) self.kwargs = kwargs def ToString(self): @@ -217,13 +244,17 @@ def ToString(self): return repr(self) def __repr__(self): - return "{}({}, {})".format(TopologyRule, self.topology_type, self.joint_type) + return "{}({}, {})".format(TopologyRule, self.topology_type, self.joint_type, self.max_distance) - def comply(self, elements, max_distance=1e-3): + def comply(self, elements, max_distance=1e-6): + print("before topo max_distance", self.max_distance) + if not self.max_distance: + self.max_distance = max_distance + print("topo max_distance", self.max_distance) try: elements = list(elements) solver = ConnectionSolver() - topo_results = solver.find_topology(elements[0], elements[1], max_distance=max_distance) + topo_results = solver.find_topology(elements[0], elements[1], max_distance=self.max_distance) return ( self.topology_type == topo_results[0], [topo_results[1], topo_results[2]], diff --git a/src/compas_timber/ghpython/components/CT_Joint_Rule_Category/code.py b/src/compas_timber/ghpython/components/CT_Joint_Rule_Category/code.py index cdfea7057e..98d9fe415a 100644 --- a/src/compas_timber/ghpython/components/CT_Joint_Rule_Category/code.py +++ b/src/compas_timber/ghpython/components/CT_Joint_Rule_Category/code.py @@ -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(): diff --git a/src/compas_timber/ghpython/components/CT_Joint_Rule_Direct/code.py b/src/compas_timber/ghpython/components/CT_Joint_Rule_Direct/code.py index 20e1086710..d8946df3c2 100644 --- a/src/compas_timber/ghpython/components/CT_Joint_Rule_Direct/code.py +++ b/src/compas_timber/ghpython/components/CT_Joint_Rule_Direct/code.py @@ -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(): diff --git a/src/compas_timber/ghpython/components/CT_Joint_Rule_From_List/code.py b/src/compas_timber/ghpython/components/CT_Joint_Rule_From_List/code.py index cdbeccbf66..683f8148dd 100644 --- a/src/compas_timber/ghpython/components/CT_Joint_Rule_From_List/code.py +++ b/src/compas_timber/ghpython/components/CT_Joint_Rule_From_List/code.py @@ -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(): diff --git a/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_L/code.py b/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_L/code.py index d25b7c29d2..e7f6be721b 100644 --- a/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_L/code.py +++ b/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_L/code.py @@ -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(): diff --git a/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_T/code.py b/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_T/code.py index 011a18ec8b..8f14110939 100644 --- a/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_T/code.py +++ b/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_T/code.py @@ -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(): diff --git a/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_X/code.py b/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_X/code.py index ef2204b51d..6658b3f536 100644 --- a/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_X/code.py +++ b/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_X/code.py @@ -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(): From 9861c19eb9f1f40242001e944334dc6216a1e360 Mon Sep 17 00:00:00 2001 From: obucklin Date: Mon, 3 Feb 2025 11:42:03 +0100 Subject: [PATCH 02/10] found problem --- src/compas_timber/design/workflow.py | 56 ++++++++++++++++++---------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/src/compas_timber/design/workflow.py b/src/compas_timber/design/workflow.py index c8c7f4a287..751d7d6ed4 100644 --- a/src/compas_timber/design/workflow.py +++ b/src/compas_timber/design/workflow.py @@ -82,29 +82,36 @@ def joints_from_beams_and_rules(elements, rules, max_distance=1e-6): A list of joint definitions that can be applied to the given elements. """ + print("elements", elements) elements = elements if isinstance(elements, list) else list(elements) + print("elements list", elements) direct_rules = JointRule.get_direct_rules(rules) solver = ConnectionSolver() - max_rule_distance = max([rule.max_distance for rule in rules if rule.max_distance]) + print("max_distance in", 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 print("max_rule_distance", max_rule_distance) element_pairs = solver.find_intersecting_pairs(elements, rtree=True, max_distance=max_rule_distance) - print("element_pairs", [[beam.key for beam in pair]for pair in element_pairs]) joint_defs = [] unmatched_pairs = [] + print("element_pairs", element_pairs) for rule in direct_rules: - if rule.comply(element_pairs, max_distance=max_distance): + print("direct rule", rule) + if rule.comply(element_pairs, model_max_distance=max_distance): joint_defs.append(JointDefinition(rule.joint_type, rule.elements, **rule.kwargs)) while element_pairs: pair = element_pairs.pop() + print(pair) match_found = False for rule in direct_rules: # see if pair is used in a direct rule + print("direct rule", rule) if rule.contains(pair): 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 @@ -113,13 +120,14 @@ def joints_from_beams_and_rules(elements, rules, max_distance=1e-6): for rule in JointRule.get_topology_rules(rules): # see if pair is used in a topology rule print("topo rule", rule, rule.max_distance) print([beam.key for beam in pair]) - 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)) break if not match_found: unmatched_pairs.append(pair) + print("jdefs", joint_defs) return joint_defs, unmatched_pairs @@ -146,12 +154,18 @@ def contains(self, elements): except TypeError: raise UserWarning("unable to comply direct joint element sets") - def comply(self, elements, max_distance=1e-6): - if not self.max_distance: - self.max_distance = max_distance + def comply(self, elements, model_max_distance=1e-6): + + if self.max_distance: + max_distance = self.max_distance + else: + max_distance = model_max_distance + print("element count", len(elements)) + print("pairs", list(combinations(list(elements), 2))) try: - for pair in combinations(elements, 2): - if distance_line_line(pair[0].centerline, pair[1].centerline) > self.max_distance: + for pair in combinations(list(elements), 2): + print("pair", pair) + if distance_line_line(pair[0].centerline, pair[1].centerline) > max_distance: return False except TypeError: raise UserWarning("unable to comply direct joint element sets") @@ -175,16 +189,18 @@ 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): - if not self.max_distance: - self.max_distance = max_distance + 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 elements = list(elements) if element_cats == set([self.category_a, self.category_b]): solver = ConnectionSolver() - found_topology = solver.find_topology(elements[0], elements[1], max_distance=self.max_distance)[0] + found_topology = solver.find_topology(elements[0], elements[1], max_distance=max_distance)[0] supported_topo = self.joint_type.SUPPORTED_TOPOLOGY if not isinstance(supported_topo, list): supported_topo = [supported_topo] @@ -246,15 +262,15 @@ def ToString(self): def __repr__(self): return "{}({}, {})".format(TopologyRule, self.topology_type, self.joint_type, self.max_distance) - def comply(self, elements, max_distance=1e-6): - print("before topo max_distance", self.max_distance) - if not self.max_distance: - self.max_distance = max_distance - print("topo max_distance", self.max_distance) + 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() - topo_results = solver.find_topology(elements[0], elements[1], max_distance=self.max_distance) + topo_results = solver.find_topology(elements[0], elements[1], max_distance=max_distance) return ( self.topology_type == topo_results[0], [topo_results[1], topo_results[2]], From b2e41469b31807cfb04ea9cfdbc68bc271c1a586 Mon Sep 17 00:00:00 2001 From: obucklin Date: Mon, 3 Feb 2025 14:00:10 +0100 Subject: [PATCH 03/10] started test --- src/compas_timber/design/workflow.py | 61 ++++++++------ tests/compas_timber/test_joint_rules.py | 107 ++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 27 deletions(-) create mode 100644 tests/compas_timber/test_joint_rules.py diff --git a/src/compas_timber/design/workflow.py b/src/compas_timber/design/workflow.py index 5b9fd8305a..2ad7f20668 100644 --- a/src/compas_timber/design/workflow.py +++ b/src/compas_timber/design/workflow.py @@ -1,12 +1,13 @@ from itertools import combinations -from compas.geometry import distance_line_line +from compas.geometry import distance_point_point +from compas.geometry import intersection_line_line +from compas.geometry import closest_point_on_segment 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.elements import beam from compas_timber.utils import intersection_line_line_param @@ -82,32 +83,23 @@ def joints_from_beams_and_rules(elements, rules, max_distance=1e-6): A list of joint definitions that can be applied to the given elements. """ - print("elements", elements) elements = elements if isinstance(elements, list) else list(elements) - print("elements list", elements) direct_rules = JointRule.get_direct_rules(rules) solver = ConnectionSolver() - print("max_distance in", 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 - print("max_rule_distance", max_rule_distance) element_pairs = solver.find_intersecting_pairs(elements, rtree=True, max_distance=max_rule_distance) joint_defs = [] unmatched_pairs = [] - print("element_pairs", element_pairs) - for rule in direct_rules: - print("direct rule", rule) - if rule.comply(element_pairs, model_max_distance=max_distance): - joint_defs.append(JointDefinition(rule.joint_type, rule.elements, **rule.kwargs)) while element_pairs: pair = element_pairs.pop() - print(pair) match_found = False - for rule in direct_rules: # see if pair is used in a direct rule - print("direct rule", rule) - if rule.contains(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 @@ -118,8 +110,6 @@ def joints_from_beams_and_rules(elements, rules, max_distance=1e-6): if not match_found: for rule in JointRule.get_topology_rules(rules): # see if pair is used in a topology rule - print("topo rule", rule, rule.max_distance) - print([beam.key for beam in pair]) comply, ordered_pair = rule.comply(pair, model_max_distance=max_distance) if comply: match_found = True @@ -127,7 +117,6 @@ def joints_from_beams_and_rules(elements, rules, max_distance=1e-6): break if not match_found: unmatched_pairs.append(pair) - print("jdefs", joint_defs) return joint_defs, unmatched_pairs @@ -160,13 +149,10 @@ def comply(self, elements, model_max_distance=1e-6): max_distance = self.max_distance else: max_distance = model_max_distance - print("element count", len(elements)) - print("pairs", list(combinations(list(elements), 2))) + try: for pair in combinations(list(elements), 2): - print("pair", pair) - if distance_line_line(pair[0].centerline, pair[1].centerline) > max_distance: - return False + return distance_segment_segment(pair[0].centerline, pair[1].centerline) <= max_distance except TypeError: raise UserWarning("unable to comply direct joint element sets") @@ -251,8 +237,6 @@ 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 - print("topo __init__ max_distance", self.max_distance) - print(kwargs) self.kwargs = kwargs def ToString(self): @@ -456,3 +440,26 @@ def add_joint_error(self, error): self.joint_errors.extend(error) else: self.joint_errors.append(error) + + +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) diff --git a/tests/compas_timber/test_joint_rules.py b/tests/compas_timber/test_joint_rules.py new file mode 100644 index 0000000000..4e72710e17 --- /dev/null +++ b/tests/compas_timber/test_joint_rules.py @@ -0,0 +1,107 @@ +import os + +import compas +import pytest +from compas.data import json_load +from compas.geometry import Frame +from compas.geometry import Line +from compas.geometry import Point +from compas.geometry import Vector + +from compas_timber.connections import LButtJoint +from compas_timber.connections import LLapJoint +from compas_timber.connections import TButtJoint +from compas_timber.connections import TLapJoint +from compas_timber.connections import XLapJoint +from compas_timber.connections import find_neighboring_beams +from compas_timber.elements import Beam +from compas_timber.model import TimberModel +from compas_timber.connections import JointTopology + + +import pytest +from compas.geometry import Line, Point +from compas_timber.elements import Beam +from compas_timber.connections import LMiterJoint, TButtJoint, XLapJoint +from compas_timber.design import JointRule, DirectRule, CategoryRule, TopologyRule + +@pytest.fixture +def beams(): + """ + 0 <=> 1:L + 1 <=> 2:T + 2 <=> 3:L + 3 <=> 0:X + + """ + w = 0.2 + h = 0.2 + lines = [ + Line(Point(-1, 0, 0), Point(1, 0, 0)), + Line(Point(1, 0, 0), Point(1, 2, 0)), + Line(Point(1, 1, 0), Point(0, 1, 0)), + Line(Point(0, 1, 0), Point(0, -1, 0)), + ] + return [Beam.from_centerline(line, w, h) for line in lines] + +def test_joints_from_beams_and_topo_rules(beams): + rules = [ + TopologyRule(JointTopology.TOPO_L, LMiterJoint), + TopologyRule(JointTopology.TOPO_T, TButtJoint), + TopologyRule(JointTopology.TOPO_X, XLapJoint), + ] + joint_defs, unmatched_pairs = JointRule.joints_from_beams_and_rules(beams, rules) + assert len(joint_defs) == 4 + assert len(unmatched_pairs) == 0 + assert joint_defs[0].type == "LMiterJoint" + assert joint_defs[1].type == "TButtJoint" + assert joint_defs[2].type == "LMiterJoint" + assert joint_defs[3].type == "XLapJoint" + + + +def test_joints_from_beams_and_rules_with_max_distance(beams): + rules = [ + TopologyRule(JointTopology.TOPO_L, LMiterJoint, max_distance=0.2), + TopologyRule(JointTopology.TOPO_T, TButtJoint, max_distance=0.2), + TopologyRule(JointTopology.TOPO_X, XLapJoint, max_distance=0.2), + ] + joint_defs, unmatched_pairs = JointRule.joints_from_beams_and_rules(beams, rules, max_distance=0.5) + assert len(joint_defs) == 0 + assert len(unmatched_pairs) == 4 + + rules = [ + TopologyRule(JointTopology.TOPO_L, LMiterJoint, max_distance=0.5), + TopologyRule(JointTopology.TOPO_T, TButtJoint, max_distance=0.5), + TopologyRule(JointTopology.TOPO_X, XLapJoint, max_distance=0.5), + ] + joint_defs, unmatched_pairs = JointRule.joints_from_beams_and_rules(beams, rules, max_distance=0.2) + assert len(joint_defs) == 4 + assert len(unmatched_pairs) == 0 + + + +def test_direct_rule_contains(beams): + rule = DirectRule(LMiterJoint, beams[:2]) + assert rule.contains(beams[:2]) is True + assert rule.contains(beams[1:3]) is False + assert rule.contains(beams[2:]) is False + assert rule.contains([beams[0],beams[3]]) is False + +def test_direct_rule_comply(beams): + rule = DirectRule(LMiterJoint, beams[:2], max_distance=0.5) + assert rule.comply(beams[:2]) is True + assert rule.comply(beams[2:]) is False + +def test_category_rule_comply(beams): + for beam in beams: + beam.attributes["category"] = "A" + beams[1].attributes["category"] = "B" + rule = CategoryRule(LMiterJoint, "A", "B") + assert rule.comply(beams[:2]) is True + assert rule.comply(beams[2:]) is False + +def test_topology_rule_comply(beams): + rule = TopologyRule(JointTopology.TOPO_L, LMiterJoint) + assert rule.comply(beams[:2])[0] is True + assert rule.comply(beams[2:])[0] is False From 6326d0893843134451719600fde2e12c51299d71 Mon Sep 17 00:00:00 2001 From: oliver bucklin Date: Wed, 5 Feb 2025 10:23:55 +0100 Subject: [PATCH 04/10] added joint rule tests --- src/compas_timber/design/workflow.py | 5 +- tests/compas_timber/test_joint_rules.py | 159 ++++++++++++++++++++---- 2 files changed, 141 insertions(+), 23 deletions(-) diff --git a/src/compas_timber/design/workflow.py b/src/compas_timber/design/workflow.py index 2ad7f20668..993af3ad51 100644 --- a/src/compas_timber/design/workflow.py +++ b/src/compas_timber/design/workflow.py @@ -144,7 +144,10 @@ def contains(self, elements): 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: diff --git a/tests/compas_timber/test_joint_rules.py b/tests/compas_timber/test_joint_rules.py index 4e72710e17..26d8d1cf76 100644 --- a/tests/compas_timber/test_joint_rules.py +++ b/tests/compas_timber/test_joint_rules.py @@ -8,6 +8,9 @@ from compas.geometry import Point from compas.geometry import Vector + +from compas_timber.connections import JointTopology +from compas_timber.connections import ConnectionSolver from compas_timber.connections import LButtJoint from compas_timber.connections import LLapJoint from compas_timber.connections import TButtJoint @@ -16,7 +19,6 @@ from compas_timber.connections import find_neighboring_beams from compas_timber.elements import Beam from compas_timber.model import TimberModel -from compas_timber.connections import JointTopology import pytest @@ -44,6 +46,65 @@ def beams(): ] return [Beam.from_centerline(line, w, h) for line in lines] + +@pytest.fixture +def separated_beams(): + """ + 0 <=> 1:L + 1 <=> 2:T + 2 <=> 3:L + 3 <=> 0:X + + """ + w = 0.2 + h = 0.2 + lines = [ + Line(Point(-1, 0, 0), Point(1, 0, 0)), + Line(Point(1, 0, 0.1), Point(1, 2, 0.1)), + Line(Point(1, 1, 0), Point(0, 1, 0)), + Line(Point(0, 1, 0.1), Point(0, -1, 0.1)), + ] + return [Beam.from_centerline(line, w, h) for line in lines] + +@pytest.fixture +def L_beams(): + """ + 0 <=> 1:L + 1 <=> 2:T + 2 <=> 3:L + 3 <=> 0:X + + """ + w = 0.2 + h = 0.2 + lines = [ + Line(Point(-1, 0, 0), Point(1, 0, 0)), + Line(Point(1, 0, 0), Point(1, 1, 0)), + Line(Point(1, 1, 0), Point(0, 1, 0)), + Line(Point(0, 1, 0), Point(0, -1, 0)), + ] + return [Beam.from_centerline(line, w, h) for line in lines] + +@pytest.fixture +def L_beams_separated(): + """ + 0 <=> 1:L + 1 <=> 2:T + 2 <=> 3:L + 3 <=> 0:X + + """ + w = 0.2 + h = 0.2 + lines = [ + Line(Point(-1, 0, 0), Point(1, 0, 0)), + Line(Point(1, 0, 0.1), Point(1, 1, 0.1)), + Line(Point(1, 1, 0), Point(0, 1, 0)), + Line(Point(0, 1, 0.1), Point(0, -1, 0.1)), + ] + return [Beam.from_centerline(line, w, h) for line in lines] + + def test_joints_from_beams_and_topo_rules(beams): rules = [ TopologyRule(JointTopology.TOPO_L, LMiterJoint), @@ -53,32 +114,37 @@ def test_joints_from_beams_and_topo_rules(beams): joint_defs, unmatched_pairs = JointRule.joints_from_beams_and_rules(beams, rules) assert len(joint_defs) == 4 assert len(unmatched_pairs) == 0 - assert joint_defs[0].type == "LMiterJoint" - assert joint_defs[1].type == "TButtJoint" - assert joint_defs[2].type == "LMiterJoint" - assert joint_defs[3].type == "XLapJoint" + names = set([joint_def.joint_type.__name__ for joint_def in joint_defs]) + assert names == set(["LMiterJoint", "TButtJoint", "XLapJoint"]) - -def test_joints_from_beams_and_rules_with_max_distance(beams): +def test_joints_from_beams_and_rules_with_max_distance(separated_beams): rules = [ - TopologyRule(JointTopology.TOPO_L, LMiterJoint, max_distance=0.2), - TopologyRule(JointTopology.TOPO_T, TButtJoint, max_distance=0.2), - TopologyRule(JointTopology.TOPO_X, XLapJoint, max_distance=0.2), + TopologyRule(JointTopology.TOPO_L, LMiterJoint), + TopologyRule(JointTopology.TOPO_T, TButtJoint), + TopologyRule(JointTopology.TOPO_X, XLapJoint), ] - joint_defs, unmatched_pairs = JointRule.joints_from_beams_and_rules(beams, rules, max_distance=0.5) + joint_defs, unmatched_pairs = JointRule.joints_from_beams_and_rules(separated_beams, rules) assert len(joint_defs) == 0 assert len(unmatched_pairs) == 4 rules = [ - TopologyRule(JointTopology.TOPO_L, LMiterJoint, max_distance=0.5), - TopologyRule(JointTopology.TOPO_T, TButtJoint, max_distance=0.5), - TopologyRule(JointTopology.TOPO_X, XLapJoint, max_distance=0.5), + TopologyRule(JointTopology.TOPO_L, LMiterJoint), + TopologyRule(JointTopology.TOPO_T, TButtJoint, max_distance=0.15), + TopologyRule(JointTopology.TOPO_X, XLapJoint), ] - joint_defs, unmatched_pairs = JointRule.joints_from_beams_and_rules(beams, rules, max_distance=0.2) - assert len(joint_defs) == 4 - assert len(unmatched_pairs) == 0 + joint_defs, unmatched_pairs = JointRule.joints_from_beams_and_rules(separated_beams, rules) + assert len(joint_defs) == 1 + assert len(unmatched_pairs) == 3 + rules = [ + TopologyRule(JointTopology.TOPO_L, LMiterJoint), + TopologyRule(JointTopology.TOPO_T, TButtJoint, max_distance=0.05), + TopologyRule(JointTopology.TOPO_X, XLapJoint), + ] + joint_defs, unmatched_pairs = JointRule.joints_from_beams_and_rules(separated_beams, rules, max_distance=0.15) + assert len(joint_defs) == 3 + assert len(unmatched_pairs) == 1 def test_direct_rule_contains(beams): @@ -89,9 +155,18 @@ def test_direct_rule_contains(beams): assert rule.contains([beams[0],beams[3]]) is False def test_direct_rule_comply(beams): - rule = DirectRule(LMiterJoint, beams[:2], max_distance=0.5) - assert rule.comply(beams[:2]) is True - assert rule.comply(beams[2:]) is False + rule = DirectRule(LMiterJoint, [beams[0], beams[1]]) + assert rule.comply([beams[0], beams[1]]) is True + assert rule.comply([beams[2], beams[3]]) is True + assert rule.comply([beams[1], beams[2]]) is True + assert rule.comply([beams[3], beams[0]]) is True + +def test_direct_rule_comply_max_distance(separated_beams): + rule = DirectRule(LMiterJoint, [separated_beams[0], separated_beams[1]], max_distance=0.05) + assert rule.comply([separated_beams[0], separated_beams[1]]) is False + assert rule.comply([separated_beams[2], separated_beams[3]]) is False + assert rule.comply([separated_beams[1], separated_beams[2]]) is False + assert rule.comply([separated_beams[3], separated_beams[0]]) is False def test_category_rule_comply(beams): for beam in beams: @@ -103,5 +178,45 @@ def test_category_rule_comply(beams): def test_topology_rule_comply(beams): rule = TopologyRule(JointTopology.TOPO_L, LMiterJoint) - assert rule.comply(beams[:2])[0] is True - assert rule.comply(beams[2:])[0] is False + assert rule.comply([beams[0], beams[1]])[0] is True + assert rule.comply([beams[1], beams[2]])[0] is False + assert rule.comply([beams[2], beams[3]])[0] is True + assert rule.comply([beams[3], beams[0]])[0] is False + + +def test_different_rules(L_beams): + for beam in L_beams: + beam.attributes["category"] = "A" + L_beams[1].attributes["category"] = "B" + rules = [ + DirectRule(LLapJoint, L_beams[:2]), + CategoryRule(LButtJoint, "A", "B"), + TopologyRule(JointTopology.TOPO_L, LMiterJoint) + ] + joint_defs, unmatched_pairs = JointRule.joints_from_beams_and_rules(L_beams, rules) + joints_names = set([joint_def.joint_type.__name__ for joint_def in joint_defs]) + assert joints_names == set(["LLapJoint", "LButtJoint", "LMiterJoint"]) + assert len(joint_defs) == 3 + +def test_different_rules_max_distance(L_beams_separated): + for beam in L_beams_separated: + beam.attributes["category"] = "A" + L_beams_separated[1].attributes["category"] = "B" + rules = [ + DirectRule(LLapJoint, L_beams_separated[:2]), + CategoryRule(LButtJoint, "A", "B"), + TopologyRule(JointTopology.TOPO_L, LMiterJoint) + ] + joint_defs, unmatched_pairs = JointRule.joints_from_beams_and_rules(L_beams_separated, rules) + joints_names = set([joint_def.joint_type.__name__ for joint_def in joint_defs]) + assert len(joint_defs) == 0 + + rules = [ + DirectRule(LLapJoint, L_beams_separated[:2]), + CategoryRule(LButtJoint, "A", "B"), + TopologyRule(JointTopology.TOPO_L, LMiterJoint, max_distance=0.15) + ] + joint_defs, unmatched_pairs = JointRule.joints_from_beams_and_rules(L_beams_separated, rules) + joints_names = set([joint_def.joint_type.__name__ for joint_def in joint_defs]) + assert joints_names == set(["LMiterJoint"]) + assert len(joint_defs) == 3 \ No newline at end of file From 4c47e6eff439e0075513d5986b407b0fe492b3ef Mon Sep 17 00:00:00 2001 From: oliver bucklin Date: Wed, 5 Feb 2025 10:30:11 +0100 Subject: [PATCH 05/10] lint format test --- src/compas_timber/design/workflow.py | 19 ++++--- .../components/CT_Joint_Rule_Category/code.py | 2 +- .../components/CT_Joint_Rule_Direct/code.py | 2 +- .../CT_Joint_Rule_From_List/code.py | 2 +- .../CT_Joint_Rule_Topology_L/code.py | 2 +- .../CT_Joint_Rule_Topology_T/code.py | 2 +- .../CT_Joint_Rule_Topology_X/code.py | 2 +- tests/compas_timber/test_joint_rules.py | 57 +++++++------------ 8 files changed, 38 insertions(+), 50 deletions(-) diff --git a/src/compas_timber/design/workflow.py b/src/compas_timber/design/workflow.py index 993af3ad51..81a32144b1 100644 --- a/src/compas_timber/design/workflow.py +++ b/src/compas_timber/design/workflow.py @@ -1,7 +1,8 @@ from itertools import combinations + +from compas.geometry import closest_point_on_segment from compas.geometry import distance_point_point from compas.geometry import intersection_line_line -from compas.geometry import closest_point_on_segment from compas_timber.connections import ConnectionSolver from compas_timber.connections import JointTopology @@ -95,8 +96,8 @@ def joints_from_beams_and_rules(elements, rules, max_distance=1e-6): pair = element_pairs.pop() match_found = False 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 + 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 @@ -123,7 +124,7 @@ def joints_from_beams_and_rules(elements, rules, max_distance=1e-6): class DirectRule(JointRule): """Creates a Joint Rule that directly joins multiple elements.""" - def __init__(self, joint_type, elements, max_distance = None, **kwargs): + def __init__(self, joint_type, elements, max_distance=None, **kwargs): self.elements = elements self.joint_type = joint_type self.max_distance = max_distance @@ -163,7 +164,7 @@ def comply(self, elements, model_max_distance=1e-6): class CategoryRule(JointRule): """Based on the category attribute attached to the elements, this rule assigns""" - def __init__(self, joint_type, category_a, category_b, topos=None, max_distance = None, **kwargs): + 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 @@ -236,7 +237,7 @@ class TopologyRule(JointRule): The keyword arguments to be passed to the joint. """ - def __init__(self, topology_type, joint_type, max_distance = None, **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 @@ -247,7 +248,11 @@ def ToString(self): return repr(self) def __repr__(self): - return "{}({}, {})".format(TopologyRule, self.topology_type, self.joint_type, self.max_distance) + return "{}({}, {})".format( + TopologyRule, + self.topology_type, + self.joint_type, + ) def comply(self, elements, model_max_distance=1e-6): if self.max_distance: diff --git a/src/compas_timber/ghpython/components/CT_Joint_Rule_Category/code.py b/src/compas_timber/ghpython/components/CT_Joint_Rule_Category/code.py index 98d9fe415a..b66ba81ea6 100644 --- a/src/compas_timber/ghpython/components/CT_Joint_Rule_Category/code.py +++ b/src/compas_timber/ghpython/components/CT_Joint_Rule_Category/code.py @@ -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")]+["max_distance"] + return [name for name in names if (name != "key") and (name != "frame")] + ["max_distance"] def AppendAdditionalMenuItems(self, menu): for name in self.classes.keys(): diff --git a/src/compas_timber/ghpython/components/CT_Joint_Rule_Direct/code.py b/src/compas_timber/ghpython/components/CT_Joint_Rule_Direct/code.py index d8946df3c2..5575f8f381 100644 --- a/src/compas_timber/ghpython/components/CT_Joint_Rule_Direct/code.py +++ b/src/compas_timber/ghpython/components/CT_Joint_Rule_Direct/code.py @@ -68,7 +68,7 @@ def RunScript(self, *args): return Rules def arg_names(self): - return inspect.getargspec(self.joint_type.__init__)[0][1:]+["max_distance"] + return inspect.getargspec(self.joint_type.__init__)[0][1:] + ["max_distance"] def AppendAdditionalMenuItems(self, menu): for name in self.classes.keys(): diff --git a/src/compas_timber/ghpython/components/CT_Joint_Rule_From_List/code.py b/src/compas_timber/ghpython/components/CT_Joint_Rule_From_List/code.py index 683f8148dd..be21c058d9 100644 --- a/src/compas_timber/ghpython/components/CT_Joint_Rule_From_List/code.py +++ b/src/compas_timber/ghpython/components/CT_Joint_Rule_From_List/code.py @@ -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 :]+["max_distance"] + return inspect.getargspec(self.joint_type.__init__)[0][self.arg_start_index :] + ["max_distance"] def AppendAdditionalMenuItems(self, menu): for name in self.classes.keys(): diff --git a/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_L/code.py b/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_L/code.py index e7f6be721b..680338a04c 100644 --- a/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_L/code.py +++ b/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_L/code.py @@ -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")]+["max_distance"] + return [name for name in names if (name != "key") and (name != "frame")] + ["max_distance"] def AppendAdditionalMenuItems(self, menu): for name in self.classes.keys(): diff --git a/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_T/code.py b/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_T/code.py index 8f14110939..7a595f5efd 100644 --- a/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_T/code.py +++ b/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_T/code.py @@ -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")]+["max_distance"] + return [name for name in names if (name != "key") and (name != "frame")] + ["max_distance"] def AppendAdditionalMenuItems(self, menu): for name in self.classes.keys(): diff --git a/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_X/code.py b/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_X/code.py index f11db6bf1c..9da5fff75f 100644 --- a/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_X/code.py +++ b/src/compas_timber/ghpython/components/CT_Joint_Rule_Topology_X/code.py @@ -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")]+["max_distance"] + return [name for name in names if (name != "key") and (name != "frame")] + ["max_distance"] def AppendAdditionalMenuItems(self, menu): for name in self.classes.keys(): diff --git a/tests/compas_timber/test_joint_rules.py b/tests/compas_timber/test_joint_rules.py index 26d8d1cf76..d15e6eda14 100644 --- a/tests/compas_timber/test_joint_rules.py +++ b/tests/compas_timber/test_joint_rules.py @@ -1,31 +1,19 @@ -import os - -import compas import pytest -from compas.data import json_load -from compas.geometry import Frame from compas.geometry import Line from compas.geometry import Point -from compas.geometry import Vector - from compas_timber.connections import JointTopology -from compas_timber.connections import ConnectionSolver from compas_timber.connections import LButtJoint from compas_timber.connections import LLapJoint from compas_timber.connections import TButtJoint -from compas_timber.connections import TLapJoint from compas_timber.connections import XLapJoint -from compas_timber.connections import find_neighboring_beams +from compas_timber.connections import LMiterJoint from compas_timber.elements import Beam -from compas_timber.model import TimberModel - +from compas_timber.design import JointRule +from compas_timber.design import DirectRule +from compas_timber.design import CategoryRule +from compas_timber.design import TopologyRule -import pytest -from compas.geometry import Line, Point -from compas_timber.elements import Beam -from compas_timber.connections import LMiterJoint, TButtJoint, XLapJoint -from compas_timber.design import JointRule, DirectRule, CategoryRule, TopologyRule @pytest.fixture def beams(): @@ -66,6 +54,7 @@ def separated_beams(): ] return [Beam.from_centerline(line, w, h) for line in lines] + @pytest.fixture def L_beams(): """ @@ -85,6 +74,7 @@ def L_beams(): ] return [Beam.from_centerline(line, w, h) for line in lines] + @pytest.fixture def L_beams_separated(): """ @@ -152,7 +142,8 @@ def test_direct_rule_contains(beams): assert rule.contains(beams[:2]) is True assert rule.contains(beams[1:3]) is False assert rule.contains(beams[2:]) is False - assert rule.contains([beams[0],beams[3]]) is False + assert rule.contains([beams[0], beams[3]]) is False + def test_direct_rule_comply(beams): rule = DirectRule(LMiterJoint, [beams[0], beams[1]]) @@ -161,6 +152,7 @@ def test_direct_rule_comply(beams): assert rule.comply([beams[1], beams[2]]) is True assert rule.comply([beams[3], beams[0]]) is True + def test_direct_rule_comply_max_distance(separated_beams): rule = DirectRule(LMiterJoint, [separated_beams[0], separated_beams[1]], max_distance=0.05) assert rule.comply([separated_beams[0], separated_beams[1]]) is False @@ -168,6 +160,7 @@ def test_direct_rule_comply_max_distance(separated_beams): assert rule.comply([separated_beams[1], separated_beams[2]]) is False assert rule.comply([separated_beams[3], separated_beams[0]]) is False + def test_category_rule_comply(beams): for beam in beams: beam.attributes["category"] = "A" @@ -176,6 +169,7 @@ def test_category_rule_comply(beams): assert rule.comply(beams[:2]) is True assert rule.comply(beams[2:]) is False + def test_topology_rule_comply(beams): rule = TopologyRule(JointTopology.TOPO_L, LMiterJoint) assert rule.comply([beams[0], beams[1]])[0] is True @@ -188,35 +182,24 @@ def test_different_rules(L_beams): for beam in L_beams: beam.attributes["category"] = "A" L_beams[1].attributes["category"] = "B" - rules = [ - DirectRule(LLapJoint, L_beams[:2]), - CategoryRule(LButtJoint, "A", "B"), - TopologyRule(JointTopology.TOPO_L, LMiterJoint) - ] + rules = [DirectRule(LLapJoint, L_beams[:2]), CategoryRule(LButtJoint, "A", "B"), TopologyRule(JointTopology.TOPO_L, LMiterJoint)] joint_defs, unmatched_pairs = JointRule.joints_from_beams_and_rules(L_beams, rules) - joints_names = set([joint_def.joint_type.__name__ for joint_def in joint_defs]) + joints_names = set([joint_def.joint_type.__name__ for joint_def in joint_defs]) assert joints_names == set(["LLapJoint", "LButtJoint", "LMiterJoint"]) assert len(joint_defs) == 3 + def test_different_rules_max_distance(L_beams_separated): for beam in L_beams_separated: beam.attributes["category"] = "A" L_beams_separated[1].attributes["category"] = "B" - rules = [ - DirectRule(LLapJoint, L_beams_separated[:2]), - CategoryRule(LButtJoint, "A", "B"), - TopologyRule(JointTopology.TOPO_L, LMiterJoint) - ] + rules = [DirectRule(LLapJoint, L_beams_separated[:2]), CategoryRule(LButtJoint, "A", "B"), TopologyRule(JointTopology.TOPO_L, LMiterJoint)] joint_defs, unmatched_pairs = JointRule.joints_from_beams_and_rules(L_beams_separated, rules) - joints_names = set([joint_def.joint_type.__name__ for joint_def in joint_defs]) + joints_names = set([joint_def.joint_type.__name__ for joint_def in joint_defs]) assert len(joint_defs) == 0 - rules = [ - DirectRule(LLapJoint, L_beams_separated[:2]), - CategoryRule(LButtJoint, "A", "B"), - TopologyRule(JointTopology.TOPO_L, LMiterJoint, max_distance=0.15) - ] + rules = [DirectRule(LLapJoint, L_beams_separated[:2]), CategoryRule(LButtJoint, "A", "B"), TopologyRule(JointTopology.TOPO_L, LMiterJoint, max_distance=0.15)] joint_defs, unmatched_pairs = JointRule.joints_from_beams_and_rules(L_beams_separated, rules) - joints_names = set([joint_def.joint_type.__name__ for joint_def in joint_defs]) + joints_names = set([joint_def.joint_type.__name__ for joint_def in joint_defs]) assert joints_names == set(["LMiterJoint"]) - assert len(joint_defs) == 3 \ No newline at end of file + assert len(joint_defs) == 3 From d1cbb4e5b9e0e46c44c083c2ecc60af1e73b494b Mon Sep 17 00:00:00 2001 From: oliver bucklin Date: Wed, 5 Feb 2025 10:38:35 +0100 Subject: [PATCH 06/10] changelog --- CHANGELOG.md | 2 ++ src/compas_timber/design/workflow.py | 28 +--------------------------- src/compas_timber/utils/__init__.py | 26 +++++++++++++++++++++++++- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca5e8b825d..501b578517 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/src/compas_timber/design/workflow.py b/src/compas_timber/design/workflow.py index 81a32144b1..4cbb593712 100644 --- a/src/compas_timber/design/workflow.py +++ b/src/compas_timber/design/workflow.py @@ -1,14 +1,11 @@ from itertools import combinations -from compas.geometry import closest_point_on_segment -from compas.geometry import distance_point_point -from compas.geometry import intersection_line_line - 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 @@ -448,26 +445,3 @@ def add_joint_error(self, error): self.joint_errors.extend(error) else: self.joint_errors.append(error) - - -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) diff --git a/src/compas_timber/utils/__init__.py b/src/compas_timber/utils/__init__.py index 86d66f2b69..c4a9ff8592 100644 --- a/src/compas_timber/utils/__init__.py +++ b/src/compas_timber/utils/__init__.py @@ -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): @@ -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"] From e41310a6d7afd6dd19b5a5e3a3e25282d578f7fb Mon Sep 17 00:00:00 2001 From: oliver bucklin Date: Wed, 5 Feb 2025 10:47:41 +0100 Subject: [PATCH 07/10] added rule documentation --- src/compas_timber/design/workflow.py | 32 ++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/compas_timber/design/workflow.py b/src/compas_timber/design/workflow.py index 4cbb593712..724e0a58a1 100644 --- a/src/compas_timber/design/workflow.py +++ b/src/compas_timber/design/workflow.py @@ -119,7 +119,19 @@ 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. + + 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 @@ -159,7 +171,23 @@ def comply(self, elements, model_max_distance=1e-6): 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 + + 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 From c0738e6099c8438cc6c71964cdb2b4707ace5c18 Mon Sep 17 00:00:00 2001 From: obucklin Date: Fri, 7 Feb 2025 11:48:16 +0100 Subject: [PATCH 08/10] fixed TOL in joint rules --- src/compas_timber/design/workflow.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/compas_timber/design/workflow.py b/src/compas_timber/design/workflow.py index 724e0a58a1..c8a4056d76 100644 --- a/src/compas_timber/design/workflow.py +++ b/src/compas_timber/design/workflow.py @@ -7,6 +7,7 @@ from compas_timber.connections import XLapJoint from compas_timber.utils import distance_segment_segment from compas_timber.utils import intersection_line_line_param +from compas.tolerance import TOL class CollectionDef(object): @@ -63,7 +64,7 @@ def get_topology_rules(rules, use_defaults=False): 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 @@ -153,12 +154,12 @@ def contains(self, elements): except TypeError: raise UserWarning("unable to comply direct joint element sets") - def comply(self, elements, model_max_distance=1e-6): + def comply(self, elements, model_max_distance=TOL.absolute): """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: + if self.max_distance is not None: max_distance = self.max_distance else: max_distance = model_max_distance @@ -204,7 +205,7 @@ 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, model_max_distance=1e-6): + def comply(self, elements, model_max_distance=TOL.absolute): if self.max_distance: max_distance = self.max_distance else: @@ -279,7 +280,7 @@ def __repr__(self): self.joint_type, ) - def comply(self, elements, model_max_distance=1e-6): + def comply(self, elements, model_max_distance=TOL.absolute): if self.max_distance: max_distance = self.max_distance else: From 74d1b0acb4141eee61bf84e85d2efe0ccd8fa10a Mon Sep 17 00:00:00 2001 From: obucklin Date: Fri, 7 Feb 2025 12:06:00 +0100 Subject: [PATCH 09/10] fixed tolerance, docstrings --- src/compas_timber/design/workflow.py | 66 ++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/src/compas_timber/design/workflow.py b/src/compas_timber/design/workflow.py index c8a4056d76..6d7c3fe9d6 100644 --- a/src/compas_timber/design/workflow.py +++ b/src/compas_timber/design/workflow.py @@ -156,8 +156,22 @@ def contains(self, elements): def comply(self, elements, model_max_distance=TOL.absolute): """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. + 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 @@ -206,7 +220,28 @@ def __repr__(self): return "{}({}, {}, {}, {})".format(CategoryRule.__name__, self.joint_type.__name__, self.category_a, self.category_b, self.topos) def comply(self, elements, model_max_distance=TOL.absolute): - if self.max_distance: + """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 @@ -259,6 +294,9 @@ 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. """ @@ -281,7 +319,27 @@ def __repr__(self): ) def comply(self, elements, model_max_distance=TOL.absolute): - if self.max_distance: + """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. + + 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 From db630779e311524dea37936a97565a027b844443 Mon Sep 17 00:00:00 2001 From: obucklin Date: Fri, 7 Feb 2025 12:06:37 +0100 Subject: [PATCH 10/10] format lint test --- src/compas_timber/design/workflow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/compas_timber/design/workflow.py b/src/compas_timber/design/workflow.py index 6d7c3fe9d6..e04e4e94d9 100644 --- a/src/compas_timber/design/workflow.py +++ b/src/compas_timber/design/workflow.py @@ -1,5 +1,7 @@ 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 @@ -7,7 +9,6 @@ from compas_timber.connections import XLapJoint from compas_timber.utils import distance_segment_segment from compas_timber.utils import intersection_line_line_param -from compas.tolerance import TOL class CollectionDef(object):