diff --git a/CHANGELOG.md b/CHANGELOG.md index 3758611c..b433e6ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas_model.elements.FastenersElement`. * Added `compas_model.elements.ScrewElement`. * Added `Element.is_dirty`. +* Added `compas_model.models.BlockModel.from_barrel_vault`. +* Added `compas_model.elements.BeamTProfileElement`. + ### Changed diff --git a/data/frame.json b/data/frame.json new file mode 100644 index 00000000..27fffb27 --- /dev/null +++ b/data/frame.json @@ -0,0 +1 @@ +{"Model::Line::Segments": [{"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 0.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "cf28f7ab-9674-46d2-bcf8-11677a4dfc3c"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 0.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "e748b58d-50cf-47da-ac7c-696cecd38c3c"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 0.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "b861dcd1-ac54-4777-954b-fc07f5446f54"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 0.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "91d8c7f1-d691-4fe7-8eea-a77b0f09aa78"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 3800.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "b36ddcaa-b602-4e13-964d-f7938a1095a7"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 3800.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "07ba1c41-4a7a-468e-8ba2-ef933dd39063"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 3800.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "7c70770f-da40-4456-9d60-195a56aa7021"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 3800.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "e2192a46-5b59-45f9-88a3-70eefc706cd0"}], "Model::Mesh::Floor": [{"dtype": "compas.datastructures/Mesh", "data": {"attributes": {}, "default_vertex_attributes": {"x": 0.0, "y": 0.0, "z": 0.0}, "default_edge_attributes": {}, "default_face_attributes": {}, "vertex": {"0": {"x": -3000.0, "y": -3000.0, "z": 3800.0}, "1": {"x": -3000.0, "y": 3000.0, "z": 3800.0}, "2": {"x": 3000.0, "y": 3000.0, "z": 3800.0}, "3": {"x": 3000.0, "y": -3000.0, "z": 3800.0}}, "face": {"0": [0, 1, 2, 3]}, "facedata": {"0": {}}, "edgedata": {}, "max_vertex": 3, "max_face": 0}, "guid": "ca32458c-6b40-4074-837a-32acb1d87e41"}]} \ No newline at end of file diff --git a/docs/api/compas_model.elements.rst b/docs/api/compas_model.elements.rst index a9b1859f..184a565b 100644 --- a/docs/api/compas_model.elements.rst +++ b/docs/api/compas_model.elements.rst @@ -11,8 +11,5 @@ Classes :toctree: generated/ :nosignatures: - BlockElement - BlockFeature - BlockGeometry Element - Feature + diff --git a/docs/examples/contacts/boolean_modifier.py b/docs/examples/contacts/boolean_modifier.py new file mode 100644 index 00000000..5039f57a --- /dev/null +++ b/docs/examples/contacts/boolean_modifier.py @@ -0,0 +1,46 @@ +from math import pi + +from compas.geometry.rotation import Rotation +from compas.geometry.translation import Translation +from compas.tolerance import TOL +from compas_model.elements import BeamSquareElement +from compas_model.models import Model +from compas_viewer import Viewer +from compas_viewer.config import Config + +# ============================================================================= +# Model +# ============================================================================= + +model = Model() + +beam0: BeamSquareElement = BeamSquareElement( + width=0.2, + depth=0.3, + length=6, +) + +beam1: BeamSquareElement = BeamSquareElement( + width=0.2, + depth=0.3, + length=6, +) + +beam_node = model.add_element(beam0) +model.add_element(beam1, parent=beam_node) +rotation: Rotation = Rotation.from_axis_and_angle([0, 1, 0], pi / 4, point=beam1.axis.midpoint) +translation = Translation.from_vector([0, 0.1, 0]) +beam1.transformation = translation * rotation + +model.compute_contact(beam0, beam1) # this method works when Beam class has modifier method + +# ============================================================================= +# Vizualize +# ============================================================================= +TOL.lineardeflection = 1000 + +config = Config() +viewer = Viewer(config=config) +for element in model.elements(): + viewer.scene.add(element.modelgeometry) +viewer.show() diff --git a/docs/examples/contacts/contact.py b/docs/examples/contacts/contact.py new file mode 100644 index 00000000..3e366325 --- /dev/null +++ b/docs/examples/contacts/contact.py @@ -0,0 +1,48 @@ +from pathlib import Path + +from compas import json_load +from compas.datastructures import Mesh +from compas.geometry import Line +from compas.geometry import Polygon +from compas.tolerance import TOL +from compas_model.elements import ColumnHeadCrossElement +from compas_model.elements import PlateElement +from compas_model.models import Model +from compas_viewer import Viewer +from compas_viewer.config import Config + +# ============================================================================= +# Model +# ============================================================================= +model: Model = Model() + +# ============================================================================= +# Add Elements to CellNetwork Edge +# ============================================================================= + + +column_head = ColumnHeadCrossElement(width=0.150, depth=0.150, height=0.300, offset=0.210) +plate: PlateElement = PlateElement(Polygon([[-2.850, -2.850, 0], [-2.850, 2.850, 0], [2.850, 2.850, 0], [2.850, -2.850, 0]]), 0.200) + +model.add_element(column_head) +model.add_element(plate) + +# ============================================================================= +# Add Interaction +# TODO: Check with other default ColumnHead Planes to Debug compas_occ. +# ============================================================================= +model.compute_contact(column_head, plate) + +# ============================================================================= +# Vizualize +# ============================================================================= +TOL.lineardeflection = 1000 + +config = Config() +viewer = Viewer(config=config) +for element in model.elements(): + viewer.scene.add(element.modelgeometry) +# viewer.scene.add(beam1.axis.midpoint) +# viewer.scene.add(beam1.axis) + +viewer.show() diff --git a/docs/examples/elements/beam.py b/docs/examples/elements/beam.py new file mode 100644 index 00000000..8b58f6d1 --- /dev/null +++ b/docs/examples/elements/beam.py @@ -0,0 +1,28 @@ +from compas.tolerance import TOL +from compas_model.elements import BeamTProfileElement +from compas_viewer import Viewer +from compas_viewer.config import Config + +scale = 1 +beam: BeamTProfileElement = BeamTProfileElement( + width=0.2 * scale, + height=0.3 * scale, + step_height_left=0.1 * scale, + step_height_right=0.1 * scale, + step_width_left=0.05 * scale, + step_width_right=0.05 * scale, + length=6 * scale, +) + +TOL.lineardeflection = 1000 + +config = Config() + +config.camera.target = [0, 0.1 * scale, 0] +config.camera.position = [0, -0.2 * scale, 7 * scale] +config.camera.near = 0.1 * scale +config.camera.far = 10 * scale +viewer = Viewer(config=config) +viewer.scene.add(beam.elementgeometry) + +viewer.show() diff --git a/docs/examples/elements/column.py b/docs/examples/elements/column.py new file mode 100644 index 00000000..8aeba29a --- /dev/null +++ b/docs/examples/elements/column.py @@ -0,0 +1,19 @@ +from compas_model.elements import ColumnSquareElement +from compas_viewer import Viewer +from compas_viewer.config import Config + +scale = 1 +column: ColumnSquareElement = ColumnSquareElement() + + +config = Config() + +config.camera.target = [0, 0.1 * scale, 0] +config.camera.position = [0, -0.2 * scale, 7 * scale] +config.camera.near = 0.1 * scale +config.camera.far = 10 * scale +viewer = Viewer(config=config) +viewer.scene.add(column.elementgeometry) +viewer.scene.add(column.axis) + +viewer.show() diff --git a/docs/examples/elements/cylinder.py b/docs/examples/elements/cylinder.py new file mode 100644 index 00000000..3f857284 --- /dev/null +++ b/docs/examples/elements/cylinder.py @@ -0,0 +1,19 @@ +from compas_model.elements import CableElement +from compas_viewer import Viewer +from compas_viewer.config import Config + +scale = 1 +column: CableElement = CableElement() + + +config = Config() + +config.camera.target = [0, 0.1 * scale, 0] +config.camera.position = [0, -0.2 * scale, 7 * scale] +config.camera.near = 0.1 * scale +config.camera.far = 10 * scale +viewer = Viewer(config=config) +viewer.scene.add(column.elementgeometry) +viewer.scene.add(column.axis) + +viewer.show() diff --git a/docs/examples/masonry/000_frame.py b/docs/examples/masonry/000_frame.py new file mode 100644 index 00000000..f1dca2e2 --- /dev/null +++ b/docs/examples/masonry/000_frame.py @@ -0,0 +1,58 @@ +from pathlib import Path + +from compas import json_dump +from compas.datastructures import Mesh +from compas.geometry import Line +from compas.geometry import Point +from compas_viewer import Viewer +from compas_viewer.config import Config + +# ============================================================================= +# Create Geometry +# ============================================================================= + +points = [ + Point(-3000, -3000, 0), + Point(-3000, 3000, 0), + Point(3000, 3000, 0), + Point(3000, -3000, 0), + Point(-3000, -3000, 3800), + Point(-3000, 3000, 3800), + Point(3000, 3000, 3800), + Point(3000, -3000, 3800), +] + +lines = [ + Line(points[0], points[0 + 4]), + Line(points[1], points[1 + 4]), + Line(points[2], points[2 + 4]), + Line(points[3], points[3 + 4]), + Line(points[4], points[5]), + Line(points[6], points[7]), + Line(points[5], points[6]), + Line(points[7], points[4]), +] + +mesh = Mesh.from_vertices_and_faces(points[4:], [[0, 1, 2, 3]]) + + +# ============================================================================= +# Serialize the Frame into a JSON file. +# ============================================================================= + +model_input: dict[str, list[any]] = {"Model::Line::Segments": lines, "Model::Mesh::Floor": [mesh]} +json_dump(model_input, Path("data/frame.json")) + +# ============================================================================= +# Vizualize +# ============================================================================= + +config = Config() +config.camera.target = [0, 0, 100] +config.camera.position = [10000, -10000, 10000] +config.camera.near = 10 +config.camera.far = 100000 +viewer = Viewer(config=config) +viewer.scene.add(lines) +viewer.scene.add(mesh) +viewer.show() \ No newline at end of file diff --git a/docs/examples/masonry/001_frame_model.py b/docs/examples/masonry/001_frame_model.py new file mode 100644 index 00000000..550eb2b8 --- /dev/null +++ b/docs/examples/masonry/001_frame_model.py @@ -0,0 +1,85 @@ +from pathlib import Path + +from compas import json_load +from compas.geometry import Frame +from compas.geometry import Line +from compas.geometry import Translation +from compas.geometry import Vector +from compas.geometry.transformation import Transformation +from compas.tolerance import TOL +from compas_model.elements import BeamTProfileElement +from compas_model.elements import CableElement +from compas_model.elements import ColumnSquareElement +from compas_model.models import BlockModel +from compas_model.models import Model +from compas_viewer import Viewer +from compas_viewer.config import Config + +# ============================================================================= +# JSON file with the geometry of the model. +# ============================================================================= +rhino_geometry: dict[str, list[any]] = json_load(Path("data/frame.json")) +lines: list[Line] = rhino_geometry["Model::Line::Segments"] + +# ============================================================================= +# Model +# ============================================================================= +model = Model() + +# Add columns +for i in range(0, 4): + column: ColumnSquareElement = ColumnSquareElement(300, 300, lines[i].length) + column.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), Frame(lines[i].start)) + model.add_element(column) + + +# Add two beams +for i in range(4, len(lines) - 2): + beam: BeamTProfileElement = BeamTProfileElement(width=300, height=700, step_width_left=75, step_height_left=150, length=lines[i].length) + target_frame: Frame = Frame(lines[i].start, Vector.Zaxis().cross(lines[i].vector), Vector.Zaxis()) + beam.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), target_frame) * Translation.from_vector([0, beam.height * 0.5, 0]) + beam.extend(150) + model.add_element(beam) + +# Add two cables +for i in range(6, len(lines)): + cable: CableElement = CableElement(length=lines[i].length, radius=10) + target_frame: Frame = Frame(lines[i].start, Vector.Zaxis().cross(lines[i].vector), Vector.Zaxis()) + cable.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), target_frame) * Translation.from_vector([0, beam.height * 0.1, 0]) + cable.extend(200) + model.add_element(cable) + + +# Add blocks, by moving them by the height of the first column. +blockmodel: BlockModel = BlockModel.from_barrel_vault(span=6000, length=6000, thickness=250, rise=600, vou_span=5, vou_length=5) +for block in blockmodel.elements(): + block.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), Frame([0, 0, lines[0].end[2]])) * block.transformation + model.add_element(block) + + +# Add Interactions +for element in list(model.elements()): + if isinstance(element, BeamTProfileElement): + for block in blockmodel.elements(): + if isinstance(element, BeamTProfileElement): + model.compute_contact(element, block) # beam -> cuts -> block + +for element in list(model.elements()): + if isinstance(element, CableElement): + for beam in list(model.elements()): + if isinstance(beam, BeamTProfileElement): + model.compute_contact(element, beam) # cable -> cuts -> beam + +# ============================================================================= +# Vizualize +# ============================================================================= +TOL.lineardeflection = 100 +config = Config() +config.camera.target = [0, 0, 100] +config.camera.position = [10000, -10000, 10000] +config.camera.near = 10 +config.camera.far = 100000 +viewer = Viewer(config=config) +for element in list(model.elements()): + viewer.scene.add(element.modelgeometry, hide_coplanaredges=False) +viewer.show() diff --git a/docs/examples/masonry/002_block_model.py b/docs/examples/masonry/002_block_model.py new file mode 100644 index 00000000..3645d7e2 --- /dev/null +++ b/docs/examples/masonry/002_block_model.py @@ -0,0 +1,19 @@ +from compas.tolerance import TOL +from compas_model.models import BlockModel +from compas_viewer.config import Config +from compas_viewer.viewer import Viewer + +model: BlockModel = BlockModel.from_barrel_vault(span=6000, length=6000, thickness=250, rise=600, vou_span=5, vou_length=5) + + +TOL.lineardeflection = 1000 + +config = Config() +config.camera.target = [0, 0, 100] +config.camera.position = [10000, -10000, 10000] +config.camera.near = 10 +config.camera.far = 100000 +viewer = Viewer(config=config) +for element in list(model.elements()): + viewer.scene.add(element.modelgeometry, hide_coplanaredges=True) +viewer.show() diff --git a/src/compas_model/algorithms/modifiers.py b/src/compas_model/algorithms/modifiers.py index 34c846c6..4cc86120 100644 --- a/src/compas_model/algorithms/modifiers.py +++ b/src/compas_model/algorithms/modifiers.py @@ -22,9 +22,16 @@ def slice(geometry: Union[Brep, Mesh], slice_plane: Plane) -> Union[Brep, Mesh]: :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` The sliced geometry. """ + try: - split_meshes: Optional[list] = geometry.slice(slice_plane) - return split_meshes[0] if split_meshes else geometry + if isinstance(geometry, Brep): + geometry.make_solid() + slice_plane_flipped = Plane(slice_plane.point, -slice_plane.normal) + geometry.trim(slice_plane_flipped) + return geometry + else: + split_meshes: Optional[list] = geometry.slice(slice_plane) + return split_meshes[0] if split_meshes else geometry except Exception: print("SlicerModifier is not successful.") return geometry @@ -46,14 +53,20 @@ def boolean_difference(target_geometry, source_geometry): :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` The geometry after boolean difference. """ - from compas_cgal.booleans import boolean_difference_mesh_mesh - target_geometry_copy = target_geometry.copy() - source_geometry_copy = source_geometry.copy() - target_geometry_copy.unify_cycles() - source_geometry_copy.unify_cycles() + if isinstance(target_geometry, Brep) and isinstance(source_geometry, Brep): + try: + return Brep.from_boolean_difference(target_geometry, source_geometry) + except Exception: + print("Boolean difference is not successful.") + return target_geometry + else: + from compas_cgal.booleans import boolean_difference_mesh_mesh + + mesh0: Mesh = target_geometry.copy() if not isinstance(target_geometry, Brep) else Mesh.from_polygons(target_geometry.to_polygons()) + mesh1: Mesh = source_geometry.copy() if not isinstance(source_geometry, Brep) else Mesh.from_polygons(source_geometry.to_polygons()) - A = target_geometry_copy.to_vertices_and_faces(triangulated=True) - B = source_geometry_copy.to_vertices_and_faces(triangulated=True) - V, F = boolean_difference_mesh_mesh(A, B) - return Mesh.from_vertices_and_faces(V, F) if len(V) > 0 and len(F) > 0 else target_geometry_copy + A = mesh0.to_vertices_and_faces(triangulated=True) + B = mesh1.to_vertices_and_faces(triangulated=True) + V, F = boolean_difference_mesh_mesh(A, B) + return Mesh.from_vertices_and_faces(V, F) if V and F else mesh0 diff --git a/src/compas_model/elements/__init__.py b/src/compas_model/elements/__init__.py index d7d88f74..f35a9d53 100644 --- a/src/compas_model/elements/__init__.py +++ b/src/compas_model/elements/__init__.py @@ -11,6 +11,7 @@ from .beam import BeamElement from .beam import BeamIProfileElement from .beam import BeamSquareElement +from .beam import BeamTProfileElement from .column import ColumnFeature from .column import ColumnElement from .column import ColumnRoundElement @@ -18,6 +19,8 @@ from .fasteners import FastenersFeature from .fasteners import FastenersElement from .fasteners import ScrewElement +from .cable import CableFeature +from .cable import CableElement __all__ = [ @@ -34,6 +37,7 @@ BeamElement, BeamIProfileElement, BeamSquareElement, + BeamTProfileElement, ColumnFeature, ColumnElement, ColumnRoundElement, @@ -41,4 +45,6 @@ FastenersFeature, FastenersElement, ScrewElement, + CableFeature, + CableElement, ] diff --git a/src/compas_model/elements/beam.py b/src/compas_model/elements/beam.py index 30e3df73..e13701be 100644 --- a/src/compas_model/elements/beam.py +++ b/src/compas_model/elements/beam.py @@ -1,20 +1,28 @@ +from typing import TYPE_CHECKING from typing import Optional +from typing import Union from compas.datastructures import Mesh from compas.geometry import Box +from compas.geometry import Brep from compas.geometry import Frame from compas.geometry import Line from compas.geometry import Plane from compas.geometry import Point from compas.geometry import Polygon from compas.geometry import Transformation +from compas.geometry import Translation from compas.geometry import bounding_box from compas.geometry import intersection_line_plane +from compas.geometry import mirror_points_line from compas.geometry import oriented_bounding_box from compas.itertools import pairwise +from compas_model.elements.element import Element +from compas_model.elements.element import Feature +from compas_model.interactions import BooleanModifier -from .element import Element -from .element import Feature +if TYPE_CHECKING: + from compas_model.elements import BlockElement class BeamFeature(Feature): @@ -24,7 +32,211 @@ class BeamFeature(Feature): class BeamElement(Element): """Class representing a beam element.""" - pass + @property + def length(self) -> float: + return self._length + + @length.setter + def length(self, length: float): + self._length = length + + self.section = Polygon(list(self.points)) + + self.axis = Line([0, 0, 0], [0, 0, length]) + self.frame_top = Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) + self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() + + def extend(self, distance: float) -> None: + """Extend the beam. + + Parameters + ---------- + distance : float + The distance to extend the beam. + """ + self.length = self.length + distance * 2 + xform: Transformation = Translation.from_vector([0, 0, -distance]) + self.transformation = self.transformation * xform + + def compute_aabb(self, inflate: float = 0.0) -> Box: + """Compute the axis-aligned bounding box of the element. + + Parameters + ---------- + inflate : float, optional + The inflation factor of the bounding box. + + Returns + ------- + :class:`compas.geometry.Box` + The axis-aligned bounding box. + """ + points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore + box: Box = Box.from_bounding_box(bounding_box(points)) + box.xsize += inflate + box.ysize += inflate + box.zsize += inflate + return box + + def compute_obb(self, inflate: float = 0.0) -> Box: + """Compute the oriented bounding box of the element. + + Parameters + ---------- + inflate : float, optional + The inflation factor of the bounding box. + + Returns + ------- + :class:`compas.geometry.Box` + The oriented bounding box. + """ + points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore + box: Box = Box.from_bounding_box(oriented_bounding_box(points)) + box.xsize += inflate + box.ysize += inflate + box.zsize += inflate + return box + + def compute_collision_mesh(self) -> Mesh: + """Compute the collision mesh of the element. + + Returns + ------- + :class:`compas.datastructures.Mesh` + The collision mesh. + """ + from compas.geometry import convex_hull_numpy + + points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore + vertices, faces = convex_hull_numpy(points) + vertices = [points[index] for index in vertices] # type: ignore + return Mesh.from_vertices_and_faces(vertices, faces) + + @property + def face_polygons(self) -> list[Polygon]: + return [self.geometry.face_polygon(face) for face in self.geometry.faces()] # type: ignore + + def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: + """Compute the top and bottom polygons of the beam. + + Returns + ------- + tuple[:class:`compas.geometry.Polygon`, :class:`compas.geometry.Polygon`] + """ + + plane0: Plane = Plane.from_frame(self.frame) + plane1: Plane = Plane.from_frame(self.frame_top) + points0: list[list[float]] = [] + points1: list[list[float]] = [] + for i in range(len(self.section.points)): + line: Line = Line(self.section.points[i], self.section.points[i] + self.axis.vector) + result0: Optional[list[float]] = intersection_line_plane(line, plane0) + result1: Optional[list[float]] = intersection_line_plane(line, plane1) + if not result0 or not result1: + raise ValueError("The line does not intersect the plane") + points0.append(result0) + points1.append(result1) + return Polygon(points0), Polygon(points1) + + # def compute_elementgeometry(self) -> Mesh: + # """Compute the shape of the beam from the given polygons . + # This shape is relative to the frame of the element. + + # Returns + # ------- + # :class:`compas.datastructures.Mesh` + + # """ + + # from compas.geometry import earclip_polygon + + # offset: int = len(self.polygon_bottom) + # vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore + + # triangles: list[list[int]] = earclip_polygon(Polygon(self.polygon_bottom.points)) + # top_faces: list[list[int]] = [] + # bottom_faces: list[list[int]] = [] + # for i in range(len(triangles)): + # triangle_top: list[int] = [] + # triangle_bottom: list[int] = [] + # for j in range(3): + # triangle_top.append(triangles[i][j] + offset) + # triangle_bottom.append(triangles[i][j]) + # triangle_bottom.reverse() + # top_faces.append(triangle_top) + # bottom_faces.append(triangle_bottom) + # faces: list[list[int]] = bottom_faces + top_faces + + # bottom: list[int] = list(range(offset)) + # top: list[int] = [i + offset for i in bottom] + # for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): + # faces.append([c, d, b, a]) + # mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + # return mesh + + def compute_elementgeometry(self) -> Brep: + """Compute the shape of the beam from the given polygons . + This shape is relative to the frame of the element. + + Returns + ------- + :class:`compas.datastructures.Brep` + """ + + polygons: list[Polygon] = [] + + offset: int = len(self.polygon_bottom) + vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore + polygons.append(self.polygon_bottom) + polygons.append(self.polygon_top) + + bottom: list[int] = list(range(offset)) + top: list[int] = [i + offset for i in bottom] + for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): + polygons.append(Polygon([vertices[c], vertices[d], vertices[b], vertices[a]])) + brep: Brep = Brep.from_polygons(polygons) + return brep + + def compute_contact(self, target_element: Element, type: str = "") -> BooleanModifier: + """Computes the contact interaction of the geometry of the elements that is used in the model's add_contact method. + + Returns + ------- + :class:`compas_model.interactions.BooleanModifier` + The ContactInteraction that is applied to the neighboring element. One pair can have one or multiple variants. + target_element : Element + The target element to compute the contact interaction. + type : str, optional + The type of contact interaction, if different contact are possible between the two elements. + + """ + # Traverse up to the class one before the Element class.add() + # Create a function name based on the target_element class name. + parent_class = target_element.__class__ + while parent_class.__bases__[0] != Element: + parent_class = parent_class.__bases__[0] + + parent_class_name = parent_class.__name__.lower().replace("element", "") + method_name = f"_compute_contact_with_{parent_class_name}" + method = getattr(self, method_name, None) + if method is None: + raise ValueError(f"Unsupported target element type: {type(target_element)}") + + return method(target_element, type) + + def _compute_contact_with_beam(self, target_element: "BeamElement", type: str) -> Union["BooleanModifier", None]: + # Scenario: + # A cable applies boolean difference with a block geometry. + return BooleanModifier(self.elementgeometry.transformed(self.modeltransformation)) + + def _compute_contact_with_block(self, target_element: "BlockElement", type: str) -> Union["BooleanModifier", None]: + # Scenario: + # A beam with a profile applies boolean difference with a block geometry. + if target_element.is_support: + return BooleanModifier(self.elementgeometry.transformed(self.modeltransformation)) + else: + return None class BeamSquareElement(BeamElement): @@ -115,134 +327,64 @@ def __init__( self.points: list[list[float]] = [[-width * 1, -depth * 0.5, 0], [-width * 1, depth * 0.5, 0], [width * 0, depth * 0.5, 0], [width * 0, -depth * 0.5, 0]] - self.section: Polygon = Polygon(self.points).translated([0, 0, 0.5 * length]) - self.axis: Line = Line([0, 0, 0], [0, 0, length]).translated([0, 0, 0.5 * length]) + self.section: Polygon = Polygon(self.points) + self.axis: Line = Line([0, 0, 0], [0, 0, length]) self.frame_top: Frame = frame_top or Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() - @property - def length(self) -> float: - return self._length - - @length.setter - def length(self, length: float): - self._length = length - - # Create the polygon of the I profile - self.section = Polygon(list(self.points)).translated([0, 0, 0.5 * length]) - - self.axis = Line([0, 0, 0], [0, 0, length]).translated([0, 0, 0.5 * length]) - self.frame_top = Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) - self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() - - @property - def face_polygons(self) -> list[Polygon]: - return [self.geometry.face_polygon(face) for face in self.geometry.faces()] # type: ignore - - def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: - """Compute the top and bottom polygons of the beam. - - Returns - ------- - tuple[:class:`compas.geometry.Polygon`, :class:`compas.geometry.Polygon`] - """ - - plane0: Plane = Plane.from_frame(self.frame) - plane1: Plane = Plane.from_frame(self.frame_top) - points0: list[list[float]] = [] - points1: list[list[float]] = [] - for i in range(len(self.section.points)): - line: Line = Line(self.section.points[i], self.section.points[i] + self.axis.vector) - result0: Optional[list[float]] = intersection_line_plane(line, plane0) - result1: Optional[list[float]] = intersection_line_plane(line, plane1) - if not result0 or not result1: - raise ValueError("The line does not intersect the plane") - points0.append(result0) - points1.append(result1) - return Polygon(points0), Polygon(points1) - - def compute_elementgeometry(self) -> Mesh: + def compute_elementgeometry(self) -> Brep: """Compute the shape of the beam from the given polygons . This shape is relative to the frame of the element. Returns ------- - :class:`compas.datastructures.Mesh` - - """ - - offset: int = len(self.polygon_bottom) - vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore - bottom: list[int] = list(range(offset)) - top: list[int] = [i + offset for i in bottom] - faces: list[list[int]] = [bottom[::-1], top] - for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): - faces.append([a, b, d, c]) - mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) - return mesh - - # ============================================================================= - # Implementations of abstract methods - # ============================================================================= - - def compute_aabb(self, inflate: float = 0.0) -> Box: - """Compute the axis-aligned bounding box of the element. - - Parameters - ---------- - inflate : float, optional - The inflation factor of the bounding box. - - Returns - ------- - :class:`compas.geometry.Box` - The axis-aligned bounding box. + :class:`compas.datastructures.Brep` """ - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - box: Box = Box.from_bounding_box(bounding_box(points)) - box.xsize += inflate - box.ysize += inflate - box.zsize += inflate - return box - - def compute_obb(self, inflate: float = 0.0) -> Box: - """Compute the oriented bounding box of the element. - - Parameters - ---------- - inflate : float, optional - The inflation factor of the bounding box. - - Returns - ------- - :class:`compas.geometry.Box` - The oriented bounding box. - """ - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - box: Box = Box.from_bounding_box(oriented_bounding_box(points)) - box.xsize += inflate - box.ysize += inflate - box.zsize += inflate - return box - - def compute_collision_mesh(self) -> Mesh: - """Compute the collision mesh of the element. - - Returns - ------- - :class:`compas.datastructures.Mesh` - The collision mesh. - """ - from compas.geometry import convex_hull_numpy - - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - vertices, faces = convex_hull_numpy(points) - vertices = [points[index] for index in vertices] # type: ignore - return Mesh.from_vertices_and_faces(vertices, faces) - - # ============================================================================= - # Constructors - # ============================================================================= + box: Box = Box.from_width_height_depth(self.width, self.length, self.depth) + box.translate( + [ + self.width * -0.5, + 0, + self.length * 0.5, + ] + ) + return Brep.from_box(box) + + # def compute_elementgeometry(self) -> Mesh: + # """Compute the shape of the beam from the given polygons . + # This shape is relative to the frame of the element. + + # Returns + # ------- + # :class:`compas.datastructures.Mesh` + + # """ + + # from compas.geometry import earclip_polygon + + # offset: int = len(self.polygon_bottom) + # vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore + + # triangles: list[list[int]] = earclip_polygon(Polygon(self.polygon_bottom.points)) + # top_faces: list[list[int]] = [] + # bottom_faces: list[list[int]] = [] + # for i in range(len(triangles)): + # triangle_top: list[int] = [] + # triangle_bottom: list[int] = [] + # for j in range(3): + # triangle_top.append(triangles[i][j] + offset) + # triangle_bottom.append(triangles[i][j]) + # triangle_bottom.reverse() + # top_faces.append(triangle_top) + # bottom_faces.append(triangle_bottom) + # faces: list[list[int]] = bottom_faces + top_faces + + # bottom: list[int] = list(range(offset)) + # top: list[int] = [i + offset for i in bottom] + # for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): + # faces.append([c, d, b, a]) + # mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + # return mesh class BeamIProfileElement(BeamElement): @@ -334,131 +476,134 @@ def __init__( ] # Create the polygon of the I profile - self.section: Polygon = Polygon(list(self.points)).translated([0, 0, 0.5 * length]) - - self.axis: Line = Line([0, 0, 0], [0, 0, length]).translated([0, 0, 0.5 * length]) + self.section: Polygon = Polygon(self.points) + self.axis: Line = Line([0, 0, 0], [0, 0, length]) self.frame_top: Frame = frame_top or Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() - @property - def length(self) -> float: - return self._length - - @length.setter - def length(self, length: float): - self._length = length - - # Create the polygon of the I profile - self.section = Polygon(list(self.points)).translated([0, 0, 0.5 * length]) - - self.axis = Line([0, 0, 0], [0, 0, length]).translated([0, 0, 0.5 * length]) - self.frame_top = Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) - self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() - - @property - def face_polygons(self) -> list[Polygon]: - return [self.geometry.face_polygon(face) for face in self.geometry.faces()] # type: ignore - - def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: - """Compute the top and bottom polygons of the beam. - - Returns - ------- - tuple[:class:`compas.geometry.Polygon`, :class:`compas.geometry.Polygon`] - """ - - plane0: Plane = Plane.from_frame(self.frame) - plane1: Plane = Plane.from_frame(self.frame_top) - points0: list[list[float]] = [] - points1: list[list[float]] = [] - for i in range(len(self.section.points)): - line: Line = Line(self.section.points[i], self.section.points[i] + self.axis.vector) - result0: Optional[list[float]] = intersection_line_plane(line, plane0) - result1: Optional[list[float]] = intersection_line_plane(line, plane1) - if not result0 or not result1: - raise ValueError("The line does not intersect the plane") - points0.append(result0) - points1.append(result1) - return Polygon(points0), Polygon(points1) - - def compute_elementgeometry(self) -> Mesh: - """Compute the shape of the beam from the given polygons . - This shape is relative to the frame of the element. - - Returns - ------- - :class:`compas.datastructures.Mesh` + # ============================================================================= + # Constructors + # ============================================================================ - """ - offset: int = len(self.polygon_bottom) - vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore - bottom: list[int] = list(range(offset)) - top: list[int] = [i + offset for i in bottom] - faces: list[list[int]] = [bottom[::-1], top] - for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): - faces.append([a, b, d, c]) - mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) - return mesh +class BeamTProfileElement(BeamElement): + """Class representing a beam element with I profile. - # ============================================================================= - # Implementations of abstract methods - # ============================================================================= + Parameters + ---------- + width : float, optional + The width of the beam. + height : float, optional + The height of the beam. + step_width_left : float, optional + The step width on the left side of the beam. + step_height_left : float, optional + The step height on the left side of the beam. + length : float, optional + The length of the beam. + inverted : bool, optional + Flag indicating if the beam section is inverted as upside down letter T. + step_width_right : float, optional + The step width on the right side of the beam, if None then the left side step width is used. + step_height_right : float, optional + The step height on the right side of the beam, if None then the left side step height is used. + frame_bottom : :class:`compas.geometry.Plane`, optional + The frame of the bottom polygon. + frame_top : :class:`compas.geometry.Plane`, optional + The frame of the top polygon. + name : str, optional + The name of the element. - def compute_aabb(self, inflate: float = 0.0) -> Box: - """Compute the axis-aligned bounding box of the element. + Attributes + ---------- + axis : :class:`compas.geometry.Line` + The axis of the beam. + section : :class:`compas.geometry.Polygon` + The section of the beam. + polygon_bottom : :class:`compas.geometry.Polygon` + The bottom polygon of the beam. + polygon_top : :class:`compas.geometry.Polygon` + The top polygon of the beam. + transformation : :class:`compas.geometry.Transformation` + The transformation applied to the beam. + material : :class:`compas_model.Material` + The material of the beam. + """ - Parameters - ---------- - inflate : float, optional - The inflation factor of the bounding box. + @property + def __data__(self) -> dict: + return { + "width": self.width, + "height": self.height, + "step_width_left": self.step_width_left, + "step_height_left": self.step_height_left, + "length": self.length, + "inverted": self.inverted, + "step_height_right": self.step_height_right, + "step_width_right": self.step_width_right, + "frame_top": self.frame_top, + "is_support": self.is_support, + "frame": self.frame, + "transformation": self.transformation, + "features": self._features, + "name": self.name, + } - Returns - ------- - :class:`compas.geometry.Box` - The axis-aligned bounding box. - """ - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - box: Box = Box.from_bounding_box(bounding_box(points)) - box.xsize += inflate - box.ysize += inflate - box.zsize += inflate - return box + def __init__( + self, + width: float = 0.1, + height: float = 0.2, + step_width_left: float = 0.02, + step_height_left: float = 0.02, + length: float = 3.0, + inverted: bool = False, + step_height_right: Optional[float] = None, + step_width_right: Optional[float] = None, + frame_top: Optional[Plane] = None, + is_support: bool = False, + frame: Frame = Frame.worldXY(), + transformation: Optional[Transformation] = None, + features: Optional[list[BeamFeature]] = None, + name: Optional[str] = None, + ) -> "BeamIProfileElement": + super().__init__(frame=frame, transformation=transformation, features=features, name=name) - def compute_obb(self, inflate: float = 0.0) -> Box: - """Compute the oriented bounding box of the element. + self.is_support: bool = is_support - Parameters - ---------- - inflate : float, optional - The inflation factor of the bounding box. + self.width: float = abs(width) + self.height: float = abs(height) + self.step_width_left: float = abs(step_width_left) + self.step_width_right: float = abs(step_width_right) if step_width_right is not None else step_width_left + self.step_height_left: float = abs(step_height_left) + self.step_height_right: float = abs(step_height_right) if step_height_right is not None else step_height_left + self.inverted: bool = inverted + self._length: float = abs(length) - Returns - ------- - :class:`compas.geometry.Box` - The oriented bounding box. - """ - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - box: Box = Box.from_bounding_box(oriented_bounding_box(points)) - box.xsize += inflate - box.ysize += inflate - box.zsize += inflate - return box + self.step_width_left = min(self.step_width_left, width * 0.5 * 0.999) + self.step_width_right = min(self.step_width_right, width * 0.5 * 0.999) + self.step_height_left = min(self.step_height_left, height) + self.step_height_right = min(self.step_height_right, height) - def compute_collision_mesh(self) -> Mesh: - """Compute the collision mesh of the element. + self.points: list[float] = [ + [self.width * 0.5, -self.height * 0.5, 0], + [-self.width * 0.5, -self.height * 0.5, 0], + [-self.width * 0.5, -self.height * 0.5 + self.step_height_left, 0], + [-self.width * 0.5 + self.step_width_left, -self.height * 0.5 + self.step_height_left, 0], + [-self.width * 0.5 + self.step_width_left, self.height * 0.5, 0], + [self.width * 0.5 - self.step_width_right, self.height * 0.5, 0], + [self.width * 0.5 - self.step_width_right, -self.height * 0.5 + self.step_height_right, 0], + [self.width * 0.5, -self.height * 0.5 + self.step_height_right, 0], + ] - Returns - ------- - :class:`compas.datastructures.Mesh` - The collision mesh. - """ - from compas.geometry import convex_hull_numpy + if inverted: + mirror_line: Line = Line([0, 0, 0], [1, 0, 0]) + self.points = mirror_points_line(self.points, mirror_line) - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - vertices, faces = convex_hull_numpy(points) - vertices = [points[index] for index in vertices] # type: ignore - return Mesh.from_vertices_and_faces(vertices, faces) + # Create the polygon of the T profile + self.section: Polygon = Polygon(self.points) + self.axis: Line = Line([0, 0, 0], [0, 0, length]) + self.frame_top: Frame = frame_top or Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) + self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() # ============================================================================= # Constructors diff --git a/src/compas_model/elements/block.py b/src/compas_model/elements/block.py index 37b30d08..6d95646f 100644 --- a/src/compas_model/elements/block.py +++ b/src/compas_model/elements/block.py @@ -159,8 +159,7 @@ def __init__( name: Optional[str] = None, ) -> None: super().__init__(frame=frame, transformation=transformation, features=features, name=name) - - self.shape = shape if isinstance(shape, BlockGeometry) else shape.copy(cls=BlockGeometry) + self.shape = shape if isinstance(shape, BlockGeometry) else shape.copy() self.is_support = is_support # ============================================================================= @@ -172,28 +171,29 @@ def compute_elementgeometry(self) -> Union[Mesh, Brep]: # apply features? return geometry - def compute_modelgeometry(self) -> Union[Mesh, Brep]: - if not self.model: - raise Exception - - geometry = self.elementgeometry.transformed(self.modeltransformation) - - # apply effect of interactions? - node = self.graphnode - nbrs = self.model.graph.neighbors_in(node) - for nbr in nbrs: - element = self.model.graph.node_element(nbr) - if element: - for interaction in self.model.graph.edge_interactions((nbr, node)): - # example interactions are - # cutters, boolean operations, slicers, ... - if hasattr(interaction, "apply"): - try: - interaction.apply(geometry) - except Exception: - pass - - return geometry + # def compute_modelgeometry(self) -> Union[Mesh, Brep]: + # if not self.model: + # raise Exception + + # geometry = self.elementgeometry.transformed(self.modeltransformation) + + # # apply effect of interactions? + # node = self.graphnode + # nbrs = self.model.graph.neighbors_in(node) + # for nbr in nbrs: + # element = self.model.graph.node_element(nbr) + # if element: + # for interaction in self.model.graph.edge_interactions((nbr, node)): + # # example interactions are + # # cutters, boolean operations, slicers, ... + # if hasattr(interaction, "apply"): + # try: + # interaction.apply(geometry) + # except Exception as e: + # print(str(e)) + # pass + + # return geometry def compute_aabb(self) -> Box: points = self.modelgeometry.vertices_attributes("xyz") diff --git a/src/compas_model/elements/cable.py b/src/compas_model/elements/cable.py new file mode 100644 index 00000000..666e4ee9 --- /dev/null +++ b/src/compas_model/elements/cable.py @@ -0,0 +1,303 @@ +from typing import TYPE_CHECKING +from typing import Optional +from typing import Union + +from compas.datastructures import Mesh +from compas.geometry import Box +from compas.geometry import Frame +from compas.geometry import Line +from compas.geometry import Plane +from compas.geometry import Polygon +from compas.geometry import Transformation +from compas.geometry import Translation +from compas.geometry import bounding_box +from compas.geometry import intersection_line_plane +from compas.geometry import oriented_bounding_box +from compas_model.interactions import BooleanModifier + +from .element import Element +from .element import Feature + +if TYPE_CHECKING: + from compas_model.elements import BeamElement + + +class CableFeature(Feature): + pass + + +class CableElement(Element): + """Class representing a Cable element with a round section. + + Parameters + ---------- + radius : float + Radius of the Cable. + sides : int + Number of sides of the Cable's polygonal section. + length : float + Length of the Cable. + frame_top : Optional[:class:`compas.geometry.Plane`] + Second frame of the Cable that is used to cut the second end, while the first frame is used to cut the first end. + is_support : bool + Flag indicating if the Cable is a support. + frame : :class:`compas.geometry.Frame` + Main frame of the Cable. + transformation : Optional[:class:`compas.geometry.Transformation`] + Transformation applied to the Cable. + features : Optional[list[:class:`compas_model.features.CableFeature`]] + Features of the Cable. + name : Optional[str] + If no name is defined, the class name is given. + + Attributes + ---------- + radius : float + Radius of the Cable. + sides : int + Number of sides of the Cable's polygonal section. + length : float + length of the Cable. + is_support : bool + Flag indicating if the Cable is a support. + frame : :class:`compas.geometry.Frame` + Main frame of the Cable. + frame_top : :class:`compas.geometry.Frame` + Second frame of the Cable. + axis : :class:`compas.geometry.Line` + Line axis of the Cable. + section : :class:`compas.geometry.Polygon` + Section polygon of the Cable. + polygon_bottom : :class:`compas.geometry.Polygon` + The bottom polygon of the Cable. + polygon_top : :class:`compas.geometry.Polygon` + The top polygon of the Cable. + transformation : :class:`compas.geometry.Transformation` + Transformation applied to the Cable. + features : list[:class:`compas_model.features.CableFeature`] + Features of the Cable. + name : str + The name of the Cable. + """ + + @property + def __data__(self) -> dict: + return { + "radius": self.radius, + "sides": self.sides, + "length": self.length, + "frame_top": self.frame_top, + "is_support": self.is_support, + "frame": self.frame, + "transformation": self.transformation, + "features": self._features, + "name": self.name, + } + + def __init__( + self, + radius: float = 0.4, + sides: int = 24, + length: float = 3.0, + frame_top: Optional[Plane] = None, + is_support: bool = False, + frame: Frame = Frame.worldXY(), + transformation: Optional[Transformation] = None, + features: Optional[list[CableFeature]] = None, + name: Optional[str] = None, + ) -> "CableElement": + super().__init__(frame=frame, transformation=transformation, features=features, name=name) + + self.is_support: bool = is_support + + self.radius = radius + self.sides = sides + self._length = length + self.axis: Line = Line([0, 0, 0], [0, 0, self._length]) + self.section: Polygon = Polygon.from_sides_and_radius_xy(sides, radius) + self.frame_top: Frame = frame_top or Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) + self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() + + @property + def face_polygons(self) -> list[Polygon]: + return [self.geometry.face_polygon(face) for face in self.geometry.faces()] # type: ignore + + @property + def length(self) -> float: + return self._length + + @length.setter + def length(self, length: float): + self._length = length + self.axis: Line = Line([0, 0, 0], [0, 0, self._length]) + self.frame_top: Frame = Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) + self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() + + def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: + """Compute the top and bottom polygons of the Cable. + + Returns + ------- + tuple[:class:`compas.geometry.Polygon`, :class:`compas.geometry.Polygon`] + """ + + plane0: Plane = Plane.from_frame(self.frame) + plane1: Plane = Plane.from_frame(self.frame_top) + points0: list[list[float]] = [] + points1: list[list[float]] = [] + for i in range(len(self.section.points)): + line: Line = Line(self.section.points[i], self.section.points[i] + self.axis.vector) + result0: Optional[list[float]] = intersection_line_plane(line, plane0) + result1: Optional[list[float]] = intersection_line_plane(line, plane1) + if not result0 or not result1: + raise ValueError("The line does not intersect the plane") + points0.append(result0) + points1.append(result1) + return Polygon(points0), Polygon(points1) + + # def compute_elementgeometry(self) -> Mesh: + # """Compute the shape of the Cable from the given polygons. + # This shape is relative to the frame of the element. + + # Returns + # ------- + # :class:`compas.datastructures.Mesh` + + # """ + # from compas.geometry import Point + # from compas.itertools import pairwise + + # offset: int = len(self.polygon_bottom) + # vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore + # bottom: list[int] = list(range(offset)) + # top: list[int] = [i + offset for i in bottom] + # faces: list[list[int]] = [bottom[::-1], top] + # for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): + # faces.append([a, b, d, c]) + # mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + # return mesh + + def compute_elementgeometry(self) -> Mesh: + """Compute the shape of the Cable from the given polygons. + This shape is relative to the frame of the element. + + Returns + ------- + :class:`compas.datastructures.Mesh` + + """ + + from compas.geometry import Brep + from compas.geometry import Cylinder + + cylinder: Cylinder = Cylinder.from_line_and_radius(self.axis, self.radius) + brep: Brep = Brep.from_cylinder(cylinder) + + return brep + + # ============================================================================= + # Implementations of abstract methods + # ============================================================================= + + def compute_aabb(self, inflate: float = 0.0) -> Box: + """Compute the axis-aligned bounding box of the element. + + Parameters + ---------- + inflate : float, optional + The inflation factor of the bounding box. + + Returns + ------- + :class:`compas.geometry.Box` + The axis-aligned bounding box. + """ + points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore + box: Box = Box.from_bounding_box(bounding_box(points)) + box.xsize += inflate + box.ysize += inflate + box.zsize += inflate + return box + + def compute_obb(self, inflate: float = 0.0) -> Box: + """Compute the oriented bounding box of the element. + + Parameters + ---------- + inflate : float, optional + The inflation factor of the bounding box. + + Returns + ------- + :class:`compas.geometry.Box` + The oriented bounding box. + """ + points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore + box: Box = Box.from_bounding_box(oriented_bounding_box(points)) + box.xsize += inflate + box.ysize += inflate + box.zsize += inflate + return box + + def compute_collision_mesh(self) -> Mesh: + """Compute the collision mesh of the element. + + Returns + ------- + :class:`compas.datastructures.Mesh` + The collision mesh. + """ + from compas.geometry import convex_hull_numpy + + points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore + vertices, faces = convex_hull_numpy(points) + vertices = [points[index] for index in vertices] # type: ignore + return Mesh.from_vertices_and_faces(vertices, faces) + + def extend(self, distance: float) -> None: + """Extend the beam. + + Parameters + ---------- + distance : float + The distance to extend the beam. + """ + self.length = self.length + distance * 2 + xform: Transformation = Translation.from_vector([0, 0, -distance]) + self.transformation = self.transformation * xform + + def compute_contact(self, target_element: Element, type: str = "") -> BooleanModifier: + """Computes the contact interaction of the geometry of the elements that is used in the model's add_contact method. + + Returns + ------- + :class:`compas_model.interactions.BooleanModifier` + The ContactInteraction that is applied to the neighboring element. One pair can have one or multiple variants. + target_element : Element + The target element to compute the contact interaction. + type : str, optional + The type of contact interaction, if different contact are possible between the two elements. + + """ + # Traverse up to the class one before the Element class.add() + # Create a function name based on the target_element class name. + parent_class = target_element.__class__ + while parent_class.__bases__[0] != Element: + parent_class = parent_class.__bases__[0] + + parent_class_name = parent_class.__name__.lower().replace("element", "") + method_name = f"_compute_contact_with_{parent_class_name}" + method = getattr(self, method_name, None) + if method is None: + raise ValueError(f"Unsupported target element type: {type(target_element)}") + + return method(target_element, type) + + def _compute_contact_with_beam(self, target_element: "BeamElement", type: str) -> Union["BooleanModifier", None]: + # Scenario: + # A cable applies boolean difference with a block geometry. + return BooleanModifier(self.elementgeometry.transformed(self.modeltransformation)) + + # ============================================================================= + # Constructors + # ============================================================================= diff --git a/src/compas_model/elements/column.py b/src/compas_model/elements/column.py index 09f7c435..bf6bfabf 100644 --- a/src/compas_model/elements/column.py +++ b/src/compas_model/elements/column.py @@ -2,6 +2,7 @@ from compas.datastructures import Mesh from compas.geometry import Box +from compas.geometry import Brep from compas.geometry import Frame from compas.geometry import Line from compas.geometry import Plane @@ -24,7 +25,101 @@ class ColumnFeature(Feature): class ColumnElement(Element): """Class representing a beam element.""" - pass + @property + def face_polygons(self) -> list[Polygon]: + return [self.geometry.face_polygon(face) for face in self.geometry.faces()] # type: ignore + + @property + def height(self) -> float: + return self._height + + @height.setter + def height(self, height: float): + self._height = height + self.axis: Line = Line([0, 0, 0], [0, 0, self._height]) + self.frame_top: Frame = Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) + self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() + + def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: + """Compute the top and bottom polygons of the column. + + Returns + ------- + tuple[:class:`compas.geometry.Polygon`, :class:`compas.geometry.Polygon`] + """ + + plane0: Plane = Plane.from_frame(self.frame) + plane1: Plane = Plane.from_frame(self.frame_top) + points0: list[list[float]] = [] + points1: list[list[float]] = [] + for i in range(len(self.section.points)): + line: Line = Line(self.section.points[i], self.section.points[i] + self.axis.vector) + result0: Optional[list[float]] = intersection_line_plane(line, plane0) + result1: Optional[list[float]] = intersection_line_plane(line, plane1) + if not result0 or not result1: + raise ValueError("The line does not intersect the plane") + points0.append(result0) + points1.append(result1) + return Polygon(points0), Polygon(points1) + + # ============================================================================= + # Implementations of abstract methods + # ============================================================================= + + def compute_aabb(self, inflate: float = 0.0) -> Box: + """Compute the axis-aligned bounding box of the element. + + Parameters + ---------- + inflate : float, optional + The inflation factor of the bounding box. + + Returns + ------- + :class:`compas.geometry.Box` + The axis-aligned bounding box. + """ + points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore + box: Box = Box.from_bounding_box(bounding_box(points)) + box.xsize += inflate + box.ysize += inflate + box.zsize += inflate + return box + + def compute_obb(self, inflate: float = 0.0) -> Box: + """Compute the oriented bounding box of the element. + + Parameters + ---------- + inflate : float, optional + The inflation factor of the bounding box. + + Returns + ------- + :class:`compas.geometry.Box` + The oriented bounding box. + """ + points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore + box: Box = Box.from_bounding_box(oriented_bounding_box(points)) + box.xsize += inflate + box.ysize += inflate + box.zsize += inflate + return box + + def compute_collision_mesh(self) -> Mesh: + """Compute the collision mesh of the element. + + Returns + ------- + :class:`compas.datastructures.Mesh` + The collision mesh. + """ + from compas.geometry import convex_hull_numpy + + points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore + vertices, faces = convex_hull_numpy(points) + vertices = [points[index] for index in vertices] # type: ignore + return Mesh.from_vertices_and_faces(vertices, faces) class ColumnSquareElement(ColumnElement): @@ -113,121 +208,38 @@ def __init__( self.frame_top: Frame = frame_top or Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() - @property - def height(self) -> float: - return self._height - - @height.setter - def height(self, height: float): - self._height = height - self.axis: Line = Line([0, 0, 0], [0, 0, self._height]) - self.frame_top: Frame = Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) - self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() - - @property - def face_polygons(self) -> list[Polygon]: - return [self.geometry.face_polygon(face) for face in self.geometry.faces()] # type: ignore + # def compute_elementgeometry(self) -> Mesh: + # """Compute the shape of the column from the given polygons. + # This shape is relative to the frame of the element. - def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: - """Compute the top and bottom polygons of the column. + # Returns + # ------- + # :class:`compas.datastructures.Mesh` - Returns - ------- - tuple[:class:`compas.geometry.Polygon`, :class:`compas.geometry.Polygon`] - """ + # """ - plane0: Plane = Plane.from_frame(self.frame) - plane1: Plane = Plane.from_frame(self.frame_top) - points0: list[list[float]] = [] - points1: list[list[float]] = [] - for i in range(len(self.section.points)): - line: Line = Line(self.section.points[i], self.section.points[i] + self.axis.vector) - result0: Optional[list[float]] = intersection_line_plane(line, plane0) - result1: Optional[list[float]] = intersection_line_plane(line, plane1) - if not result0 or not result1: - raise ValueError("The line does not intersect the plane") - points0.append(result0) - points1.append(result1) - return Polygon(points0), Polygon(points1) + # offset: int = len(self.polygon_bottom) + # vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore + # bottom: list[int] = list(range(offset)) + # top: list[int] = [i + offset for i in bottom] + # faces: list[list[int]] = [bottom[::-1], top] + # for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): + # faces.append([a, b, d, c]) + # mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + # return mesh - def compute_elementgeometry(self) -> Mesh: + def compute_elementgeometry(self) -> Brep: """Compute the shape of the column from the given polygons. This shape is relative to the frame of the element. Returns ------- - :class:`compas.datastructures.Mesh` - - """ - - offset: int = len(self.polygon_bottom) - vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore - bottom: list[int] = list(range(offset)) - top: list[int] = [i + offset for i in bottom] - faces: list[list[int]] = [bottom[::-1], top] - for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): - faces.append([a, b, d, c]) - mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) - return mesh - - # ============================================================================= - # Implementations of abstract methods - # ============================================================================= - - def compute_aabb(self, inflate: float = 0.0) -> Box: - """Compute the axis-aligned bounding box of the element. - - Parameters - ---------- - inflate : float, optional - The inflation factor of the bounding box. - - Returns - ------- - :class:`compas.geometry.Box` - The axis-aligned bounding box. - """ - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - box: Box = Box.from_bounding_box(bounding_box(points)) - box.xsize += inflate - box.ysize += inflate - box.zsize += inflate - return box - - def compute_obb(self, inflate: float = 0.0) -> Box: - """Compute the oriented bounding box of the element. - - Parameters - ---------- - inflate : float, optional - The inflation factor of the bounding box. - - Returns - ------- - :class:`compas.geometry.Box` - The oriented bounding box. - """ - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - box: Box = Box.from_bounding_box(oriented_bounding_box(points)) - box.xsize += inflate - box.ysize += inflate - box.zsize += inflate - return box - - def compute_collision_mesh(self) -> Mesh: - """Compute the collision mesh of the element. + :class:`compas.geometry.Brep` - Returns - ------- - :class:`compas.datastructures.Mesh` - The collision mesh. """ - from compas.geometry import convex_hull_numpy - - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - vertices, faces = convex_hull_numpy(points) - vertices = [points[index] for index in vertices] # type: ignore - return Mesh.from_vertices_and_faces(vertices, faces) + box: Box = Box.from_width_height_depth(self.width, self.height, self.depth).translated([0, 0, self.height * 0.5]) + brep = Brep.from_box(box) + return brep # ============================================================================= # Constructors @@ -326,43 +338,6 @@ def __init__( self.frame_top: Frame = frame_top or Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() - @property - def face_polygons(self) -> list[Polygon]: - return [self.geometry.face_polygon(face) for face in self.geometry.faces()] # type: ignore - - @property - def height(self) -> float: - return self._height - - @height.setter - def height(self, height: float): - self._height = height - self.axis: Line = Line([0, 0, 0], [0, 0, self._height]) - self.frame_top: Frame = Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) - self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() - - def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: - """Compute the top and bottom polygons of the column. - - Returns - ------- - tuple[:class:`compas.geometry.Polygon`, :class:`compas.geometry.Polygon`] - """ - - plane0: Plane = Plane.from_frame(self.frame) - plane1: Plane = Plane.from_frame(self.frame_top) - points0: list[list[float]] = [] - points1: list[list[float]] = [] - for i in range(len(self.section.points)): - line: Line = Line(self.section.points[i], self.section.points[i] + self.axis.vector) - result0: Optional[list[float]] = intersection_line_plane(line, plane0) - result1: Optional[list[float]] = intersection_line_plane(line, plane1) - if not result0 or not result1: - raise ValueError("The line does not intersect the plane") - points0.append(result0) - points1.append(result1) - return Polygon(points0), Polygon(points1) - def compute_elementgeometry(self) -> Mesh: """Compute the shape of the column from the given polygons. This shape is relative to the frame of the element. @@ -383,65 +358,6 @@ def compute_elementgeometry(self) -> Mesh: mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) return mesh - # ============================================================================= - # Implementations of abstract methods - # ============================================================================= - - def compute_aabb(self, inflate: float = 0.0) -> Box: - """Compute the axis-aligned bounding box of the element. - - Parameters - ---------- - inflate : float, optional - The inflation factor of the bounding box. - - Returns - ------- - :class:`compas.geometry.Box` - The axis-aligned bounding box. - """ - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - box: Box = Box.from_bounding_box(bounding_box(points)) - box.xsize += inflate - box.ysize += inflate - box.zsize += inflate - return box - - def compute_obb(self, inflate: float = 0.0) -> Box: - """Compute the oriented bounding box of the element. - - Parameters - ---------- - inflate : float, optional - The inflation factor of the bounding box. - - Returns - ------- - :class:`compas.geometry.Box` - The oriented bounding box. - """ - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - box: Box = Box.from_bounding_box(oriented_bounding_box(points)) - box.xsize += inflate - box.ysize += inflate - box.zsize += inflate - return box - - def compute_collision_mesh(self) -> Mesh: - """Compute the collision mesh of the element. - - Returns - ------- - :class:`compas.datastructures.Mesh` - The collision mesh. - """ - from compas.geometry import convex_hull_numpy - - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - vertices, faces = convex_hull_numpy(points) - vertices = [points[index] for index in vertices] # type: ignore - return Mesh.from_vertices_and_faces(vertices, faces) - # ============================================================================= # Constructors # ============================================================================= diff --git a/src/compas_model/elements/column_head.py b/src/compas_model/elements/column_head.py index 0a5b4f2d..e0e17117 100644 --- a/src/compas_model/elements/column_head.py +++ b/src/compas_model/elements/column_head.py @@ -605,8 +605,10 @@ def _compute_contact_with_plate(self, target_element: "PlateElement", type: str) direction0 = ColumnHeadCrossElement.closest_direction(polygon[v0_prev] - polygon[v0]) # CardinalDirections direction1 = ColumnHeadCrossElement.closest_direction(polygon[v0_next] - polygon[v0]) # CardinalDirections direction_angled = ColumnHeadCrossElement.get_direction_combination(direction0, direction1) - polygon: Polygon = self.modelgeometry.face_polygon(list(self.modelgeometry.faces_where(conditions={"direction": direction_angled}))[0]) - contact_frame: Frame = polygon.frame.translated([0, 0, 0.1]) + face_ids: list[int] = list(self.modelgeometry.faces_where(conditions={"direction": direction_angled})) + face_id: int = face_ids[0] if len(face_ids) > 0 else 5 + polygon: Polygon = self.modelgeometry.face_polygon(face_id) + contact_frame: Frame = polygon.frame return ContactInterface(points=[], frame=contact_frame) diff --git a/src/compas_model/elements/element.py b/src/compas_model/elements/element.py index 266fc8b4..be9db2c2 100644 --- a/src/compas_model/elements/element.py +++ b/src/compas_model/elements/element.py @@ -310,17 +310,15 @@ def compute_modelgeometry(self) -> Union[Brep, Mesh]: :class:`compas.datastructures.Mesh` | :class:`compas.geometry.Brep` """ + graph = self.model.graph - elements = list(self.model.elements()) xform = self.modeltransformation modelgeometry = self.elementgeometry.transformed(xform) for neighbor in graph.neighbors_in(self.graphnode): for interaction in graph.edge_interactions((neighbor, self.graphnode)): - if isinstance(interaction, ContactInterface): + if isinstance(interaction, ContactInterface) or isinstance(interaction, BooleanModifier): modelgeometry = interaction.apply(modelgeometry) - elif isinstance(interaction, BooleanModifier): - modelgeometry = interaction.apply(modelgeometry, elements[neighbor].modelgeometry) self.is_dirty = False diff --git a/src/compas_model/interactions/boolean_modifier.py b/src/compas_model/interactions/boolean_modifier.py index 5f9310ad..3ee16a4a 100644 --- a/src/compas_model/interactions/boolean_modifier.py +++ b/src/compas_model/interactions/boolean_modifier.py @@ -11,6 +11,8 @@ class BooleanModifier(Interaction): Parameters ---------- + cutter : :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` + The geometry to be used as the modifier name : str, optional The name of the interaction. @@ -19,16 +21,17 @@ class BooleanModifier(Interaction): @property def __data__(self): # type: () -> dict - return {"name": self.name} + return {"name": self.name, "cutter": self.cutter} - def __init__(self, name=None): - # type: (str | None) -> None + def __init__(self, cutter: Union[Brep, Mesh], name=None): + # type: (Union[Brep, Mesh], str | None) -> None super(BooleanModifier, self).__init__(name=name) + self.cutter = cutter def __repr__(self): return '{}(name="{}")'.format(self.__class__.__name__, self.name) - def apply(self, targetgeometry: Union[Brep, Mesh], sourcegeometry: Union[Brep, Mesh]): + def apply(self, targetgeometry: Union[Brep, Mesh]): """Apply the interaction to the affected geometry. Parameters @@ -41,4 +44,4 @@ def apply(self, targetgeometry: Union[Brep, Mesh], sourcegeometry: Union[Brep, M # Local import is needed otherwise, remove contact interactions in algorithms module. from compas_model.algorithms.modifiers import boolean_difference - return boolean_difference(targetgeometry, sourcegeometry) + return boolean_difference(targetgeometry, self.cutter) diff --git a/src/compas_model/models/blockmodel.py b/src/compas_model/models/blockmodel.py index 709699d9..0eef946e 100644 --- a/src/compas_model/models/blockmodel.py +++ b/src/compas_model/models/blockmodel.py @@ -1,12 +1,23 @@ +from math import radians from typing import Optional +from compas.datastructures import Mesh from compas.geometry import Brep +from compas.geometry import Frame +from compas.geometry import Point +from compas.geometry import Rotation +from compas.geometry import Transformation +from compas.geometry import add_vectors +from compas.geometry import angle_vectors +from compas.geometry import subtract_vectors +from compas.geometry import transform_points +from compas.geometry import translate_points from compas.tolerance import Tolerance from compas_model.algorithms.nnbrs import find_nearest_neighbours +from compas_model.elements import BlockElement from compas_model.elements import Element from compas_model.interactions import ContactInterface - -from .model import Model +from compas_model.models import Model try: from compas_occ.brep import OCCBrepFace as BrepFace @@ -81,3 +92,134 @@ def compute_interfaces(self, deflection=None, tolerance=1, max_distance=50, min_ # do something with the interactions self.add_interaction(A, B, interaction=interaction) + + @classmethod + def from_barrel_vault( + cls, + span: float = 6.0, + length: float = 6.0, + thickness: float = 0.25, + rise: float = 0.6, + vou_span: int = 9, + vou_length: int = 6, + zero_is_centerline_or_lowestpoint: bool = False, + ) -> "BlockModel": + """ + Creates block elements from the barrel vault geometry. + + Parameters + ---------- + span : float + span of the vault + length : float + length of the vault perpendicular to the span + thickness : float + thickness of the vault + rise : float + rise of the vault from 0.0 to middle axis of the vault thickness + vou_span : int + number of voussoirs in the span direction + vou_length : int + number of voussoirs in the length direction + zero_is_centerline_or_lowestpoint : bool + if True, the lowest point of the vault is at the center line of the arch, otherwise the center line of the vault is lowest mesh z-coordinate. + + Returns + ------- + list[:class:`compas.datastructures.Mesh`] + A list of meshes representing the geometry of the barrel vault. + """ + radius: float = rise / 2 + span**2 / (8 * rise) + top: list[float] = [0, 0, rise] + left: list[float] = [-span / 2, 0, 0] + center: list[float] = [0.0, 0.0, rise - radius] + vector: list[float] = subtract_vectors(left, center) + springing: float = angle_vectors(vector, [-1.0, 0.0, 0.0]) + sector: float = radians(180) - 2 * springing + angle: float = sector / vou_span + + a: list[float] = [0, -length / 2, rise - (thickness / 2)] + d: list[float] = add_vectors(top, [0, -length / 2, (thickness / 2)]) + + R: Rotation = Rotation.from_axis_and_angle([0, 1.0, 0], 0.5 * sector, center) + bottom: list[list[float]] = transform_points([a, d], R) + brick_pts: list[list[list[float]]] = [] + for i in range(vou_span + 1): + R_angle: Rotation = Rotation.from_axis_and_angle([0, 1.0, 0], -angle * i, center) + points: list[list[float]] = transform_points(bottom, R_angle) + brick_pts.append(points) + + depth: float = length / vou_length + grouped_data: list[list[float]] = [pair[0] + pair[1] for pair in zip(brick_pts, brick_pts[1:])] + + meshes: list[Mesh] = [] + for i in range(vou_length): + for l, group in enumerate(grouped_data): # noqa: E741 + is_support: bool = l == 0 or l == (len(grouped_data) - 1) + if l % 2 == 0: + point_l: list[list[float]] = [group[0], group[1], group[2], group[3]] + point_list: list[list[float]] = [ + [group[0][0], group[0][1] + (depth * i), group[0][2]], + [group[1][0], group[1][1] + (depth * i), group[1][2]], + [group[2][0], group[2][1] + (depth * i), group[2][2]], + [group[3][0], group[3][1] + (depth * i), group[3][2]], + ] + p_t: list[list[float]] = translate_points(point_l, [0, depth * (i + 1), 0]) + vertices: list[list[float]] = point_list + p_t + faces: list[list[int]] = [[0, 1, 3, 2], [0, 4, 5, 1], [4, 6, 7, 5], [6, 2, 3, 7], [1, 5, 7, 3], [2, 6, 4, 0]] + mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + mesh.attributes["is_support"] = is_support + meshes.append(mesh) + else: + point_l: list[list[float]] = [group[0], group[1], group[2], group[3]] + points_base: list[list[float]] = translate_points(point_l, [0, depth / 2, 0]) + points_b_t: list[list[float]] = translate_points(points_base, [0, depth * i, 0]) + points_t: list[list[float]] = translate_points(points_base, [0, depth * (i + 1), 0]) + vertices: list[list[float]] = points_b_t + points_t + if i != vou_length - 1: + faces: list[list[int]] = [[0, 1, 3, 2], [0, 4, 5, 1], [4, 6, 7, 5], [6, 2, 3, 7], [1, 5, 7, 3], [2, 6, 4, 0]] + mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + mesh.attributes["is_support"] = is_support + meshes.append(mesh) + + for l, group in enumerate(grouped_data): # noqa: E741 + is_support: bool = l == 0 or l == (len(grouped_data) - 1) + if l % 2 != 0: + point_l: list[list[float]] = [group[0], group[1], group[2], group[3]] + p_t: list[list[float]] = translate_points(point_l, [0, depth / 2, 0]) + vertices: list[list[float]] = point_l + p_t + faces: list[list[int]] = [[0, 1, 3, 2], [0, 4, 5, 1], [4, 6, 7, 5], [6, 2, 3, 7], [1, 5, 7, 3], [2, 6, 4, 0]] + mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + mesh.attributes["is_support"] = is_support + meshes.append(mesh) + + point_f: list[list[float]] = translate_points(point_l, [0, length, 0]) + p_f: list[list[float]] = translate_points(point_f, [0, -depth / 2, 0]) + vertices: list[list[float]] = p_f + point_f + faces: list[list[int]] = [[0, 1, 3, 2], [0, 4, 5, 1], [4, 6, 7, 5], [6, 2, 3, 7], [1, 5, 7, 3], [2, 6, 4, 0]] + mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + mesh.attributes["is_support"] = is_support + meshes.append(mesh) + + # Find the lowest z-coordinate and move all the block to zero. + if not zero_is_centerline_or_lowestpoint: + min_z: float = min([min(mesh.vertex_coordinates(key)[2] for key in mesh.vertices()) for mesh in meshes]) + for mesh in meshes: + mesh.translate([0, 0, -min_z]) + + # Translate blocks to xy frame and create blockmodel. + blockmodel: BlockModel = BlockModel() + + for mesh in meshes: + origin: Point = mesh.face_polygon(5).frame.point + xform: Transformation = Transformation.from_frame_to_frame( + Frame(origin, mesh.vertex_point(0) - mesh.vertex_point(2), mesh.vertex_point(4) - mesh.vertex_point(2)), Frame.worldXY() + ) + mesh_xy: Mesh = mesh.transformed(xform) + brep: Brep = Brep.from_mesh(mesh_xy) + + block: BlockElement = BlockElement(shape=brep, is_support=mesh_xy.attributes["is_support"]) + block.transformation = xform.inverse() + blockmodel.add_element(block) + + return blockmodel diff --git a/src/compas_model/models/model.py b/src/compas_model/models/model.py index 61c88c47..833ee654 100644 --- a/src/compas_model/models/model.py +++ b/src/compas_model/models/model.py @@ -411,6 +411,7 @@ def compute_contact(self, a: Element, b: Element, type: str = "") -> tuple[int, raise Exception("Something went wrong: the elements are not in the interaction graph.") interaction: Interaction = a.compute_contact(b, type) + if interaction: # Whether we add contact if there is an edge or not we will decide later. edge = self._graph.add_edge(node_a, node_b) @@ -419,8 +420,6 @@ def compute_contact(self, a: Element, b: Element, type: str = "") -> tuple[int, self.graph.edge_attribute(edge, name="interactions", value=interactions) self._guid_element[str(b.guid)].is_dirty = True return edge - else: - raise Exception("No contact interaction found between the two elements.") def remove_element(self, element: Element) -> None: """Remove an element from the model.