diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f740991..a7ef6ca75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added `Plate` element. +* Added attribute `plates` to `TimberModel`. + ### Changed +* Renamed `beam` to `element` in different locations to make it more generic. + ### Removed +* Removed `add_beam` from `TimberModel`, use `add_element` instead. +* Removed `add_plate` from `TimberModel`, use `add_element` instead. +* Removed `add_wall` from `TimberModel`, use `add_element` instead. + ## [0.9.1] 2024-07-05 diff --git a/src/compas_timber/design/wall_from_surface.py b/src/compas_timber/design/wall_from_surface.py index ab2005734..de120d313 100644 --- a/src/compas_timber/design/wall_from_surface.py +++ b/src/compas_timber/design/wall_from_surface.py @@ -306,7 +306,6 @@ def generate_perimeter_elements(self): angle_vectors(segment.direction, self.z_axis, deg=True) < 1 or angle_vectors(segment.direction, self.z_axis, deg=True) > 179 ): - if self.lintel_posts: element.type = "jack_stud" else: diff --git a/src/compas_timber/design/workflow.py b/src/compas_timber/design/workflow.py index 28eef2300..9359f3e2a 100644 --- a/src/compas_timber/design/workflow.py +++ b/src/compas_timber/design/workflow.py @@ -181,12 +181,12 @@ class FeatureDefinition(object): """ - def __init__(self, feature, beams): + def __init__(self, feature, elements): self.feature = feature - self.beams = beams + self.elements = elements def __repr__(self): - return "{}({}, {})".format(FeatureDefinition.__name__, repr(self.feature), self.beams) + return "{}({}, {})".format(FeatureDefinition.__name__, repr(self.feature), self.elements) def ToString(self): return repr(self) diff --git a/src/compas_timber/elements/__init__.py b/src/compas_timber/elements/__init__.py index f4813a761..6fc15a33a 100644 --- a/src/compas_timber/elements/__init__.py +++ b/src/compas_timber/elements/__init__.py @@ -1,4 +1,5 @@ from .beam import Beam +from .plate import Plate from .wall import Wall from .features import BrepSubtraction from .features import CutFeature @@ -9,6 +10,7 @@ __all__ = [ "Wall", "Beam", + "Plate", "CutFeature", "DrillFeature", "MillVolume", diff --git a/src/compas_timber/elements/features.py b/src/compas_timber/elements/features.py index d4e7bdc1c..2f9194d2b 100644 --- a/src/compas_timber/elements/features.py +++ b/src/compas_timber/elements/features.py @@ -8,22 +8,22 @@ class FeatureApplicationError(Exception): - """Raised when a feature cannot be applied to a beam geometry. + """Raised when a feature cannot be applied to an element geometry. Attributes ---------- feature_geometry : :class:`~compas.geometry.Geometry` The geometry of the feature that could not be applied. - beam_geometry : :class:`~compas.geometry.Geometry` - The geometry of the beam that could not be modified. + element_geometry : :class:`~compas.geometry.Geometry` + The geometry of the element that could not be modified. message : str The error message. """ - def __init__(self, feature_geometry, beam_geometry, message): + def __init__(self, feature_geometry, element_geometry, message): self.feature_geometry = feature_geometry - self.beam_geometry = beam_geometry + self.element_geometry = element_geometry self.message = message @@ -52,12 +52,12 @@ def is_joinery(self): class CutFeature(Feature): - """Indicates a cut to be made on a beam. + """Indicates a cut to be made on an element. Parameters ---------- cutting_plane : :class:`compas.geometry.Frame` - The plane to cut the beam with. + The plane to cut the element with. """ @@ -71,13 +71,13 @@ def __data__(self): data_dict["cutting_plane"] = self.cutting_plane return data_dict - def apply(self, beam_geometry): - """Apply the feature to the beam geometry. + def apply(self, element_geometry): + """Apply the feature to the element geometry. Raises ------ :class:`compas_timber.elements.FeatureApplicationError` - If the cutting plane does not intersect with the beam geometry. + If the cutting plane does not intersect with the element geometry. Returns ------- @@ -86,17 +86,17 @@ def apply(self, beam_geometry): """ try: - return beam_geometry.trimmed(self.cutting_plane) + return element_geometry.trimmed(self.cutting_plane) except BrepTrimmingError: raise FeatureApplicationError( self.cutting_plane, - beam_geometry, - "The cutting plane does not intersect with beam geometry.", + element_geometry, + "The cutting plane does not intersect with element geometry.", ) class DrillFeature(Feature): - """Parametric drill hole to be made on a beam. + """Parametric drill hole to be made on an element. Parameters ---------- @@ -123,13 +123,13 @@ def __data__(self): data_dict["length"] = self.length return data_dict - def apply(self, beam_geometry): - """Apply the feature to the beam geometry. + def apply(self, element_geometry): + """Apply the feature to the element geometry. Raises ------ :class:`compas_timber.elements.FeatureApplicationError` - If the drill volume is not contained in the beam geometry. + If the drill volume is not contained in the element geometry. Returns ------- @@ -137,28 +137,28 @@ def apply(self, beam_geometry): The resulting geometry after processing. """ - print("applying drill hole feature to beam") + print("applying drill hole feature to element") plane = Plane(point=self.line.start, normal=self.line.vector) plane.point += plane.normal * 0.5 * self.length drill_volume = Cylinder(frame=Frame.from_plane(plane), radius=self.diameter / 2.0, height=self.length) try: - return beam_geometry - Brep.from_cylinder(drill_volume) + return element_geometry - Brep.from_cylinder(drill_volume) except IndexError: raise FeatureApplicationError( drill_volume, - beam_geometry, - "The drill volume is not contained in the beam geometry.", + element_geometry, + "The drill volume is not contained in the element geometry.", ) class MillVolume(Feature): - """A volume to be milled out of a beam. + """A volume to be milled out of an element. Parameters ---------- volume : :class:`compas.geometry.Polyhedron` | :class:`compas.datastructures.Mesh` - The volume to be milled out of the beam. + The volume to be milled out of the element. """ @@ -172,13 +172,13 @@ def __init__(self, volume, **kwargs): super(MillVolume, self).__init__(**kwargs) self.mesh_volume = volume - def apply(self, beam_geometry): - """Apply the feature to the beam geometry. + def apply(self, element_geometry): + """Apply the feature to the element geometry. Raises ------ :class:`compas_timber.elements.FeatureApplicationError` - If the volume does not intersect with the beam geometry. + If the volume does not intersect with the element geometry. Returns ------- @@ -192,22 +192,22 @@ def apply(self, beam_geometry): mesh = self.mesh_volume.to_mesh() volume = Brep.from_mesh(mesh) try: - return beam_geometry - volume + return element_geometry - volume except IndexError: raise FeatureApplicationError( volume, - beam_geometry, - "The volume does not intersect with beam geometry.", + element_geometry, + "The volume does not intersect with element geometry.", ) class BrepSubtraction(Feature): - """Generic volume subtraction from a beam. + """Generic volume subtraction from an element. Parameters ---------- volume : :class:`compas.geometry.Brep` - The volume to be subtracted from the beam. + The volume to be subtracted from the element. """ @@ -221,13 +221,13 @@ def __data__(self): data_dict["volume"] = self.volume return data_dict - def apply(self, beam_geometry): - """Apply the feature to the beam geometry. + def apply(self, element_geometry): + """Apply the feature to the element geometry. Raises ------ :class:`compas_timber.elements.FeatureApplicationError` - If the volume does not intersect with the beam geometry. + If the volume does not intersect with the element geometry. Returns ------- @@ -236,10 +236,10 @@ def apply(self, beam_geometry): """ try: - return beam_geometry - self.volume + return element_geometry - self.volume except IndexError: raise FeatureApplicationError( self.volume, - beam_geometry, - "The volume does not intersect with beam geometry.", + element_geometry, + "The volume does not intersect with element geometry.", ) diff --git a/src/compas_timber/elements/plate.py b/src/compas_timber/elements/plate.py new file mode 100644 index 000000000..362abe838 --- /dev/null +++ b/src/compas_timber/elements/plate.py @@ -0,0 +1,242 @@ +from compas.geometry import Box +from compas.geometry import Brep +from compas.geometry import Frame +from compas.geometry import Transformation +from compas.geometry import Vector +from compas.geometry import angle_vectors_signed +from compas.geometry import dot_vectors +from compas_model.elements import Element +from compas_model.elements import reset_computed + +from .features import FeatureApplicationError + + +class Plate(Element): + """ + A class to represent timber plates (plywood, CLT, etc.) with uniform thickness. + + Parameters + ---------- + outline : :class:`~compas.geometry.RhinoCurve` + A line representing the outline of this plate. + thickness : float + Thickness of the plate material. + vector : :class:`~compas.geometry.Vector`, optional + The vector of the plate. Default is None. + + + Attributes + ---------- + frame : :class:`~compas.geometry.Frame` + The coordinate system (frame) of this plate. + shape : :class:`~compas.geometry.Brep` + An extrusion representing the base geometry of this plate. + outline : :class:`~compas.geometry.Polyline` + A line representing the outline of this plate. + thickness : float + Thickness of the plate material. + aabb : tuple(float, float, float, float, float, float) + An axis-aligned bounding box of this plate as a 6 valued tuple of (xmin, ymin, zmin, xmax, ymax, zmax). + + + """ + + @property + def __data__(self): + data = super(Plate, self).__data__ + data["outline"] = self.outline + data["thickness"] = self.thickness + data["vector"] = self.vector + return data + + def __init__(self, outline, thickness, vector=None, frame=None, **kwargs): + super(Plate, self).__init__(**kwargs) + if not outline.is_closed: + raise ValueError("The outline points are not coplanar.") + self.outline = outline + self.thickness = thickness + self.set_frame_and_outline(outline, vector) + self.attributes = {} + self.attributes.update(kwargs) + self.debug_info = [] + + def __repr__(self): + # type: () -> str + return "Plate(outline={!r}, thickness={})".format(self.outline, self.thickness) + + def __str__(self): + return "Plate {} with thickness {:.3f} with vector {} at {}".format( + self.outline, + self.thickness, + self.vector, + self.frame, + ) + + # ========================================================================== + # Computed attributes + # ========================================================================== + + @property + def blank(self): + return self.obb + + @property + def vector(self): + return self.frame.zaxis * self.thickness + + @property + def shape(self): + brep = Brep.from_extrusion(self.outline, self.vector) + return brep + + @property + def has_features(self): + # TODO: move to compas_future... Part + return len(self.features) > 0 + + # ========================================================================== + # Implementations of abstract methods + # ========================================================================== + + def set_frame_and_outline(self, outline, vector=None): + frame = Frame.from_points(outline.points[0], outline.points[1], outline.points[-2]) + aggregate_angle = 0.0 # this is used to determine if the outline is clockwise or counterclockwise + for i in range(len(outline.points) - 1): + first_vector = Vector.from_start_end(outline.points[i - 1], outline.points[i]) + second_vector = Vector.from_start_end(outline.points[i], outline.points[i + 1]) + aggregate_angle += angle_vectors_signed(first_vector, second_vector, frame.zaxis) + if aggregate_angle > 0: + frame = Frame(frame.point, frame.xaxis, -frame.yaxis) + # flips the frame if the frame.point is at an interior corner + + if vector is not None and dot_vectors(frame.zaxis, vector) < 0: + # if the vector is pointing in the opposite direction from self.frame.normal + frame = Frame(frame.point, frame.yaxis, frame.xaxis) + self.outline.reverse() + # flips the frame if the frame.point is at an exterior corner + + self.frame = frame + + 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` + + """ + plate_geo = self.shape + if include_features: + for feature in self.features: + try: + plate_geo = feature.apply(plate_geo) + except FeatureApplicationError as error: + self.debug_info.append(error) + return plate_geo + + def compute_aabb(self, inflate=0.0): + # type: (float) -> compas.geometry.Box + """Computes the Axis Aligned Bounding Box (AABB) of the element. + + Parameters + ---------- + inflate : float, optional + Offset of box to avoid floating point errors. + + Returns + ------- + :class:`~compas.geometry.Box` + The AABB of the element. + + """ + vertices = [point for point in self.outline.points] + for point in self.outline.points: + vertices.append(point + self.vector) + box = Box.from_points(vertices) + box.xsize += inflate + box.ysize += inflate + box.zsize += inflate + return box + + 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. + + """ + vertices = [point for point in self.outline.points] + for point in self.outline.points: + vertices.append(point + self.vector) + for point in vertices: + point.transform(Transformation.from_change_of_basis(Frame.worldXY(), self.frame)) + obb = Box.from_points(vertices) + obb.xsize += inflate + obb.ysize += inflate + obb.zsize += inflate + + obb.transform(Transformation.from_change_of_basis(self.frame, Frame.worldXY())) + + return obb + + 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. + + """ + return self.obb.to_mesh() + + # ========================================================================== + # Features + # ========================================================================== + + @reset_computed + def add_features(self, features): + """Adds one or more features to the plate. + + Parameters + ---------- + features : :class:`~compas_timber.parts.Feature` | list(:class:`~compas_timber.parts.Feature`) + The feature to be added. + + """ + if not isinstance(features, list): + features = [features] + self.features.extend(features) + + @reset_computed + def remove_features(self, features=None): + """Removes a feature from the plate. + + Parameters + ---------- + feature : :class:`~compas_timber.parts.Feature` | list(:class:`~compas_timber.parts.Feature`) + The feature to be removed. If None, all features will be removed. + + """ + if features is None: + self.features = [] + else: + if not isinstance(features, list): + features = [features] + self.features = [f for f in self.features if f not in features] diff --git a/src/compas_timber/ghpython/components/CT_Model/code.py b/src/compas_timber/ghpython/components/CT_Model/code.py index 2cbfae84a..3ebc28648 100644 --- a/src/compas_timber/ghpython/components/CT_Model/code.py +++ b/src/compas_timber/ghpython/components/CT_Model/code.py @@ -112,27 +112,31 @@ def get_joints_from_rules(self, beams, rules, topologies): ) return joints - def RunScript(self, Beams, JointRules, Features, MaxDistance, CreateGeometry): - if not Beams: + def RunScript(self, Elements, JointRules, Features, MaxDistance, CreateGeometry): + if not Elements: self.AddRuntimeMessage(Warning, "Input parameter Beams failed to collect data") if not JointRules: self.AddRuntimeMessage(Warning, "Input parameter JointRules failed to collect data") - if not (Beams): # shows beams even if no joints are found + if not (Elements): # shows beams even if no joints are found return if MaxDistance is None: MaxDistance = TOL.ABSOLUTE # compared to calculted distance, so shouldn't be just 0.0 Model = TimberModel() debug_info = DebugInfomation() - for beam in Beams: - # prepare beams for downstream processing - beam.remove_features() - beam.remove_blank_extension() - beam.debug_info = [] - Model.add_beam(beam) + for element in Elements: + # prepare elements for downstream processing + if element is None: + continue + element.remove_features() + if hasattr(element, "remove_blank_extension"): + element.remove_blank_extension() + element.debug_info = [] + Model.add_element(element) + topologies = [] solver = ConnectionSolver() - found_pairs = solver.find_intersecting_pairs(Beams, rtree=True, max_distance=MaxDistance) + found_pairs = solver.find_intersecting_pairs(Model.beams, rtree=True, max_distance=MaxDistance) for pair in found_pairs: beam_a, beam_b = pair detected_topo, beam_a, beam_b = solver.find_topology(beam_a, beam_b, max_distance=MaxDistance) @@ -140,8 +144,7 @@ def RunScript(self, Beams, JointRules, Features, MaxDistance, CreateGeometry): topologies.append({"detected_topo": detected_topo, "beam_a": beam_a, "beam_b": beam_b}) Model.set_topologies(topologies) - beams = Model.beams - joints = self.get_joints_from_rules(beams, JointRules, topologies) + joints = self.get_joints_from_rules(Model.beams, JointRules, topologies) if joints: handled_beams = [] @@ -165,18 +168,18 @@ def RunScript(self, Beams, JointRules, Features, MaxDistance, CreateGeometry): if Features: features = [f for f in Features if f is not None] for f_def in features: - for beam in f_def.beams: - beam.add_features(f_def.feature) + for element in f_def.elements: + element.add_features(f_def.feature) Geometry = None scene = Scene() - for beam in Model.beams: + for element in Model.elements(): if CreateGeometry: - scene.add(beam.geometry) - if beam.debug_info: - debug_info.add_feature_error(beam.debug_info) + scene.add(element.geometry) + if element.debug_info: + debug_info.add_feature_error(element.debug_info) else: - scene.add(beam.blank) + scene.add(element.blank) if debug_info.has_errors: self.AddRuntimeMessage(Warning, "Error found during joint creation. See DebugInfo output for details.") diff --git a/src/compas_timber/ghpython/components/CT_Model/metadata.json b/src/compas_timber/ghpython/components/CT_Model/metadata.json index 26f446321..f4f955569 100644 --- a/src/compas_timber/ghpython/components/CT_Model/metadata.json +++ b/src/compas_timber/ghpython/components/CT_Model/metadata.json @@ -10,7 +10,7 @@ "iconDisplay": 0, "inputParameters": [ { - "name": "Beams", + "name": "Elements", "description": "Collection of Beams", "typeHintID": "none", "scriptParamAccess": 1, diff --git a/src/compas_timber/ghpython/components/CT_Plate/code.py b/src/compas_timber/ghpython/components/CT_Plate/code.py new file mode 100644 index 000000000..27ce5591f --- /dev/null +++ b/src/compas_timber/ghpython/components/CT_Plate/code.py @@ -0,0 +1,87 @@ +"""Creates a Beam from a LineCurve.""" + +import rhinoscriptsyntax as rs +from compas.scene import Scene +from compas_rhino.conversions import curve_to_compas +from ghpythonlib.componentbase import executingcomponent as component +from Grasshopper.Kernel.GH_RuntimeMessageLevel import Error +from Grasshopper.Kernel.GH_RuntimeMessageLevel import Warning +from Rhino.RhinoDoc import ActiveDoc + +from compas_timber.elements import Plate as CTPlate +from compas_timber.ghpython.rhino_object_name_attributes import update_rhobj_attributes_name + + +class Plate(component): + def RunScript(self, outline, thickness, vector, category, updateRefObj): + # minimum inputs required + if not outline: + self.AddRuntimeMessage(Warning, "Input parameter 'Outline' failed to collect data") + if not thickness: + self.AddRuntimeMessage(Warning, "Input parameter 'Thickness' failed to collect data") + if not vector: + vector = [None] + # reformat unset parameters for consistency + + if not category: + category = [None] + + plates = [] + scene = Scene() + + if outline and thickness: + # check list lengths for consistency + N = len(outline) + if len(thickness) not in (1, N): + self.AddRuntimeMessage( + Error, " In 'T' I need either one or the same number of inputs as the Crv parameter." + ) + if len(category) not in (0, 1, N): + self.AddRuntimeMessage( + Error, " In 'Category' I need either none, one or the same number of inputs as the Crv parameter." + ) + if len(vector) not in (0, 1, N): + self.AddRuntimeMessage( + Error, " In 'Vector' I need either none, one or the same number of inputs as the Crv parameter." + ) + + # duplicate data if None or single value + if len(thickness) != N: + thickness = [thickness[0] for _ in range(N)] + if len(vector) != N: + vector = [vector[0] for _ in range(N)] + if len(category) != N: + category = [category[0] for _ in range(N)] + + for line, t, v, c in zip(outline, thickness, vector, category): + guid, geometry = self._get_guid_and_geometry(line) + rhino_polyline = rs.coercecurve(geometry) + line = curve_to_compas(rhino_polyline) + + plate = CTPlate(line, t, v) + plate.attributes["rhino_guid"] = str(guid) if guid else None + plate.attributes["category"] = c + + if updateRefObj and guid: + update_rhobj_attributes_name(guid, "outline", str(line)) + update_rhobj_attributes_name(guid, "thickness", str(t)) + update_rhobj_attributes_name(guid, "category", c) + + plates.append(plate) + scene.add(plate.shape) + + geo = scene.draw() + + return plates, geo + + def _get_guid_and_geometry(self, line): # TODO: move to ghpython_helpers + # internalized curves and GH geometry will not have persistent GUIDs, referenced Rhino objects will + # type hint on the input has to be 'ghdoc' for this to work + guid = None + geometry = line + rhino_obj = ActiveDoc.Objects.FindId(line) + + if rhino_obj: + guid = line + geometry = rhino_obj.Geometry + return guid, geometry diff --git a/src/compas_timber/ghpython/components/CT_Plate/icon.png b/src/compas_timber/ghpython/components/CT_Plate/icon.png new file mode 100644 index 000000000..adef94214 Binary files /dev/null and b/src/compas_timber/ghpython/components/CT_Plate/icon.png differ diff --git a/src/compas_timber/ghpython/components/CT_Plate/metadata.json b/src/compas_timber/ghpython/components/CT_Plate/metadata.json new file mode 100644 index 000000000..5897defb1 --- /dev/null +++ b/src/compas_timber/ghpython/components/CT_Plate/metadata.json @@ -0,0 +1,54 @@ +{ + "name": "Plate", + "nickname": "Plate", + "category": "COMPAS Timber", + "subcategory": "Plates", + "description": "Creates a Plate from a Polyline.", + "exposure": 2, + "ghpython": { + "isAdvancedMode": true, + "iconDisplay": 0, + "inputParameters": [ + { + "name": "Outline", + "description": "Referenced polyline, Guid of polyline in the active Rhino document.", + "typeHintID": "ghdoc", + "scriptParamAccess": 1 + }, + { + "name": "Thickness", + "description": "Thickness of the plate material.", + "typeHintID": "float", + "scriptParamAccess": 1 + }, + { + "name": "Vector", + "description": "Determines direction of the plate extrusion", + "typeHintID": "vector", + "scriptParamAccess": 1 + }, + { + "name": "Category", + "description": "Category of a beam.", + "typeHintID": "str", + "scriptParamAccess": 1 + }, + { + "name": "updateRefObj", + "description": "(optional) If True, the attributes in the referenced object will be updated/overwritten with the new values given here.", + "typeHintID": "bool", + "scriptParamAccess": 0 + } + ], + "outputParameters": [ + { + "name": "Plate", + "description": "Plate object(s)." + }, + { + "name": "Geometry", + "description": "Shape of the plate." + } + ] + } +} diff --git a/src/compas_timber/ghpython/components/CT_SurfaceModelOptions/code.py b/src/compas_timber/ghpython/components/CT_SurfaceModelOptions/code.py index 2d80b2552..d96552198 100644 --- a/src/compas_timber/ghpython/components/CT_SurfaceModelOptions/code.py +++ b/src/compas_timber/ghpython/components/CT_SurfaceModelOptions/code.py @@ -2,11 +2,9 @@ class SurfaceModelOptions(component): - def RunScript( self, sheeting_outside, sheeting_inside, lintel_posts, edge_stud_offset, custom_dimensions, joint_overrides ): - if sheeting_outside is not None and not isinstance(sheeting_outside, float): raise TypeError("sheeting_outside expected a float, got: {}".format(type(sheeting_outside))) if sheeting_inside is not None and not isinstance(sheeting_inside, float): diff --git a/src/compas_timber/model/model.py b/src/compas_timber/model/model.py index 916142211..ba38aea65 100644 --- a/src/compas_timber/model/model.py +++ b/src/compas_timber/model/model.py @@ -2,6 +2,7 @@ from compas_model.models import Model from compas_timber.elements import Beam +from compas_timber.elements import Plate from compas_timber.elements import Wall @@ -34,6 +35,8 @@ def __from_data__(cls, data): for element in model.elements(): if isinstance(element, Beam): model._beams.append(element) + elif isinstance(element, Plate): + model._plates.append(element) elif isinstance(element, Wall): model._walls.append(element) for interaction in model.interactions(): @@ -44,18 +47,26 @@ def __from_data__(cls, data): def __init__(self, *args, **kwargs): super(TimberModel, self).__init__() self._beams = [] + self._plates = [] self._walls = [] self._joints = [] self._topologies = [] # added to avoid calculating multiple times def __str__(self): - return "TimberModel ({}) with {} beam(s) and {} joint(s).".format(self.guid, len(self.beams), len(self.joints)) + return "TimberModel ({}) with {} beam(s), {} plate(s) and {} joint(s).".format( + self.guid, len(self.beams), len(self._plates), len(self.joints) + ) @property def beams(self): # type: () -> list[Beam] return self._beams + @property + def plates(self): + # type: () -> list[Plate] + return self._plates + @property def joints(self): # type: () -> list[Joint] @@ -88,7 +99,7 @@ def center_of_mass(self): @property def volume(self): # type: () -> float - return sum([beam.blank.volume for beam in self._beams]) + return sum([beam.blank.volume for beam in self._beams]) # TODO: add volume for plates def beam_by_guid(self, guid): # type: (str) -> Beam @@ -107,31 +118,20 @@ def beam_by_guid(self, guid): """ return self._guid_element[guid] - def add_beam(self, beam): - # type: (Beam) -> None - """Adds a Beam to this model. - - Parameters - ---------- - beam : :class:`~compas_timber.elements.Beam` - The beam to add to the model. - - """ - _ = self.add_element(beam) - self._beams.append(beam) - - def add_wall(self, wall): - # type: (Wall) -> None - """Adds a Wall to this model. - - Parameters - ---------- - wall : :class:`~compas_timber.elements.Wall` - The wall to add to the model. - - """ - _ = self.add_element(wall) - self._walls.append(wall) + def add_element(self, element, **kwargs): + # TODO: make the distincion in the properties rather than here + # then get rid of this overrloading altogether. + node = super(TimberModel, self).add_element(element, **kwargs) + + if isinstance(element, Beam): + self._beams.append(element) + elif isinstance(element, Wall): + self._walls.append(element) + elif isinstance(element, Plate): + self._plates.append(element) + else: + raise NotImplementedError("Element type not supported: {}".format(type(element))) + return node def add_joint(self, joint, beams): # type: (Joint, tuple[Beam]) -> None diff --git a/tests/compas_timber/test_btlx.py b/tests/compas_timber/test_btlx.py index 28942bb5e..6e97699b5 100644 --- a/tests/compas_timber/test_btlx.py +++ b/tests/compas_timber/test_btlx.py @@ -68,7 +68,6 @@ def test_beam_ref_faces_attribute(mock_beam): def test_beam_ref_edges(mock_beam): - ref_edges_expected = ( Line( Point(x=-48.67193560518159, y=20.35704602012424, z=0.0005429194857271558), diff --git a/tests/compas_timber/test_joint.py b/tests/compas_timber/test_joint.py index 93b5aa72c..d225e1408 100644 --- a/tests/compas_timber/test_joint.py +++ b/tests/compas_timber/test_joint.py @@ -27,7 +27,7 @@ def example_model(): model = TimberModel() for line in centerlines: b = Beam.from_centerline(line, w, h) - model.add_beam(b) + model.add_element(b) return model @@ -68,8 +68,8 @@ def test_create(mocker): model = TimberModel() b1 = Beam(Frame.worldXY(), length=1.0, width=0.1, height=0.1) b2 = Beam(Frame.worldYZ(), length=1.0, width=0.1, height=0.1) - model.add_beam(b1) - model.add_beam(b2) + model.add_element(b1) + model.add_element(b2) _ = TButtJoint.create(model, b1, b2) assert len(model.beams) == 2 @@ -80,8 +80,8 @@ def test_deepcopy(mocker, t_topo_beams): mocker.patch("compas_timber.connections.Joint.add_features") model = TimberModel() beam_a, beam_b = t_topo_beams - model.add_beam(beam_a) - model.add_beam(beam_b) + model.add_element(beam_a) + model.add_element(beam_b) t_butt = TButtJoint.create(model, beam_a, beam_b) model_copy = model.copy() @@ -97,8 +97,8 @@ def test_deepcopy(mocker, t_topo_beams): def test_joint_create_t_butt(t_topo_beams): model = TimberModel() main_beam, cross_beam = t_topo_beams - model.add_beam(main_beam) - model.add_beam(cross_beam) + model.add_element(main_beam) + model.add_element(cross_beam) joint = TButtJoint.create(model, main_beam, cross_beam) assert joint.main_beam is main_beam @@ -109,8 +109,8 @@ def test_joint_create_t_butt(t_topo_beams): def test_joint_create_l_butt(l_topo_beams): model = TimberModel() beam_a, beam_b = l_topo_beams - model.add_beam(beam_a) - model.add_beam(beam_b) + model.add_element(beam_a) + model.add_element(beam_b) joint = LButtJoint.create(model, beam_a, beam_b) assert joint.main_beam is beam_a @@ -121,8 +121,8 @@ def test_joint_create_l_butt(l_topo_beams): def test_joint_create_x_half_lap(x_topo_beams): model = TimberModel() beam_a, beam_b = x_topo_beams - model.add_beam(beam_a) - model.add_beam(beam_b) + model.add_element(beam_a) + model.add_element(beam_b) joint = XHalfLapJoint.create(model, beam_a, beam_b) assert joint.main_beam is beam_a @@ -133,8 +133,8 @@ def test_joint_create_x_half_lap(x_topo_beams): def test_joint_create_t_lap(t_topo_beams): model = TimberModel() main_beam, cross_beam = t_topo_beams - model.add_beam(main_beam) - model.add_beam(cross_beam) + model.add_element(main_beam) + model.add_element(cross_beam) joint = THalfLapJoint.create(model, main_beam, cross_beam) assert joint.main_beam is main_beam @@ -145,8 +145,8 @@ def test_joint_create_t_lap(t_topo_beams): def test_joint_create_l_lap(l_topo_beams): model = TimberModel() beam_a, beam_b = l_topo_beams - model.add_beam(beam_a) - model.add_beam(beam_b) + model.add_element(beam_a) + model.add_element(beam_b) joint = LHalfLapJoint.create(model, beam_a, beam_b) assert joint.main_beam is beam_a @@ -158,8 +158,8 @@ def test_joint_create_kwargs_passthrough_lbutt(): model = TimberModel() small = Beam.from_endpoints(Point(0, 0, 0), Point(0, 1, 0), 0.1, 0.1, z_vector=Vector(0, 0, 1)) large = Beam.from_endpoints(Point(0, 0, 0), Point(1, 0, 0), 0.2, 0.2, z_vector=Vector(0, 0, 1)) - model.add_beam(small) - model.add_beam(large) + model.add_element(small) + model.add_element(large) # main beam butts by default, first beam is by default main, they are swapped if necessary when small_beam_butts=True joint_a = LButtJoint.create(model, small, large, small_beam_butts=True) @@ -193,8 +193,8 @@ def test_joint_create_kwargs_passthrough_xhalflap(): model = TimberModel() beam_a = Beam.from_endpoints(Point(0.5, 0, 0), Point(0.5, 1, 0), 0.2, 0.2, z_vector=Vector(0, 0, 1)) beam_b = Beam.from_endpoints(Point(0, 0.5, 0), Point(1, 0.5, 0), 0.2, 0.2, z_vector=Vector(0, 0, 1)) - model.add_beam(beam_a) - model.add_beam(beam_b) + model.add_element(beam_a) + model.add_element(beam_b) joint = XHalfLapJoint.create(model, beam_a, beam_b, cut_plane_bias=0.4) diff --git a/tests/compas_timber/test_model.py b/tests/compas_timber/test_model.py index ca2d21add..9657c1aac 100644 --- a/tests/compas_timber/test_model.py +++ b/tests/compas_timber/test_model.py @@ -15,10 +15,10 @@ def test_create(): assert model -def test_add_beam(): +def test_add_element(): A = TimberModel() B = Beam(Frame.worldXY(), width=0.1, height=0.1, length=1.0) - A.add_beam(B) + A.add_element(B) assert B in A.beams assert len(list(A.graph.nodes())) == 1 @@ -32,8 +32,8 @@ def test_add_joint(): b1 = Beam(Frame.worldXY(), length=1.0, width=0.1, height=0.1) b2 = Beam(Frame.worldYZ(), length=1.0, width=0.1, height=0.1) - model.add_beam(b1) - model.add_beam(b2) + model.add_element(b1) + model.add_element(b2) _ = LButtJoint.create(model, b1, b2) assert len(model.beams) == 2 @@ -47,8 +47,8 @@ def test_copy(mocker): B1 = Beam(F1, length=1.0, width=0.1, height=0.12) B2 = Beam(F2, length=1.0, width=0.1, height=0.12) A = TimberModel() - A.add_beam(B1) - A.add_beam(B2) + A.add_element(B1) + A.add_element(B2) _ = LButtJoint.create(A, B1, B2) A_copy = A.copy() @@ -63,8 +63,8 @@ def test_deepcopy(mocker): B1 = Beam(F1, length=1.0, width=0.1, height=0.12) B2 = Beam(F2, length=1.0, width=0.1, height=0.12) A = TimberModel() - A.add_beam(B1) - A.add_beam(B2) + A.add_element(B1) + A.add_element(B2) _ = LButtJoint.create(A, B1, B2) A_copy = A.copy() @@ -77,9 +77,9 @@ def test_beams_have_keys_after_serialization(): B1 = Beam(Frame.worldXY(), length=1.0, width=0.1, height=0.1) B2 = Beam(Frame.worldYZ(), length=1.0, width=0.1, height=0.1) B3 = Beam(Frame.worldZX(), length=1.0, width=0.1, height=0.1) - A.add_beam(B1) - A.add_beam(B2) - A.add_beam(B3) + A.add_element(B1) + A.add_element(B2) + A.add_element(B3) keys = [beam.guid for beam in A.beams] A = json_loads(json_dumps(A)) @@ -94,8 +94,8 @@ def test_serialization_with_l_butt_joints(mocker): B1 = Beam(F1, length=1.0, width=0.1, height=0.12) B2 = Beam(F2, length=1.0, width=0.1, height=0.12) A = TimberModel() - A.add_beam(B1) - A.add_beam(B2) + A.add_element(B1) + A.add_element(B2) _ = LButtJoint.create(A, B1, B2) A = json_loads(json_dumps(A)) @@ -106,8 +106,8 @@ def test_serialization_with_t_butt_joints(mocker): a = TimberModel() b1 = Beam(Frame.worldXY(), length=1.0, width=0.1, height=0.1) b2 = Beam(Frame.worldYZ(), length=1.0, width=0.1, height=0.1) - a.add_beam(b1) - a.add_beam(b2) + a.add_element(b1) + a.add_element(b2) _ = TButtJoint.create(a, b1, b2) a = json_loads(json_dumps(a)) diff --git a/tests/compas_timber/test_sequencer.py b/tests/compas_timber/test_sequencer.py index 6d76fac7f..0c7da0c7a 100644 --- a/tests/compas_timber/test_sequencer.py +++ b/tests/compas_timber/test_sequencer.py @@ -17,7 +17,7 @@ def mock_model(): b4 = Beam(Frame.worldXY(), length=2.0, width=0.1, height=0.1) b5 = Beam(Frame.worldXY(), length=2.0, width=0.1, height=0.1) for b in [b1, b2, b3, b4, b5]: - model.add_beam(b) + model.add_element(b) return model diff --git a/tests/compas_timber/test_t_butt_joint.py b/tests/compas_timber/test_t_butt_joint.py index c9e901a4c..5dd997334 100644 --- a/tests/compas_timber/test_t_butt_joint.py +++ b/tests/compas_timber/test_t_butt_joint.py @@ -10,8 +10,8 @@ def test_create(): B1 = Beam.from_endpoints(Point(0, 0.5, 0), Point(1, 0.5, 0), z_vector=Vector(0, 0, 1), width=0.100, height=0.200) B2 = Beam.from_endpoints(Point(0, 0.0, 0), Point(0, 1.0, 0), z_vector=Vector(0, 0, 1), width=0.100, height=0.200) A = TimberModel() - A.add_beam(B1) - A.add_beam(B2) + A.add_element(B1) + A.add_element(B2) instance = TButtJoint.create(A, B1, B2) assert len(instance.beams) == 2