From 51b6f1dd83e6d3f5ae7b2d3d16e36e2f6808e4fb Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Tue, 5 Mar 2024 19:05:14 +0100 Subject: [PATCH] WIP: TimberAssembly -> TimberModel --- src/compas_timber/assembly/__init__.py | 4 +- src/compas_timber/assembly/assembly.py | 134 +++------------ src/compas_timber/connections/joint.py | 15 +- src/compas_timber/connections/l_butt.py | 44 ++--- src/compas_timber/connections/l_halflap.py | 8 +- src/compas_timber/connections/l_miter.py | 41 ++--- src/compas_timber/connections/lap_joint.py | 40 ++--- src/compas_timber/connections/t_butt.py | 35 ++-- src/compas_timber/connections/t_halflap.py | 6 +- src/compas_timber/connections/x_halflap.py | 4 +- .../ghpython/components/CT_Assembly/code.py | 42 ++--- src/compas_timber/parts/beam.py | 159 ++++++++++++++---- 12 files changed, 234 insertions(+), 298 deletions(-) diff --git a/src/compas_timber/assembly/__init__.py b/src/compas_timber/assembly/__init__.py index 286e164e4..1abc4433a 100644 --- a/src/compas_timber/assembly/__init__.py +++ b/src/compas_timber/assembly/__init__.py @@ -1,3 +1,3 @@ -from .assembly import TimberAssembly +from .assembly import TimberModel -__all__ = ["TimberAssembly"] +__all__ = ["TimberModel"] diff --git a/src/compas_timber/assembly/assembly.py b/src/compas_timber/assembly/assembly.py index 826ea677d..fbc312b07 100644 --- a/src/compas_timber/assembly/assembly.py +++ b/src/compas_timber/assembly/assembly.py @@ -1,12 +1,10 @@ -from compas.datastructures import Assembly -from compas.datastructures import AssemblyError +from compas_model.model import Model -from compas_timber.connections import Joint -from compas_timber.connections import BeamJoinningError from compas_timber.parts import Beam +from compas_timber.connections import Joint -class TimberAssembly(Assembly): +class TimberModel(Model): """Represents a timber assembly containing beams and joints etc. Attributes @@ -15,19 +13,25 @@ class TimberAssembly(Assembly): A list of beams assigned to this assembly. joints : list(:class:`~compas_timber.connections.Joint`) A list of joints assigned to this assembly. - part_keys : list(int) - A list of the keys of the parts included in this assembly. - beam_keys : list(int) - A list of the keys of the beams included in this assembly. - joint_keys : list(int) - A list of the keys of the joints included in this assembly. topologies : list(dict) A list of JointTopology for assembly. dict is: {"detected_topo": detected_topo, "beam_a_key": beam_a_key, "beam_b_key":beam_b_key} See :class:`~compas_timber.connections.JointTopology`. """ + @classmethod + def __from_data__(cls, data): + model = super(TimberModel, cls).__from_data__(data) + for element in model.elementlist: + if isinstance(element, Beam): + model._beams.append(element) + if isinstance(element, Joint): + model._joints.append(element) + for joint in model.joints: + joint.add_features() + return model + def __init__(self, *args, **kwargs): - super(TimberAssembly, self).__init__() + super(TimberModel, self).__init__() self._beams = [] self._joints = [] self._topologies = [] # added to avoid calculating multiple times @@ -44,19 +48,6 @@ def __str__(self): self.guid, len(self.beams), len(self.joints) ) - @classmethod - def __from_data__(cls, data): - assembly = super(TimberAssembly, cls).__from_data__(data) - for part in assembly.parts(): - if isinstance(part, Beam): - assembly._beams.append(part) - if isinstance(part, Joint): - assembly._joints.append(part) - part.restore_beams_from_keys(assembly) - for joint in assembly._joints: - joint.add_features() - return assembly - @property def beams(self): return self._beams @@ -65,33 +56,6 @@ def beams(self): def joints(self): return self._joints - @property - def part_keys(self): - return [part.key for part in self.parts()] - - @property - def beam_keys(self): - return [beam.key for beam in self._beams] - - @property - def joint_keys(self): - return [joint.key for joint in self._joints] - - def contains(self, obj): - """Returns True if this assembly contains the given object, False otherwise. - - Parameters - ---------- - obj: :class:`~compas.data.Data` - The object to look for. - - Returns - ------- - bool - - """ - return obj.guid in self._parts - def add_beam(self, beam): """Adds a Beam to this assembly. @@ -106,12 +70,8 @@ def add_beam(self, beam): The graph key identifier of the added beam. """ - if beam in self._beams: - raise AssemblyError("This beam has already been added to this assembly!") - key = self.add_part(part=beam, type="part_beam") + _ = self.add_element(beam) self._beams.append(beam) - beam.is_added_to_assembly = True - return key def add_joint(self, joint, parts): """Add a joint object to the assembly. @@ -130,31 +90,15 @@ def add_joint(self, joint, parts): The identifier of the joint in the current assembly graph. """ - self._validate_joining_operation(joint, parts) + # self._validate_joining_operation(joint, parts) # create an unconnected node in the graph for the joint object - key = self.add_part(part=joint, type="joint") - # joint.assembly = self + # TODO: for each two parts pairwise do.. in the meantime, allow only two parts + if len(parts) != 2: + raise ValueError("Expected 2 parts. Got instead: {}".format(len(parts))) + a, b = parts + _ = self.add_interaction(a, b, interaction=joint) self._joints.append(joint) - # adds links to the beams - for part in parts: - self.add_connection(part, joint) - return key - - def _validate_joining_operation(self, joint, parts): - if not parts: - raise AssemblyError("Cannot add this joint to assembly: no parts given.") - - if self.contains(joint): - raise AssemblyError("This joint has already been added to this assembly.") - - # TODO: rethink this assertion, maybe it should be possible to have more than 1 joint for the same set of parts - if not [self.contains(part) for part in parts]: - raise AssemblyError("Cannot add this joint to assembly: some of the parts are not in this assembly.") - - if self.are_parts_joined(parts): - raise BeamJoinningError(beams=parts, joint=joint, debug_info="Beams are already joined.") - def remove_joint(self, joint): """Removes this joint object from the assembly. @@ -164,36 +108,10 @@ def remove_joint(self, joint): The joint to remove. """ - del self._parts[joint.guid] - self.graph.delete_node(joint.key) - self._joints.remove(joint) # TODO: make it automatic - joint.assembly = None # TODO: should not be needed - # TODO: distroy joint? + a, b = joint.parts + self.remove_interaction(a, b) + self._joints.remove(joint) - def are_parts_joined(self, parts): - """Checks if there is already a joint defined for the same set of parts. - - Parameters - ---------- - parts : list(:class:`~compas.datastructure.Part`) - The parts to check. - - Returns - ------- - bool - - """ - n = len(parts) - neighbor_keys = [set(self.graph.neighborhood(self._parts[part.guid], ring=1)) for part in parts] - for i in range(n - 1): - nki = neighbor_keys[i] - for j in range(i + 1, n): - nkj = neighbor_keys[j] - nkx = nki.intersection(nkj) - for x in nkx: - if self.graph.node[x]["type"] == "joint": - return True - return False def set_topologies(self, topologies): self._topologies = topologies diff --git a/src/compas_timber/connections/joint.py b/src/compas_timber/connections/joint.py index 8f887e50a..c48475f27 100644 --- a/src/compas_timber/connections/joint.py +++ b/src/compas_timber/connections/joint.py @@ -4,6 +4,8 @@ from compas.geometry import angle_vectors from compas.geometry import intersection_line_line +from compas_model.interactions import Interaction + from .solver import JointTopology @@ -34,7 +36,7 @@ def __init__(self, beams, joint, debug_info=None, debug_geometries=None): self.debug_geometries = debug_geometries or [] -class Joint(Data): +class Joint(Interaction): """Base class for a joint connecting two beams. This is a base class and should not be instantiated directly. @@ -51,15 +53,8 @@ class Joint(Data): SUPPORTED_TOPOLOGY = JointTopology.TOPO_UNKNOWN - def __init__(self, frame=None, key=None): - super(Joint, self).__init__() - self.frame = frame or Frame.worldXY() - self.key = key - self.attributes = {} - - @property - def __data__(self): - return {"frame": self.frame.__data__, "key": self.key, "beams": [beam.key for beam in self.beams]} + def __init__(self): + super(Joint, self).__init__(name=self.__class__.__name__) @property def beams(self): diff --git a/src/compas_timber/connections/l_butt.py b/src/compas_timber/connections/l_butt.py index 6c3309168..0c99903fb 100644 --- a/src/compas_timber/connections/l_butt.py +++ b/src/compas_timber/connections/l_butt.py @@ -39,6 +39,16 @@ class LButtJoint(Joint): SUPPORTED_TOPOLOGY = JointTopology.TOPO_L + @property + def __data__(self): + data = super(LButtJoint, self).__data__ + data["main_beam_key"] = self.main_beam_key + data["cross_beam_key"] = self.cross_beam_key + data["small_beam_butts"] = self.small_beam_butts + data["modify_cross"] = self.modify_cross + data["reject_i"] = self.reject_i + return data + def __init__( self, main_beam=None, cross_beam=None, small_beam_butts=False, modify_cross=True, reject_i=False, **kwargs ): @@ -50,38 +60,13 @@ def __init__( self.main_beam = main_beam self.cross_beam = cross_beam - self.main_beam_key = main_beam.key if main_beam else None - self.cross_beam_key = cross_beam.key if cross_beam else None + self.main_beam_key = main_beam.guid if main_beam else None + self.cross_beam_key = cross_beam.guid if cross_beam else None self.modify_cross = modify_cross self.small_beam_butts = small_beam_butts self.reject_i = reject_i self.features = [] - @property - def __data__(self): - data_dict = { - "main_beam_key": self.main_beam_key, - "cross_beam_key": self.cross_beam_key, - "small_beam_butts": self.small_beam_butts, - "modify_cross": self.modify_cross, - "reject_i": self.reject_i, - } - data_dict.update(super(LButtJoint, self).__data__) - return data_dict - - @classmethod - def __from_data__(cls, value): - instance = cls( - frame=Frame.__from_data__(value["frame"]), - key=value["key"], - small_beam_butts=value["small_beam_butts"], - modify_cross=value["modify_cross"], - reject_i=value["reject_i"], - ) - instance.main_beam_key = value["main_beam_key"] - instance.cross_beam_key = value["cross_beam_key"] - return instance - @property def beams(self): return [self.main_beam, self.cross_beam] @@ -142,14 +127,13 @@ def add_features(self): if self.modify_cross: self.cross_beam.add_blank_extension( - start_cross + extension_tolerance, end_cross + extension_tolerance, self.key + start_cross + extension_tolerance, end_cross + extension_tolerance, self.guid ) f_cross = CutFeature(cross_cutting_plane) self.cross_beam.add_features(f_cross) self.features.append(f_cross) - self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.key) - + self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.guid) f_main = CutFeature(main_cutting_plane) self.main_beam.add_features(f_main) self.features.append(f_main) diff --git a/src/compas_timber/connections/l_halflap.py b/src/compas_timber/connections/l_halflap.py index 5f32771d1..04645e000 100644 --- a/src/compas_timber/connections/l_halflap.py +++ b/src/compas_timber/connections/l_halflap.py @@ -48,8 +48,8 @@ class LHalfLapJoint(LapJoint): SUPPORTED_TOPOLOGY = JointTopology.TOPO_L - def __init__(self, main_beam=None, cross_beam=None, flip_lap_side=False, cut_plane_bias=0.5, frame=None, key=None): - super(LHalfLapJoint, self).__init__(main_beam, cross_beam, flip_lap_side, cut_plane_bias, frame, key) + def __init__(self, main_beam=None, cross_beam=None, flip_lap_side=False, cut_plane_bias=0.5): + super(LHalfLapJoint, self).__init__(main_beam, cross_beam, flip_lap_side, cut_plane_bias) @property def joint_type(self): @@ -69,9 +69,9 @@ def add_features(self): start_cross, end_cross = self.cross_beam.extension_to_plane(cross_cutting_frame) extension_tolerance = 0.01 # TODO: this should be proportional to the unit used - self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.key) + self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.guid) self.cross_beam.add_blank_extension( - start_cross + extension_tolerance, end_cross + extension_tolerance, self.key + start_cross + extension_tolerance, end_cross + extension_tolerance, self.guid ) self.main_beam.add_features(MillVolume(negative_brep_main_beam)) diff --git a/src/compas_timber/connections/l_miter.py b/src/compas_timber/connections/l_miter.py index d026b716e..7229e394f 100644 --- a/src/compas_timber/connections/l_miter.py +++ b/src/compas_timber/connections/l_miter.py @@ -42,33 +42,23 @@ class LMiterJoint(Joint): SUPPORTED_TOPOLOGY = JointTopology.TOPO_L - def __init__(self, beam_a=None, beam_b=None, cutoff=None, frame=None, key=None): - super(LMiterJoint, self).__init__(frame, key) + @property + def __data__(self): + data = super(LMiterJoint, self).__data__ + data["beam_a"] = self.beam_a_key + data["beam_b"] = self.beam_b_key + data["cutoff"] = self.cutoff + return data + + def __init__(self, beam_a=None, beam_b=None, cutoff=None): + super(LMiterJoint, self).__init__() self.beam_a = beam_a self.beam_b = beam_b - self.beam_a_key = beam_a.key if beam_a else None - self.beam_b_key = beam_b.key if beam_b else None + self.beam_a_key = beam_a.guid if beam_a else None + self.beam_b_key = beam_b.guid if beam_b else None self.cutoff = cutoff # for very acute angles, limit the extension of the tip/beak of the joint self.features = [] - @property - def __data__(self): - data_dict = { - "beam_a": self.beam_a_key, - "beam_b": self.beam_b_key, - "cutoff": self.cutoff, - } - data_dict.update(super(LMiterJoint, self).__data__) - return data_dict - - @classmethod - def __from_data__(cls, value): - instance = cls(frame=Frame.__from_data__(value["frame"]), key=value["key"], cutoff=value["cutoff"]) - instance.beam_a_key = value["beam_a"] - instance.beam_b_key = value["beam_b"] - instance.cutoff = value["cutoff"] - return instance - @property def joint_type(self): return "L-Miter" @@ -78,6 +68,8 @@ def beams(self): return [self.beam_a, self.beam_b] def get_cutting_planes(self): + assert self.beam_a and self.beam_b + vA = Vector(*self.beam_a.frame.xaxis) # frame.axis gives a reference, not a copy vB = Vector(*self.beam_b.frame.xaxis) @@ -139,9 +131,8 @@ def add_features(self): except Exception as ex: raise BeamJoinningError(self.beams, self, debug_info=str(ex)) - self.beam_a.add_blank_extension(start_a, end_a, self.key) - self.beam_b.add_blank_extension(start_b, end_b, self.key) - + self.beam_a.add_blank_extension(start_a, end_a, self.guid) + self.beam_b.add_blank_extension(start_b, end_b, self.guid) f1, f2 = CutFeature(plane_a), CutFeature(plane_b) self.beam_a.add_features(f1) self.beam_b.add_features(f2) diff --git a/src/compas_timber/connections/lap_joint.py b/src/compas_timber/connections/lap_joint.py index 380e37885..2bade5d33 100644 --- a/src/compas_timber/connections/lap_joint.py +++ b/src/compas_timber/connections/lap_joint.py @@ -44,39 +44,25 @@ class LapJoint(Joint): """ - def __init__(self, main_beam=None, cross_beam=None, flip_lap_side=False, cut_plane_bias=0.5, frame=None, key=None): - super(LapJoint, self).__init__(frame=frame, key=key) + @property + def __data__(self): + data = super(LapJoint, self).__data__ + data["main_beam"] = self.main_beam_key + data["cross_beam"] = self.cross_beam_key + data["flip_lap_side"] = self.flip_lap_side + data["cut_plane_bias"] = self.cut_plane_bias + return data + + def __init__(self, main_beam=None, cross_beam=None, flip_lap_side=False, cut_plane_bias=0.5): + super(LapJoint, self).__init__() self.main_beam = main_beam self.cross_beam = cross_beam self.flip_lap_side = flip_lap_side self.cut_plane_bias = cut_plane_bias - self.main_beam_key = main_beam.key if main_beam else None - self.cross_beam_key = cross_beam.key if cross_beam else None + self.main_beam_key = main_beam.guid if main_beam else None + self.cross_beam_key = cross_beam.guid if cross_beam else None self.features = [] - @property - def __data__(self): - data_dict = { - "main_beam": self.main_beam_key, - "cross_beam": self.cross_beam_key, - "flip_lap_side": self.flip_lap_side, - "cut_plane_bias": self.cut_plane_bias, - } - data_dict.update(super(LapJoint, self).__data__) - return data_dict - - @classmethod - def __from_data__(cls, value): - instance = cls( - frame=Frame.__from_data__(value["frame"]), - key=value["key"], - cut_plane_bias=value["cut_plane_bias"], - flip_lap_side=value["flip_lap_side"], - ) - instance.main_beam_key = value["main_beam"] - instance.cross_beam_key = value["cross_beam"] - return instance - @property def beams(self): return [self.main_beam, self.cross_beam] diff --git a/src/compas_timber/connections/t_butt.py b/src/compas_timber/connections/t_butt.py index 81ac3eb90..5b0a2372e 100644 --- a/src/compas_timber/connections/t_butt.py +++ b/src/compas_timber/connections/t_butt.py @@ -39,32 +39,23 @@ class TButtJoint(Joint): SUPPORTED_TOPOLOGY = JointTopology.TOPO_T - def __init__(self, main_beam=None, cross_beam=None, gap=None, frame=None, key=None): - super(TButtJoint, self).__init__(frame, key) - self.main_beam_key = main_beam.key if main_beam else None - self.cross_beam_key = cross_beam.key if cross_beam else None + @property + def __data__(self): + data = super(TButtJoint, self).__data__ + data["main_beam_key"] = self.main_beam_key + data["cross_beam_key"] = self.cross_beam_key + data["gap"] = self.gap + return data + + def __init__(self, main_beam=None, cross_beam=None, gap=None): + super(TButtJoint, self).__init__() + self.main_beam_key = main_beam.guid if main_beam else None + self.cross_beam_key = cross_beam.guid if cross_beam else None self.main_beam = main_beam self.cross_beam = cross_beam self.gap = gap self.features = [] - @property - def __data__(self): - data_dict = { - "main_beam_key": self.main_beam_key, - "cross_beam_key": self.cross_beam_key, - "gap": self.gap, - } - data_dict.update(super(TButtJoint, self).__data__) - return data_dict - - @classmethod - def __from_data__(cls, value): - instance = cls(frame=Frame.__from_data__(value["frame"]), key=value["key"], gap=value["gap"]) - instance.main_beam_key = value["main_beam_key"] - instance.cross_beam_key = value["cross_beam_key"] - return instance - @property def beams(self): return [self.main_beam, self.cross_beam] @@ -105,7 +96,7 @@ def add_features(self): raise BeamJoinningError(beams=self.beams, joint=self, debug_info=str(ex)) extension_tolerance = 0.01 # TODO: this should be proportional to the unit used - self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.key) + self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.guid) trim_feature = CutFeature(cutting_plane) self.main_beam.add_features(trim_feature) diff --git a/src/compas_timber/connections/t_halflap.py b/src/compas_timber/connections/t_halflap.py index cfe5102ad..b5ddb5cba 100644 --- a/src/compas_timber/connections/t_halflap.py +++ b/src/compas_timber/connections/t_halflap.py @@ -48,8 +48,8 @@ class THalfLapJoint(LapJoint): SUPPORTED_TOPOLOGY = JointTopology.TOPO_T - def __init__(self, main_beam=None, cross_beam=None, flip_lap_side=False, cut_plane_bias=0.5, frame=None, key=None): - super(THalfLapJoint, self).__init__(main_beam, cross_beam, flip_lap_side, cut_plane_bias, frame, key) + def __init__(self, main_beam=None, cross_beam=None, flip_lap_side=False, cut_plane_bias=0.5): + super(THalfLapJoint, self).__init__(main_beam, cross_beam, flip_lap_side, cut_plane_bias) @property def joint_type(self): @@ -71,7 +71,7 @@ def add_features(self): raise BeamJoinningError(beams=self.beams, joint=self, debug_info=str(ex)) extension_tolerance = 0.01 # TODO: this should be proportional to the unit used - self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.key) + self.main_beam.add_blank_extension(start_main + extension_tolerance, end_main + extension_tolerance, self.guid) self.main_beam.add_features(MillVolume(negative_brep_main_beam)) self.cross_beam.add_features(MillVolume(negative_brep_cross_beam)) diff --git a/src/compas_timber/connections/x_halflap.py b/src/compas_timber/connections/x_halflap.py index 86fa136ec..c20d55393 100644 --- a/src/compas_timber/connections/x_halflap.py +++ b/src/compas_timber/connections/x_halflap.py @@ -46,8 +46,8 @@ class XHalfLapJoint(LapJoint): SUPPORTED_TOPOLOGY = JointTopology.TOPO_X - def __init__(self, main_beam=None, cross_beam=None, flip_lap_side=False, cut_plane_bias=0.5, frame=None, key=None): - super(XHalfLapJoint, self).__init__(main_beam, cross_beam, flip_lap_side, cut_plane_bias, frame, key) + def __init__(self, main_beam=None, cross_beam=None, flip_lap_side=False, cut_plane_bias=0.5): + super(XHalfLapJoint, self).__init__(main_beam, cross_beam, flip_lap_side, cut_plane_bias) @property def joint_type(self): diff --git a/src/compas_timber/ghpython/components/CT_Assembly/code.py b/src/compas_timber/ghpython/components/CT_Assembly/code.py index e17b06218..076dac1e3 100644 --- a/src/compas_timber/ghpython/components/CT_Assembly/code.py +++ b/src/compas_timber/ghpython/components/CT_Assembly/code.py @@ -2,8 +2,7 @@ from ghpythonlib.componentbase import executingcomponent as component from Grasshopper.Kernel.GH_RuntimeMessageLevel import Warning -from compas_timber.assembly import TimberAssembly -from compas_timber.consumers import BrepGeometryConsumer +from compas_timber.assembly import TimberModel from compas_timber.connections import ConnectionSolver from compas_timber.connections import JointTopology from compas_timber.connections import BeamJoinningError @@ -15,17 +14,6 @@ class Assembly(component): - def __init__(self): - # maintains relationship of old_beam.id => new_beam_obj for referencing - # lets us modify copies of the beams while referencing them using their old identities. - self._beam_map = {} - - def _get_copied_beams(self, old_beams): - """For the given old_beams returns their respective copies.""" - new_beams = [] - for beam in old_beams: - new_beams.append(self._beam_map[id(beam)]) - return new_beams def get_joints_from_rules(self, beams, rules, topologies): if not isinstance(rules, list): @@ -101,9 +89,10 @@ def RunScript(self, Beams, JointRules, Features, MaxDistance, CreateGeometry): if not (Beams): # shows beams even if no joints are found return - Assembly = TimberAssembly() + Assembly = TimberModel() debug_info = DebugInfomation() - + for beam in Beams: + Assembly.add_beam(beam) topologies = [] solver = ConnectionSolver() found_pairs = solver.find_intersecting_pairs(Beams, rtree=True, max_distance=MaxDistance) @@ -114,14 +103,7 @@ def RunScript(self, Beams, JointRules, Features, MaxDistance, CreateGeometry): topologies.append({"detected_topo": detected_topo, "beam_a": beam_a, "beam_b": beam_b}) Assembly.set_topologies(topologies) - self._beam_map = {} - beams = [b for b in Beams if b is not None] - for beam in beams: - c_beam = beam.copy() - Assembly.add_beam(c_beam) - self._beam_map[id(beam)] = c_beam beams = Assembly.beams - joints = self.get_joints_from_rules(beams, JointRules, topologies) if joints: @@ -129,7 +111,7 @@ def RunScript(self, Beams, JointRules, Features, MaxDistance, CreateGeometry): joints = [j for j in joints if j is not None] # apply reversed. later joints in orginal list override ealier ones for joint in joints[::-1]: - beams_to_pair = self._get_copied_beams(joint.beams) + beams_to_pair = joint.beams beam_pair_ids = set([id(beam) for beam in beams_to_pair]) if beam_pair_ids in handled_beams: continue @@ -149,14 +131,12 @@ def RunScript(self, Beams, JointRules, Features, MaxDistance, CreateGeometry): Geometry = None scene = Scene() - if CreateGeometry: - vis_consumer = BrepGeometryConsumer(Assembly) - for result in vis_consumer.result: - scene.add(result.geometry) - if result.debug_info: - debug_info.add_feature_error(result.debug_info) - else: - for beam in Assembly.beams: + for beam in Assembly.beams: + if CreateGeometry: + scene.add(beam.geometry) + if beam.debug_infos: + debug_info.add_feature_error(beam.debug_infos) + else: scene.add(beam.blank) if debug_info.has_errors: diff --git a/src/compas_timber/parts/beam.py b/src/compas_timber/parts/beam.py index 1440767c7..69a1ff9ca 100644 --- a/src/compas_timber/parts/beam.py +++ b/src/compas_timber/parts/beam.py @@ -1,6 +1,10 @@ import math -from compas.datastructures import Part +import compas +import compas.geometry +import compas.datastructures +from compas_model.elements import Element +from compas.geometry import Brep from compas.geometry import Box from compas.geometry import Frame from compas.geometry import Line @@ -10,23 +14,18 @@ from compas.geometry import add_vectors from compas.geometry import angle_vectors from compas.geometry import cross_vectors +from compas.geometry import bounding_box +from compas.geometry import oriented_bounding_box from compas_timber.utils.compas_extra import intersection_line_plane +from .features import FeatureApplicationError + ANGLE_TOLERANCE = 1e-3 # [radians] DEFAULT_TOLERANCE = 1e-6 -def _create_box(frame, xsize, ysize, zsize): - # mesh reference point is always worldXY, geometry is transformed to actual frame on Beam.geometry - # TODO: Alternative: Add frame information to MeshGeometry, otherwise Frame is only implied by the vertex values - boxframe = frame.copy() - depth_offset = boxframe.xaxis * xsize * 0.5 - boxframe.point += depth_offset - return Box(xsize, ysize, zsize, frame=boxframe) - - -class Beam(Part): +class Beam(Element): """ A class to represent timber beams (studs, slats, etc.) with rectangular cross-sections. @@ -82,38 +81,42 @@ class Beam(Part): """ + @property + def __data__(self): + data = super(Beam, self).__data__ + data["width"] = self.width, + data["height"] = self.height, + data["length"] = self.length, + return data + def __init__(self, frame, length, width, height, **kwargs): - super(Beam, self).__init__(frame=frame) + super(Beam, self).__init__(frame=frame, **kwargs) self.width = width self.height = height self.length = length self.features = [] + self.attributes = {} + self.attributes.update(kwargs) self._blank_extensions = {} + self.debug_infos = [] - @property - def __data__(self): - data = { - "frame": self.frame.__data__, - "key": self.key, - "width": self.width, - "height": self.height, - "length": self.length, - } - return data + def __repr__(self): + # type: () -> str + return "Beam(frame={!r}, length={}, width={}, height={})".format( + self.frame, self.length, self.width, self.height + ) - @classmethod - def __from_data__(cls, data): - instance = cls(Frame.__from_data__(data["frame"]), data["length"], data["width"], data["height"]) - instance.key = data["key"] - return instance + # ========================================================================== + # Computed attributes + # ========================================================================== @property def shape(self): - return _create_box(self.frame, self.length, self.width, self.height) + return self._create_shape(self.frame, self.length, self.width, self.height) @property def blank(self): - return _create_box(self.blank_frame, self.blank_length, self.width, self.height) + return self._create_shape(self.blank_frame, self.blank_length, self.width, self.height) @property def blank_length(self): @@ -172,11 +175,7 @@ def centerline_end(self): @property def aabb(self): - vertices, _ = self.blank.to_vertices_and_faces() - x = [p.x for p in vertices] - y = [p.y for p in vertices] - z = [p.z for p in vertices] - return min(x), min(y), min(z), max(x), max(y), max(z) + return self.compute_aabb() @property def long_edges(self): @@ -206,6 +205,88 @@ def __str__(self): self.frame, ) + # ========================================================================== + # Implementations of abstract methods + # ========================================================================== + def compute_geometry(self, include_features=True): + # type: (bool) -> compas.datastructures.Mesh | compas.geometry.Brep + """Compute the geometry of the element. + + Parameters + ---------- + include_features : bool, optional + If ``True``, include the features in the computed geometry. + If ``False``, return only the base geometry. + + Returns + ------- + :class:`compas.datastructures.Mesh` | :class:`compas.geometry.Brep` + + """ + blank_geo = Brep.from_box(self.blank) + if include_features: + for feature in self.features: + try: + blank_geo = feature.apply(blank_geo) + except FeatureApplicationError as error: + self.debug_infos.append(error) + return blank_geo + + def compute_aabb(self, inflate=0.0): + # type: (float) -> tuple[float, float, float, float, float, float] + """Computes the Axis Aligned Bounding Box (AABB) of the element. + + Parameters + ---------- + inflate : float, optional + Offset of box to avoid floating point errors. + + Returns + ------- + tuple(float, float, float, float, float, float) + The AABB of the element. + + """ + vertices, _ = self.blank.to_vertices_and_faces() + x = [p.x for p in vertices] + y = [p.y for p in vertices] + z = [p.z for p in vertices] + return min(x), min(y), min(z), max(x), max(y), max(z) + + def compute_obb(self, inflate=0.0): + # type: (float | None) -> compas.geometry.Box + """Computes the Oriented Bounding Box (OBB) of the element. + + Parameters + ---------- + inflate : float + Offset of box to avoid floating point errors. + + Returns + ------- + :class:`compas.geometry.Box` + The OBB of the element. + + """ + raise NotImplementedError + + def compute_collision_mesh(self): + # type: () -> compas.datastructures.Mesh + """Computes the collision geometry of the element. + + Returns + ------- + :class:`compas.datastructures.Mesh` + The collision geometry of the element. + + """ + raise NotImplementedError + + + # ========================================================================== + # Alternative constructors + # ========================================================================== + @classmethod def from_centerline(cls, centerline, width, height, z_vector=None): """Define the beam from its centerline. @@ -265,6 +346,15 @@ def from_endpoints(cls, point_start, point_end, width, height, z_vector=None): line = Line(point_start, point_end) return cls.from_centerline(line, width, height, z_vector) + @staticmethod + def _create_shape(frame, xsize, ysize, zsize): + # mesh reference point is always worldXY, geometry is transformed to actual frame on Beam.geometry + # TODO: Alternative: Add frame information to MeshGeometry, otherwise Frame is only implied by the vertex values + boxframe = frame.copy() + depth_offset = boxframe.xaxis * xsize * 0.5 + boxframe.point += depth_offset + return Box(xsize, ysize, zsize, frame=boxframe) + def add_features(self, features): """Adds one or more features to the beam. @@ -277,6 +367,7 @@ def add_features(self, features): if not isinstance(features, list): features = [features] self.features.extend(features) + self._geometry = None def remove_features(self, features=None): """Removes a feature from the beam.