From 92803d27d57412cf70542dd1a294ba0cceffbd0b Mon Sep 17 00:00:00 2001 From: petras Date: Sun, 12 Jan 2025 12:24:54 +0100 Subject: [PATCH 01/17] ADD BlockModel.from_barrel_vault constructor. --- CHANGELOG.md | 2 + src/compas_model/models/blockmodel.py | 116 ++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3758611c..cfb10f51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas_model.elements.FastenersElement`. * Added `compas_model.elements.ScrewElement`. * Added `Element.is_dirty`. +* Added `compas_model.models.BlockModel.from_barrel_vault`. + ### Changed diff --git a/src/compas_model/models/blockmodel.py b/src/compas_model/models/blockmodel.py index 709699d9..64e91c44 100644 --- a/src/compas_model/models/blockmodel.py +++ b/src/compas_model/models/blockmodel.py @@ -1,6 +1,17 @@ +from math import radians from typing import Optional +from compas.datastructures import Mesh from compas.geometry import Brep +from compas.geometry import Frame +from compas.geometry import Point +from compas.geometry import Rotation +from compas.geometry import Transformation +from compas.geometry import add_vectors +from compas.geometry import angle_vectors +from compas.geometry import subtract_vectors +from compas.geometry import transform_points +from compas.geometry import translate_points from compas.tolerance import Tolerance from compas_model.algorithms.nnbrs import find_nearest_neighbours from compas_model.elements import Element @@ -81,3 +92,108 @@ def compute_interfaces(self, deflection=None, tolerance=1, max_distance=50, min_ # do something with the interactions self.add_interaction(A, B, interaction=interaction) + + @classmethod + def from_barrel_vault(span: float = 6.0, length: float = 6.0, thickness: float = 0.25, rise: float = 0.6, vou_span: int = 9, vou_length: int = 6) -> "BlockModel": + """ + Creates block elements from the barrel vault geometry. + + Parameters + ---------- + span : float + span of the vault + length : float + length of the vault perpendicular to the span + thickness : float + thickness of the vault + rise : float + rise of the vault from 0.0 to middle axis of the vault thickness + vou_span : int + number of voussoirs in the span direction + vou_length : int + number of voussoirs in the length direction + + Returns + ------- + list[:class:`compas.datastructures.Mesh`] + A list of meshes representing the geometry of the barrel vault. + """ + radius: float = rise / 2 + span**2 / (8 * rise) + top: list[float] = [0, 0, rise] + left: list[float] = [-span / 2, 0, 0] + center: list[float] = [0.0, 0.0, rise - radius] + vector: list[float] = subtract_vectors(left, center) + springing: float = angle_vectors(vector, [-1.0, 0.0, 0.0]) + sector: float = radians(180) - 2 * springing + angle: float = sector / vou_span + + a: list[float] = [0, 0, rise - (thickness / 2)] + d: list[float] = add_vectors(top, [0, 0, (thickness / 2)]) + + R: Rotation = Rotation.from_axis_and_angle([0, 1.0, 0], 0.5 * sector, center) + bottom: list[list[float]] = transform_points([a, d], R) + brick_pts: list[list[list[float]]] = [] + for i in range(vou_span + 1): + R_angle: Rotation = Rotation.from_axis_and_angle([0, 1.0, 0], -angle * i, center) + points: list[list[float]] = transform_points(bottom, R_angle) + brick_pts.append(points) + + depth: float = length / vou_length + grouped_data: list[list[float]] = [pair[0] + pair[1] for pair in zip(brick_pts, brick_pts[1:])] + + meshes: list[Mesh] = [] + for i in range(vou_length): + for l, group in enumerate(grouped_data): # noqa: E741 + if l % 2 == 0: + point_l: list[list[float]] = [group[0], group[1], group[2], group[3]] + point_list: list[list[float]] = [ + [group[0][0], group[0][1] + (depth * i), group[0][2]], + [group[1][0], group[1][1] + (depth * i), group[1][2]], + [group[2][0], group[2][1] + (depth * i), group[2][2]], + [group[3][0], group[3][1] + (depth * i), group[3][2]], + ] + p_t: list[list[float]] = translate_points(point_l, [0, depth * (i + 1), 0]) + vertices: list[list[float]] = point_list + p_t + faces: list[list[int]] = [[0, 1, 3, 2], [0, 4, 5, 1], [4, 6, 7, 5], [6, 2, 3, 7], [1, 5, 7, 3], [2, 6, 4, 0]] + mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + meshes.append(mesh) + else: + point_l: list[list[float]] = [group[0], group[1], group[2], group[3]] + points_base: list[list[float]] = translate_points(point_l, [0, depth / 2, 0]) + points_b_t: list[list[float]] = translate_points(points_base, [0, depth * i, 0]) + points_t: list[list[float]] = translate_points(points_base, [0, depth * (i + 1), 0]) + vertices: list[list[float]] = points_b_t + points_t + if i != vou_length - 1: + faces: list[list[int]] = [[0, 1, 3, 2], [0, 4, 5, 1], [4, 6, 7, 5], [6, 2, 3, 7], [1, 5, 7, 3], [2, 6, 4, 0]] + mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + meshes.append(mesh) + + for l, group in enumerate(grouped_data): # noqa: E741 + if l % 2 != 0: + point_l: list[list[float]] = [group[0], group[1], group[2], group[3]] + p_t: list[list[float]] = translate_points(point_l, [0, depth / 2, 0]) + vertices: list[list[float]] = point_l + p_t + faces: list[list[int]] = [[0, 1, 3, 2], [0, 4, 5, 1], [4, 6, 7, 5], [6, 2, 3, 7], [1, 5, 7, 3], [2, 6, 4, 0]] + mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + meshes.append(mesh) + + point_f: list[list[float]] = translate_points(point_l, [0, length, 0]) + p_f: list[list[float]] = translate_points(point_f, [0, -depth / 2, 0]) + vertices: list[list[float]] = p_f + point_f + faces: list[list[int]] = [[0, 1, 3, 2], [0, 4, 5, 1], [4, 6, 7, 5], [6, 2, 3, 7], [1, 5, 7, 3], [2, 6, 4, 0]] + mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + meshes.append(mesh) + + # Translate blocks to xy frame and create blockmodel. + blockmodel: BlockModel = BlockModel() + from compas_model.elements import BlockElement + + for mesh in meshes: + origin: Point = mesh.face_polygon(0).center + xform: Transformation = Transformation.from_frame_to_frame(Frame(origin, vertices[1] - vertices[0], vertices[2] - vertices[0]), Frame.worldXY()) + mesh_xy: Mesh = mesh.transformed(xform) + block: BlockElement = BlockElement(shape=mesh_xy) + block.transformation = xform + blockmodel.add_element(block) + + return blockmodel From 615004c71fb7a8688cb3195efb6a5eb4466be2de Mon Sep 17 00:00:00 2001 From: petras Date: Sun, 12 Jan 2025 13:27:13 +0100 Subject: [PATCH 02/17] ADD-FIX support attribute to barrel vault and fix the block tranformation. --- src/compas_model/models/blockmodel.py | 36 ++++++++++++++++----------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/compas_model/models/blockmodel.py b/src/compas_model/models/blockmodel.py index 64e91c44..e1e18cad 100644 --- a/src/compas_model/models/blockmodel.py +++ b/src/compas_model/models/blockmodel.py @@ -94,29 +94,29 @@ def compute_interfaces(self, deflection=None, tolerance=1, max_distance=50, min_ self.add_interaction(A, B, interaction=interaction) @classmethod - def from_barrel_vault(span: float = 6.0, length: float = 6.0, thickness: float = 0.25, rise: float = 0.6, vou_span: int = 9, vou_length: int = 6) -> "BlockModel": + def from_barrel_vault(cls, span: float = 6.0, length: float = 6.0, thickness: float = 0.25, rise: float = 0.6, vou_span: int = 9, vou_length: int = 6) -> "BlockModel": """ Creates block elements from the barrel vault geometry. Parameters ---------- span : float - span of the vault + span of the vault length : float - length of the vault perpendicular to the span + length of the vault perpendicular to the span thickness : float - thickness of the vault + thickness of the vault rise : float - rise of the vault from 0.0 to middle axis of the vault thickness + rise of the vault from 0.0 to middle axis of the vault thickness vou_span : int - number of voussoirs in the span direction + number of voussoirs in the span direction vou_length : int - number of voussoirs in the length direction + number of voussoirs in the length direction Returns ------- list[:class:`compas.datastructures.Mesh`] - A list of meshes representing the geometry of the barrel vault. + A list of meshes representing the geometry of the barrel vault. """ radius: float = rise / 2 + span**2 / (8 * rise) top: list[float] = [0, 0, rise] @@ -127,8 +127,8 @@ def from_barrel_vault(span: float = 6.0, length: float = 6.0, thickness: float = sector: float = radians(180) - 2 * springing angle: float = sector / vou_span - a: list[float] = [0, 0, rise - (thickness / 2)] - d: list[float] = add_vectors(top, [0, 0, (thickness / 2)]) + a: list[float] = [0, -length / 2, rise - (thickness / 2)] + d: list[float] = add_vectors(top, [0, -length / 2, (thickness / 2)]) R: Rotation = Rotation.from_axis_and_angle([0, 1.0, 0], 0.5 * sector, center) bottom: list[list[float]] = transform_points([a, d], R) @@ -144,6 +144,7 @@ def from_barrel_vault(span: float = 6.0, length: float = 6.0, thickness: float = meshes: list[Mesh] = [] for i in range(vou_length): for l, group in enumerate(grouped_data): # noqa: E741 + is_support: bool = l == 0 or l == (len(grouped_data) - 1) if l % 2 == 0: point_l: list[list[float]] = [group[0], group[1], group[2], group[3]] point_list: list[list[float]] = [ @@ -156,6 +157,7 @@ def from_barrel_vault(span: float = 6.0, length: float = 6.0, thickness: float = vertices: list[list[float]] = point_list + p_t faces: list[list[int]] = [[0, 1, 3, 2], [0, 4, 5, 1], [4, 6, 7, 5], [6, 2, 3, 7], [1, 5, 7, 3], [2, 6, 4, 0]] mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + mesh.attributes["is_support"] = is_support meshes.append(mesh) else: point_l: list[list[float]] = [group[0], group[1], group[2], group[3]] @@ -166,15 +168,18 @@ def from_barrel_vault(span: float = 6.0, length: float = 6.0, thickness: float = if i != vou_length - 1: faces: list[list[int]] = [[0, 1, 3, 2], [0, 4, 5, 1], [4, 6, 7, 5], [6, 2, 3, 7], [1, 5, 7, 3], [2, 6, 4, 0]] mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + mesh.attributes["is_support"] = is_support meshes.append(mesh) for l, group in enumerate(grouped_data): # noqa: E741 + is_support: bool = l == 0 or l == (len(grouped_data) - 1) if l % 2 != 0: point_l: list[list[float]] = [group[0], group[1], group[2], group[3]] p_t: list[list[float]] = translate_points(point_l, [0, depth / 2, 0]) vertices: list[list[float]] = point_l + p_t faces: list[list[int]] = [[0, 1, 3, 2], [0, 4, 5, 1], [4, 6, 7, 5], [6, 2, 3, 7], [1, 5, 7, 3], [2, 6, 4, 0]] mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + mesh.attributes["is_support"] = is_support meshes.append(mesh) point_f: list[list[float]] = translate_points(point_l, [0, length, 0]) @@ -182,6 +187,7 @@ def from_barrel_vault(span: float = 6.0, length: float = 6.0, thickness: float = vertices: list[list[float]] = p_f + point_f faces: list[list[int]] = [[0, 1, 3, 2], [0, 4, 5, 1], [4, 6, 7, 5], [6, 2, 3, 7], [1, 5, 7, 3], [2, 6, 4, 0]] mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + mesh.attributes["is_support"] = is_support meshes.append(mesh) # Translate blocks to xy frame and create blockmodel. @@ -189,11 +195,13 @@ def from_barrel_vault(span: float = 6.0, length: float = 6.0, thickness: float = from compas_model.elements import BlockElement for mesh in meshes: - origin: Point = mesh.face_polygon(0).center - xform: Transformation = Transformation.from_frame_to_frame(Frame(origin, vertices[1] - vertices[0], vertices[2] - vertices[0]), Frame.worldXY()) + origin: Point = mesh.face_polygon(5).frame.point + xform: Transformation = Transformation.from_frame_to_frame( + Frame(origin, mesh.vertex_point(0) - mesh.vertex_point(2), mesh.vertex_point(4) - mesh.vertex_point(2)), Frame.worldXY() + ) mesh_xy: Mesh = mesh.transformed(xform) - block: BlockElement = BlockElement(shape=mesh_xy) - block.transformation = xform + block: BlockElement = BlockElement(shape=mesh_xy, is_support=mesh_xy.attributes["is_support"]) + block.transformation = xform.inverse() blockmodel.add_element(block) return blockmodel From 6ec43c3370744ae547595936c6f06f4fef7463c9 Mon Sep 17 00:00:00 2001 From: petras Date: Sun, 12 Jan 2025 14:45:18 +0100 Subject: [PATCH 03/17] ADD BeamTProfileElement --- CHANGELOG.md | 1 + docs/examples/elements/beam.py | 19 ++ src/compas_model/elements/__init__.py | 2 + src/compas_model/elements/beam.py | 239 ++++++++++++++++++++++++++ 4 files changed, 261 insertions(+) create mode 100644 docs/examples/elements/beam.py diff --git a/CHANGELOG.md b/CHANGELOG.md index cfb10f51..b433e6ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas_model.elements.ScrewElement`. * Added `Element.is_dirty`. * Added `compas_model.models.BlockModel.from_barrel_vault`. +* Added `compas_model.elements.BeamTProfileElement`. ### Changed diff --git a/docs/examples/elements/beam.py b/docs/examples/elements/beam.py new file mode 100644 index 00000000..289cbe6c --- /dev/null +++ b/docs/examples/elements/beam.py @@ -0,0 +1,19 @@ +from compas_model.elements import BeamTProfileElement +from compas_viewer import Viewer +from compas_viewer.config import Config + +scale = 1 +beam : BeamTProfileElement = BeamTProfileElement(width=0.2*scale, height=0.3*scale, step_height_left=0.1*scale, step_height_right=0.1*scale, step_width_left=0.05*scale, step_width_right=0.05*scale, length=6*scale) + + +config = Config() + +config.camera.target = [0, 0.1*scale, 0] +config.camera.position = [0, -0.2*scale, 7*scale] +config.camera.near = 0.1*scale +config.camera.far = 10*scale +viewer = Viewer(config=config) +viewer.scene.add(beam.elementgeometry) + +viewer.show() + diff --git a/src/compas_model/elements/__init__.py b/src/compas_model/elements/__init__.py index d7d88f74..387626fb 100644 --- a/src/compas_model/elements/__init__.py +++ b/src/compas_model/elements/__init__.py @@ -11,6 +11,7 @@ from .beam import BeamElement from .beam import BeamIProfileElement from .beam import BeamSquareElement +from .beam import BeamTProfileElement from .column import ColumnFeature from .column import ColumnElement from .column import ColumnRoundElement @@ -34,6 +35,7 @@ BeamElement, BeamIProfileElement, BeamSquareElement, + BeamTProfileElement, ColumnFeature, ColumnElement, ColumnRoundElement, diff --git a/src/compas_model/elements/beam.py b/src/compas_model/elements/beam.py index 30e3df73..54d55688 100644 --- a/src/compas_model/elements/beam.py +++ b/src/compas_model/elements/beam.py @@ -10,6 +10,7 @@ from compas.geometry import Transformation from compas.geometry import bounding_box from compas.geometry import intersection_line_plane +from compas.geometry import mirror_points_line from compas.geometry import oriented_bounding_box from compas.itertools import pairwise @@ -405,6 +406,244 @@ def compute_elementgeometry(self) -> 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 BeamTProfileElement(BeamElement): + """Class representing a beam element with I profile. + + Parameters + ---------- + width : float, optional + The width of the beam. + height : float, optional + The height of the beam. + step_width_left : float, optional + The step width on the left side of the beam. + step_width_right : float, optional + The step width on the right side of the beam, if None then the left side step width is used. + step_height_left : float, optional + The step height on the left side of the beam. + step_height_right : float, optional + The step height on the right side of the beam, if None then the left side step height is used. + 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, + "height": self.height, + "step_width_left": self.step_width_left, + "step_width_right": self.step_width_right, + "step_height_left": self.step_height_left, + "step_height_right": self.step_height_right, + "length": self.length, + "inverted": self.inverted, + "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, + height: float = 0.2, + step_width_left: float = 0.02, + step_width_right: Optional[float] = None, + step_height_left: float = 0.02, + step_height_right: Optional[float] = None, + length: float = 3.0, + inverted: bool = False, + 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.height: float = height + self.step_width_left: float = step_width_left + self.step_width_right: float = step_width_right if step_width_right is not None else step_width_left + self.step_height_left: float = step_height_left + self.step_height_right: float = step_height_right if step_height_right is not None else step_height_left + self.inverted: bool = inverted + self._length: float = length + + self.points: list[float] = [ + [self.width * 0.5, -self.height * 0.5, 0], + [-self.width * 0.5, -self.height * 0.5, 0], + [-self.width * 0.5, -self.height * 0.5 + self.step_height_left, 0], + [-self.width * 0.5 + self.step_width_left, -self.height * 0.5 + self.step_height_left, 0], + [-self.width * 0.5 + self.step_width_left, self.height * 0.5, 0], + [self.width * 0.5 - self.step_width_right, self.height * 0.5, 0], + [self.width * 0.5 - self.step_width_right, -self.height * 0.5 + self.step_height_right, 0], + [self.width * 0.5, -self.height * 0.5 + self.step_height_right, 0], + ] + + if inverted: + mirror_line: Line = Line([0, 0, 0], [1, 0, 0]) + self.points = mirror_points_line(self.points, mirror_line) + + # Create the polygon of the T 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. From 114a191ecc777fd1a4bcc6ecc3e4639f07ea7559 Mon Sep 17 00:00:00 2001 From: petras Date: Sun, 12 Jan 2025 14:51:43 +0100 Subject: [PATCH 04/17] ADD frame geometry. --- data/frame.json | 1 + docs/examples/masonry/000_frame.py | 58 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 data/frame.json create mode 100644 docs/examples/masonry/000_frame.py diff --git a/data/frame.json b/data/frame.json new file mode 100644 index 00000000..7c1bfb1f --- /dev/null +++ b/data/frame.json @@ -0,0 +1 @@ +{"Model::Line::Segments": [{"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 0.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "6832eeb6-f42e-423e-a683-728be44cf225"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 0.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "062d37dd-88d6-4b5d-9475-e738062653d1"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 0.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "0a250ea7-1617-4bf9-9cc4-a9de38ccb256"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 0.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "46f06cec-a3fd-42c0-9296-b6d219458a95"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 3800.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "b7af0f78-ad70-4f5c-9bdc-e4758119eeca"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 3800.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "bb1adec5-9f7f-4e5a-b535-4ccb2abadad0"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 3800.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "9a05995b-dd85-4608-8082-5b08f6cab6aa"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 3800.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "eb9a3bde-3b34-4c29-8b23-667faa171312"}], "Model::Mesh::Floor": [{"dtype": "compas.datastructures/Mesh", "data": {"attributes": {}, "default_vertex_attributes": {"x": 0.0, "y": 0.0, "z": 0.0}, "default_edge_attributes": {}, "default_face_attributes": {}, "vertex": {"0": {"x": -3000.0, "y": -3000.0, "z": 3800.0}, "1": {"x": -3000.0, "y": 3000.0, "z": 3800.0}, "2": {"x": 3000.0, "y": 3000.0, "z": 3800.0}, "3": {"x": 3000.0, "y": -3000.0, "z": 3800.0}}, "face": {"0": [0, 1, 2, 3]}, "facedata": {"0": {}}, "edgedata": {}, "max_vertex": 3, "max_face": 0}, "guid": "4afb38fd-6da8-45b0-9784-3f7d9bd862cc"}]} \ No newline at end of file diff --git a/docs/examples/masonry/000_frame.py b/docs/examples/masonry/000_frame.py new file mode 100644 index 00000000..b5fc4452 --- /dev/null +++ b/docs/examples/masonry/000_frame.py @@ -0,0 +1,58 @@ +from pathlib import Path + +from compas import json_dump +from compas.datastructures import Mesh +from compas.geometry import Line +from compas.geometry import Point +from compas_viewer import Viewer +from compas_viewer.config import Config + +# ============================================================================= +# Create Geometry +# ============================================================================= + +points = [ + Point(-3000, -3000, 0), + Point(-3000, 3000, 0), + Point(3000, 3000, 0), + Point(3000, -3000, 0), + Point(-3000, -3000, 3800), + Point(-3000, 3000, 3800), + Point(3000, 3000, 3800), + Point(3000, -3000, 3800), +] + +lines = [ + Line(points[0], points[0 + 4]), + Line(points[1], points[1 + 4]), + Line(points[2], points[2 + 4]), + Line(points[3], points[3 + 4]), + Line(points[4], points[5]), + Line(points[5], points[6]), + Line(points[6], points[7]), + Line(points[7], points[4]), +] + +mesh = Mesh.from_vertices_and_faces(points[4:], [[0, 1, 2, 3]]) + + +# ============================================================================= +# Serialize the Frame into a JSON file. +# ============================================================================= + +model_input: dict[str, list[any]] = {"Model::Line::Segments": lines, "Model::Mesh::Floor": [mesh]} +json_dump(model_input, Path("data/frame.json")) + +# ============================================================================= +# Vizualize +# ============================================================================= + +config = Config() +config.camera.target = [0, 0, 100] +config.camera.position = [10000, -10000, 10000] +config.camera.near = 10 +config.camera.far = 100000 +viewer = Viewer(config=config) +viewer.scene.add(lines) +viewer.scene.add(mesh) +viewer.show() \ No newline at end of file From 6820e0cdc8182540126ba2fe8f13946df56676e7 Mon Sep 17 00:00:00 2001 From: petras Date: Sun, 12 Jan 2025 16:10:13 +0100 Subject: [PATCH 05/17] WIP frame model with two T Beams and Barrel Vault. --- data/frame.json | 2 +- docs/examples/masonry/000_frame.py | 4 +- docs/examples/masonry/001_frame_model.py | 72 ++++++++++++++++++++++++ src/compas_model/elements/beam.py | 14 ++++- src/compas_model/models/blockmodel.py | 31 +++++++--- 5 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 docs/examples/masonry/001_frame_model.py diff --git a/data/frame.json b/data/frame.json index 7c1bfb1f..bc5b72ec 100644 --- a/data/frame.json +++ b/data/frame.json @@ -1 +1 @@ -{"Model::Line::Segments": [{"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 0.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "6832eeb6-f42e-423e-a683-728be44cf225"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 0.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "062d37dd-88d6-4b5d-9475-e738062653d1"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 0.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "0a250ea7-1617-4bf9-9cc4-a9de38ccb256"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 0.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "46f06cec-a3fd-42c0-9296-b6d219458a95"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 3800.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "b7af0f78-ad70-4f5c-9bdc-e4758119eeca"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 3800.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "bb1adec5-9f7f-4e5a-b535-4ccb2abadad0"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 3800.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "9a05995b-dd85-4608-8082-5b08f6cab6aa"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 3800.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "eb9a3bde-3b34-4c29-8b23-667faa171312"}], "Model::Mesh::Floor": [{"dtype": "compas.datastructures/Mesh", "data": {"attributes": {}, "default_vertex_attributes": {"x": 0.0, "y": 0.0, "z": 0.0}, "default_edge_attributes": {}, "default_face_attributes": {}, "vertex": {"0": {"x": -3000.0, "y": -3000.0, "z": 3800.0}, "1": {"x": -3000.0, "y": 3000.0, "z": 3800.0}, "2": {"x": 3000.0, "y": 3000.0, "z": 3800.0}, "3": {"x": 3000.0, "y": -3000.0, "z": 3800.0}}, "face": {"0": [0, 1, 2, 3]}, "facedata": {"0": {}}, "edgedata": {}, "max_vertex": 3, "max_face": 0}, "guid": "4afb38fd-6da8-45b0-9784-3f7d9bd862cc"}]} \ No newline at end of file +{"Model::Line::Segments": [{"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 0.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "662c2a4e-26f2-4e57-859c-e2c7e227d01d"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 0.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "e1dce19f-97af-4b97-98a3-a8ba3daa5bf2"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 0.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "2cb1d076-3f2c-4d42-a786-333518fe2452"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 0.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "30f2fbef-4f84-4814-b460-9849401d489a"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 3800.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "6c298089-9333-4e46-8d8f-625719548a20"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 3800.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "52e6cc9b-3fa6-4b19-ba01-df2d5aec5aeb"}], "Model::Mesh::Floor": [{"dtype": "compas.datastructures/Mesh", "data": {"attributes": {}, "default_vertex_attributes": {"x": 0.0, "y": 0.0, "z": 0.0}, "default_edge_attributes": {}, "default_face_attributes": {}, "vertex": {"0": {"x": -3000.0, "y": -3000.0, "z": 3800.0}, "1": {"x": -3000.0, "y": 3000.0, "z": 3800.0}, "2": {"x": 3000.0, "y": 3000.0, "z": 3800.0}, "3": {"x": 3000.0, "y": -3000.0, "z": 3800.0}}, "face": {"0": [0, 1, 2, 3]}, "facedata": {"0": {}}, "edgedata": {}, "max_vertex": 3, "max_face": 0}, "guid": "d0e115e1-7955-4c94-a5af-30e33ffa9f67"}]} \ No newline at end of file diff --git a/docs/examples/masonry/000_frame.py b/docs/examples/masonry/000_frame.py index b5fc4452..2ae02ae0 100644 --- a/docs/examples/masonry/000_frame.py +++ b/docs/examples/masonry/000_frame.py @@ -28,9 +28,9 @@ Line(points[2], points[2 + 4]), Line(points[3], points[3 + 4]), Line(points[4], points[5]), - Line(points[5], points[6]), + # Line(points[5], points[6]), Line(points[6], points[7]), - Line(points[7], points[4]), + # Line(points[7], points[4]), ] mesh = Mesh.from_vertices_and_faces(points[4:], [[0, 1, 2, 3]]) diff --git a/docs/examples/masonry/001_frame_model.py b/docs/examples/masonry/001_frame_model.py new file mode 100644 index 00000000..717fa1ce --- /dev/null +++ b/docs/examples/masonry/001_frame_model.py @@ -0,0 +1,72 @@ +from pathlib import Path + +from compas import json_load +from compas.geometry import Frame +from compas.datastructures import Mesh +from compas.geometry import Line +from compas.geometry import Vector +from compas_model.models import Model +from compas_model.models import BlockModel +from compas_model.elements import ColumnSquareElement +from compas_model.elements import BeamTProfileElement +from compas_model.elements import BlockElement +from compas.geometry.transformation import Transformation +from compas.geometry import Translation +from compas_viewer import Viewer +from compas_viewer.config import Config + +# ============================================================================= +# JSON file with the geometry of the model. +# ============================================================================= +rhino_geometry: dict[str, list[any]] = json_load(Path("data/frame.json")) +lines: list[Line] = rhino_geometry["Model::Line::Segments"] +surfaces: list[Mesh] = rhino_geometry["Model::Mesh::Floor"] + +# ============================================================================= +# Model +# ============================================================================= +model = Model() + +# Add columns +for i in range(0, 4): + line : Line = lines[i] + column: ColumnSquareElement = ColumnSquareElement(300,300, line.length) + target_frame : Frame = Frame(line.start) + xform : Transformation = Transformation.from_frame_to_frame(Frame.worldXY(), target_frame) + column.transformation = xform + model.add_element(column) + + + +# Add two beams +for i in range(4,len(lines)): + line : Line = lines[i] + beam: BeamTProfileElement = BeamTProfileElement(300,700,100,100,150,150, line.length) + target_frame : Frame = Frame(line.start, Vector.Zaxis().cross(line.vector), Vector.Zaxis()) + xform_offset : Transformation = Translation.from_vector([0, beam.height*0.5, 0]) + xform_to_beam : Transformation = Transformation.from_frame_to_frame(Frame.worldXY(), target_frame) + beam.transformation = xform_to_beam * xform_offset + beam.extend(150) + model.add_element(beam) + + +# Add blocks +blockmodel : BlockModel = BlockModel.from_barrel_vault(6000, 6000,250, 600, 5,5) +barrel_vault_elements : list[BlockElement] = list(blockmodel.elements()) +for block in barrel_vault_elements: + xform : Transformation = Transformation.from_frame_to_frame(Frame.worldXY(), Frame([0,0,lines[0].end[2]])) + block.transformation = xform * block.transformation + model.add_element(block) + +# ============================================================================= +# Vizualize +# ============================================================================= +config = Config() +config.camera.target = [0, 0, 100] +config.camera.position = [10000, -10000, 10000] +config.camera.near = 10 +config.camera.far = 100000 +viewer = Viewer(config=config) +for element in list(model.elements()): + viewer.scene.add(element.modelgeometry) +viewer.show() \ No newline at end of file diff --git a/src/compas_model/elements/beam.py b/src/compas_model/elements/beam.py index 54d55688..77b64f78 100644 --- a/src/compas_model/elements/beam.py +++ b/src/compas_model/elements/beam.py @@ -8,6 +8,7 @@ from compas.geometry import Point from compas.geometry import Polygon from compas.geometry import Transformation +from compas.geometry import Translation from compas.geometry import bounding_box from compas.geometry import intersection_line_plane from compas.geometry import mirror_points_line @@ -587,7 +588,6 @@ def length(self) -> float: 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]) @@ -699,6 +699,18 @@ def compute_collision_mesh(self) -> Mesh: vertices = [points[index] for index in vertices] # type: ignore return Mesh.from_vertices_and_faces(vertices, faces) + def extend(self, distance: float) -> None: + """Extend the beam. + + Parameters + ---------- + distance : float + The distance to extend the beam. + """ + self.length = self.length + distance * 2 + xform: Transformation = Translation.from_vector([0, 0, -distance]) + self.transformation = self.transformation * xform + # ============================================================================= # Constructors # ============================================================================= diff --git a/src/compas_model/models/blockmodel.py b/src/compas_model/models/blockmodel.py index e1e18cad..ee49a054 100644 --- a/src/compas_model/models/blockmodel.py +++ b/src/compas_model/models/blockmodel.py @@ -94,24 +94,35 @@ def compute_interfaces(self, deflection=None, tolerance=1, max_distance=50, min_ self.add_interaction(A, B, interaction=interaction) @classmethod - def from_barrel_vault(cls, span: float = 6.0, length: float = 6.0, thickness: float = 0.25, rise: float = 0.6, vou_span: int = 9, vou_length: int = 6) -> "BlockModel": + def from_barrel_vault( + cls, + span: float = 6.0, + length: float = 6.0, + thickness: float = 0.25, + rise: float = 0.6, + vou_span: int = 9, + vou_length: int = 6, + zero_is_centerline_or_lowestpoint: bool = False, + ) -> "BlockModel": """ Creates block elements from the barrel vault geometry. Parameters ---------- span : float - span of the vault + span of the vault length : float - length of the vault perpendicular to the span + length of the vault perpendicular to the span thickness : float - thickness of the vault + thickness of the vault rise : float - rise of the vault from 0.0 to middle axis of the vault thickness + rise of the vault from 0.0 to middle axis of the vault thickness vou_span : int - number of voussoirs in the span direction + number of voussoirs in the span direction vou_length : int - number of voussoirs in the length direction + number of voussoirs in the length direction + zero_is_centerline_or_lowestpoint : bool + if True, the lowest point of the vault is at the center line of the arch, otherwise the center line of the vault is lowest mesh z-coordinate. Returns ------- @@ -190,6 +201,12 @@ def from_barrel_vault(cls, span: float = 6.0, length: float = 6.0, thickness: fl mesh.attributes["is_support"] = is_support meshes.append(mesh) + # Find the lowest z-coordinate and move all the block to zero. + if not zero_is_centerline_or_lowestpoint: + min_z: float = min([min(mesh.vertex_coordinates(key)[2] for key in mesh.vertices()) for mesh in meshes]) + for mesh in meshes: + mesh.translate([0, 0, -min_z]) + # Translate blocks to xy frame and create blockmodel. blockmodel: BlockModel = BlockModel() from compas_model.elements import BlockElement From b49159bd45fb5e79bbccef9f6f26440f2bc99ef0 Mon Sep 17 00:00:00 2001 From: petras Date: Sun, 12 Jan 2025 21:45:34 +0100 Subject: [PATCH 06/17] ADD boolean modifier and tested with BeamTProfileElement class. --- docs/api/compas_model.elements.rst | 5 +- docs/examples/masonry/001_frame_model.py | 50 ++++--- src/compas_model/algorithms/modifiers.py | 10 +- src/compas_model/elements/beam.py | 123 ++++++++++++++---- src/compas_model/elements/block.py | 45 +++---- src/compas_model/elements/element.py | 6 +- .../interactions/boolean_modifier.py | 13 +- src/compas_model/models/model.py | 2 - 8 files changed, 164 insertions(+), 90 deletions(-) diff --git a/docs/api/compas_model.elements.rst b/docs/api/compas_model.elements.rst index a9b1859f..184a565b 100644 --- a/docs/api/compas_model.elements.rst +++ b/docs/api/compas_model.elements.rst @@ -11,8 +11,5 @@ Classes :toctree: generated/ :nosignatures: - BlockElement - BlockFeature - BlockGeometry Element - Feature + diff --git a/docs/examples/masonry/001_frame_model.py b/docs/examples/masonry/001_frame_model.py index 717fa1ce..5c11cd48 100644 --- a/docs/examples/masonry/001_frame_model.py +++ b/docs/examples/masonry/001_frame_model.py @@ -2,16 +2,14 @@ from compas import json_load from compas.geometry import Frame -from compas.datastructures import Mesh from compas.geometry import Line +from compas.geometry import Translation from compas.geometry import Vector -from compas_model.models import Model -from compas_model.models import BlockModel -from compas_model.elements import ColumnSquareElement -from compas_model.elements import BeamTProfileElement -from compas_model.elements import BlockElement from compas.geometry.transformation import Transformation -from compas.geometry import Translation +from compas_model.elements import BeamTProfileElement +from compas_model.elements import ColumnSquareElement +from compas_model.models import BlockModel +from compas_model.models import Model from compas_viewer import Viewer from compas_viewer.config import Config @@ -20,7 +18,6 @@ # ============================================================================= rhino_geometry: dict[str, list[any]] = json_load(Path("data/frame.json")) lines: list[Line] = rhino_geometry["Model::Line::Segments"] -surfaces: list[Mesh] = rhino_geometry["Model::Mesh::Floor"] # ============================================================================= # Model @@ -29,38 +26,37 @@ # Add columns for i in range(0, 4): - line : Line = lines[i] - column: ColumnSquareElement = ColumnSquareElement(300,300, line.length) - target_frame : Frame = Frame(line.start) - xform : Transformation = Transformation.from_frame_to_frame(Frame.worldXY(), target_frame) - column.transformation = xform + column: ColumnSquareElement = ColumnSquareElement(300,300, lines[i].length) + column.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), Frame(lines[i].start)) model.add_element(column) - # Add two beams for i in range(4,len(lines)): - line : Line = lines[i] - beam: BeamTProfileElement = BeamTProfileElement(300,700,100,100,150,150, line.length) - target_frame : Frame = Frame(line.start, Vector.Zaxis().cross(line.vector), Vector.Zaxis()) - xform_offset : Transformation = Translation.from_vector([0, beam.height*0.5, 0]) - xform_to_beam : Transformation = Transformation.from_frame_to_frame(Frame.worldXY(), target_frame) - beam.transformation = xform_to_beam * xform_offset + beam: BeamTProfileElement = BeamTProfileElement(width=300, height=700, step_width_left=75, step_height_left=150, length=lines[i].length) + target_frame : Frame = Frame(lines[i].start, Vector.Zaxis().cross(lines[i].vector), Vector.Zaxis()) + beam.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), target_frame) * Translation.from_vector([0, beam.height*0.5, 0]) beam.extend(150) model.add_element(beam) -# Add blocks -blockmodel : BlockModel = BlockModel.from_barrel_vault(6000, 6000,250, 600, 5,5) -barrel_vault_elements : list[BlockElement] = list(blockmodel.elements()) -for block in barrel_vault_elements: - xform : Transformation = Transformation.from_frame_to_frame(Frame.worldXY(), Frame([0,0,lines[0].end[2]])) - block.transformation = xform * block.transformation +# Add blocks, by moving them by the height of the first column. +blockmodel : BlockModel = BlockModel.from_barrel_vault(span=6000, length=6000, thickness=250, rise=600, vou_span=5, vou_length=5) +for block in blockmodel.elements(): + block.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), Frame([0,0,lines[0].end[2]])) * block.transformation model.add_element(block) + +# Add Interactions +for beam in list(model.elements()): + for block in blockmodel.elements(): + if isinstance(beam, BeamTProfileElement): + model.compute_contact(beam, block) # beam -> cuts -> block + # ============================================================================= # Vizualize # ============================================================================= + config = Config() config.camera.target = [0, 0, 100] config.camera.position = [10000, -10000, 10000] @@ -68,5 +64,5 @@ config.camera.far = 100000 viewer = Viewer(config=config) for element in list(model.elements()): - viewer.scene.add(element.modelgeometry) + viewer.scene.add(element.modelgeometry, hide_coplanaredges=True) viewer.show() \ No newline at end of file diff --git a/src/compas_model/algorithms/modifiers.py b/src/compas_model/algorithms/modifiers.py index 34c846c6..09048b84 100644 --- a/src/compas_model/algorithms/modifiers.py +++ b/src/compas_model/algorithms/modifiers.py @@ -46,14 +46,18 @@ def boolean_difference(target_geometry, source_geometry): :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` The geometry after boolean difference. """ + from compas_cgal.booleans import boolean_difference_mesh_mesh target_geometry_copy = target_geometry.copy() source_geometry_copy = source_geometry.copy() - target_geometry_copy.unify_cycles() - source_geometry_copy.unify_cycles() + + # unify_cycles method cycles often fail when they are cut several times. + # 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 + mesh: Mesh = Mesh.from_vertices_and_faces(V, F) if len(V) > 0 and len(F) > 0 else target_geometry_copy + return mesh diff --git a/src/compas_model/elements/beam.py b/src/compas_model/elements/beam.py index 77b64f78..b4a61b5d 100644 --- a/src/compas_model/elements/beam.py +++ b/src/compas_model/elements/beam.py @@ -1,4 +1,6 @@ +from typing import TYPE_CHECKING from typing import Optional +from typing import Union from compas.datastructures import Mesh from compas.geometry import Box @@ -14,9 +16,12 @@ from compas.geometry import mirror_points_line from compas.geometry import oriented_bounding_box from compas.itertools import pairwise +from compas_model.elements.element import Element +from compas_model.elements.element import Feature +from compas_model.interactions import BooleanModifier -from .element import Element -from .element import Feature +if TYPE_CHECKING: + from compas_model.elements import BlockElement class BeamFeature(Feature): @@ -390,16 +395,31 @@ def compute_elementgeometry(self) -> Mesh: Returns ------- :class:`compas.datastructures.Mesh` - """ + from compas.geometry import earclip_polygon + offset: int = len(self.polygon_bottom) vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore + + triangles: list[list[int]] = earclip_polygon(Polygon(self.polygon_bottom.points)) + top_faces: list[list[int]] = [] + bottom_faces: list[list[int]] = [] + for i in range(len(triangles)): + triangle_top: list[int] = [] + triangle_bottom: list[int] = [] + for j in range(3): + triangle_top.append(triangles[i][j] + offset) + triangle_bottom.append(triangles[i][j]) + triangle_bottom.reverse() + top_faces.append(triangle_top) + bottom_faces.append(triangle_bottom) + faces: list[list[int]] = bottom_faces + top_faces + bottom: list[int] = list(range(offset)) top: list[int] = [i + offset for i in bottom] - 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]) + faces.append([c, d, b, a]) mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) return mesh @@ -478,14 +498,16 @@ class BeamTProfileElement(BeamElement): The height of the beam. step_width_left : float, optional The step width on the left side of the beam. - step_width_right : float, optional - The step width on the right side of the beam, if None then the left side step width is used. step_height_left : float, optional The step height on the left side of the beam. - step_height_right : float, optional - The step height on the right side of the beam, if None then the left side step height is used. length : float, optional The length of the beam. + inverted : bool, optional + Flag indicating if the beam section is inverted as upside down letter T. + step_width_right : float, optional + The step width on the right side of the beam, if None then the left side step width is used. + step_height_right : float, optional + The step height on the right side of the beam, if None then the left side step height is used. frame_bottom : :class:`compas.geometry.Plane`, optional The frame of the bottom polygon. frame_top : :class:`compas.geometry.Plane`, optional @@ -515,11 +537,11 @@ def __data__(self) -> dict: "width": self.width, "height": self.height, "step_width_left": self.step_width_left, - "step_width_right": self.step_width_right, "step_height_left": self.step_height_left, - "step_height_right": self.step_height_right, "length": self.length, "inverted": self.inverted, + "step_height_right": self.step_height_right, + "step_width_right": self.step_width_right, "frame_top": self.frame_top, "is_support": self.is_support, "frame": self.frame, @@ -533,11 +555,11 @@ def __init__( width: float = 0.1, height: float = 0.2, step_width_left: float = 0.02, - step_width_right: Optional[float] = None, step_height_left: float = 0.02, - step_height_right: Optional[float] = None, length: float = 3.0, inverted: bool = False, + step_height_right: Optional[float] = None, + step_width_right: Optional[float] = None, frame_top: Optional[Plane] = None, is_support: bool = False, frame: Frame = Frame.worldXY(), @@ -549,14 +571,19 @@ def __init__( self.is_support: bool = is_support - self.width: float = width - self.height: float = height - self.step_width_left: float = step_width_left - self.step_width_right: float = step_width_right if step_width_right is not None else step_width_left - self.step_height_left: float = step_height_left - self.step_height_right: float = step_height_right if step_height_right is not None else step_height_left + self.width: float = abs(width) + self.height: float = abs(height) + self.step_width_left: float = abs(step_width_left) + self.step_width_right: float = abs(step_width_right) if step_width_right is not None else step_width_left + self.step_height_left: float = abs(step_height_left) + self.step_height_right: float = abs(step_height_right) if step_height_right is not None else step_height_left self.inverted: bool = inverted - self._length: float = length + self._length: float = abs(length) + + self.step_width_left = min(self.step_width_left, width*0.5*0.999) + self.step_width_right = min(self.step_width_right, width*0.5*0.999) + self.step_height_left = min(self.step_height_left, height) + self.step_height_right = min(self.step_height_right, height) self.points: list[float] = [ [self.width * 0.5, -self.height * 0.5, 0], @@ -627,16 +654,31 @@ def compute_elementgeometry(self) -> Mesh: Returns ------- :class:`compas.datastructures.Mesh` - """ + from compas.geometry import earclip_polygon + offset: int = len(self.polygon_bottom) vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore + + triangles: list[list[int]] = earclip_polygon(Polygon(self.polygon_bottom.points)) + top_faces: list[list[int]] = [] + bottom_faces: list[list[int]] = [] + for i in range(len(triangles)): + triangle_top: list[int] = [] + triangle_bottom: list[int] = [] + for j in range(3): + triangle_top.append(triangles[i][j] + offset) + triangle_bottom.append(triangles[i][j]) + triangle_bottom.reverse() + top_faces.append(triangle_top) + bottom_faces.append(triangle_bottom) + faces: list[list[int]] = bottom_faces + top_faces + bottom: list[int] = list(range(offset)) top: list[int] = [i + offset for i in bottom] - 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]) + faces.append([c, d, b, a]) mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) return mesh @@ -711,6 +753,41 @@ def extend(self, distance: float) -> None: xform: Transformation = Translation.from_vector([0, 0, -distance]) self.transformation = self.transformation * xform + def compute_contact(self, target_element: Element, type: str = "") -> BooleanModifier: + """Computes the contact interaction of the geometry of the elements that is used in the model's add_contact method. + + Returns + ------- + :class:`compas_model.interactions.BooleanModifier` + The ContactInteraction that is applied to the neighboring element. One pair can have one or multiple variants. + target_element : Element + The target element to compute the contact interaction. + type : str, optional + The type of contact interaction, if different contact are possible between the two elements. + + """ + # Traverse up to the class one before the Element class.add() + # Create a function name based on the target_element class name. + parent_class = target_element.__class__ + while parent_class.__bases__[0] != Element: + parent_class = parent_class.__bases__[0] + + parent_class_name = parent_class.__name__.lower().replace("element", "") + method_name = f"_compute_contact_with_{parent_class_name}" + method = getattr(self, method_name, None) + if method is None: + raise ValueError(f"Unsupported target element type: {type(target_element)}") + + return method(target_element, type) + + def _compute_contact_with_block(self, target_element: "BlockElement", type: str) -> Union["BooleanModifier", None]: + # Scenario: + # A beam with a profile applies boolean difference with a block geometry. + if target_element.is_support: + return BooleanModifier(self.elementgeometry.transformed(self.modeltransformation)) + else: + return None + # ============================================================================= # Constructors # ============================================================================= diff --git a/src/compas_model/elements/block.py b/src/compas_model/elements/block.py index 37b30d08..5c855871 100644 --- a/src/compas_model/elements/block.py +++ b/src/compas_model/elements/block.py @@ -172,28 +172,29 @@ def compute_elementgeometry(self) -> Union[Mesh, Brep]: # apply features? return geometry - def compute_modelgeometry(self) -> Union[Mesh, Brep]: - if not self.model: - raise Exception - - geometry = self.elementgeometry.transformed(self.modeltransformation) - - # apply effect of interactions? - node = self.graphnode - nbrs = self.model.graph.neighbors_in(node) - for nbr in nbrs: - element = self.model.graph.node_element(nbr) - if element: - for interaction in self.model.graph.edge_interactions((nbr, node)): - # example interactions are - # cutters, boolean operations, slicers, ... - if hasattr(interaction, "apply"): - try: - interaction.apply(geometry) - except Exception: - pass - - return geometry + # def compute_modelgeometry(self) -> Union[Mesh, Brep]: + # if not self.model: + # raise Exception + + # geometry = self.elementgeometry.transformed(self.modeltransformation) + + # # apply effect of interactions? + # node = self.graphnode + # nbrs = self.model.graph.neighbors_in(node) + # for nbr in nbrs: + # element = self.model.graph.node_element(nbr) + # if element: + # for interaction in self.model.graph.edge_interactions((nbr, node)): + # # example interactions are + # # cutters, boolean operations, slicers, ... + # if hasattr(interaction, "apply"): + # try: + # interaction.apply(geometry) + # except Exception as e: + # print(str(e)) + # pass + + # return geometry def compute_aabb(self) -> Box: points = self.modelgeometry.vertices_attributes("xyz") diff --git a/src/compas_model/elements/element.py b/src/compas_model/elements/element.py index 266fc8b4..be9db2c2 100644 --- a/src/compas_model/elements/element.py +++ b/src/compas_model/elements/element.py @@ -310,17 +310,15 @@ def compute_modelgeometry(self) -> Union[Brep, Mesh]: :class:`compas.datastructures.Mesh` | :class:`compas.geometry.Brep` """ + graph = self.model.graph - elements = list(self.model.elements()) xform = self.modeltransformation modelgeometry = self.elementgeometry.transformed(xform) for neighbor in graph.neighbors_in(self.graphnode): for interaction in graph.edge_interactions((neighbor, self.graphnode)): - if isinstance(interaction, ContactInterface): + if isinstance(interaction, ContactInterface) or isinstance(interaction, BooleanModifier): modelgeometry = interaction.apply(modelgeometry) - elif isinstance(interaction, BooleanModifier): - modelgeometry = interaction.apply(modelgeometry, elements[neighbor].modelgeometry) self.is_dirty = False diff --git a/src/compas_model/interactions/boolean_modifier.py b/src/compas_model/interactions/boolean_modifier.py index 5f9310ad..3ee16a4a 100644 --- a/src/compas_model/interactions/boolean_modifier.py +++ b/src/compas_model/interactions/boolean_modifier.py @@ -11,6 +11,8 @@ class BooleanModifier(Interaction): Parameters ---------- + cutter : :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` + The geometry to be used as the modifier name : str, optional The name of the interaction. @@ -19,16 +21,17 @@ class BooleanModifier(Interaction): @property def __data__(self): # type: () -> dict - return {"name": self.name} + return {"name": self.name, "cutter": self.cutter} - def __init__(self, name=None): - # type: (str | None) -> None + def __init__(self, cutter: Union[Brep, Mesh], name=None): + # type: (Union[Brep, Mesh], str | None) -> None super(BooleanModifier, self).__init__(name=name) + self.cutter = cutter def __repr__(self): return '{}(name="{}")'.format(self.__class__.__name__, self.name) - def apply(self, targetgeometry: Union[Brep, Mesh], sourcegeometry: Union[Brep, Mesh]): + def apply(self, targetgeometry: Union[Brep, Mesh]): """Apply the interaction to the affected geometry. Parameters @@ -41,4 +44,4 @@ def apply(self, targetgeometry: Union[Brep, Mesh], sourcegeometry: Union[Brep, M # Local import is needed otherwise, remove contact interactions in algorithms module. from compas_model.algorithms.modifiers import boolean_difference - return boolean_difference(targetgeometry, sourcegeometry) + return boolean_difference(targetgeometry, self.cutter) diff --git a/src/compas_model/models/model.py b/src/compas_model/models/model.py index 61c88c47..30bc1099 100644 --- a/src/compas_model/models/model.py +++ b/src/compas_model/models/model.py @@ -419,8 +419,6 @@ def compute_contact(self, a: Element, b: Element, type: str = "") -> tuple[int, self.graph.edge_attribute(edge, name="interactions", value=interactions) self._guid_element[str(b.guid)].is_dirty = True return edge - else: - raise Exception("No contact interaction found between the two elements.") def remove_element(self, element: Element) -> None: """Remove an element from the model. From a9802d4e0beb43aa22d8f28fdd9ffd65a1aa6a9c Mon Sep 17 00:00:00 2001 From: petras Date: Sun, 12 Jan 2025 21:59:18 +0100 Subject: [PATCH 07/17] LINT --- src/compas_model/elements/beam.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compas_model/elements/beam.py b/src/compas_model/elements/beam.py index b4a61b5d..6ac9771a 100644 --- a/src/compas_model/elements/beam.py +++ b/src/compas_model/elements/beam.py @@ -580,8 +580,8 @@ def __init__( self.inverted: bool = inverted self._length: float = abs(length) - self.step_width_left = min(self.step_width_left, width*0.5*0.999) - self.step_width_right = min(self.step_width_right, width*0.5*0.999) + self.step_width_left = min(self.step_width_left, width * 0.5 * 0.999) + self.step_width_right = min(self.step_width_right, width * 0.5 * 0.999) self.step_height_left = min(self.step_height_left, height) self.step_height_right = min(self.step_height_right, height) From 49203e9d2d4b405063c5b8cd4d678adb49adc96b Mon Sep 17 00:00:00 2001 From: petras Date: Sun, 12 Jan 2025 22:40:17 +0100 Subject: [PATCH 08/17] ADD cable class. --- data/frame.json | 2 +- docs/examples/masonry/000_frame.py | 4 +- docs/examples/masonry/001_frame_model.py | 27 ++- src/compas_model/elements/__init__.py | 4 + src/compas_model/elements/cable.py | 285 +++++++++++++++++++++++ src/compas_model/models/model.py | 1 + 6 files changed, 315 insertions(+), 8 deletions(-) create mode 100644 src/compas_model/elements/cable.py diff --git a/data/frame.json b/data/frame.json index bc5b72ec..f3e301c1 100644 --- a/data/frame.json +++ b/data/frame.json @@ -1 +1 @@ -{"Model::Line::Segments": [{"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 0.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "662c2a4e-26f2-4e57-859c-e2c7e227d01d"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 0.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "e1dce19f-97af-4b97-98a3-a8ba3daa5bf2"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 0.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "2cb1d076-3f2c-4d42-a786-333518fe2452"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 0.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "30f2fbef-4f84-4814-b460-9849401d489a"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 3800.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "6c298089-9333-4e46-8d8f-625719548a20"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 3800.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "52e6cc9b-3fa6-4b19-ba01-df2d5aec5aeb"}], "Model::Mesh::Floor": [{"dtype": "compas.datastructures/Mesh", "data": {"attributes": {}, "default_vertex_attributes": {"x": 0.0, "y": 0.0, "z": 0.0}, "default_edge_attributes": {}, "default_face_attributes": {}, "vertex": {"0": {"x": -3000.0, "y": -3000.0, "z": 3800.0}, "1": {"x": -3000.0, "y": 3000.0, "z": 3800.0}, "2": {"x": 3000.0, "y": 3000.0, "z": 3800.0}, "3": {"x": 3000.0, "y": -3000.0, "z": 3800.0}}, "face": {"0": [0, 1, 2, 3]}, "facedata": {"0": {}}, "edgedata": {}, "max_vertex": 3, "max_face": 0}, "guid": "d0e115e1-7955-4c94-a5af-30e33ffa9f67"}]} \ No newline at end of file +{"Model::Line::Segments": [{"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 0.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "48c887bb-60ad-45bc-8673-2bbd12e10871"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 0.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "f87cfd2e-1685-4851-8768-11844258fccd"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 0.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "063208cf-96e5-40d1-aba0-8f081191356c"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 0.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "86019d51-e365-4c3c-a7be-fbf5653e6aa5"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 3800.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "b1453ac4-6825-44e4-b965-002ebc19b6b8"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 3800.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "14ca6313-04bd-44a9-b5da-20aa5045ed62"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 3800.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "82e760c0-be02-4881-a315-20c1c5b184b0"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 3800.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "c2a0a8f7-2696-41e7-8a5a-f97868b11acd"}], "Model::Mesh::Floor": [{"dtype": "compas.datastructures/Mesh", "data": {"attributes": {}, "default_vertex_attributes": {"x": 0.0, "y": 0.0, "z": 0.0}, "default_edge_attributes": {}, "default_face_attributes": {}, "vertex": {"0": {"x": -3000.0, "y": -3000.0, "z": 3800.0}, "1": {"x": -3000.0, "y": 3000.0, "z": 3800.0}, "2": {"x": 3000.0, "y": 3000.0, "z": 3800.0}, "3": {"x": 3000.0, "y": -3000.0, "z": 3800.0}}, "face": {"0": [0, 1, 2, 3]}, "facedata": {"0": {}}, "edgedata": {}, "max_vertex": 3, "max_face": 0}, "guid": "4f03a8f2-0a57-4204-86a1-66244ffdeed6"}]} \ No newline at end of file diff --git a/docs/examples/masonry/000_frame.py b/docs/examples/masonry/000_frame.py index 2ae02ae0..f1dca2e2 100644 --- a/docs/examples/masonry/000_frame.py +++ b/docs/examples/masonry/000_frame.py @@ -28,9 +28,9 @@ Line(points[2], points[2 + 4]), Line(points[3], points[3 + 4]), Line(points[4], points[5]), - # Line(points[5], points[6]), Line(points[6], points[7]), - # Line(points[7], points[4]), + Line(points[5], points[6]), + Line(points[7], points[4]), ] mesh = Mesh.from_vertices_and_faces(points[4:], [[0, 1, 2, 3]]) diff --git a/docs/examples/masonry/001_frame_model.py b/docs/examples/masonry/001_frame_model.py index 5c11cd48..1cb3620f 100644 --- a/docs/examples/masonry/001_frame_model.py +++ b/docs/examples/masonry/001_frame_model.py @@ -8,6 +8,7 @@ from compas.geometry.transformation import Transformation from compas_model.elements import BeamTProfileElement from compas_model.elements import ColumnSquareElement +from compas_model.elements import CableElement from compas_model.models import BlockModel from compas_model.models import Model from compas_viewer import Viewer @@ -32,13 +33,22 @@ # Add two beams -for i in range(4,len(lines)): +for i in range(4,len(lines)-2): beam: BeamTProfileElement = BeamTProfileElement(width=300, height=700, step_width_left=75, step_height_left=150, length=lines[i].length) target_frame : Frame = Frame(lines[i].start, Vector.Zaxis().cross(lines[i].vector), Vector.Zaxis()) beam.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), target_frame) * Translation.from_vector([0, beam.height*0.5, 0]) beam.extend(150) model.add_element(beam) +# Add two cables +for i in range(6,len(lines)): + cable: CableElement = CableElement(length=lines[i].length, radius=10) + target_frame : Frame = Frame(lines[i].start, Vector.Zaxis().cross(lines[i].vector), Vector.Zaxis()) + cable.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), target_frame) * Translation.from_vector([0, beam.height*0.1, 0]) + cable.extend(200) + model.add_element(cable) + + # Add blocks, by moving them by the height of the first column. blockmodel : BlockModel = BlockModel.from_barrel_vault(span=6000, length=6000, thickness=250, rise=600, vou_span=5, vou_length=5) @@ -48,17 +58,24 @@ # Add Interactions -for beam in list(model.elements()): - for block in blockmodel.elements(): +for element in list(model.elements()): + if isinstance(element, BeamTProfileElement): + for block in blockmodel.elements(): + if isinstance(element, BeamTProfileElement): + model.compute_contact(element, block) # beam -> cuts -> block + +for element in list(model.elements()): + if isinstance(element, CableElement): + for beam in list(model.elements()): if isinstance(beam, BeamTProfileElement): - model.compute_contact(beam, block) # beam -> cuts -> block + model.compute_contact(element, beam) # cable -> cuts -> beam # ============================================================================= # Vizualize # ============================================================================= config = Config() -config.camera.target = [0, 0, 100] +config.camera.target = [0, 0, 100] config.camera.position = [10000, -10000, 10000] config.camera.near = 10 config.camera.far = 100000 diff --git a/src/compas_model/elements/__init__.py b/src/compas_model/elements/__init__.py index 387626fb..f35a9d53 100644 --- a/src/compas_model/elements/__init__.py +++ b/src/compas_model/elements/__init__.py @@ -19,6 +19,8 @@ from .fasteners import FastenersFeature from .fasteners import FastenersElement from .fasteners import ScrewElement +from .cable import CableFeature +from .cable import CableElement __all__ = [ @@ -43,4 +45,6 @@ FastenersFeature, FastenersElement, ScrewElement, + CableFeature, + CableElement, ] diff --git a/src/compas_model/elements/cable.py b/src/compas_model/elements/cable.py new file mode 100644 index 00000000..2cfa79bb --- /dev/null +++ b/src/compas_model/elements/cable.py @@ -0,0 +1,285 @@ +from typing import TYPE_CHECKING +from typing import Optional +from typing import Union + +from compas.datastructures import Mesh +from compas.geometry import Box +from compas.geometry import Frame +from compas.geometry import Line +from compas.geometry import Plane +from compas.geometry import Point +from compas.geometry import Polygon +from compas.geometry import Transformation +from compas.geometry import Translation +from compas.geometry import bounding_box +from compas.geometry import intersection_line_plane +from compas.geometry import oriented_bounding_box +from compas.itertools import pairwise +from compas_model.interactions import BooleanModifier + +from .element import Element +from .element import Feature + +if TYPE_CHECKING: + from compas_model.elements import BeamElement + + +class CableFeature(Feature): + pass + + +class CableElement(Element): + """Class representing a Cable element with a round section. + + Parameters + ---------- + radius : float + Radius of the Cable. + sides : int + Number of sides of the Cable's polygonal section. + length : float + Length of the Cable. + frame_top : Optional[:class:`compas.geometry.Plane`] + Second frame of the Cable that is used to cut the second end, while the first frame is used to cut the first end. + is_support : bool + Flag indicating if the Cable is a support. + frame : :class:`compas.geometry.Frame` + Main frame of the Cable. + transformation : Optional[:class:`compas.geometry.Transformation`] + Transformation applied to the Cable. + features : Optional[list[:class:`compas_model.features.CableFeature`]] + Features of the Cable. + name : Optional[str] + If no name is defined, the class name is given. + + Attributes + ---------- + radius : float + Radius of the Cable. + sides : int + Number of sides of the Cable's polygonal section. + length : float + length of the Cable. + is_support : bool + Flag indicating if the Cable is a support. + frame : :class:`compas.geometry.Frame` + Main frame of the Cable. + frame_top : :class:`compas.geometry.Frame` + Second frame of the Cable. + axis : :class:`compas.geometry.Line` + Line axis of the Cable. + section : :class:`compas.geometry.Polygon` + Section polygon of the Cable. + polygon_bottom : :class:`compas.geometry.Polygon` + The bottom polygon of the Cable. + polygon_top : :class:`compas.geometry.Polygon` + The top polygon of the Cable. + transformation : :class:`compas.geometry.Transformation` + Transformation applied to the Cable. + features : list[:class:`compas_model.features.CableFeature`] + Features of the Cable. + name : str + The name of the Cable. + """ + + @property + def __data__(self) -> dict: + return { + "radius": self.radius, + "sides": self.sides, + "length": self.length, + "frame_top": self.frame_top, + "is_support": self.is_support, + "frame": self.frame, + "transformation": self.transformation, + "features": self._features, + "name": self.name, + } + + def __init__( + self, + radius: float = 0.4, + sides: int = 24, + length: float = 3.0, + frame_top: Optional[Plane] = None, + is_support: bool = False, + frame: Frame = Frame.worldXY(), + transformation: Optional[Transformation] = None, + features: Optional[list[CableFeature]] = None, + name: Optional[str] = None, + ) -> "CableElement": + super().__init__(frame=frame, transformation=transformation, features=features, name=name) + + self.is_support: bool = is_support + + self.radius = radius + self.sides = sides + self._length = length + self.axis: Line = Line([0, 0, 0], [0, 0, self._length]) + self.section: Polygon = Polygon.from_sides_and_radius_xy(sides, radius) + self.frame_top: Frame = frame_top or Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) + self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() + + @property + def face_polygons(self) -> list[Polygon]: + return [self.geometry.face_polygon(face) for face in self.geometry.faces()] # type: ignore + + @property + def length(self) -> float: + return self._length + + @length.setter + def length(self, length: float): + self._length = length + self.axis: Line = Line([0, 0, 0], [0, 0, self._length]) + self.frame_top: Frame = Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) + self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() + + def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: + """Compute the top and bottom polygons of the Cable. + + Returns + ------- + tuple[:class:`compas.geometry.Polygon`, :class:`compas.geometry.Polygon`] + """ + + plane0: Plane = Plane.from_frame(self.frame) + plane1: Plane = Plane.from_frame(self.frame_top) + points0: list[list[float]] = [] + points1: list[list[float]] = [] + for i in range(len(self.section.points)): + line: Line = Line(self.section.points[i], self.section.points[i] + self.axis.vector) + result0: Optional[list[float]] = intersection_line_plane(line, plane0) + result1: Optional[list[float]] = intersection_line_plane(line, plane1) + if not result0 or not result1: + raise ValueError("The line does not intersect the plane") + points0.append(result0) + points1.append(result1) + return Polygon(points0), Polygon(points1) + + def compute_elementgeometry(self) -> Mesh: + """Compute the shape of the Cable from the given polygons. + This shape is relative to the frame of the element. + + Returns + ------- + :class:`compas.datastructures.Mesh` + + """ + + 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) + + def extend(self, distance: float) -> None: + """Extend the beam. + + Parameters + ---------- + distance : float + The distance to extend the beam. + """ + self.length = self.length + distance * 2 + xform: Transformation = Translation.from_vector([0, 0, -distance]) + self.transformation = self.transformation * xform + + def compute_contact(self, target_element: Element, type: str = "") -> BooleanModifier: + """Computes the contact interaction of the geometry of the elements that is used in the model's add_contact method. + + Returns + ------- + :class:`compas_model.interactions.BooleanModifier` + The ContactInteraction that is applied to the neighboring element. One pair can have one or multiple variants. + target_element : Element + The target element to compute the contact interaction. + type : str, optional + The type of contact interaction, if different contact are possible between the two elements. + + """ + # Traverse up to the class one before the Element class.add() + # Create a function name based on the target_element class name. + parent_class = target_element.__class__ + while parent_class.__bases__[0] != Element: + parent_class = parent_class.__bases__[0] + + parent_class_name = parent_class.__name__.lower().replace("element", "") + method_name = f"_compute_contact_with_{parent_class_name}" + method = getattr(self, method_name, None) + if method is None: + raise ValueError(f"Unsupported target element type: {type(target_element)}") + + return method(target_element, type) + + def _compute_contact_with_beam(self, target_element: "BeamElement", type: str) -> Union["BooleanModifier", None]: + # Scenario: + # A cable applies boolean difference with a block geometry. + return BooleanModifier(self.elementgeometry.transformed(self.modeltransformation)) + + # ============================================================================= + # Constructors + # ============================================================================= diff --git a/src/compas_model/models/model.py b/src/compas_model/models/model.py index 30bc1099..833ee654 100644 --- a/src/compas_model/models/model.py +++ b/src/compas_model/models/model.py @@ -411,6 +411,7 @@ def compute_contact(self, a: Element, b: Element, type: str = "") -> tuple[int, raise Exception("Something went wrong: the elements are not in the interaction graph.") interaction: Interaction = a.compute_contact(b, type) + if interaction: # Whether we add contact if there is an edge or not we will decide later. edge = self._graph.add_edge(node_a, node_b) From fcf4d12aedd83bcc7199e26e5ff886aa7f553251 Mon Sep 17 00:00:00 2001 From: petras Date: Tue, 14 Jan 2025 10:16:42 +0100 Subject: [PATCH 09/17] CHANGE column elementgeometry is made BRep instead of mesh. --- data/frame.json | 2 +- docs/examples/elements/column.py | 19 ++++++++++++ docs/examples/masonry/001_frame_model.py | 33 ++++++++++---------- src/compas_model/algorithms/modifiers.py | 2 ++ src/compas_model/elements/column.py | 38 ++++++++++++++++-------- 5 files changed, 64 insertions(+), 30 deletions(-) create mode 100644 docs/examples/elements/column.py diff --git a/data/frame.json b/data/frame.json index f3e301c1..f68cabf6 100644 --- a/data/frame.json +++ b/data/frame.json @@ -1 +1 @@ -{"Model::Line::Segments": [{"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 0.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "48c887bb-60ad-45bc-8673-2bbd12e10871"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 0.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "f87cfd2e-1685-4851-8768-11844258fccd"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 0.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "063208cf-96e5-40d1-aba0-8f081191356c"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 0.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "86019d51-e365-4c3c-a7be-fbf5653e6aa5"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 3800.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "b1453ac4-6825-44e4-b965-002ebc19b6b8"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 3800.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "14ca6313-04bd-44a9-b5da-20aa5045ed62"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 3800.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "82e760c0-be02-4881-a315-20c1c5b184b0"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 3800.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "c2a0a8f7-2696-41e7-8a5a-f97868b11acd"}], "Model::Mesh::Floor": [{"dtype": "compas.datastructures/Mesh", "data": {"attributes": {}, "default_vertex_attributes": {"x": 0.0, "y": 0.0, "z": 0.0}, "default_edge_attributes": {}, "default_face_attributes": {}, "vertex": {"0": {"x": -3000.0, "y": -3000.0, "z": 3800.0}, "1": {"x": -3000.0, "y": 3000.0, "z": 3800.0}, "2": {"x": 3000.0, "y": 3000.0, "z": 3800.0}, "3": {"x": 3000.0, "y": -3000.0, "z": 3800.0}}, "face": {"0": [0, 1, 2, 3]}, "facedata": {"0": {}}, "edgedata": {}, "max_vertex": 3, "max_face": 0}, "guid": "4f03a8f2-0a57-4204-86a1-66244ffdeed6"}]} \ No newline at end of file +{"Model::Line::Segments": [{"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 0.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "f2396cc5-ec49-4a8a-b3bc-236d1c54e756"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 0.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "f0784e5b-af66-4787-a32b-ee7b79b5a4fc"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 0.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "c6e12893-2d6c-4b4b-a6d3-98fd287bf9c4"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 0.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "a2360552-cd42-467c-9480-a22d83527033"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 3800.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "01314dff-bc56-4aad-a715-87fdc7e67691"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 3800.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "e1b0292b-7283-4017-8b19-d74ab9ca8179"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 3800.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "5cc86d81-acf8-4725-be20-0e78bc5fff68"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 3800.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "fe67d4ee-90da-4f4a-97a3-76c0aff1a1da"}], "Model::Mesh::Floor": [{"dtype": "compas.datastructures/Mesh", "data": {"attributes": {}, "default_vertex_attributes": {"x": 0.0, "y": 0.0, "z": 0.0}, "default_edge_attributes": {}, "default_face_attributes": {}, "vertex": {"0": {"x": -3000.0, "y": -3000.0, "z": 3800.0}, "1": {"x": -3000.0, "y": 3000.0, "z": 3800.0}, "2": {"x": 3000.0, "y": 3000.0, "z": 3800.0}, "3": {"x": 3000.0, "y": -3000.0, "z": 3800.0}}, "face": {"0": [0, 1, 2, 3]}, "facedata": {"0": {}}, "edgedata": {}, "max_vertex": 3, "max_face": 0}, "guid": "8ea782e3-1036-497e-bd66-9117ef460bf6"}]} \ No newline at end of file diff --git a/docs/examples/elements/column.py b/docs/examples/elements/column.py new file mode 100644 index 00000000..8aeba29a --- /dev/null +++ b/docs/examples/elements/column.py @@ -0,0 +1,19 @@ +from compas_model.elements import ColumnSquareElement +from compas_viewer import Viewer +from compas_viewer.config import Config + +scale = 1 +column: ColumnSquareElement = ColumnSquareElement() + + +config = Config() + +config.camera.target = [0, 0.1 * scale, 0] +config.camera.position = [0, -0.2 * scale, 7 * scale] +config.camera.near = 0.1 * scale +config.camera.far = 10 * scale +viewer = Viewer(config=config) +viewer.scene.add(column.elementgeometry) +viewer.scene.add(column.axis) + +viewer.show() diff --git a/docs/examples/masonry/001_frame_model.py b/docs/examples/masonry/001_frame_model.py index 1cb3620f..5dd3dcb5 100644 --- a/docs/examples/masonry/001_frame_model.py +++ b/docs/examples/masonry/001_frame_model.py @@ -7,8 +7,8 @@ from compas.geometry import Vector from compas.geometry.transformation import Transformation from compas_model.elements import BeamTProfileElement -from compas_model.elements import ColumnSquareElement from compas_model.elements import CableElement +from compas_model.elements import ColumnSquareElement from compas_model.models import BlockModel from compas_model.models import Model from compas_viewer import Viewer @@ -27,33 +27,32 @@ # Add columns for i in range(0, 4): - column: ColumnSquareElement = ColumnSquareElement(300,300, lines[i].length) + column: ColumnSquareElement = ColumnSquareElement(300, 300, lines[i].length) column.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), Frame(lines[i].start)) model.add_element(column) # Add two beams -for i in range(4,len(lines)-2): +for i in range(4, len(lines) - 2): beam: BeamTProfileElement = BeamTProfileElement(width=300, height=700, step_width_left=75, step_height_left=150, length=lines[i].length) - target_frame : Frame = Frame(lines[i].start, Vector.Zaxis().cross(lines[i].vector), Vector.Zaxis()) - beam.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), target_frame) * Translation.from_vector([0, beam.height*0.5, 0]) + target_frame: Frame = Frame(lines[i].start, Vector.Zaxis().cross(lines[i].vector), Vector.Zaxis()) + beam.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), target_frame) * Translation.from_vector([0, beam.height * 0.5, 0]) beam.extend(150) model.add_element(beam) # Add two cables -for i in range(6,len(lines)): +for i in range(6, len(lines)): cable: CableElement = CableElement(length=lines[i].length, radius=10) - target_frame : Frame = Frame(lines[i].start, Vector.Zaxis().cross(lines[i].vector), Vector.Zaxis()) - cable.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), target_frame) * Translation.from_vector([0, beam.height*0.1, 0]) + target_frame: Frame = Frame(lines[i].start, Vector.Zaxis().cross(lines[i].vector), Vector.Zaxis()) + cable.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), target_frame) * Translation.from_vector([0, beam.height * 0.1, 0]) cable.extend(200) model.add_element(cable) - # Add blocks, by moving them by the height of the first column. -blockmodel : BlockModel = BlockModel.from_barrel_vault(span=6000, length=6000, thickness=250, rise=600, vou_span=5, vou_length=5) +blockmodel: BlockModel = BlockModel.from_barrel_vault(span=6000, length=6000, thickness=250, rise=600, vou_span=5, vou_length=5) for block in blockmodel.elements(): - block.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), Frame([0,0,lines[0].end[2]])) * block.transformation + block.transformation = Transformation.from_frame_to_frame(Frame.worldXY(), Frame([0, 0, lines[0].end[2]])) * block.transformation model.add_element(block) @@ -61,25 +60,25 @@ for element in list(model.elements()): if isinstance(element, BeamTProfileElement): for block in blockmodel.elements(): - if isinstance(element, BeamTProfileElement): - model.compute_contact(element, block) # beam -> cuts -> block + if isinstance(element, BeamTProfileElement): + model.compute_contact(element, block) # beam -> cuts -> block for element in list(model.elements()): if isinstance(element, CableElement): for beam in list(model.elements()): if isinstance(beam, BeamTProfileElement): - model.compute_contact(element, beam) # cable -> cuts -> beam - + model.compute_contact(element, beam) # cable -> cuts -> beam + # ============================================================================= # Vizualize # ============================================================================= config = Config() -config.camera.target = [0, 0, 100] +config.camera.target = [0, 0, 100] config.camera.position = [10000, -10000, 10000] config.camera.near = 10 config.camera.far = 100000 viewer = Viewer(config=config) for element in list(model.elements()): viewer.scene.add(element.modelgeometry, hide_coplanaredges=True) -viewer.show() \ No newline at end of file +viewer.show() diff --git a/src/compas_model/algorithms/modifiers.py b/src/compas_model/algorithms/modifiers.py index 09048b84..100c6dfc 100644 --- a/src/compas_model/algorithms/modifiers.py +++ b/src/compas_model/algorithms/modifiers.py @@ -47,6 +47,8 @@ def boolean_difference(target_geometry, source_geometry): The geometry after boolean difference. """ + target_geometry.boolean_difference(source_geometry) + from compas_cgal.booleans import boolean_difference_mesh_mesh target_geometry_copy = target_geometry.copy() diff --git a/src/compas_model/elements/column.py b/src/compas_model/elements/column.py index 09f7c435..5e27402f 100644 --- a/src/compas_model/elements/column.py +++ b/src/compas_model/elements/column.py @@ -2,6 +2,7 @@ from compas.datastructures import Mesh from compas.geometry import Box +from compas.geometry import Brep from compas.geometry import Frame from compas.geometry import Line from compas.geometry import Plane @@ -150,25 +151,38 @@ def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: points1.append(result1) return Polygon(points0), Polygon(points1) - def compute_elementgeometry(self) -> Mesh: + # 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 + + def compute_elementgeometry(self) -> Brep: """Compute the shape of the column from the given polygons. This shape is relative to the frame of the element. Returns ------- - :class:`compas.datastructures.Mesh` + :class:`compas.geometry.Brep` """ - - 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 + box: Box = Box.from_width_height_depth(self.width, self.height, self.depth).translated([0, 0, self.height * 0.5]) + brep = Brep.from_box(box) + return brep # ============================================================================= # Implementations of abstract methods From 939d97b846f31cf15111b2b51c85bda17017c8c5 Mon Sep 17 00:00:00 2001 From: petras Date: Tue, 14 Jan 2025 10:28:17 +0100 Subject: [PATCH 10/17] CHANGE cylinder elementgeometry is made as Brep. --- docs/examples/elements/cylinder.py | 19 +++++++++++++++ src/compas_model/elements/cable.py | 39 +++++++++++++++++++++--------- 2 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 docs/examples/elements/cylinder.py diff --git a/docs/examples/elements/cylinder.py b/docs/examples/elements/cylinder.py new file mode 100644 index 00000000..3f857284 --- /dev/null +++ b/docs/examples/elements/cylinder.py @@ -0,0 +1,19 @@ +from compas_model.elements import CableElement +from compas_viewer import Viewer +from compas_viewer.config import Config + +scale = 1 +column: CableElement = CableElement() + + +config = Config() + +config.camera.target = [0, 0.1 * scale, 0] +config.camera.position = [0, -0.2 * scale, 7 * scale] +config.camera.near = 0.1 * scale +config.camera.far = 10 * scale +viewer = Viewer(config=config) +viewer.scene.add(column.elementgeometry) +viewer.scene.add(column.axis) + +viewer.show() diff --git a/src/compas_model/elements/cable.py b/src/compas_model/elements/cable.py index 2cfa79bb..9cdd80e3 100644 --- a/src/compas_model/elements/cable.py +++ b/src/compas_model/elements/cable.py @@ -7,14 +7,12 @@ from compas.geometry import Frame from compas.geometry import Line from compas.geometry import Plane -from compas.geometry import Point from compas.geometry import Polygon from compas.geometry import Transformation from compas.geometry import Translation from compas.geometry import bounding_box from compas.geometry import intersection_line_plane from compas.geometry import oriented_bounding_box -from compas.itertools import pairwise from compas_model.interactions import BooleanModifier from .element import Element @@ -157,6 +155,27 @@ def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: points1.append(result1) return Polygon(points0), Polygon(points1) + # def compute_elementgeometry(self) -> Mesh: + # """Compute the shape of the Cable from the given polygons. + # This shape is relative to the frame of the element. + + # Returns + # ------- + # :class:`compas.datastructures.Mesh` + + # """ + # from compas.geometry import Point + # from compas.itertools import pairwise + # offset: int = len(self.polygon_bottom) + # vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore + # bottom: list[int] = list(range(offset)) + # top: list[int] = [i + offset for i in bottom] + # faces: list[list[int]] = [bottom[::-1], top] + # for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): + # faces.append([a, b, d, c]) + # mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + # return mesh + def compute_elementgeometry(self) -> Mesh: """Compute the shape of the Cable from the given polygons. This shape is relative to the frame of the element. @@ -167,15 +186,13 @@ def compute_elementgeometry(self) -> 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 + from compas.geometry import Brep + from compas.geometry import Cylinder + + cylinder: Cylinder = Cylinder.from_line_and_radius(self.axis, self.radius) + brep: Brep = Brep.from_cylinder(cylinder) + + return brep # ============================================================================= # Implementations of abstract methods From ff5f94d6eefb10e4143f3f048d11a0b73d3919fb Mon Sep 17 00:00:00 2001 From: petras Date: Tue, 14 Jan 2025 15:03:59 +0100 Subject: [PATCH 11/17] ADD blocks as breps. --- docs/examples/masonry/002_block_model.py | 19 +++++++++++++++++++ src/compas_model/models/blockmodel.py | 9 +++++---- 2 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 docs/examples/masonry/002_block_model.py diff --git a/docs/examples/masonry/002_block_model.py b/docs/examples/masonry/002_block_model.py new file mode 100644 index 00000000..86dfb98e --- /dev/null +++ b/docs/examples/masonry/002_block_model.py @@ -0,0 +1,19 @@ +from compas_model.models import BlockModel +from compas_viewer.config import Config +from compas_viewer.viewer import Viewer +from compas.tolerance import TOL + +model: BlockModel = BlockModel.from_barrel_vault(span=6000, length=6000, thickness=250, rise=600, vou_span=5, vou_length=5) + + +TOL.lineardeflection = 1000 + +config = Config() +config.camera.target = [0, 0, 100] +config.camera.position = [10000, -10000, 10000] +config.camera.near = 10 +config.camera.far = 100000 +viewer = Viewer(config=config) +for element in list(model.elements()): + viewer.scene.add(element.modelgeometry, hide_coplanaredges=True) +viewer.show() diff --git a/src/compas_model/models/blockmodel.py b/src/compas_model/models/blockmodel.py index ee49a054..0eef946e 100644 --- a/src/compas_model/models/blockmodel.py +++ b/src/compas_model/models/blockmodel.py @@ -14,10 +14,10 @@ from compas.geometry import translate_points from compas.tolerance import Tolerance from compas_model.algorithms.nnbrs import find_nearest_neighbours +from compas_model.elements import BlockElement from compas_model.elements import Element from compas_model.interactions import ContactInterface - -from .model import Model +from compas_model.models import Model try: from compas_occ.brep import OCCBrepFace as BrepFace @@ -209,7 +209,6 @@ def from_barrel_vault( # Translate blocks to xy frame and create blockmodel. blockmodel: BlockModel = BlockModel() - from compas_model.elements import BlockElement for mesh in meshes: origin: Point = mesh.face_polygon(5).frame.point @@ -217,7 +216,9 @@ def from_barrel_vault( Frame(origin, mesh.vertex_point(0) - mesh.vertex_point(2), mesh.vertex_point(4) - mesh.vertex_point(2)), Frame.worldXY() ) mesh_xy: Mesh = mesh.transformed(xform) - block: BlockElement = BlockElement(shape=mesh_xy, is_support=mesh_xy.attributes["is_support"]) + brep: Brep = Brep.from_mesh(mesh_xy) + + block: BlockElement = BlockElement(shape=brep, is_support=mesh_xy.attributes["is_support"]) block.transformation = xform.inverse() blockmodel.add_element(block) From a49c2d12564e5fa3845d26bd7cb645a26be06f40 Mon Sep 17 00:00:00 2001 From: petras Date: Tue, 14 Jan 2025 17:19:45 +0100 Subject: [PATCH 12/17] ADD boolean_modifier can be applied for BReps and Meshes. --- docs/examples/elements/beam.py | 21 +++++--- docs/examples/masonry/001_frame_model.py | 5 +- src/compas_model/algorithms/modifiers.py | 29 ++++++----- src/compas_model/elements/beam.py | 64 ++++++++++++++++-------- src/compas_model/elements/block.py | 3 +- src/compas_model/elements/cable.py | 1 + 6 files changed, 81 insertions(+), 42 deletions(-) diff --git a/docs/examples/elements/beam.py b/docs/examples/elements/beam.py index 289cbe6c..8b58f6d1 100644 --- a/docs/examples/elements/beam.py +++ b/docs/examples/elements/beam.py @@ -1,19 +1,28 @@ +from compas.tolerance import TOL from compas_model.elements import BeamTProfileElement from compas_viewer import Viewer from compas_viewer.config import Config scale = 1 -beam : BeamTProfileElement = BeamTProfileElement(width=0.2*scale, height=0.3*scale, step_height_left=0.1*scale, step_height_right=0.1*scale, step_width_left=0.05*scale, step_width_right=0.05*scale, length=6*scale) +beam: BeamTProfileElement = BeamTProfileElement( + width=0.2 * scale, + height=0.3 * scale, + step_height_left=0.1 * scale, + step_height_right=0.1 * scale, + step_width_left=0.05 * scale, + step_width_right=0.05 * scale, + length=6 * scale, +) +TOL.lineardeflection = 1000 config = Config() -config.camera.target = [0, 0.1*scale, 0] -config.camera.position = [0, -0.2*scale, 7*scale] -config.camera.near = 0.1*scale -config.camera.far = 10*scale +config.camera.target = [0, 0.1 * scale, 0] +config.camera.position = [0, -0.2 * scale, 7 * scale] +config.camera.near = 0.1 * scale +config.camera.far = 10 * scale viewer = Viewer(config=config) viewer.scene.add(beam.elementgeometry) viewer.show() - diff --git a/docs/examples/masonry/001_frame_model.py b/docs/examples/masonry/001_frame_model.py index 5dd3dcb5..550eb2b8 100644 --- a/docs/examples/masonry/001_frame_model.py +++ b/docs/examples/masonry/001_frame_model.py @@ -6,6 +6,7 @@ from compas.geometry import Translation from compas.geometry import Vector from compas.geometry.transformation import Transformation +from compas.tolerance import TOL from compas_model.elements import BeamTProfileElement from compas_model.elements import CableElement from compas_model.elements import ColumnSquareElement @@ -72,7 +73,7 @@ # ============================================================================= # Vizualize # ============================================================================= - +TOL.lineardeflection = 100 config = Config() config.camera.target = [0, 0, 100] config.camera.position = [10000, -10000, 10000] @@ -80,5 +81,5 @@ config.camera.far = 100000 viewer = Viewer(config=config) for element in list(model.elements()): - viewer.scene.add(element.modelgeometry, hide_coplanaredges=True) + viewer.scene.add(element.modelgeometry, hide_coplanaredges=False) viewer.show() diff --git a/src/compas_model/algorithms/modifiers.py b/src/compas_model/algorithms/modifiers.py index 100c6dfc..18a2be9b 100644 --- a/src/compas_model/algorithms/modifiers.py +++ b/src/compas_model/algorithms/modifiers.py @@ -47,19 +47,24 @@ def boolean_difference(target_geometry, source_geometry): The geometry after boolean difference. """ - target_geometry.boolean_difference(source_geometry) + is_brep0: bool = isinstance(target_geometry, Brep) + is_brep1: bool = isinstance(source_geometry, Brep) - from compas_cgal.booleans import boolean_difference_mesh_mesh + if is_brep0 and is_brep1: + try: + return Brep.from_boolean_difference(target_geometry, source_geometry) + except Exception: + print("Boolean difference is not successful.") + return target_geometry - target_geometry_copy = target_geometry.copy() - source_geometry_copy = source_geometry.copy() + else: + from compas_cgal.booleans import boolean_difference_mesh_mesh - # unify_cycles method cycles often fail when they are cut several times. - # target_geometry_copy.unify_cycles() - # source_geometry_copy.unify_cycles() + mesh0: Mesh = target_geometry.copy() if not is_brep0 else target_geometry.to_meshes()[0] + mesh1: Mesh = source_geometry.copy() if not is_brep1 else source_geometry.to_meshes()[0] - 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) - mesh: Mesh = Mesh.from_vertices_and_faces(V, F) if len(V) > 0 and len(F) > 0 else target_geometry_copy - return mesh + 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/elements/beam.py b/src/compas_model/elements/beam.py index 6ac9771a..1ad711fb 100644 --- a/src/compas_model/elements/beam.py +++ b/src/compas_model/elements/beam.py @@ -4,6 +4,7 @@ from compas.datastructures import Mesh from compas.geometry import Box +from compas.geometry import Brep from compas.geometry import Frame from compas.geometry import Line from compas.geometry import Plane @@ -647,40 +648,63 @@ def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: points1.append(result1) return Polygon(points0), Polygon(points1) - def compute_elementgeometry(self) -> Mesh: + # def compute_elementgeometry(self) -> Mesh: + # """Compute the shape of the beam from the given polygons . + # This shape is relative to the frame of the element. + + # Returns + # ------- + # :class:`compas.datastructures.Mesh` + # """ + + # from compas.geometry import earclip_polygon + + # offset: int = len(self.polygon_bottom) + # vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore + + # triangles: list[list[int]] = earclip_polygon(Polygon(self.polygon_bottom.points)) + # top_faces: list[list[int]] = [] + # bottom_faces: list[list[int]] = [] + # for i in range(len(triangles)): + # triangle_top: list[int] = [] + # triangle_bottom: list[int] = [] + # for j in range(3): + # triangle_top.append(triangles[i][j] + offset) + # triangle_bottom.append(triangles[i][j]) + # triangle_bottom.reverse() + # top_faces.append(triangle_top) + # bottom_faces.append(triangle_bottom) + # faces: list[list[int]] = bottom_faces + top_faces + + # bottom: list[int] = list(range(offset)) + # top: list[int] = [i + offset for i in bottom] + # for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): + # faces.append([c, d, b, a]) + # mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + # return mesh + + def compute_elementgeometry(self) -> Brep: """Compute the shape of the beam from the given polygons . This shape is relative to the frame of the element. Returns ------- - :class:`compas.datastructures.Mesh` + :class:`compas.datastructures.Brep` """ - from compas.geometry import earclip_polygon + polygons: list[Polygon] = [] offset: int = len(self.polygon_bottom) vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore - - triangles: list[list[int]] = earclip_polygon(Polygon(self.polygon_bottom.points)) - top_faces: list[list[int]] = [] - bottom_faces: list[list[int]] = [] - for i in range(len(triangles)): - triangle_top: list[int] = [] - triangle_bottom: list[int] = [] - for j in range(3): - triangle_top.append(triangles[i][j] + offset) - triangle_bottom.append(triangles[i][j]) - triangle_bottom.reverse() - top_faces.append(triangle_top) - bottom_faces.append(triangle_bottom) - faces: list[list[int]] = bottom_faces + top_faces + polygons.append(self.polygon_bottom) + polygons.append(self.polygon_top) bottom: list[int] = list(range(offset)) top: list[int] = [i + offset for i in bottom] for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): - faces.append([c, d, b, a]) - mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) - return mesh + polygons.append(Polygon([vertices[c], vertices[d], vertices[b], vertices[a]])) + brep: Brep = Brep.from_polygons(polygons) + return brep # ============================================================================= # Implementations of abstract methods diff --git a/src/compas_model/elements/block.py b/src/compas_model/elements/block.py index 5c855871..6d95646f 100644 --- a/src/compas_model/elements/block.py +++ b/src/compas_model/elements/block.py @@ -159,8 +159,7 @@ def __init__( name: Optional[str] = None, ) -> None: super().__init__(frame=frame, transformation=transformation, features=features, name=name) - - self.shape = shape if isinstance(shape, BlockGeometry) else shape.copy(cls=BlockGeometry) + self.shape = shape if isinstance(shape, BlockGeometry) else shape.copy() self.is_support = is_support # ============================================================================= diff --git a/src/compas_model/elements/cable.py b/src/compas_model/elements/cable.py index 9cdd80e3..666e4ee9 100644 --- a/src/compas_model/elements/cable.py +++ b/src/compas_model/elements/cable.py @@ -166,6 +166,7 @@ def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: # """ # from compas.geometry import Point # from compas.itertools import pairwise + # offset: int = len(self.polygon_bottom) # vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore # bottom: list[int] = list(range(offset)) From 8549ef5c0aec312b038a99412692b3e0821892c3 Mon Sep 17 00:00:00 2001 From: petras Date: Tue, 14 Jan 2025 17:58:59 +0100 Subject: [PATCH 13/17] REFACTOR beam element and add example for beam boolean_modifier.py --- docs/examples/contacts/boolean_modifier.py | 50 ++ docs/examples/contacts/contact.py | 0 src/compas_model/elements/beam.py | 753 +++++++-------------- 3 files changed, 290 insertions(+), 513 deletions(-) create mode 100644 docs/examples/contacts/boolean_modifier.py create mode 100644 docs/examples/contacts/contact.py diff --git a/docs/examples/contacts/boolean_modifier.py b/docs/examples/contacts/boolean_modifier.py new file mode 100644 index 00000000..365bd0bb --- /dev/null +++ b/docs/examples/contacts/boolean_modifier.py @@ -0,0 +1,50 @@ +from math import pi + +from compas.geometry.rotation import Rotation +from compas.geometry.translation import Translation +from compas.tolerance import TOL +from compas_model.elements import BeamSquareElement +from compas_model.models import Model +from compas_viewer import Viewer +from compas_viewer.config import Config + +# ============================================================================= +# Model +# ============================================================================= + +model = Model() + +scale = 1 +beam0: BeamSquareElement = BeamSquareElement( + width=0.2, + depth=0.3, + length=6, +) + +beam1: BeamSquareElement = BeamSquareElement( + width=0.2, + depth=0.3, + length=6, +) + +model.add_element(beam0) +model.add_element(beam1) +rotation: Rotation = Rotation.from_axis_and_angle([0, 1, 0], pi / 4, point=beam1.axis.midpoint) +translation = Translation.from_vector([0, 0.1, 0]) +beam1.transformation = translation * rotation + +model.compute_contact(beam0, beam1) # this method works when Beam class has modifier method + +# ============================================================================= +# Vizualize +# ============================================================================= +TOL.lineardeflection = 1000 + +config = Config() +viewer = Viewer(config=config) +for element in model.elements(): + viewer.scene.add(element.modelgeometry) +# viewer.scene.add(beam1.axis.midpoint) +# viewer.scene.add(beam1.axis) + +viewer.show() diff --git a/docs/examples/contacts/contact.py b/docs/examples/contacts/contact.py new file mode 100644 index 00000000..e69de29b diff --git a/src/compas_model/elements/beam.py b/src/compas_model/elements/beam.py index 1ad711fb..78a82791 100644 --- a/src/compas_model/elements/beam.py +++ b/src/compas_model/elements/beam.py @@ -1,4 +1,3 @@ -from typing import TYPE_CHECKING from typing import Optional from typing import Union @@ -21,9 +20,6 @@ from compas_model.elements.element import Feature from compas_model.interactions import BooleanModifier -if TYPE_CHECKING: - from compas_model.elements import BlockElement - class BeamFeature(Feature): pass @@ -32,7 +28,203 @@ class BeamFeature(Feature): class BeamElement(Element): """Class representing a beam element.""" - pass + @property + def length(self) -> float: + return self._length + + @length.setter + def length(self, length: float): + self._length = length + + self.section = Polygon(list(self.points)) + + self.axis = Line([0, 0, 0], [0, 0, length]) + self.frame_top = Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) + self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() + + def extend(self, distance: float) -> None: + """Extend the beam. + + Parameters + ---------- + distance : float + The distance to extend the beam. + """ + self.length = self.length + distance * 2 + xform: Transformation = Translation.from_vector([0, 0, -distance]) + self.transformation = self.transformation * xform + + def compute_aabb(self, inflate: float = 0.0) -> Box: + """Compute the axis-aligned bounding box of the element. + + Parameters + ---------- + inflate : float, optional + The inflation factor of the bounding box. + + Returns + ------- + :class:`compas.geometry.Box` + The axis-aligned bounding box. + """ + points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore + box: Box = Box.from_bounding_box(bounding_box(points)) + box.xsize += inflate + box.ysize += inflate + box.zsize += inflate + return box + + def compute_obb(self, inflate: float = 0.0) -> Box: + """Compute the oriented bounding box of the element. + + Parameters + ---------- + inflate : float, optional + The inflation factor of the bounding box. + + Returns + ------- + :class:`compas.geometry.Box` + The oriented bounding box. + """ + points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore + box: Box = Box.from_bounding_box(oriented_bounding_box(points)) + box.xsize += inflate + box.ysize += inflate + box.zsize += inflate + return box + + def compute_collision_mesh(self) -> Mesh: + """Compute the collision mesh of the element. + + Returns + ------- + :class:`compas.datastructures.Mesh` + The collision mesh. + """ + from compas.geometry import convex_hull_numpy + + points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore + vertices, faces = convex_hull_numpy(points) + vertices = [points[index] for index in vertices] # type: ignore + return Mesh.from_vertices_and_faces(vertices, faces) + + @property + def face_polygons(self) -> list[Polygon]: + return [self.geometry.face_polygon(face) for face in self.geometry.faces()] # type: ignore + + def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: + """Compute the top and bottom polygons of the beam. + + Returns + ------- + tuple[:class:`compas.geometry.Polygon`, :class:`compas.geometry.Polygon`] + """ + + plane0: Plane = Plane.from_frame(self.frame) + plane1: Plane = Plane.from_frame(self.frame_top) + points0: list[list[float]] = [] + points1: list[list[float]] = [] + for i in range(len(self.section.points)): + line: Line = Line(self.section.points[i], self.section.points[i] + self.axis.vector) + result0: Optional[list[float]] = intersection_line_plane(line, plane0) + result1: Optional[list[float]] = intersection_line_plane(line, plane1) + if not result0 or not result1: + raise ValueError("The line does not intersect the plane") + points0.append(result0) + points1.append(result1) + return Polygon(points0), Polygon(points1) + + # def compute_elementgeometry(self) -> Mesh: + # """Compute the shape of the beam from the given polygons . + # This shape is relative to the frame of the element. + + # Returns + # ------- + # :class:`compas.datastructures.Mesh` + + # """ + + # from compas.geometry import earclip_polygon + + # offset: int = len(self.polygon_bottom) + # vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore + + # triangles: list[list[int]] = earclip_polygon(Polygon(self.polygon_bottom.points)) + # top_faces: list[list[int]] = [] + # bottom_faces: list[list[int]] = [] + # for i in range(len(triangles)): + # triangle_top: list[int] = [] + # triangle_bottom: list[int] = [] + # for j in range(3): + # triangle_top.append(triangles[i][j] + offset) + # triangle_bottom.append(triangles[i][j]) + # triangle_bottom.reverse() + # top_faces.append(triangle_top) + # bottom_faces.append(triangle_bottom) + # faces: list[list[int]] = bottom_faces + top_faces + + # bottom: list[int] = list(range(offset)) + # top: list[int] = [i + offset for i in bottom] + # for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): + # faces.append([c, d, b, a]) + # mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + # return mesh + + def compute_elementgeometry(self) -> Brep: + """Compute the shape of the beam from the given polygons . + This shape is relative to the frame of the element. + + Returns + ------- + :class:`compas.datastructures.Brep` + """ + + polygons: list[Polygon] = [] + + offset: int = len(self.polygon_bottom) + vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore + polygons.append(self.polygon_bottom) + polygons.append(self.polygon_top) + + bottom: list[int] = list(range(offset)) + top: list[int] = [i + offset for i in bottom] + for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): + polygons.append(Polygon([vertices[c], vertices[d], vertices[b], vertices[a]])) + brep: Brep = Brep.from_polygons(polygons) + return brep + + def compute_contact(self, target_element: Element, type: str = "") -> BooleanModifier: + """Computes the contact interaction of the geometry of the elements that is used in the model's add_contact method. + + Returns + ------- + :class:`compas_model.interactions.BooleanModifier` + The ContactInteraction that is applied to the neighboring element. One pair can have one or multiple variants. + target_element : Element + The target element to compute the contact interaction. + type : str, optional + The type of contact interaction, if different contact are possible between the two elements. + + """ + # Traverse up to the class one before the Element class.add() + # Create a function name based on the target_element class name. + parent_class = target_element.__class__ + while parent_class.__bases__[0] != Element: + parent_class = parent_class.__bases__[0] + + parent_class_name = parent_class.__name__.lower().replace("element", "") + method_name = f"_compute_contact_with_{parent_class_name}" + method = getattr(self, method_name, None) + if method is None: + raise ValueError(f"Unsupported target element type: {type(target_element)}") + + return method(target_element, type) + + def _compute_contact_with_beam(self, target_element: "BeamElement", type: str) -> Union["BooleanModifier", None]: + # Scenario: + # A cable applies boolean difference with a block geometry. + return BooleanModifier(self.elementgeometry.transformed(self.modeltransformation)) class BeamSquareElement(BeamElement): @@ -123,135 +315,11 @@ def __init__( self.points: list[list[float]] = [[-width * 1, -depth * 0.5, 0], [-width * 1, depth * 0.5, 0], [width * 0, depth * 0.5, 0], [width * 0, -depth * 0.5, 0]] - self.section: Polygon = Polygon(self.points).translated([0, 0, 0.5 * length]) - self.axis: Line = Line([0, 0, 0], [0, 0, length]).translated([0, 0, 0.5 * length]) + self.section: Polygon = Polygon(self.points) + self.axis: Line = Line([0, 0, 0], [0, 0, length]) self.frame_top: Frame = frame_top or Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() - @property - def length(self) -> float: - return self._length - - @length.setter - def length(self, length: float): - self._length = length - - # Create the polygon of the I profile - self.section = Polygon(list(self.points)).translated([0, 0, 0.5 * length]) - - self.axis = Line([0, 0, 0], [0, 0, length]).translated([0, 0, 0.5 * length]) - self.frame_top = Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) - self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() - - @property - def face_polygons(self) -> list[Polygon]: - return [self.geometry.face_polygon(face) for face in self.geometry.faces()] # type: ignore - - def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: - """Compute the top and bottom polygons of the beam. - - Returns - ------- - tuple[:class:`compas.geometry.Polygon`, :class:`compas.geometry.Polygon`] - """ - - plane0: Plane = Plane.from_frame(self.frame) - plane1: Plane = Plane.from_frame(self.frame_top) - points0: list[list[float]] = [] - points1: list[list[float]] = [] - for i in range(len(self.section.points)): - line: Line = Line(self.section.points[i], self.section.points[i] + self.axis.vector) - result0: Optional[list[float]] = intersection_line_plane(line, plane0) - result1: Optional[list[float]] = intersection_line_plane(line, plane1) - if not result0 or not result1: - raise ValueError("The line does not intersect the plane") - points0.append(result0) - points1.append(result1) - return Polygon(points0), Polygon(points1) - - def compute_elementgeometry(self) -> Mesh: - """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. @@ -304,184 +372,48 @@ def __data__(self) -> dict: "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` - """ - - from compas.geometry import earclip_polygon - - offset: int = len(self.polygon_bottom) - vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore - - triangles: list[list[int]] = earclip_polygon(Polygon(self.polygon_bottom.points)) - top_faces: list[list[int]] = [] - bottom_faces: list[list[int]] = [] - for i in range(len(triangles)): - triangle_top: list[int] = [] - triangle_bottom: list[int] = [] - for j in range(3): - triangle_top.append(triangles[i][j] + offset) - triangle_bottom.append(triangles[i][j]) - triangle_bottom.reverse() - top_faces.append(triangle_top) - bottom_faces.append(triangle_bottom) - faces: list[list[int]] = bottom_faces + top_faces - - bottom: list[int] = list(range(offset)) - top: list[int] = [i + offset for i in bottom] - for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): - faces.append([c, d, b, a]) - mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) - return mesh - - # ============================================================================= - # 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 __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) - def compute_collision_mesh(self) -> Mesh: - """Compute the collision mesh of the element. + self.is_support: bool = is_support - Returns - ------- - :class:`compas.datastructures.Mesh` - The collision mesh. - """ - from compas.geometry import convex_hull_numpy + self.width: float = width + self.depth: float = depth + self.thickness: float = thickness + self._length: float = length - 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) + 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(self.points) + self.axis: Line = Line([0, 0, 0], [0, 0, length]) + self.frame_top: Frame = frame_top or Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) + self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() # ============================================================================= # Constructors @@ -602,216 +534,11 @@ def __init__( self.points = mirror_points_line(self.points, mirror_line) # Create the polygon of the T profile - self.section: Polygon = Polygon(list(self.points)).translated([0, 0, 0.5 * length]) - - self.axis: Line = Line([0, 0, 0], [0, 0, length]).translated([0, 0, 0.5 * length]) + self.section: Polygon = Polygon(self.points) + self.axis: Line = Line([0, 0, 0], [0, 0, length]) self.frame_top: Frame = frame_top or Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() - @property - def length(self) -> float: - return self._length - - @length.setter - def length(self, length: float): - self._length = length - - 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` - # """ - - # from compas.geometry import earclip_polygon - - # offset: int = len(self.polygon_bottom) - # vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore - - # triangles: list[list[int]] = earclip_polygon(Polygon(self.polygon_bottom.points)) - # top_faces: list[list[int]] = [] - # bottom_faces: list[list[int]] = [] - # for i in range(len(triangles)): - # triangle_top: list[int] = [] - # triangle_bottom: list[int] = [] - # for j in range(3): - # triangle_top.append(triangles[i][j] + offset) - # triangle_bottom.append(triangles[i][j]) - # triangle_bottom.reverse() - # top_faces.append(triangle_top) - # bottom_faces.append(triangle_bottom) - # faces: list[list[int]] = bottom_faces + top_faces - - # bottom: list[int] = list(range(offset)) - # top: list[int] = [i + offset for i in bottom] - # for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): - # faces.append([c, d, b, a]) - # mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) - # return mesh - - def compute_elementgeometry(self) -> Brep: - """Compute the shape of the beam from the given polygons . - This shape is relative to the frame of the element. - - Returns - ------- - :class:`compas.datastructures.Brep` - """ - - polygons: list[Polygon] = [] - - offset: int = len(self.polygon_bottom) - vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore - polygons.append(self.polygon_bottom) - polygons.append(self.polygon_top) - - bottom: list[int] = list(range(offset)) - top: list[int] = [i + offset for i in bottom] - for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): - polygons.append(Polygon([vertices[c], vertices[d], vertices[b], vertices[a]])) - brep: Brep = Brep.from_polygons(polygons) - return brep - - # ============================================================================= - # Implementations of abstract methods - # ============================================================================= - - def compute_aabb(self, inflate: float = 0.0) -> Box: - """Compute the axis-aligned bounding box of the element. - - Parameters - ---------- - inflate : float, optional - The inflation factor of the bounding box. - - Returns - ------- - :class:`compas.geometry.Box` - The axis-aligned bounding box. - """ - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - box: Box = Box.from_bounding_box(bounding_box(points)) - box.xsize += inflate - box.ysize += inflate - box.zsize += inflate - return box - - def compute_obb(self, inflate: float = 0.0) -> Box: - """Compute the oriented bounding box of the element. - - Parameters - ---------- - inflate : float, optional - The inflation factor of the bounding box. - - Returns - ------- - :class:`compas.geometry.Box` - The oriented bounding box. - """ - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - box: Box = Box.from_bounding_box(oriented_bounding_box(points)) - box.xsize += inflate - box.ysize += inflate - box.zsize += inflate - return box - - def compute_collision_mesh(self) -> Mesh: - """Compute the collision mesh of the element. - - Returns - ------- - :class:`compas.datastructures.Mesh` - The collision mesh. - """ - from compas.geometry import convex_hull_numpy - - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - vertices, faces = convex_hull_numpy(points) - vertices = [points[index] for index in vertices] # type: ignore - return Mesh.from_vertices_and_faces(vertices, faces) - - def extend(self, distance: float) -> None: - """Extend the beam. - - Parameters - ---------- - distance : float - The distance to extend the beam. - """ - self.length = self.length + distance * 2 - xform: Transformation = Translation.from_vector([0, 0, -distance]) - self.transformation = self.transformation * xform - - def compute_contact(self, target_element: Element, type: str = "") -> BooleanModifier: - """Computes the contact interaction of the geometry of the elements that is used in the model's add_contact method. - - Returns - ------- - :class:`compas_model.interactions.BooleanModifier` - The ContactInteraction that is applied to the neighboring element. One pair can have one or multiple variants. - target_element : Element - The target element to compute the contact interaction. - type : str, optional - The type of contact interaction, if different contact are possible between the two elements. - - """ - # Traverse up to the class one before the Element class.add() - # Create a function name based on the target_element class name. - parent_class = target_element.__class__ - while parent_class.__bases__[0] != Element: - parent_class = parent_class.__bases__[0] - - parent_class_name = parent_class.__name__.lower().replace("element", "") - method_name = f"_compute_contact_with_{parent_class_name}" - method = getattr(self, method_name, None) - if method is None: - raise ValueError(f"Unsupported target element type: {type(target_element)}") - - return method(target_element, type) - - def _compute_contact_with_block(self, target_element: "BlockElement", type: str) -> Union["BooleanModifier", None]: - # Scenario: - # A beam with a profile applies boolean difference with a block geometry. - if target_element.is_support: - return BooleanModifier(self.elementgeometry.transformed(self.modeltransformation)) - else: - return None - # ============================================================================= # Constructors # ============================================================================= From 8bf61e83c1ae006dea0f54254ddd170d9a754f88 Mon Sep 17 00:00:00 2001 From: petras Date: Tue, 14 Jan 2025 18:04:05 +0100 Subject: [PATCH 14/17] FIX bring back beam to column contact. --- src/compas_model/elements/beam.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/compas_model/elements/beam.py b/src/compas_model/elements/beam.py index 78a82791..c4a6e274 100644 --- a/src/compas_model/elements/beam.py +++ b/src/compas_model/elements/beam.py @@ -1,3 +1,4 @@ +from typing import TYPE_CHECKING from typing import Optional from typing import Union @@ -20,6 +21,9 @@ from compas_model.elements.element import Feature from compas_model.interactions import BooleanModifier +if TYPE_CHECKING: + from compas_model.elements import BlockElement + class BeamFeature(Feature): pass @@ -226,6 +230,14 @@ def _compute_contact_with_beam(self, target_element: "BeamElement", type: str) - # A cable applies boolean difference with a block geometry. return BooleanModifier(self.elementgeometry.transformed(self.modeltransformation)) + def _compute_contact_with_block(self, target_element: "BlockElement", type: str) -> Union["BooleanModifier", None]: + # Scenario: + # A beam with a profile applies boolean difference with a block geometry. + if target_element.is_support: + return BooleanModifier(self.elementgeometry.transformed(self.modeltransformation)) + else: + return None + class BeamSquareElement(BeamElement): """Class representing a beam element with a square section. From c426f470b20b1c4a6bbe97830c0d71dda5704514 Mon Sep 17 00:00:00 2001 From: petras Date: Tue, 14 Jan 2025 19:23:56 +0100 Subject: [PATCH 15/17] ADD-REFACTOR BRep slicer and cleanup of elements. --- data/frame.json | 2 +- docs/examples/contacts/contact.py | 49 ++++ docs/examples/masonry/002_block_model.py | 2 +- src/compas_model/algorithms/modifiers.py | 27 ++- src/compas_model/elements/beam.py | 54 +++++ src/compas_model/elements/column.py | 288 ++++++++--------------- src/compas_model/elements/column_head.py | 6 +- 7 files changed, 223 insertions(+), 205 deletions(-) diff --git a/data/frame.json b/data/frame.json index f68cabf6..f41e3aa6 100644 --- a/data/frame.json +++ b/data/frame.json @@ -1 +1 @@ -{"Model::Line::Segments": [{"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 0.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "f2396cc5-ec49-4a8a-b3bc-236d1c54e756"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 0.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "f0784e5b-af66-4787-a32b-ee7b79b5a4fc"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 0.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "c6e12893-2d6c-4b4b-a6d3-98fd287bf9c4"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 0.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "a2360552-cd42-467c-9480-a22d83527033"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 3800.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "01314dff-bc56-4aad-a715-87fdc7e67691"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 3800.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "e1b0292b-7283-4017-8b19-d74ab9ca8179"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 3800.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "5cc86d81-acf8-4725-be20-0e78bc5fff68"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 3800.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "fe67d4ee-90da-4f4a-97a3-76c0aff1a1da"}], "Model::Mesh::Floor": [{"dtype": "compas.datastructures/Mesh", "data": {"attributes": {}, "default_vertex_attributes": {"x": 0.0, "y": 0.0, "z": 0.0}, "default_edge_attributes": {}, "default_face_attributes": {}, "vertex": {"0": {"x": -3000.0, "y": -3000.0, "z": 3800.0}, "1": {"x": -3000.0, "y": 3000.0, "z": 3800.0}, "2": {"x": 3000.0, "y": 3000.0, "z": 3800.0}, "3": {"x": 3000.0, "y": -3000.0, "z": 3800.0}}, "face": {"0": [0, 1, 2, 3]}, "facedata": {"0": {}}, "edgedata": {}, "max_vertex": 3, "max_face": 0}, "guid": "8ea782e3-1036-497e-bd66-9117ef460bf6"}]} \ No newline at end of file +{"Model::Line::Segments": [{"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 0.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "f798d788-04e2-4d81-b7c8-c16b9fcd76bc"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 0.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "ea0def26-37f1-4d75-8e8b-67be87bba499"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 0.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "c2153501-a82b-431d-a451-622e20ba1a33"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 0.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "f2f6fe7b-6324-48bc-86d0-23eb4996f88a"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 3800.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "95402966-8cf1-4201-982d-6ba4c943ab85"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 3800.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "92d711c9-4194-4c5d-9c64-1b2d88b2fdbe"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 3800.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "22d6527a-a265-4b0b-91ae-5c70418636d1"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 3800.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "98aa249e-0a9c-4c77-8e37-fb905240af88"}], "Model::Mesh::Floor": [{"dtype": "compas.datastructures/Mesh", "data": {"attributes": {}, "default_vertex_attributes": {"x": 0.0, "y": 0.0, "z": 0.0}, "default_edge_attributes": {}, "default_face_attributes": {}, "vertex": {"0": {"x": -3000.0, "y": -3000.0, "z": 3800.0}, "1": {"x": -3000.0, "y": 3000.0, "z": 3800.0}, "2": {"x": 3000.0, "y": 3000.0, "z": 3800.0}, "3": {"x": 3000.0, "y": -3000.0, "z": 3800.0}}, "face": {"0": [0, 1, 2, 3]}, "facedata": {"0": {}}, "edgedata": {}, "max_vertex": 3, "max_face": 0}, "guid": "c768e1f4-3982-4275-8663-4b904833c7bf"}]} \ No newline at end of file diff --git a/docs/examples/contacts/contact.py b/docs/examples/contacts/contact.py index e69de29b..b8e012e3 100644 --- a/docs/examples/contacts/contact.py +++ b/docs/examples/contacts/contact.py @@ -0,0 +1,49 @@ +from pathlib import Path + +from compas_model.elements import ColumnHeadCrossElement +from compas_model.elements import PlateElement +from compas_viewer import Viewer +from compas_viewer.config import Config + +from compas import json_load +from compas.datastructures import Mesh +from compas.geometry import Line +from compas.geometry import Polygon +from compas_model.models import Model +from compas.tolerance import TOL + +# ============================================================================= +# Model +# ============================================================================= +model: Model = Model() + +# ============================================================================= +# Add Elements to CellNetwork Edge +# ============================================================================= + + +column_head = ColumnHeadCrossElement(width=0.150, depth=0.150, height=0.300, offset=0.210) +plate: PlateElement = PlateElement(Polygon([[-2.850, -2.850, 0], [-2.850, 2.850, 0], [2.850, 2.850, 0], [2.850, -2.850, 0]]), 0.200) + +model.add_element(column_head) +model.add_element(plate) + +# ============================================================================= +# Add Interaction +# TODO: Check with other default ColumnHead Planes to Debug compas_occ. +# ============================================================================= +model.compute_contact(column_head, plate) + +# ============================================================================= +# Vizualize +# ============================================================================= +TOL.lineardeflection = 1000 + +config = Config() +viewer = Viewer(config=config) +for element in model.elements(): + viewer.scene.add(element.modelgeometry) +# viewer.scene.add(beam1.axis.midpoint) +# viewer.scene.add(beam1.axis) + +viewer.show() diff --git a/docs/examples/masonry/002_block_model.py b/docs/examples/masonry/002_block_model.py index 86dfb98e..3645d7e2 100644 --- a/docs/examples/masonry/002_block_model.py +++ b/docs/examples/masonry/002_block_model.py @@ -1,7 +1,7 @@ +from compas.tolerance import TOL from compas_model.models import BlockModel from compas_viewer.config import Config from compas_viewer.viewer import Viewer -from compas.tolerance import TOL model: BlockModel = BlockModel.from_barrel_vault(span=6000, length=6000, thickness=250, rise=600, vou_span=5, vou_length=5) diff --git a/src/compas_model/algorithms/modifiers.py b/src/compas_model/algorithms/modifiers.py index 18a2be9b..84c20022 100644 --- a/src/compas_model/algorithms/modifiers.py +++ b/src/compas_model/algorithms/modifiers.py @@ -22,12 +22,23 @@ def slice(geometry: Union[Brep, Mesh], slice_plane: Plane) -> Union[Brep, Mesh]: :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` The sliced geometry. """ - try: - split_meshes: Optional[list] = geometry.slice(slice_plane) - return split_meshes[0] if split_meshes else geometry - except Exception: - print("SlicerModifier is not successful.") - return geometry + # print(geometry.copy().aabb()) + if isinstance(geometry, Brep): + try: + size: float = 1000 # TODO: compute bounding box and take diagonal length instead, but there is no bounding box method in Brep + splitter = Brep.from_plane(slice_plane, domain_u=(-size, size), domain_v=(-size, size)) + split_breps: Optional[list] = geometry.split(splitter) + return split_breps[0] if split_breps else geometry + except Exception: + print("SlicerModifier is not successful.") + return geometry + else: + 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): @@ -60,8 +71,8 @@ def boolean_difference(target_geometry, source_geometry): else: from compas_cgal.booleans import boolean_difference_mesh_mesh - mesh0: Mesh = target_geometry.copy() if not is_brep0 else target_geometry.to_meshes()[0] - mesh1: Mesh = source_geometry.copy() if not is_brep1 else source_geometry.to_meshes()[0] + mesh0: Mesh = target_geometry.copy() if not is_brep0 else Mesh.from_polygons(target_geometry.to_polygons()) + mesh1: Mesh = source_geometry.copy() if not is_brep1 else Mesh.from_polygons(source_geometry.to_polygons()) A = mesh0.to_vertices_and_faces(triangulated=True) B = mesh1.to_vertices_and_faces(triangulated=True) diff --git a/src/compas_model/elements/beam.py b/src/compas_model/elements/beam.py index c4a6e274..10a19b9a 100644 --- a/src/compas_model/elements/beam.py +++ b/src/compas_model/elements/beam.py @@ -332,6 +332,60 @@ def __init__( self.frame_top: Frame = frame_top or Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() + # def compute_elementgeometry(self) -> Brep: + # """Compute the shape of the beam from the given polygons . + # This shape is relative to the frame of the element. + + # Returns + # ------- + # :class:`compas.datastructures.Brep` + # """ + # box: Box = Box.from_width_height_depth(self.width, self.length, self.depth) + # box.translate( + # [ + # self.width * -0.5, + # 0, + # self.length * 0.5, + # ] + # ) + # return Brep.from_box(box) + + def compute_elementgeometry(self) -> Mesh: + """Compute the shape of the beam from the given polygons . + This shape is relative to the frame of the element. + + Returns + ------- + :class:`compas.datastructures.Mesh` + + """ + + from compas.geometry import earclip_polygon + + offset: int = len(self.polygon_bottom) + vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore + + triangles: list[list[int]] = earclip_polygon(Polygon(self.polygon_bottom.points)) + top_faces: list[list[int]] = [] + bottom_faces: list[list[int]] = [] + for i in range(len(triangles)): + triangle_top: list[int] = [] + triangle_bottom: list[int] = [] + for j in range(3): + triangle_top.append(triangles[i][j] + offset) + triangle_bottom.append(triangles[i][j]) + triangle_bottom.reverse() + top_faces.append(triangle_top) + bottom_faces.append(triangle_bottom) + faces: list[list[int]] = bottom_faces + top_faces + + bottom: list[int] = list(range(offset)) + top: list[int] = [i + offset for i in bottom] + for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): + faces.append([c, d, b, a]) + mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + return mesh + class BeamIProfileElement(BeamElement): """Class representing a beam element with I profile. diff --git a/src/compas_model/elements/column.py b/src/compas_model/elements/column.py index 5e27402f..bf6bfabf 100644 --- a/src/compas_model/elements/column.py +++ b/src/compas_model/elements/column.py @@ -25,7 +25,101 @@ class ColumnFeature(Feature): class ColumnElement(Element): """Class representing a beam element.""" - pass + @property + def face_polygons(self) -> list[Polygon]: + return [self.geometry.face_polygon(face) for face in self.geometry.faces()] # type: ignore + + @property + def height(self) -> float: + return self._height + + @height.setter + def height(self, height: float): + self._height = height + self.axis: Line = Line([0, 0, 0], [0, 0, self._height]) + self.frame_top: Frame = Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) + self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() + + def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: + """Compute the top and bottom polygons of the column. + + Returns + ------- + tuple[:class:`compas.geometry.Polygon`, :class:`compas.geometry.Polygon`] + """ + + plane0: Plane = Plane.from_frame(self.frame) + plane1: Plane = Plane.from_frame(self.frame_top) + points0: list[list[float]] = [] + points1: list[list[float]] = [] + for i in range(len(self.section.points)): + line: Line = Line(self.section.points[i], self.section.points[i] + self.axis.vector) + result0: Optional[list[float]] = intersection_line_plane(line, plane0) + result1: Optional[list[float]] = intersection_line_plane(line, plane1) + if not result0 or not result1: + raise ValueError("The line does not intersect the plane") + points0.append(result0) + points1.append(result1) + return Polygon(points0), Polygon(points1) + + # ============================================================================= + # Implementations of abstract methods + # ============================================================================= + + def compute_aabb(self, inflate: float = 0.0) -> Box: + """Compute the axis-aligned bounding box of the element. + + Parameters + ---------- + inflate : float, optional + The inflation factor of the bounding box. + + Returns + ------- + :class:`compas.geometry.Box` + The axis-aligned bounding box. + """ + points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore + box: Box = Box.from_bounding_box(bounding_box(points)) + box.xsize += inflate + box.ysize += inflate + box.zsize += inflate + return box + + def compute_obb(self, inflate: float = 0.0) -> Box: + """Compute the oriented bounding box of the element. + + Parameters + ---------- + inflate : float, optional + The inflation factor of the bounding box. + + Returns + ------- + :class:`compas.geometry.Box` + The oriented bounding box. + """ + points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore + box: Box = Box.from_bounding_box(oriented_bounding_box(points)) + box.xsize += inflate + box.ysize += inflate + box.zsize += inflate + return box + + def compute_collision_mesh(self) -> Mesh: + """Compute the collision mesh of the element. + + Returns + ------- + :class:`compas.datastructures.Mesh` + The collision mesh. + """ + from compas.geometry import convex_hull_numpy + + points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore + vertices, faces = convex_hull_numpy(points) + vertices = [points[index] for index in vertices] # type: ignore + return Mesh.from_vertices_and_faces(vertices, faces) class ColumnSquareElement(ColumnElement): @@ -114,43 +208,6 @@ def __init__( self.frame_top: Frame = frame_top or Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() - @property - def 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. @@ -184,65 +241,6 @@ def compute_elementgeometry(self) -> Brep: brep = Brep.from_box(box) return brep - # ============================================================================= - # Implementations of abstract methods - # ============================================================================= - - def compute_aabb(self, inflate: float = 0.0) -> Box: - """Compute the axis-aligned bounding box of the element. - - Parameters - ---------- - inflate : float, optional - The inflation factor of the bounding box. - - Returns - ------- - :class:`compas.geometry.Box` - The axis-aligned bounding box. - """ - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - box: Box = Box.from_bounding_box(bounding_box(points)) - box.xsize += inflate - box.ysize += inflate - box.zsize += inflate - return box - - def compute_obb(self, inflate: float = 0.0) -> Box: - """Compute the oriented bounding box of the element. - - Parameters - ---------- - inflate : float, optional - The inflation factor of the bounding box. - - Returns - ------- - :class:`compas.geometry.Box` - The oriented bounding box. - """ - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - box: Box = Box.from_bounding_box(oriented_bounding_box(points)) - box.xsize += inflate - box.ysize += inflate - box.zsize += inflate - return box - - def compute_collision_mesh(self) -> Mesh: - """Compute the collision mesh of the element. - - Returns - ------- - :class:`compas.datastructures.Mesh` - The collision mesh. - """ - from compas.geometry import convex_hull_numpy - - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - vertices, faces = convex_hull_numpy(points) - vertices = [points[index] for index in vertices] # type: ignore - return Mesh.from_vertices_and_faces(vertices, faces) - # ============================================================================= # Constructors # ============================================================================= @@ -340,43 +338,6 @@ def __init__( self.frame_top: Frame = frame_top or Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() - @property - def face_polygons(self) -> list[Polygon]: - return [self.geometry.face_polygon(face) for face in self.geometry.faces()] # type: ignore - - @property - def height(self) -> float: - return self._height - - @height.setter - def height(self, height: float): - self._height = height - self.axis: Line = Line([0, 0, 0], [0, 0, self._height]) - self.frame_top: Frame = Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) - self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() - - def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]: - """Compute the top and bottom polygons of the column. - - Returns - ------- - tuple[:class:`compas.geometry.Polygon`, :class:`compas.geometry.Polygon`] - """ - - plane0: Plane = Plane.from_frame(self.frame) - plane1: Plane = Plane.from_frame(self.frame_top) - points0: list[list[float]] = [] - points1: list[list[float]] = [] - for i in range(len(self.section.points)): - line: Line = Line(self.section.points[i], self.section.points[i] + self.axis.vector) - result0: Optional[list[float]] = intersection_line_plane(line, plane0) - result1: Optional[list[float]] = intersection_line_plane(line, plane1) - if not result0 or not result1: - raise ValueError("The line does not intersect the plane") - points0.append(result0) - points1.append(result1) - return Polygon(points0), Polygon(points1) - def compute_elementgeometry(self) -> Mesh: """Compute the shape of the column from the given polygons. This shape is relative to the frame of the element. @@ -397,65 +358,6 @@ def compute_elementgeometry(self) -> Mesh: mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) return mesh - # ============================================================================= - # Implementations of abstract methods - # ============================================================================= - - def compute_aabb(self, inflate: float = 0.0) -> Box: - """Compute the axis-aligned bounding box of the element. - - Parameters - ---------- - inflate : float, optional - The inflation factor of the bounding box. - - Returns - ------- - :class:`compas.geometry.Box` - The axis-aligned bounding box. - """ - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - box: Box = Box.from_bounding_box(bounding_box(points)) - box.xsize += inflate - box.ysize += inflate - box.zsize += inflate - return box - - def compute_obb(self, inflate: float = 0.0) -> Box: - """Compute the oriented bounding box of the element. - - Parameters - ---------- - inflate : float, optional - The inflation factor of the bounding box. - - Returns - ------- - :class:`compas.geometry.Box` - The oriented bounding box. - """ - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - box: Box = Box.from_bounding_box(oriented_bounding_box(points)) - box.xsize += inflate - box.ysize += inflate - box.zsize += inflate - return box - - def compute_collision_mesh(self) -> Mesh: - """Compute the collision mesh of the element. - - Returns - ------- - :class:`compas.datastructures.Mesh` - The collision mesh. - """ - from compas.geometry import convex_hull_numpy - - points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore - vertices, faces = convex_hull_numpy(points) - vertices = [points[index] for index in vertices] # type: ignore - return Mesh.from_vertices_and_faces(vertices, faces) - # ============================================================================= # Constructors # ============================================================================= diff --git a/src/compas_model/elements/column_head.py b/src/compas_model/elements/column_head.py index 0a5b4f2d..e0e17117 100644 --- a/src/compas_model/elements/column_head.py +++ b/src/compas_model/elements/column_head.py @@ -605,8 +605,10 @@ def _compute_contact_with_plate(self, target_element: "PlateElement", type: str) direction0 = ColumnHeadCrossElement.closest_direction(polygon[v0_prev] - polygon[v0]) # CardinalDirections direction1 = ColumnHeadCrossElement.closest_direction(polygon[v0_next] - polygon[v0]) # CardinalDirections direction_angled = ColumnHeadCrossElement.get_direction_combination(direction0, direction1) - polygon: Polygon = self.modelgeometry.face_polygon(list(self.modelgeometry.faces_where(conditions={"direction": direction_angled}))[0]) - contact_frame: Frame = polygon.frame.translated([0, 0, 0.1]) + face_ids: list[int] = list(self.modelgeometry.faces_where(conditions={"direction": direction_angled})) + face_id: int = face_ids[0] if len(face_ids) > 0 else 5 + polygon: Polygon = self.modelgeometry.face_polygon(face_id) + contact_frame: Frame = polygon.frame return ContactInterface(points=[], frame=contact_frame) From eb92c2857bd9ba5185ef2a2a271edc696eb97c09 Mon Sep 17 00:00:00 2001 From: petras Date: Wed, 15 Jan 2025 15:43:46 +0100 Subject: [PATCH 16/17] FIX slice method for Brep. --- docs/examples/contacts/boolean_modifier.py | 8 +- docs/examples/contacts/contact.py | 11 ++- src/compas_model/algorithms/modifiers.py | 9 +-- src/compas_model/elements/beam.py | 86 +++++++++++----------- 4 files changed, 54 insertions(+), 60 deletions(-) diff --git a/docs/examples/contacts/boolean_modifier.py b/docs/examples/contacts/boolean_modifier.py index 365bd0bb..5039f57a 100644 --- a/docs/examples/contacts/boolean_modifier.py +++ b/docs/examples/contacts/boolean_modifier.py @@ -14,7 +14,6 @@ model = Model() -scale = 1 beam0: BeamSquareElement = BeamSquareElement( width=0.2, depth=0.3, @@ -27,8 +26,8 @@ length=6, ) -model.add_element(beam0) -model.add_element(beam1) +beam_node = model.add_element(beam0) +model.add_element(beam1, parent=beam_node) rotation: Rotation = Rotation.from_axis_and_angle([0, 1, 0], pi / 4, point=beam1.axis.midpoint) translation = Translation.from_vector([0, 0.1, 0]) beam1.transformation = translation * rotation @@ -44,7 +43,4 @@ viewer = Viewer(config=config) for element in model.elements(): viewer.scene.add(element.modelgeometry) -# viewer.scene.add(beam1.axis.midpoint) -# viewer.scene.add(beam1.axis) - viewer.show() diff --git a/docs/examples/contacts/contact.py b/docs/examples/contacts/contact.py index b8e012e3..3e366325 100644 --- a/docs/examples/contacts/contact.py +++ b/docs/examples/contacts/contact.py @@ -1,16 +1,15 @@ from pathlib import Path -from compas_model.elements import ColumnHeadCrossElement -from compas_model.elements import PlateElement -from compas_viewer import Viewer -from compas_viewer.config import Config - from compas import json_load from compas.datastructures import Mesh from compas.geometry import Line from compas.geometry import Polygon -from compas_model.models import Model from compas.tolerance import TOL +from compas_model.elements import ColumnHeadCrossElement +from compas_model.elements import PlateElement +from compas_model.models import Model +from compas_viewer import Viewer +from compas_viewer.config import Config # ============================================================================= # Model diff --git a/src/compas_model/algorithms/modifiers.py b/src/compas_model/algorithms/modifiers.py index 84c20022..1ccef321 100644 --- a/src/compas_model/algorithms/modifiers.py +++ b/src/compas_model/algorithms/modifiers.py @@ -22,13 +22,12 @@ def slice(geometry: Union[Brep, Mesh], slice_plane: Plane) -> Union[Brep, Mesh]: :class:`compas.geometry.Brep` | :class:`compas.datastructures.Mesh` The sliced geometry. """ - # print(geometry.copy().aabb()) + if isinstance(geometry, Brep): try: - size: float = 1000 # TODO: compute bounding box and take diagonal length instead, but there is no bounding box method in Brep - splitter = Brep.from_plane(slice_plane, domain_u=(-size, size), domain_v=(-size, size)) - split_breps: Optional[list] = geometry.split(splitter) - return split_breps[0] if split_breps else geometry + geometry.make_solid() + slice_plane_flipped = Plane(slice_plane.point, -slice_plane.normal) + geometry.trim(slice_plane_flipped) except Exception: print("SlicerModifier is not successful.") return geometry diff --git a/src/compas_model/elements/beam.py b/src/compas_model/elements/beam.py index 10a19b9a..e13701be 100644 --- a/src/compas_model/elements/beam.py +++ b/src/compas_model/elements/beam.py @@ -332,59 +332,59 @@ def __init__( self.frame_top: Frame = frame_top or Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis) self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons() - # def compute_elementgeometry(self) -> Brep: - # """Compute the shape of the beam from the given polygons . - # This shape is relative to the frame of the element. - - # Returns - # ------- - # :class:`compas.datastructures.Brep` - # """ - # box: Box = Box.from_width_height_depth(self.width, self.length, self.depth) - # box.translate( - # [ - # self.width * -0.5, - # 0, - # self.length * 0.5, - # ] - # ) - # return Brep.from_box(box) - - def compute_elementgeometry(self) -> Mesh: + def compute_elementgeometry(self) -> Brep: """Compute the shape of the beam from the given polygons . This shape is relative to the frame of the element. Returns ------- - :class:`compas.datastructures.Mesh` - + :class:`compas.datastructures.Brep` """ + box: Box = Box.from_width_height_depth(self.width, self.length, self.depth) + box.translate( + [ + self.width * -0.5, + 0, + self.length * 0.5, + ] + ) + return Brep.from_box(box) + + # def compute_elementgeometry(self) -> Mesh: + # """Compute the shape of the beam from the given polygons . + # This shape is relative to the frame of the element. - from compas.geometry import earclip_polygon + # Returns + # ------- + # :class:`compas.datastructures.Mesh` - offset: int = len(self.polygon_bottom) - vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore + # """ - triangles: list[list[int]] = earclip_polygon(Polygon(self.polygon_bottom.points)) - top_faces: list[list[int]] = [] - bottom_faces: list[list[int]] = [] - for i in range(len(triangles)): - triangle_top: list[int] = [] - triangle_bottom: list[int] = [] - for j in range(3): - triangle_top.append(triangles[i][j] + offset) - triangle_bottom.append(triangles[i][j]) - triangle_bottom.reverse() - top_faces.append(triangle_top) - bottom_faces.append(triangle_bottom) - faces: list[list[int]] = bottom_faces + top_faces + # from compas.geometry import earclip_polygon - bottom: list[int] = list(range(offset)) - top: list[int] = [i + offset for i in bottom] - for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): - faces.append([c, d, b, a]) - mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) - return mesh + # offset: int = len(self.polygon_bottom) + # vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore + + # triangles: list[list[int]] = earclip_polygon(Polygon(self.polygon_bottom.points)) + # top_faces: list[list[int]] = [] + # bottom_faces: list[list[int]] = [] + # for i in range(len(triangles)): + # triangle_top: list[int] = [] + # triangle_bottom: list[int] = [] + # for j in range(3): + # triangle_top.append(triangles[i][j] + offset) + # triangle_bottom.append(triangles[i][j]) + # triangle_bottom.reverse() + # top_faces.append(triangle_top) + # bottom_faces.append(triangle_bottom) + # faces: list[list[int]] = bottom_faces + top_faces + + # bottom: list[int] = list(range(offset)) + # top: list[int] = [i + offset for i in bottom] + # for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])): + # faces.append([c, d, b, a]) + # mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces) + # return mesh class BeamIProfileElement(BeamElement): From 5895c7819ecccb9da027c8fbe7fbad39504da63e Mon Sep 17 00:00:00 2001 From: petras Date: Wed, 15 Jan 2025 15:58:28 +0100 Subject: [PATCH 17/17] FIX missing return statement in slice method. --- data/frame.json | 2 +- src/compas_model/algorithms/modifiers.py | 28 +++++++++--------------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/data/frame.json b/data/frame.json index f41e3aa6..27fffb27 100644 --- a/data/frame.json +++ b/data/frame.json @@ -1 +1 @@ -{"Model::Line::Segments": [{"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 0.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "f798d788-04e2-4d81-b7c8-c16b9fcd76bc"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 0.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "ea0def26-37f1-4d75-8e8b-67be87bba499"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 0.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "c2153501-a82b-431d-a451-622e20ba1a33"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 0.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "f2f6fe7b-6324-48bc-86d0-23eb4996f88a"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 3800.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "95402966-8cf1-4201-982d-6ba4c943ab85"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 3800.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "92d711c9-4194-4c5d-9c64-1b2d88b2fdbe"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 3800.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "22d6527a-a265-4b0b-91ae-5c70418636d1"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 3800.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "98aa249e-0a9c-4c77-8e37-fb905240af88"}], "Model::Mesh::Floor": [{"dtype": "compas.datastructures/Mesh", "data": {"attributes": {}, "default_vertex_attributes": {"x": 0.0, "y": 0.0, "z": 0.0}, "default_edge_attributes": {}, "default_face_attributes": {}, "vertex": {"0": {"x": -3000.0, "y": -3000.0, "z": 3800.0}, "1": {"x": -3000.0, "y": 3000.0, "z": 3800.0}, "2": {"x": 3000.0, "y": 3000.0, "z": 3800.0}, "3": {"x": 3000.0, "y": -3000.0, "z": 3800.0}}, "face": {"0": [0, 1, 2, 3]}, "facedata": {"0": {}}, "edgedata": {}, "max_vertex": 3, "max_face": 0}, "guid": "c768e1f4-3982-4275-8663-4b904833c7bf"}]} \ No newline at end of file +{"Model::Line::Segments": [{"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 0.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "cf28f7ab-9674-46d2-bcf8-11677a4dfc3c"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 0.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "e748b58d-50cf-47da-ac7c-696cecd38c3c"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 0.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "b861dcd1-ac54-4777-954b-fc07f5446f54"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 0.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "91d8c7f1-d691-4fe7-8eea-a77b0f09aa78"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, -3000.0, 3800.0], "end": [-3000.0, 3000.0, 3800.0]}, "guid": "b36ddcaa-b602-4e13-964d-f7938a1095a7"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, 3000.0, 3800.0], "end": [3000.0, -3000.0, 3800.0]}, "guid": "07ba1c41-4a7a-468e-8ba2-ef933dd39063"}, {"dtype": "compas.geometry/Line", "data": {"start": [-3000.0, 3000.0, 3800.0], "end": [3000.0, 3000.0, 3800.0]}, "guid": "7c70770f-da40-4456-9d60-195a56aa7021"}, {"dtype": "compas.geometry/Line", "data": {"start": [3000.0, -3000.0, 3800.0], "end": [-3000.0, -3000.0, 3800.0]}, "guid": "e2192a46-5b59-45f9-88a3-70eefc706cd0"}], "Model::Mesh::Floor": [{"dtype": "compas.datastructures/Mesh", "data": {"attributes": {}, "default_vertex_attributes": {"x": 0.0, "y": 0.0, "z": 0.0}, "default_edge_attributes": {}, "default_face_attributes": {}, "vertex": {"0": {"x": -3000.0, "y": -3000.0, "z": 3800.0}, "1": {"x": -3000.0, "y": 3000.0, "z": 3800.0}, "2": {"x": 3000.0, "y": 3000.0, "z": 3800.0}, "3": {"x": 3000.0, "y": -3000.0, "z": 3800.0}}, "face": {"0": [0, 1, 2, 3]}, "facedata": {"0": {}}, "edgedata": {}, "max_vertex": 3, "max_face": 0}, "guid": "ca32458c-6b40-4074-837a-32acb1d87e41"}]} \ No newline at end of file diff --git a/src/compas_model/algorithms/modifiers.py b/src/compas_model/algorithms/modifiers.py index 1ccef321..4cc86120 100644 --- a/src/compas_model/algorithms/modifiers.py +++ b/src/compas_model/algorithms/modifiers.py @@ -23,21 +23,18 @@ def slice(geometry: Union[Brep, Mesh], slice_plane: Plane) -> Union[Brep, Mesh]: The sliced geometry. """ - if isinstance(geometry, Brep): - try: + try: + if isinstance(geometry, Brep): geometry.make_solid() slice_plane_flipped = Plane(slice_plane.point, -slice_plane.normal) geometry.trim(slice_plane_flipped) - except Exception: - print("SlicerModifier is not successful.") return geometry - else: - try: + else: split_meshes: Optional[list] = geometry.slice(slice_plane) return split_meshes[0] if split_meshes else geometry - except Exception: - print("SlicerModifier is not successful.") - return geometry + except Exception: + print("SlicerModifier is not successful.") + return geometry def boolean_difference(target_geometry, source_geometry): @@ -57,24 +54,19 @@ def boolean_difference(target_geometry, source_geometry): The geometry after boolean difference. """ - is_brep0: bool = isinstance(target_geometry, Brep) - is_brep1: bool = isinstance(source_geometry, Brep) - - if is_brep0 and is_brep1: + if isinstance(target_geometry, Brep) and isinstance(source_geometry, Brep): try: return Brep.from_boolean_difference(target_geometry, source_geometry) except Exception: print("Boolean difference is not successful.") return target_geometry - else: from compas_cgal.booleans import boolean_difference_mesh_mesh - mesh0: Mesh = target_geometry.copy() if not is_brep0 else Mesh.from_polygons(target_geometry.to_polygons()) - mesh1: Mesh = source_geometry.copy() if not is_brep1 else Mesh.from_polygons(source_geometry.to_polygons()) + mesh0: Mesh = target_geometry.copy() if not isinstance(target_geometry, Brep) else Mesh.from_polygons(target_geometry.to_polygons()) + mesh1: Mesh = source_geometry.copy() if not isinstance(source_geometry, Brep) else Mesh.from_polygons(source_geometry.to_polygons()) A = 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 + return Mesh.from_vertices_and_faces(V, F) if V and F else mesh0