diff --git a/CHANGELOG.md b/CHANGELOG.md index 3758611c..99418ed9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,20 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `Element.compute_elementgeometry()`. * Added `Element.compute_modelgeometry()` to replace `Element.compute_geometry()`. * Added `Element.compute_modeltransformation()` to replace `Element.compute_worldtransformation()`. -* Added `compas_model.elements.ColumnHeadElement`. -* Added `compas_model.elements.ColumnHeadCrossElement`. -* Added `compas_model.elements.BeamFeature`. -* Added `compas_model.elements.BeamElement`. -* Added `compas_model.elements.BeamIProfileElement`. -* Added `compas_model.elements.BeamSquareElement`. -* Added `compas_model.elements.ColumnFeature`. -* Added `compas_model.elements.ColumnElement`. -* Added `compas_model.elements.ColumnRoundElement`. -* Added `compas_model.elements.ColumnSquareElement`. -* Added `compas_model.elements.FastenersFeature`. -* Added `compas_model.elements.FastenersElement`. -* Added `compas_model.elements.ScrewElement`. * Added `Element.is_dirty`. +* Added `Model.add_modifier`. ### Changed diff --git a/src/compas_model/algorithms/__init__.py b/src/compas_model/algorithms/__init__.py index 6fe5f810..f38e13bc 100644 --- a/src/compas_model/algorithms/__init__.py +++ b/src/compas_model/algorithms/__init__.py @@ -1,9 +1,6 @@ from .interfaces import model_interfaces from .intersections import model_intersections from .overlaps import model_overlaps -from .modifiers import slice -from .modifiers import boolean_difference - __all__ = [ "model_interfaces", diff --git a/src/compas_model/algorithms/modifiers.py b/src/compas_model/algorithms/modifiers.py deleted file mode 100644 index 34c846c6..00000000 --- a/src/compas_model/algorithms/modifiers.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing import Optional -from typing import Union - -from compas.datastructures import Mesh -from compas.geometry import Brep -from compas.geometry import Plane - - -def slice(geometry: Union[Brep, Mesh], slice_plane: Plane) -> Union[Brep, Mesh]: - """Slice the target geometry by the slice plane. - NOTE: Original geometry is returned if slicing is not successful. - - Parameters - ---------- - geometry : :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` - The geometry to be affected. The same geometry can be modified multiple times. - slice_plane : :class:`compas.geometry.Plane` - The plane to slice the geometry. - - Returns - ------- - :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 - except Exception: - print("SlicerModifier is not successful.") - return geometry - - -def boolean_difference(target_geometry, source_geometry): - """Perform boolean difference on the target geometry. - NOTE: Original geometry is returned if boolean difference is not successful. - - Parameters - ---------- - target_geometry : :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` - The geometry to be affected. - source_geometry : :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` - The geometry to subtract. - - Returns - ------- - :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() - - 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 diff --git a/src/compas_model/elements/__init__.py b/src/compas_model/elements/__init__.py index d7d88f74..469df81e 100644 --- a/src/compas_model/elements/__init__.py +++ b/src/compas_model/elements/__init__.py @@ -1,44 +1,7 @@ from .element import reset_computed from .element import Element -from .block import BlockFeature -from .block import BlockElement -from .block import BlockGeometry -from .plate import PlateFeature -from .plate import PlateElement -from .column_head import ColumnHeadElement -from .column_head import ColumnHeadCrossElement -from .beam import BeamFeature -from .beam import BeamElement -from .beam import BeamIProfileElement -from .beam import BeamSquareElement -from .column import ColumnFeature -from .column import ColumnElement -from .column import ColumnRoundElement -from .column import ColumnSquareElement -from .fasteners import FastenersFeature -from .fasteners import FastenersElement -from .fasteners import ScrewElement - __all__ = [ reset_computed, Element, - BlockFeature, - BlockElement, - BlockGeometry, - PlateFeature, - PlateElement, - ColumnHeadElement, - ColumnHeadCrossElement, - BeamFeature, - BeamElement, - BeamIProfileElement, - BeamSquareElement, - ColumnFeature, - ColumnElement, - ColumnRoundElement, - ColumnSquareElement, - FastenersFeature, - FastenersElement, - ScrewElement, ] diff --git a/src/compas_model/elements/beam.py b/src/compas_model/elements/beam.py deleted file mode 100644 index 30e3df73..00000000 --- a/src/compas_model/elements/beam.py +++ /dev/null @@ -1,465 +0,0 @@ -from typing import Optional - -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 Point -from compas.geometry import Polygon -from compas.geometry import Transformation -from compas.geometry import bounding_box -from compas.geometry import intersection_line_plane -from compas.geometry import oriented_bounding_box -from compas.itertools import pairwise - -from .element import Element -from .element import Feature - - -class BeamFeature(Feature): - pass - - -class BeamElement(Element): - """Class representing a beam element.""" - - pass - - -class BeamSquareElement(BeamElement): - """Class representing a beam element with a square section. - - Parameters - ---------- - width : float - The width of the beam. - depth : float - The depth of the beam. - length : float - The length of the beam. - frame_bottom : :class:`compas.geometry.Frame` - Main frame of the beam. - frame_top : :class:`compas.geometry.Frame` - Second frame of the beam that is used to cut the second end, while the first frame is used to cut the first end. - transformation : Optional[:class:`compas.geometry.Transformation`] - Transformation applied to the beam. - features : Optional[list[:class:`compas_model.features.BeamFeature`]] - Features of the beam. - name : Optional[str] - If no name is defined, the class name is given. - - Attributes - ---------- - width : float - The width of the beam. - depth : float - The depth of the beam. - length : float - The length of the beam. - is_support : bool - Flag indicating if the beam is a support. - frame_bottom : :class:`compas.geometry.Frame` - Main frame of the beam. - frame_top : :class:`compas.geometry.Frame` - Second frame of the beam. - axis : :class:`compas.geometry.Line` - Line axis of the beam. - section : :class:`compas.geometry.Polygon` - Section polygon 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` - Transformation applied to the beam. - features : list[:class:`compas_model.features.BeamFeature`] - Features of the beam. - name : str - The name of the beam. - """ - - @property - def __data__(self) -> dict: - return { - "width": self.width, - "depth": self.depth, - "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, - width: float = 0.1, - depth: float = 0.2, - length: float = 3.0, - 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, - ) -> "BeamSquareElement": - super().__init__(frame=frame, transformation=transformation, features=features, name=name) - - self.is_support: bool = is_support - - self.width: float = width - self.depth: float = depth - self._length: float = length - - 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.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` - - """ - - 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. - - 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 - # ============================================================================= - - -class BeamIProfileElement(BeamElement): - """Class representing a beam element with I profile. - - Parameters - ---------- - width : float, optional - The width of the beam. - depth : float, optional - The depth of the beam. - thickness : float, optional - The thickness of the beam. - length : float, optional - The length of the beam. - 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. - - 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. - """ - - @property - def __data__(self) -> dict: - return { - "width": self.width, - "depth": self.depth, - "thickness": self.thickness, - "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, - width: float = 0.1, - depth: float = 0.2, - thickness: float = 0.02, - length: float = 3.0, - 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) - - self.is_support: bool = is_support - - self.width: float = width - self.depth: float = depth - self.thickness: float = thickness - self._length: float = length - - self.points: list[float] = [ - [0, -self.depth * 0.5, 0], - [0, self.depth * 0.5, 0], - [-self.thickness, self.depth * 0.5, 0], - [-self.thickness, self.thickness * 0.5, 0], - [-self.width + self.thickness, self.thickness * 0.5, 0], - [-self.width + self.thickness, self.depth * 0.5, 0], - [-self.width, self.depth * 0.5, 0], - [-self.width, -self.depth * 0.5, 0], - [-self.width + self.thickness, -self.depth * 0.5, 0], - [-self.width + self.thickness, -self.thickness * 0.5, 0], - [-self.thickness, -self.thickness * 0.5, 0], - [-self.thickness, -self.depth * 0.5, 0], - ] - - # 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.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` - - """ - - 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. - - 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/block.py b/src/compas_model/elements/block.py deleted file mode 100644 index 37b30d08..00000000 --- a/src/compas_model/elements/block.py +++ /dev/null @@ -1,231 +0,0 @@ -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 Point -from compas.geometry import Transformation -from compas.geometry import bestfit_frame_numpy -from compas.geometry import bounding_box -from compas.geometry import centroid_points -from compas.geometry import centroid_polyhedron -from compas.geometry import dot_vectors -from compas.geometry import oriented_bounding_box -from compas.geometry import volume_polyhedron -from compas.geometry.brep.brep import Brep - -from .element import Element -from .element import Feature - - -class BlockGeometry(Mesh): - """Geometric representation of a block using a mesh.""" - - @property - def top(self) -> int: - """Identify the *top* face of the block. - - Returns - ------- - int - The identifier of the face. - - """ - z = [0, 0, 1] - faces = list(self.faces()) - normals = [self.face_normal(face) for face in faces] - return sorted(zip(faces, normals), key=lambda x: dot_vectors(x[1], z))[-1][0] - - @property - def centroid(self) -> Point: - """Compute the centroid of the block. - - Returns - ------- - :class:`compas.geometry.Point` - - """ - x, y, z = centroid_points([self.vertex_coordinates(key) for key in self.vertices()]) - return Point(x, y, z) - - @property - def center(self) -> Point: - """Compute the center of mass of the block. - - Returns - ------- - :class:`compas.geometry.Point` - - """ - vertex_index = {vertex: index for index, vertex in enumerate(self.vertices())} - vertices = [self.vertex_coordinates(vertex) for vertex in self.vertices()] - faces = [[vertex_index[vertex] for vertex in self.face_vertices(face)] for face in self.faces()] - x, y, z = centroid_polyhedron((vertices, faces)) - return Point(x, y, z) - - @property - def volume(self) -> float: - """Compute the volume of the block. - - Returns - ------- - float - The volume of the block. - - """ - vertex_index = {vertex: index for index, vertex in enumerate(self.vertices())} - vertices = [self.vertex_coordinates(vertex) for vertex in self.vertices()] - faces = [[vertex_index[vertex] for vertex in self.face_vertices(face)] for face in self.faces()] - v = volume_polyhedron((vertices, faces)) - return v - - def face_frame(self, face: int) -> Frame: - """Compute the frame of a specific face. - - Parameters - ---------- - face : int - The identifier of the frame. - - Returns - ------- - :class:`compas.geometry.Frame` - - """ - xyz = self.face_coordinates(face) - normal = self.face_normal(face) - o, u, v = bestfit_frame_numpy(xyz) - frame = Frame(o, u, v) - if frame.zaxis.dot(normal) < 0: - frame.invert() - return frame - - -# A block could have features like notches, -# but we will work on it when we need it... -# A notch could be a cylinder defined in the frame of a face. -# The frame of a face should be defined in coorination with the global frame of the block. -# during interface detection the features could/should be ignored. -class BlockFeature(Feature): - pass - - -class BlockElement(Element): - """Class representing block elements. - - Parameters - ---------- - shape : :class:`compas.datastructures.Mesh` - The base shape of the block. - features : list[:class:`BlockFeature`], optional - Additional block features. - is_support : bool, optional - Flag indicating that the block is a support. - frame : :class:`compas.geometry.Frame`, optional - The coordinate frame of the block. - name : str, optional - The name of the element. - - Attributes - ---------- - shape : :class:`compas.datastructure.Mesh` - The base shape of the block. - features : list[:class:`BlockFeature`] - A list of additional block features. - is_support : bool - Flag indicating that the block is a support. - - """ - - elementgeometry: BlockGeometry - modelgeometry: BlockGeometry - - @property - def __data__(self) -> dict: - data = super().__data__ - data["shape"] = self.shape - data["features"] = self.features - data["is_support"] = self.is_support - return data - - def __init__( - self, - shape: Union[Mesh, BlockGeometry], - features: Optional[list[BlockFeature]] = None, - is_support: bool = False, - frame: Optional[Frame] = None, - transformation: Optional[Transformation] = None, - 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.is_support = is_support - - # ============================================================================= - # Implementations of abstract methods - # ============================================================================= - - def compute_elementgeometry(self) -> Union[Mesh, Brep]: - geometry = self.shape - # 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_aabb(self) -> Box: - points = self.modelgeometry.vertices_attributes("xyz") - box = Box.from_bounding_box(bounding_box(points)) - box.xsize += self.inflate_aabb - box.ysize += self.inflate_aabb - box.zsize += self.inflate_aabb - return box - - def compute_obb(self) -> Box: - points = self.modelgeometry.vertices_attributes("xyz") - box = Box.from_bounding_box(oriented_bounding_box(points)) - box.xsize += self.inflate_obb - box.ysize += self.inflate_obb - box.zsize += self.inflate_obb - return box - - def compute_collision_mesh(self) -> Mesh: - # TODO: (TvM) make this a pluggable with default implementation in core and move import to top - from compas.geometry import convex_hull_numpy - - points = self.modelgeometry.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 - # ============================================================================= - - @classmethod - def from_box(cls, box: Box) -> "BlockElement": - shape = box.to_mesh() - block = cls(shape=shape) - return block diff --git a/src/compas_model/elements/column.py b/src/compas_model/elements/column.py deleted file mode 100644 index 09f7c435..00000000 --- a/src/compas_model/elements/column.py +++ /dev/null @@ -1,447 +0,0 @@ -from typing import Optional - -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 Point -from compas.geometry import Polygon -from compas.geometry import Transformation -from compas.geometry import bounding_box -from compas.geometry import intersection_line_plane -from compas.geometry import oriented_bounding_box -from compas.itertools import pairwise - -from .element import Element -from .element import Feature - - -class ColumnFeature(Feature): - pass - - -class ColumnElement(Element): - """Class representing a beam element.""" - - pass - - -class ColumnSquareElement(ColumnElement): - """Class representing a column element with a square section. - - Parameters - ---------- - width : float - The width of the column. - depth : float - The depth of the column. - height : float - The height of the column. - frame_bottom : :class:`compas.geometry.Frame` - Main frame of the column. - frame_top : :class:`compas.geometry.Frame` - Second frame of the column that is used to cut the second end, while the first frame is used to cut the first end. - transformation : Optional[:class:`compas.geometry.Transformation`] - Transformation applied to the column. - features : Optional[list[:class:`compas_model.features.ColumnFeature`]] - Features of the column. - name : Optional[str] - If no name is defined, the class name is given. - - Attributes - ---------- - width : float - The width of the column. - depth : float - The depth of the column. - height : float - The height of the column. - is_support : bool - Flag indicating if the column is a support. - frame_bottom : :class:`compas.geometry.Frame` - Main frame of the column. - frame_top : :class:`compas.geometry.Frame` - Second frame of the column. - transformation : :class:`compas.geometry.Transformation` - Transformation applied to the column. - features : list[:class:`compas_model.features.ColumnFeature`] - Features of the column. - name : str - The name of the column. - """ - - @property - def __data__(self) -> dict: - return { - "width": self.width, - "depth": self.depth, - "height": self.height, - "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, - width: float = 0.4, - depth: float = 0.4, - height: float = 3.0, - frame_top: Optional[Plane] = None, - is_support: bool = False, - frame: Frame = Frame.worldXY(), - transformation: Optional[Transformation] = None, - features: Optional[list[ColumnFeature]] = None, - name: Optional[str] = None, - ) -> "ColumnSquareElement": - super().__init__(frame=frame, transformation=transformation, features=features, name=name) - - self.is_support: bool = is_support - - self.width = width - self.depth = depth - self._height = height - self.axis: Line = Line([0, 0, 0], [0, 0, height]) - p3: list[float] = [-width * 0.5, -depth * 0.5, 0] - p2: list[float] = [-width * 0.5, depth * 0.5, 0] - p1: list[float] = [width * 0.5, depth * 0.5, 0] - p0: list[float] = [width * 0.5, -depth * 0.5, 0] - self.section: Polygon = Polygon([p0, p1, p2, p3]) - 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_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. - - 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. - - 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 - # ============================================================================= - - -class ColumnRoundElement(ColumnElement): - """Class representing a column element with a round section. - - Parameters - ---------- - radius : float - Radius of the column. - sides : int - Number of sides of the column's polygonal section. - height : float - Height of the column. - frame_top : Optional[:class:`compas.geometry.Plane`] - Second frame of the column 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 column is a support. - frame : :class:`compas.geometry.Frame` - Main frame of the column. - transformation : Optional[:class:`compas.geometry.Transformation`] - Transformation applied to the column. - features : Optional[list[:class:`compas_model.features.ColumnFeature`]] - Features of the column. - name : Optional[str] - If no name is defined, the class name is given. - - Attributes - ---------- - radius : float - Radius of the column. - sides : int - Number of sides of the column's polygonal section. - height : float - Height of the column. - is_support : bool - Flag indicating if the column is a support. - frame : :class:`compas.geometry.Frame` - Main frame of the column. - frame_top : :class:`compas.geometry.Frame` - Second frame of the column. - axis : :class:`compas.geometry.Line` - Line axis of the column. - section : :class:`compas.geometry.Polygon` - Section polygon of the column. - polygon_bottom : :class:`compas.geometry.Polygon` - The bottom polygon of the column. - polygon_top : :class:`compas.geometry.Polygon` - The top polygon of the column. - transformation : :class:`compas.geometry.Transformation` - Transformation applied to the column. - features : list[:class:`compas_model.features.ColumnFeature`] - Features of the column. - name : str - The name of the column. - """ - - @property - def __data__(self) -> dict: - return { - "radius": self.radius, - "sides": self.sides, - "height": self.height, - "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, - height: float = 3.0, - frame_top: Optional[Plane] = None, - is_support: bool = False, - frame: Frame = Frame.worldXY(), - transformation: Optional[Transformation] = None, - features: Optional[list[ColumnFeature]] = None, - name: Optional[str] = None, - ) -> "ColumnRoundElement": - super().__init__(frame=frame, transformation=transformation, features=features, name=name) - - self.is_support: bool = is_support - - self.radius = radius - self.sides = sides - self._height = height - self.axis: Line = Line([0, 0, 0], [0, 0, self._height]) - 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 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. - - 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. - - 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 deleted file mode 100644 index 0a5b4f2d..00000000 --- a/src/compas_model/elements/column_head.py +++ /dev/null @@ -1,709 +0,0 @@ -from enum import Enum -from typing import TYPE_CHECKING -from typing import Optional - -from compas.datastructures import Mesh -from compas.geometry import Box -from compas.geometry import Frame -from compas.geometry import Point -from compas.geometry import Polygon -from compas.geometry import Transformation -from compas.geometry import Vector -from compas.geometry import bounding_box -from compas.geometry import oriented_bounding_box -from compas_model.interactions import ContactInterface - -from .element import Element - -if TYPE_CHECKING: - from compas_model.elements import BeamElement - from compas_model.elements import ColumnElement - from compas_model.elements import PlateElement - - -class ColumnHeadElement(Element): - """Base class for column head elements.""" - - pass - - -class CardinalDirections(int, Enum): - """ - Enumeration of directions where the number corresponds to the column head mesh face index. - - Attributes - ---------- - NORTH : int - The north direction. - NORTH_WEST : int - The north-west direction. - WEST : int - The west direction. - SOUTH_WEST : int - The south-west direction. - SOUTH : int - The south direction. - SOUTH_EAST : int - The south-east direction. - EAST : int - The east direction. - NORTH_EAST : int - The north-east direction. - """ - - NORTH = 0 - NORTH_WEST = 1 - WEST = 2 - SOUTH_WEST = 3 - SOUTH = 4 - SOUTH_EAST = 5 - EAST = 6 - NORTH_EAST = 7 - - -class CrossBlockShape: - """Generate Column Head shapes based on vertex and edge and face adjacency. - The class is singleton, considering the dimension of the column head is fixed and created once. - - Parameters - ---------- - width : float - The width of the column head. - depth : float - The depth of the column head. - height : float - The height of the column head. - offset : float - The offset of the column head. - v : dict[int, Point] - The points, first one is always the origin. - e : list[tuple[int, int]] - Edges starts from v0 between points v0-v1, v0-v2 and so on. - f : list[list[int]] - Faces between points v0-v1-v2-v3 and so on. If face vertices forms already given edges. Triangle mesh face is formed. - - - Example - ------- - width: float = 150 - depth: float = 150 - height: float = 300 - offset: float = 210 - v: dict[int, Point] = { - 7: Point(0, 0, 0), - 5: Point(-1, 0, 0), - 6: Point(0, 1, 0), - 8: Point(0, -1, 0), - 2: Point(1, 0, 0), - } - - e: list[tuple[int, int]] = [ - (7, 5), - (7, 6), - (7, 8), - (7, 2), - ] - - f: list[list[int]] = [[5, 7, 6, 10]] - - CrossBlockShape: CrossBlockShape = CrossBlockShape(v, e, f, width, depth, height, offset) - mesh = CrossBlockShape.mesh.scaled(0.001) - - """ - - _instance = None - _generated_meshes = {} - _last_mesh = None - - def __new__(cls, *args, **kwargs): - if cls._instance is None: - cls._instance = super(CrossBlockShape, cls).__new__(cls) - return cls._instance - - def __init__( - self, - v: dict[int, Point], - e: list[tuple[int, int]], - f: list[list[int]], - width: float = 150, - depth: float = 150, - height: float = 300, - offset: float = 210, - ): - if not hasattr(self, "_initialized"): - self._width = width - self._depth = depth - self._height = height - self._offset = offset - rules = self._generate_rules(v, e, f) - self._generated_meshes[rules] = self._generate_mesh(rules) - self._last_mesh = self._generated_meshes[rules] - self._initialized = True - - def _generate_rules(self, v: dict[Point], e: list[tuple[int, int]], f: list[list[int]]) -> list[bool]: - """ - Generate rules for generating the mesh of the column head. - ATTENTION: edge first vertex is considered the column head origin, otherwise direction are flipped. - - Parameters - ------- - v : dict - The points, first one is always the origin. - e : list - First find nearest edges, edges starts from v0 between points v0-v1, v0-v2 and so on. - f : list - Faces between points v0-v1-v2-v3 and so on. - - Returns - ------- - tuple - The generated rules. - """ - - rules = [False, False, False, False, False, False, False, False] - edge_directions: dict[tuple[int, int], CardinalDirections] = {} - - # Find the directions of the edges - for edge in e: - if edge[0] not in v: - raise ValueError(f"Vertex {edge[0]} not found in the vertices.") - if edge[1] not in v: - raise ValueError(f"Vertex {edge[1]} not found in the vertices.") - - p0 = v[edge[0]] - p1 = v[edge[1]] - vector = p1 - p0 - direction = ColumnHeadCrossElement.closest_direction(vector) - rules[direction] = True - - # track direction for face edge search - edge_directions[(edge[0], edge[1])] = direction - edge_directions[(edge[1], edge[0])] = direction - - for face in f: - face_edge_directions = [] - for i in range(len(face)): - v0 = face[i] - v1 = face[(i + 1) % len(face)] - - if (v0, v1) not in edge_directions: - continue - - face_edge_directions.append(edge_directions[(v0, v1)]) - - # Face must have two directions - if not len(face_edge_directions) == 2: - raise ValueError(f"Face {face} does not share two edges.") - - face_direction: CardinalDirections = ColumnHeadCrossElement.get_direction_combination(face_edge_directions[0], face_edge_directions[1]) - rules[face_direction] = True - - return tuple(rules) - - def _generate_mesh(self, rules: tuple[bool]) -> Mesh: - """ - Generate mesh based on the rules. - - Parameters - ---------- - - rules : tuple - The generated rules that corresponds to world direction using CardinalDirections enumerator. - - Returns - ------- - Mesh - The column head generated mesh. - - """ - - if rules in self._generated_meshes: - return self._generated_meshes[rules] - - ########################################################################################### - # Generate mesh based on the rules. - ########################################################################################### - - vertices: list[Point] = [ - # Outer ring - Point(self._width, self._depth + self._offset, -self._height), # 0 - Point(-self._width, self._depth + self._offset, -self._height), # 1 - Point(-self._width - self._offset, self._depth, -self._height), # 2 - Point(-self._width - self._offset, -self._depth, -self._height), # 3 - Point(-self._width, -self._depth - self._offset, -self._height), # 4 - Point(self._width, -self._depth - self._offset, -self._height), # 5 - Point(self._width + self._offset, -self._depth, -self._height), # 6 - Point(self._width + self._offset, self._depth, -self._height), # 7 - # Inner quad - Point(self._width, self._depth, -self._height), # 8 - Point(-self._width, self._depth, -self._height), # 9 - Point(-self._width, -self._depth, -self._height), # 10 - Point(self._width, -self._depth, -self._height), # 11 - # Top quad - Point(self._width, self._depth, 0), # 12 - Point(-self._width, self._depth, 0), # 13 - Point(-self._width, -self._depth, 0), # 14 - Point(self._width, -self._depth, 0), # 15 - ] - - # Check if two floor plate has two beams else plate cannot be connected to column head. - for i in range(4): - if rules[i * 2 + 1]: - if not rules[i * 2] or not rules[(i * 2 + 2) % 8]: - rules[i * 2 + 1] = False - - faces = [ - [8, 9, 10, 11], - [12, 13, 14, 15], - ] - - mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) - - if rules[0]: - mesh.add_face([0, 1, 9, 8]) - mesh.add_face([0, 1, 13, 12], attr_dict={"direction": CardinalDirections.NORTH}) - - if rules[1]: - mesh.add_face([1, 2, 9]) - mesh.add_face([1, 2, 13], attr_dict={"direction": CardinalDirections.NORTH_WEST}) - - if rules[2]: - mesh.add_face([2, 3, 10, 9]) - mesh.add_face([2, 3, 14, 13], attr_dict={"direction": CardinalDirections.WEST}) - - if rules[3]: - mesh.add_face([3, 4, 10]) - mesh.add_face([3, 4, 14], attr_dict={"direction": CardinalDirections.SOUTH_WEST}) - - if rules[4]: - mesh.add_face([4, 5, 11, 10]) - mesh.add_face([4, 5, 15, 14], attr_dict={"direction": CardinalDirections.SOUTH}) - - if rules[5]: - mesh.add_face([5, 6, 11]) - mesh.add_face([5, 6, 15], attr_dict={"direction": CardinalDirections.SOUTH_EAST}) - - if rules[6]: - mesh.add_face([6, 7, 8, 11]) - mesh.add_face([6, 7, 12, 15], attr_dict={"direction": CardinalDirections.EAST}) - - if rules[7]: - mesh.add_face([7, 0, 8]) - mesh.add_face([7, 0, 12], attr_dict={"direction": CardinalDirections.NORTH_EAST}) - - # Outer ring vertical triangle faces - from math import ceil - - for i in range(8): - if rules[i]: - continue - - if rules[(i - 1) % 8]: - v0 = (i) % 8 - inner_v = int(ceil(((i + 0) % 8) * 0.5)) % 4 + 8 - v1 = inner_v - v2 = inner_v + 4 - mesh.add_face([v0, v1, v2]) - - if rules[(i + 1) % 8]: - v0 = (i + 1) % 8 - inner_v = int(ceil(((i + 1) % 8) * 0.5)) % 4 + 8 - v1 = inner_v - v2 = inner_v + 4 - mesh.add_face([v0, v1, v2]) - - # Inner quad vertical triangle faces - for i in range(4): - if not rules[i * 2]: - v0 = i + 8 - v1 = (i + 1) % 4 + 8 - v2 = v1 + 4 - v3 = v0 + 4 - mesh.add_face([v0, v1, v2, v3]) - - mesh.remove_unused_vertices() - return mesh - - @property - def mesh(self): - return self._last_mesh - - -class ColumnHeadCrossElement(ColumnHeadElement): - """Create a cross column head element. Column head is inspired from the Crea project. - Column head has no access to frame, because the geometry is create depending on the given directions. - Therefore the frame is always considered as the world origin. - - Subtraction of the directions provides what type of mesh is generated: - - HALF: 1 face - - QUARTER: 2 faces - - THREE_QUARTERS: 3 faces - - FULL: 4 faces - - Parameters - ---------- - v : dict[int, Point] - The points, first one is always the origin. - e : list[tuple[int, int]] - Edges start from v0 between points v0-v1, v0-v2 and so on. - f : list[list[int]] - Faces between points v0-v1-v2-v3 and so on. If face vertices form already given edges, a triangle mesh face is formed. - width : float - The width of the column head. - depth : float - The depth of the column head. - height : float - The height of the column head. - offset : float - The offset of the column head. - - Returns - ------- - :class:`ColumnHeadCrossElement` - Column head instance - - Attributes - ---------- - shape : :class:`compas.datastructures.Mesh` - The base shape of the block. - - Example - ------- - - width: float = 150 - depth: float = 150 - height: float = 300 - offset: float = 210 - v: dict[int, Point] = { - 7: Point(0, 0, 0), - 5: Point(-1, 0, 0), - 6: Point(0, 1, 0), - 8: Point(0, -1, 0), - 2: Point(1, 0, 0), - } - - e: list[tuple[int, int]] = [ - (7, 5), - (7, 6), - (7, 8), - (7, 2), - ] - - f: list[list[int]] = [[5, 7, 6, 10]] - column_head_cross = ColumnHeadCrossElement(v=v, e=e, f=f, width=width, depth=depth, height=height, offset=offset) - """ - - @property - def __data__(self) -> dict: - return { - "v": self.v, - "e": self.e, - "f": self.f, - "width": self.width, - "depth": self.depth, - "height": self.height, - "offset": self.offset, - "is_support": self.is_support, - "transformation": self.transformation, - "name": self.name, - } - - def __init__( - self, - v: dict[int, Point] = { - 7: Point(0, 0, 0), - 5: Point(-1, 0, 0), - 6: Point(0, 1, 0), - 8: Point(0, -1, 0), - 2: Point(1, 0, 0), - }, - e: list[tuple[int, int]] = [ - (7, 5), - (7, 6), - (7, 8), - (7, 2), - ], - f: list[list[int]] = [[5, 7, 6, 10]], - width=150, - depth=150, - height=300, - offset=210, - is_support: bool = False, - transformation: Optional[Transformation] = None, - name: Optional[str] = None, - ) -> "ColumnHeadCrossElement": - super().__init__(transformation=transformation, name=name) - self.is_support = is_support - self.v = v - self.e = e - self.f = f - self.width = width - self.depth = depth - self.height = height - self.offset = offset - - @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 head. - - Returns - ------- - :class:`compas.datastructures.Mesh` - - """ - column_head_cross_shape: CrossBlockShape = CrossBlockShape(self.v, self.e, self.f, self.width, self.depth, self.height, self.offset) - return column_head_cross_shape.mesh.copy() # Copy because the meshes are created only once. - - # ============================================================================= - # 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 compute_contact(self, target_element: Element, type: str = "") -> ContactInterface: - """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.ContactInterface` - 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_column(self, target_element: "ColumnElement", type: str) -> "ContactInterface": - # Scenario: - # Iterate Columns edges model.cell_network.edges_where({"is_column": True}) - # Check if edge vertex is in self.column_head_to_vertex - # If it does, model.add_contact(...) - - # From the most distance axis point find the nearest column_head frame: - p: Point = Point(0, 0, 0).transformed(self.modeltransformation) - axis: Point = target_element.axis.transformed(target_element.modeltransformation) - column_head_is_closer_to_base: bool = axis.start.distance_to_point(p) > axis.end.distance_to_point(p) - - polygon: Polygon = self.modelgeometry.face_polygon(0) # ColumnHead is on the bottom - frame0: Frame = Frame(polygon.centroid, polygon[1] - polygon[0], (polygon[2] - polygon[1]) * -1) - polygon: Polygon = self.modelgeometry.face_polygon(1) # ColumnHead is on the top - frame1: Frame = Frame(polygon.centroid, polygon[1] - polygon[0], (polygon[2] - polygon[1]) * 1) - - contact_frame: Frame = frame0 if column_head_is_closer_to_base else frame1 - - return ContactInterface(points=[], frame=contact_frame) - - def _compute_contact_with_beam(self, target_element: "BeamElement", type: str) -> "ContactInterface": - # Scenario: - # Iterate Beams edges model.tcell_network.edges_where({"is_beam": True}) - # Check if the ColumnHead is on the left or right side of the beam- - # Based on orientation compute the Cardinal axis. - # The Cardinal axis allows to find the nearest column_head frame. - # Lastly add the contact. - - p: Point = Point(0, 0, 0).transformed(self.modeltransformation) - axis: Point = target_element.axis.transformed(target_element.modeltransformation) - column_head_is_closer_to_start: bool = axis.start.distance_to_point(p) < axis.end.distance_to_point(p) - - direction: Vector = axis[1] - axis[0] if column_head_is_closer_to_start else axis[0] - axis[1] - cardinal_direction: int = ColumnHeadCrossElement.closest_direction(direction) - polygon: Polygon = self.modelgeometry.face_polygon(list(self.modelgeometry.faces_where(conditions={"direction": cardinal_direction}))[0]) - contact_frame: Frame = Frame(polygon.centroid, polygon[1] - polygon[0], polygon[2] - polygon[1]) - - return ContactInterface(points=[], frame=contact_frame) - - def _compute_contact_with_plate(self, target_element: "PlateElement", type: str) -> "ContactInterface": - # Scenario: - # Find the closest point of the plate polygon. - # From this point take next and current point to define the CardinalDirection. - # From the CardinalDirection create the column_head frame. - - p: Point = Point(0, 0, 0).transformed(self.modeltransformation) - polygon: Polygon = target_element.polygon.transformed(target_element.modeltransformation) - - v0: int = -1 - distance: float = 0 - for i in range(len(polygon)): - d = p.distance_to_point(polygon[i]) - if d < distance or distance == 0: - distance = d - v0 = i - - v0_prev: int = (v0 + 1) % len(polygon) - v0_next: int = (v0 - 1) % len(polygon) - - 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]) - - return ContactInterface(points=[], frame=contact_frame) - - # ============================================================================= - # Constructors - # ============================================================================= - - def set_adjacency( - self, - v: list[Point], - e: list[tuple[int, int]], - f: list[list[int]], - ) -> "None": - """Rebuild the column head based on the cell netowrk adjacency. - - Parameters - ---------- - v : dict[int, Point] - The points, first one is always the origin. - e : list[tuple[int, int]] - Edges starts from v0 between points v0-v1, v0-v2 and so on. - f : list[list[int]] - Faces between points v0-v1-v2-v3 and so on. If face vertices forms already given edges. Triangle mesh face is formed. - - Returns - ------- - None - """ - - self.v = v - self.e = e - self.f = f - - @staticmethod - def closest_direction( - vector: Vector, - directions: dict[CardinalDirections, Vector] = { - CardinalDirections.NORTH: Vector(0, 1, 0), - CardinalDirections.EAST: Vector(1, 0, 0), - CardinalDirections.SOUTH: Vector(0, -1, 0), - CardinalDirections.WEST: Vector(-1, 0, 0), - }, - ) -> CardinalDirections: - """ - Find the closest cardinal direction for a given vector. - - Parameters - ------- - vector : Vector - The vector to compare. - - directions : dict - A dictionary of cardinal directions and their corresponding unit vectors. - - Returns - ------- - CardinalDirections - The closest cardinal direction. - """ - # Unitize the given vector - vector.unitize() - - # Compute dot products with cardinal direction vectors - dot_products: dict[CardinalDirections, float] = {} - for direction, unit_vector in directions.items(): - dot_product = vector.dot(unit_vector) - dot_products[direction] = dot_product - - # Find the direction with the maximum dot product - closest: CardinalDirections = max(dot_products, key=dot_products.get) - return closest - - @staticmethod - def get_direction_combination(direction1: "CardinalDirections", direction2: "CardinalDirections") -> "CardinalDirections": - """ - Get the direction combination of two directions. - - Parameters - ------- - direction1 : CardinalDirections - The first direction. - direction2 : CardinalDirections - The second direction. - - Returns - ------- - CardinalDirections - The direction combination. - """ - direction_combinations: dict[tuple[int, int], "CardinalDirections"] = { - (CardinalDirections.NORTH, CardinalDirections.WEST): CardinalDirections.NORTH_WEST, - (CardinalDirections.WEST, CardinalDirections.NORTH): CardinalDirections.NORTH_WEST, - (CardinalDirections.WEST, CardinalDirections.SOUTH): CardinalDirections.SOUTH_WEST, - (CardinalDirections.SOUTH, CardinalDirections.WEST): CardinalDirections.SOUTH_WEST, - (CardinalDirections.SOUTH, CardinalDirections.EAST): CardinalDirections.SOUTH_EAST, - (CardinalDirections.EAST, CardinalDirections.SOUTH): CardinalDirections.SOUTH_EAST, - (CardinalDirections.NORTH, CardinalDirections.EAST): CardinalDirections.NORTH_EAST, - (CardinalDirections.EAST, CardinalDirections.NORTH): CardinalDirections.NORTH_EAST, - } - return direction_combinations[(direction1, direction2)] diff --git a/src/compas_model/elements/element.py b/src/compas_model/elements/element.py index 266fc8b4..9a4baef3 100644 --- a/src/compas_model/elements/element.py +++ b/src/compas_model/elements/element.py @@ -12,9 +12,7 @@ from compas.geometry import Frame from compas.geometry import Shape from compas.geometry import Transformation -from compas_model.interactions import BooleanModifier -from compas_model.interactions import ContactInterface -from compas_model.interactions import Interaction +from compas_model.interactions.modifiers import Modifier from compas_model.materials import Material if TYPE_CHECKING: @@ -310,17 +308,16 @@ 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): + # Here check if contact, if collision, if modifier and so on... + if isinstance(interaction, Modifier): modelgeometry = interaction.apply(modelgeometry) - elif isinstance(interaction, BooleanModifier): - modelgeometry = interaction.apply(modelgeometry, elements[neighbor].modelgeometry) self.is_dirty = False @@ -359,8 +356,8 @@ def compute_collision_mesh(self) -> Mesh: """ raise NotImplementedError - def compute_contact(self, target_element: "Element", type: str = "") -> "Interaction": - """Computes the contact interaction of the geometry of the elements that is used in the model's add_contact method. + def add_modifier(self, target_element: "Element", type: str = "") -> "Modifier": + """Computes the modifier of the geometry of the elements that is used in the model's add_contact method. Returns ------- diff --git a/src/compas_model/elements/fasteners.py b/src/compas_model/elements/fasteners.py deleted file mode 100644 index 95eb826d..00000000 --- a/src/compas_model/elements/fasteners.py +++ /dev/null @@ -1,210 +0,0 @@ -from typing import Optional - -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 Point -from compas.geometry import Polygon -from compas.geometry import Transformation -from compas.geometry import Vector -from compas.geometry import bounding_box -from compas.geometry import intersection_line_plane -from compas.geometry import oriented_bounding_box -from compas.itertools import pairwise - -from .element import Element -from .element import Feature - - -class FastenersElement(Element): - """Class representing a fastener: screw, dowel, bolt and etc.""" - - -class FastenersFeature(Feature): - pass - - -class ScrewElement(Element): - """Class representing a screw, dowel, or pin. - - Parameters - ---------- - radius : float - Radius of the screw. - sides : int - Number of sides of the screw's polygonal section. - length : float - Length of the screw. - frame : :class:`compas.geometry.Frame` - Main frame of the screw. - transformation : Optional[:class:`compas.geometry.Transformation`] - Transformation applied to the screw. - features : Optional[list[:class:`compas_model.features.FastenersFeature`]] - Features of the screw. - name : Optional[str] - If no name is defined, the class name is given. - - Attributes - ---------- - axis : :class:`compas.geometry.Vector` - Line axis of the screw. - section : :class:`compas.geometry.Polygon` - Section polygon of the screw. - polygon_bottom : :class:`compas.geometry.Polygon` - The bottom polygon of the screw. - polygon_top : :class:`compas.geometry.Polygon` - The top polygon of the screw. - """ - - @property - def __data__(self) -> dict: - return { - "radius": self.radius, - "sides": self.sides, - "length": self.length, - "frame": self.frame, - "transformation": self.transformation, - "features": self._features, - "name": self.name, - } - - def __init__( - self, - radius: float = 0.4, - sides: int = 6, - length: float = 3.0, - frame: Frame = Frame.worldXY(), - transformation: Optional[Transformation] = None, - features: Optional[list[FastenersFeature]] = None, - name: Optional[str] = None, - ) -> "ScrewElement": - super().__init__(frame=frame, transformation=transformation, features=features, name=name) - - self.radius = radius - self.sides = sides - self.length = length - self.axis: Vector = Line([0, 0, 0], [0, 0, length]).vector - self.section: Polygon = Polygon.from_sides_and_radius_xy(sides, radius) - 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() - - 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(Frame(self.frame.point - self.axis * 0.5, self.frame.xaxis, self.frame.yaxis)) - plane1: Plane = Plane.from_frame(Frame(self.frame.point + self.axis * 0.5, self.frame.xaxis, self.frame.yaxis)) - 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) - 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. - - 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. - - 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/plate.py b/src/compas_model/elements/plate.py deleted file mode 100644 index 0c0e1140..00000000 --- a/src/compas_model/elements/plate.py +++ /dev/null @@ -1,139 +0,0 @@ -from typing import Optional - -import numpy as np -from numpy.typing import NDArray - -from compas.datastructures import Mesh -from compas.geometry import Box -from compas.geometry import Frame -from compas.geometry import Point -from compas.geometry import Polygon -from compas.geometry import Transformation -from compas.geometry import Vector -from compas.geometry import bounding_box -from compas.geometry import oriented_bounding_box -from compas.itertools import pairwise - -from .element import Element -from .element import Feature - - -class PlateFeature(Feature): - pass - - -class PlateElement(Element): - """Class representing a block element. - - Parameters - ---------- - polygon : :class:`compas.geometry.Polygon` - The base polygon of the plate. - thickness : float - The total offset thickness above and blow the polygon - frame : :class:`compas.geometry.Frame`, optional - The coordinate frame of the block. - name : str, optional - The name of the element. - shape : :class:`compas.datastructures.Mesh`, optional - The base shape of the element. - - Attributes - ---------- - shape : :class:`compas.datastructure.Mesh` - The base shape of the block. - is_support : bool - Flag indicating that the block is a support. - - """ - - @property - def __data__(self) -> dict: - return { - "polygon": self.polygon, - "thickness": self.thickness, - "is_support": self.is_support, - "frame": self.frame, - "transformation": self.transformation, - "features": self._features, - "name": self.name, - } - - def __init__( - self, - polygon: Polygon = Polygon.from_sides_and_radius_xy(4, 1.0), - thickness: float = 0.1, - is_support: bool = False, - frame: Frame = Frame.worldXY(), - transformation: Optional[Transformation] = None, - features: Optional[list[PlateFeature]] = None, - name: Optional[str] = None, - ) -> "PlateElement": - super().__init__(frame=frame, transformation=transformation, features=features, name=name) - self.is_support: bool = is_support - self.polygon: Polygon = polygon - self.thickness: float = thickness - normal: Vector = polygon.normal - down: Vector = normal * (0.0 * thickness) - up: Vector = normal * (-1.0 * thickness) - self.bottom: Polygon = polygon.copy() - for point in self.bottom.points: - point += down - self.top: Polygon = polygon.copy() - for point in self.top.points: - point += up - - @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 plate from the given polygons. - This shape is relative to the frame of the element. - - Returns - ------- - :class:`compas.datastructures.Mesh` - - """ - offset: int = len(self.bottom) - vertices: list[Point] = self.bottom.points + self.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: - points: list[Point] = self.geometry.vertices_attributes("xyz") - 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: - points: list[Point] = self.geometry.vertices_attributes("xyz") - 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: - from compas.geometry import convex_hull_numpy - - points: list[Point] = self.geometry.vertices_attributes("xyz") - faces: NDArray[np.intc] = convex_hull_numpy(points) - vertices: list[Point] = [points[index] for index in range(len(points))] - return Mesh.from_vertices_and_faces(vertices, faces) - - # ============================================================================= - # Constructors - # ============================================================================= diff --git a/src/compas_model/interactions/__init__.py b/src/compas_model/interactions/__init__.py index 6e5c7f39..5d6437bc 100644 --- a/src/compas_model/interactions/__init__.py +++ b/src/compas_model/interactions/__init__.py @@ -1,11 +1,5 @@ from .interaction import Interaction -from .contact import ( - ContactInterface, -) -from .boolean_modifier import BooleanModifier __all__ = [ "Interaction", - "ContactInterface", - "BooleanModifier", ] diff --git a/src/compas_model/interactions/boolean_modifier.py b/src/compas_model/interactions/boolean_modifier.py deleted file mode 100644 index 5f9310ad..00000000 --- a/src/compas_model/interactions/boolean_modifier.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Union - -from compas.datastructures import Mesh -from compas.geometry import Brep - -from .interaction import Interaction - - -class BooleanModifier(Interaction): - """Perform boolean difference on the target element. - - Parameters - ---------- - name : str, optional - The name of the interaction. - - """ - - @property - def __data__(self): - # type: () -> dict - return {"name": self.name} - - def __init__(self, name=None): - # type: (str | None) -> None - super(BooleanModifier, self).__init__(name=name) - - def __repr__(self): - return '{}(name="{}")'.format(self.__class__.__name__, self.name) - - def apply(self, targetgeometry: Union[Brep, Mesh], sourcegeometry: Union[Brep, Mesh]): - """Apply the interaction to the affected geometry. - - Parameters - ---------- - targetgeometry : :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` - The geometry to be affected iteratively. The same geometry can be modified multiple times. - sourcegeometry : :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` - The geometry to be used as the modifier. - """ - # Local import is needed otherwise, remove contact interactions in algorithms module. - from compas_model.algorithms.modifiers import boolean_difference - - return boolean_difference(targetgeometry, sourcegeometry) diff --git a/src/compas_model/interactions/collisions/__init__.py b/src/compas_model/interactions/collisions/__init__.py new file mode 100644 index 00000000..952e190d --- /dev/null +++ b/src/compas_model/interactions/collisions/__init__.py @@ -0,0 +1,7 @@ +from .contact import ( + ContactInterface, +) + +__all__ = [ + "ContactInterface", +] diff --git a/src/compas_model/interactions/contact.py b/src/compas_model/interactions/collisions/contact.py similarity index 93% rename from src/compas_model/interactions/contact.py rename to src/compas_model/interactions/collisions/contact.py index 5e6a200b..42430536 100644 --- a/src/compas_model/interactions/contact.py +++ b/src/compas_model/interactions/collisions/contact.py @@ -3,10 +3,8 @@ from typing import Union from compas.datastructures import Mesh -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 @@ -314,16 +312,3 @@ def resultantforce(self) -> list[Line]: p1 = position + forcevector p2 = position - forcevector return [Line(p1, p2)] - - def apply(self, targetgeometry: Union[Brep, Mesh]): - """Cut target geometry by the frame. - - Parameters - ---------- - targetgeometry : :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` - The geometry to be affected iteratively. The same geometry can be modified multiple times. - """ - # Local import is needed otherwise, remove contact interactions in algorithms module. - from compas_model.algorithms.modifiers import slice - - return slice(targetgeometry, Plane.from_frame(self.frame)) diff --git a/src/compas_model/interactions/contacts/__init__.py b/src/compas_model/interactions/contacts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/compas_model/interactions/modifiers/__init__.py b/src/compas_model/interactions/modifiers/__init__.py new file mode 100644 index 00000000..c529a1b2 --- /dev/null +++ b/src/compas_model/interactions/modifiers/__init__.py @@ -0,0 +1,10 @@ +from .modifier import Modifier +from .slicer_modifier import SlicerModifier +from .boolean_modifier import BooleanModifier + + +__all__ = [ + "Modifier", + "BooleanModifier", + "SlicerModifier", +] diff --git a/src/compas_model/interactions/modifiers/boolean_modifier.py b/src/compas_model/interactions/modifiers/boolean_modifier.py new file mode 100644 index 00000000..3c73da9b --- /dev/null +++ b/src/compas_model/interactions/modifiers/boolean_modifier.py @@ -0,0 +1,64 @@ +from typing import Union + +from compas.datastructures import Mesh +from compas.geometry import Brep + +from .modifier import Modifier + + +class BooleanModifier(Modifier): + """Perform boolean difference on the target element. + + 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. + + """ + + @property + def __data__(self): + # type: () -> dict + return {"name": self.name, "cutter": self.cutter} + + 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, target_geometry: Union[Brep, Mesh]): + """Apply the interaction to the affected geometry. + NOTE: If the result is not a valid geometry, the original geometry is returned. + + Parameters + ---------- + targetgeometry : :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` + The geometry to be affected iteratively. The same geometry can be modified multiple times. + sourcegeometry : :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` + The geometry to be used as the modifier. + """ + # Local import is needed otherwise, remove contact interactions in algorithms module. + if isinstance(target_geometry, Brep) and isinstance(self.cutter, Brep): + try: + return Brep.from_boolean_difference(target_geometry, self.cutter) + 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 = self.cutter.copy() if not isinstance(self.cutter, Brep) else Mesh.from_polygons(self.cutter.to_polygons()) + print(mesh0) + print(mesh1) + A = mesh0.to_vertices_and_faces(triangulated=True) + B = mesh1.to_vertices_and_faces(triangulated=True) + + V, F = boolean_difference_mesh_mesh(A, B) + mesh: Mesh = Mesh.from_vertices_and_faces(V, F) if len(V) > 0 and len(F) > 0 else mesh0 + return mesh diff --git a/src/compas_model/interactions/modifiers/modifier.py b/src/compas_model/interactions/modifiers/modifier.py new file mode 100644 index 00000000..aae3f402 --- /dev/null +++ b/src/compas_model/interactions/modifiers/modifier.py @@ -0,0 +1,6 @@ +from compas_model.interactions import Interaction + + +class Modifier(Interaction): + def apply(self): + raise NotImplementedError diff --git a/src/compas_model/interactions/modifiers/slicer_modifier.py b/src/compas_model/interactions/modifiers/slicer_modifier.py new file mode 100644 index 00000000..4ada80b3 --- /dev/null +++ b/src/compas_model/interactions/modifiers/slicer_modifier.py @@ -0,0 +1,58 @@ +from typing import Optional +from typing import Union + +from compas.datastructures import Mesh +from compas.geometry import Brep +from compas.geometry import Plane + +from .modifier import Modifier + + +class SlicerModifier(Modifier): + """Perform boolean difference on the target element. + + 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. + + """ + + @property + def __data__(self): + # type: () -> dict + return {"name": self.name, "cutter": self.cutter} + + def __init__(self, slice_plane: Plane, name=None): + # type: (Union[Brep, Mesh], str | None) -> None + super(SlicerModifier, self).__init__(name=name) + self.slice_plane = slice_plane + + def __repr__(self): + return '{}(name="{}")'.format(self.__class__.__name__, self.name) + + def apply(self, targetgeometry: Union[Brep, Mesh]): + """Cut target geometry by the frame. + NOTE: If the result is not a valid geometry, the original geometry is returned. + + Parameters + ---------- + targetgeometry : :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` + The geometry to be affected iteratively. The same geometry can be modified multiple times. + """ + # Local import is needed otherwise, remove contact interactions in algorithms module. + + try: + if isinstance(targetgeometry, Brep): + targetgeometry.make_solid() + slice_plane_flipped = Plane(self.slice_plane.point, -self.slice_plane.normal) + targetgeometry.trim(slice_plane_flipped) + return targetgeometry + else: + split_meshes: Optional[list] = targetgeometry.slice(self.slice_plane) + return split_meshes[0] if split_meshes else targetgeometry + except Exception: + print("SlicerModifier is not successful.") + return targetgeometry diff --git a/src/compas_model/models/__init__.py b/src/compas_model/models/__init__.py index ca79a1c2..f652fff4 100644 --- a/src/compas_model/models/__init__.py +++ b/src/compas_model/models/__init__.py @@ -2,7 +2,6 @@ from .elementtree import ElementTree from .interactiongraph import InteractionGraph from .model import Model -from .blockmodel import BlockModel __all__ = [ @@ -10,5 +9,4 @@ "ElementTree", "InteractionGraph", "Model", - "BlockModel", ] diff --git a/src/compas_model/models/blockmodel.py b/src/compas_model/models/blockmodel.py deleted file mode 100644 index 709699d9..00000000 --- a/src/compas_model/models/blockmodel.py +++ /dev/null @@ -1,83 +0,0 @@ -from typing import Optional - -from compas.geometry import Brep -from compas.tolerance import Tolerance -from compas_model.algorithms.nnbrs import find_nearest_neighbours -from compas_model.elements import Element -from compas_model.interactions import ContactInterface - -from .model import Model - -try: - from compas_occ.brep import OCCBrepFace as BrepFace -except ImportError: - print("compas_occ not installed. Using compas.geometry.BrepFace instead.") - - -class BlockModel(Model): - def __init__(self, name: Optional[str] = None) -> None: - super().__init__(name) - - def compute_intersections(self): - pass - - def compute_overlaps(self, deflection=None, tolerance=1, max_distance=50, min_area=0): - pass - - def compute_interfaces(self, deflection=None, tolerance=1, max_distance=50, min_area=0, nmax=10): - deflection = deflection or Tolerance().lineardeflection - - node_index = {node: index for index, node in enumerate(self.graph.nodes())} - index_node = {index: node for index, node in enumerate(self.graph.nodes())} - - geometries: list[Brep] = [self.graph.node_element(node).modelgeometry for node in self.graph.nodes()] - - cloud = [geometry.centroid for geometry in geometries] - nnbrs = find_nearest_neighbours(cloud, nmax, dims=3) - - for u in self.graph.nodes(): - A: Element = self.graph.node_element(u) # type: ignore - - i = node_index[u] - nbrs = nnbrs[i][1] - - for j in nbrs: - v = index_node[j] - - if u == v: - continue - - if self.graph.has_edge((u, v), directed=False): - continue - - B: Element = self.graph.node_element(v) # type: ignore - - faces_A, faces_B = A.modelgeometry.overlap(B.modelgeometry, deflection=deflection, tolerance=tolerance) # type: ignore - faces_A: list[BrepFace] - faces_B: list[BrepFace] - - if faces_A and faces_B: - for face_A in faces_A: - brep_A = Brep.from_brepfaces([face_A]) - - if brep_A.area < min_area: - continue - - for face_B in faces_B: - brep_B = Brep.from_brepfaces([face_B]) - - if brep_B.area < min_area: - continue - - brep_C: Brep = Brep.from_boolean_intersection(brep_A, brep_B) - - if brep_C.area < min_area: - continue - - poly_C = brep_C.to_polygons()[0] - mesh_C = brep_C.to_tesselation()[0] - - interaction = ContactInterface(points=poly_C.points, mesh=mesh_C) - - # do something with the interactions - self.add_interaction(A, B, interaction=interaction) diff --git a/src/compas_model/models/model.py b/src/compas_model/models/model.py index 61c88c47..769e4762 100644 --- a/src/compas_model/models/model.py +++ b/src/compas_model/models/model.py @@ -383,8 +383,8 @@ def add_interaction(self, a: Element, b: Element, interaction: Optional[Interact return edge - def compute_contact(self, a: Element, b: Element, type: str = "") -> tuple[int, int]: - """Add a contact interaction between two elements. + def add_modifier(self, a: Element, b: Element, type: str = "") -> tuple[int, int]: + """Add a modifier between two elements. Parameters ---------- @@ -393,7 +393,7 @@ def compute_contact(self, a: Element, b: Element, type: str = "") -> tuple[int, Order matters: interaction is applied from node V0 to node V1. The first element create and instance of the interaction. type : str, optional - The type of contact interaction, if different contact are possible between the two elements. + The type of modifier, if different contact are possible between the two elements. Returns ------- @@ -410,17 +410,18 @@ def compute_contact(self, a: Element, b: Element, type: str = "") -> tuple[int, if not self.graph.has_node(node_a) or not self.graph.has_node(node_b): 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. + modifier: Interaction = a.add_modifier(b, type) + print(modifier) + + if modifier: + # TODO: first model must have a graph edge only then modifier must be added to the graph + # If graph edge does not exist create edge otherwise add edge to the existing graph edge attributes. edge = self._graph.add_edge(node_a, node_b) interactions = self.graph.edge_interactions(edge) or [] - interactions.append(interaction) + interactions.append(modifier) 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. diff --git a/tests/test_element.py b/tests/test_element.py index 8ee9997e..4edc0c11 100644 --- a/tests/test_element.py +++ b/tests/test_element.py @@ -1,5 +1,5 @@ from compas_model.models import Model -from compas_model.elements import PlateElement +from compas_model.elements import Element from compas_model.interactions import Interaction from compas.datastructures import Mesh from typing import Optional @@ -9,7 +9,7 @@ from compas.geometry import Transformation -class MyElement(PlateElement): +class MyElement(Element): """Class representing an element for testing.""" def __init__( diff --git a/tests/test_interactiongraph.py b/tests/test_interactiongraph.py index 2dc24ace..835e7fd6 100644 --- a/tests/test_interactiongraph.py +++ b/tests/test_interactiongraph.py @@ -1,16 +1,16 @@ from pytest import fixture from compas_model.models import InteractionGraph -from compas_model.elements import PlateElement +from compas_model.elements import Element from compas_model.interactions import Interaction @fixture def mock_graph(): graph = InteractionGraph() - n_0 = graph.add_node(element=PlateElement(name="e_0")) - n_1 = graph.add_node(element=PlateElement(name="e_1")) - n_2 = graph.add_node(element=PlateElement(name="e_2")) + n_0 = graph.add_node(element=Element(name="e_0")) + n_1 = graph.add_node(element=Element(name="e_1")) + n_2 = graph.add_node(element=Element(name="e_2")) i_0_1 = Interaction(name="i_0_1") i_1_2 = Interaction(name="i_1_2") graph.add_edge(n_0, n_1, interactions=[i_0_1]) diff --git a/tests/test_model.py b/tests/test_model.py index f4dd16f5..d9f22a7f 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -4,16 +4,16 @@ from compas.data import json_loads from compas_model.models import Model -from compas_model.elements import PlateElement +from compas_model.elements import Element from compas_model.interactions import Interaction @fixture def mock_model(): model = Model() - a = PlateElement(name="a") - b = PlateElement(name="b") - c = PlateElement(name="c") + a = Element(name="a") + b = Element(name="b") + c = Element(name="c") model.add_element(a) model.add_element(b, parent=a) model.add_element(c)