diff --git a/CHANGELOG.md b/CHANGELOG.md index 133bef32..e43a6dd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `Element.compute_elementgeometry()`. * Added `Element.compute_modelgeometry()` to replace `Element.compute_geometry()`. * Added `Element.compute_modeltransformation()` to replace `Element.compute_worldtransformation()`. +* Added `Element.is_dirty`. ### Changed diff --git a/docs/api.rst b/docs/api.rst index 6d119177..e9f2bd2d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2,14 +2,14 @@ API Reference ******************************************************************************** + .. toctree:: :maxdepth: 1 :titlesonly: - api/compas_model.algorithms api/compas_model.elements api/compas_model.interactions api/compas_model.materials api/compas_model.models api/compas_model.notebook - api/compas_model.scene + api/compas_model.scene \ No newline at end of file diff --git a/docs/api/compas_model.algorithms.rst b/docs/api/compas_model.algorithms.rst deleted file mode 100644 index b518d2af..00000000 --- a/docs/api/compas_model.algorithms.rst +++ /dev/null @@ -1,14 +0,0 @@ -******************************************************************************** -compas_model.algorithms -******************************************************************************** - -.. currentmodule:: compas_model.algorithms - -Functions -========= - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - blockmodel_interfaces \ No newline at end of file diff --git a/docs/api/compas_model.models.rst b/docs/api/compas_model.models.rst index cd9ec05a..23fb894f 100644 --- a/docs/api/compas_model.models.rst +++ b/docs/api/compas_model.models.rst @@ -13,6 +13,6 @@ Classes ElementNode ElementTree - GroupNode InteractionGraph Model + BlockModel \ No newline at end of file diff --git a/docs/api/compas_model.notebook.rst b/docs/api/compas_model.notebook.rst index 551c46de..f1ab209c 100644 --- a/docs/api/compas_model.notebook.rst +++ b/docs/api/compas_model.notebook.rst @@ -4,7 +4,6 @@ compas_model.notebook .. currentmodule:: compas_model.notebook - Classes ======= @@ -12,12 +11,5 @@ Classes :toctree: generated/ :nosignatures: - scene.ThreeBlockObject - - -Functions -========= - -.. autosummary:: - :toctree: generated/ - :nosignatures: + ThreeBlockObject + ThreeModelObject \ No newline at end of file diff --git a/docs/api/compas_model.scene.rst b/docs/api/compas_model.scene.rst index 9178f00e..32b9c15a 100644 --- a/docs/api/compas_model.scene.rst +++ b/docs/api/compas_model.scene.rst @@ -4,7 +4,6 @@ compas_model.scene .. currentmodule:: compas_model.scene - Classes ======= @@ -12,4 +11,6 @@ Classes :toctree: generated/ :nosignatures: - ElementObject \ No newline at end of file + ElementObject + BlockObject + ModelObject \ No newline at end of file diff --git a/docs/examples/dem/000_stack.rst b/docs/examples/dem/000_stack.rst index 819ce9d1..2e6e8a0b 100644 --- a/docs/examples/dem/000_stack.rst +++ b/docs/examples/dem/000_stack.rst @@ -38,5 +38,5 @@ As a result, the stack requires equilibriating "glue" forces (in red) at those i Code ==== -.. literalinclude:: 100_stack.py - :language: python +.. .. literalinclude:: 100_stack.py +.. :language: python diff --git a/src/compas_model/__init__.py b/src/compas_model/__init__.py index 20dab4b0..f2e82699 100644 --- a/src/compas_model/__init__.py +++ b/src/compas_model/__init__.py @@ -22,4 +22,7 @@ "compas_model.rhino", "compas_model.rhino.scene", "compas_model.notebook.scene", + "compas_model.interactions", + "compas_model.materials", + "compas_model.models", ] diff --git a/src/compas_model/algorithms/interfaces.py b/src/compas_model/algorithms/interfaces.py index 265b6256..48a240fa 100644 --- a/src/compas_model/algorithms/interfaces.py +++ b/src/compas_model/algorithms/interfaces.py @@ -1,11 +1,12 @@ from math import fabs +from shapely.geometry import Polygon as ShapelyPolygon + from compas.datastructures import Mesh from compas.geometry import Frame from compas.geometry import Plane from compas.geometry import Polygon from compas.geometry import Transformation -from compas.geometry import Vector from compas.geometry import bestfit_frame_numpy from compas.geometry import centroid_polygon from compas.geometry import is_colinear @@ -13,8 +14,6 @@ from compas.geometry import is_parallel_vector_vector from compas.geometry import transform_points from compas.itertools import window -from shapely.geometry import Polygon as ShapelyPolygon - from compas_model.elements import BlockGeometry from compas_model.interactions import ContactInterface from compas_model.models import Model diff --git a/src/compas_model/algorithms/overlaps.py b/src/compas_model/algorithms/overlaps.py index 77ef6135..6d12ec99 100644 --- a/src/compas_model/algorithms/overlaps.py +++ b/src/compas_model/algorithms/overlaps.py @@ -1,7 +1,5 @@ from compas.geometry import Brep from compas.tolerance import TOL -from compas_occ.brep import OCCBrepFace as BrepFace - from compas_model.interactions import ContactInterface from compas_model.models import Model @@ -104,6 +102,12 @@ def brep_brep_overlaps( This means that if the """ + + try: + from compas_occ.brep import OCCBrepFace as BrepFace + except ImportError: + raise ImportError("compas_occ is required for this functionality. Please install it via conda.") + faces_A, faces_B = A.overlap(B, deflection=deflection, tolerance=tolerance) faces_A: list[BrepFace] faces_B: list[BrepFace] diff --git a/src/compas_model/analysis/cra.py b/src/compas_model/analysis/cra.py index c5bc2b43..2e68297f 100644 --- a/src/compas_model/analysis/cra.py +++ b/src/compas_model/analysis/cra.py @@ -1,7 +1,3 @@ -from compas_assembly.datastructures import Assembly -from compas_assembly.datastructures import Block -from compas_cra.equilibrium import cra_penalty_solve as _cra_penalty_solve - from compas_model.interactions import ContactInterface from compas_model.models import Model @@ -15,6 +11,14 @@ def cra_penalty_solve( verbose: bool = False, timer: bool = False, ): + try: + from compas_cra.equilibrium import cra_penalty_solve as _cra_penalty_solve + + from compas_assembly.datastructures import Assembly + from compas_assembly.datastructures import Block + except ImportError: + raise ImportError("compas_cra, compas_assembly is required for this functionality. Please install it via conda.") + assembly = Assembly() element_block = {} diff --git a/src/compas_model/elements/block.py b/src/compas_model/elements/block.py index d2e5e1f4..c3e0661c 100644 --- a/src/compas_model/elements/block.py +++ b/src/compas_model/elements/block.py @@ -14,7 +14,6 @@ from compas.geometry import oriented_bounding_box from compas.geometry import volume_polyhedron from compas.geometry.brep.brep import Brep - from compas_model.elements import Element from compas_model.elements import Feature diff --git a/src/compas_model/elements/element.py b/src/compas_model/elements/element.py index 7012d2f7..74a126e8 100644 --- a/src/compas_model/elements/element.py +++ b/src/compas_model/elements/element.py @@ -12,7 +12,6 @@ from compas.geometry import Frame from compas.geometry import Shape from compas.geometry import Transformation - from compas_model.materials import Material if TYPE_CHECKING: @@ -93,6 +92,8 @@ class Element(Data): Scaling factor to inflate the AABB with. inflate_obb : float Scaling factor to inflate the OBB with. + is_dirty : bool + Flag to indicate that modelgeometry has to be recomputed. """ @@ -141,6 +142,8 @@ def __init__( self.inflate_aabb = 0.0 self.inflate_obb = 0.0 + self._is_dirty = True + # this is not entirely correct def __repr__(self) -> str: return f"Element(frame={self.frame!r}, name={self.name})" @@ -178,6 +181,19 @@ def parent(self) -> "ElementNode": def features(self) -> list[Feature]: return self._features + @property + def is_dirty(self): + return self._is_dirty + + @is_dirty.setter + def is_dirty(self, value): + self._is_dirty = value + + if value: + elements = list(self.model.elements()) + for neighbor in self.model.graph.neighbors_out(self.graphnode): + elements[neighbor].is_dirty = value + # ========================================================================== # Computed attributes # ========================================================================== @@ -291,7 +307,24 @@ def compute_modelgeometry(self) -> Union[Brep, Mesh]: :class:`compas.datastructures.Mesh` | :class:`compas.geometry.Brep` """ - raise NotImplementedError + 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)): # graph.edge_modififers() + # Interaction types: Modifier, Collision, Interface + if hasattr(interaction, "modify"): # Modifier + modelgeometry = interaction.modify(modelgeometry, elements[neighbor].modelgeometry) + elif hasattr(interaction, "collide"): # Collision: + modelgeometry = interaction.collide(modelgeometry, elements[neighbor].modelgeometry) + elif hasattr(interaction, "overlap"): # Interface: + modelgeometry = interaction.touch(modelgeometry, elements[neighbor].modelgeometry) + + self.is_dirty = False + + return modelgeometry def compute_aabb(self) -> Box: """Computes the Axis Aligned Bounding Box (AABB) of the geometry of the element. diff --git a/src/compas_model/elements/plate.py b/src/compas_model/elements/plate.py index 73bc9062..95c10db1 100644 --- a/src/compas_model/elements/plate.py +++ b/src/compas_model/elements/plate.py @@ -4,7 +4,6 @@ from compas.geometry import bounding_box from compas.geometry import oriented_bounding_box from compas.itertools import pairwise - from compas_model.elements import Element from compas_model.elements import Feature diff --git a/src/compas_model/models/blockmodel.py b/src/compas_model/models/blockmodel.py index 039a0569..9a8b6034 100644 --- a/src/compas_model/models/blockmodel.py +++ b/src/compas_model/models/blockmodel.py @@ -2,8 +2,6 @@ from compas.geometry import Brep from compas.tolerance import Tolerance -from compas_occ.brep import OCCBrepFace as BrepFace - from compas_model.algorithms.nnbrs import find_nearest_neighbours from compas_model.elements import Element from compas_model.interactions import ContactInterface @@ -22,6 +20,11 @@ def compute_overlaps(self, deflection=None, tolerance=1, max_distance=50, min_ar pass def compute_interfaces(self, deflection=None, tolerance=1, max_distance=50, min_area=0, nmax=10): + try: + from compas_occ.brep import OCCBrepFace as BrepFace + except ImportError: + raise ImportError("compas_occ is required for this functionality. Please install it via conda.") + deflection = deflection or Tolerance().lineardeflection node_index = {node: index for index, node in enumerate(self.graph.nodes())} diff --git a/src/compas_model/models/elementnode.py b/src/compas_model/models/elementnode.py index f3b82cc5..8f45a15d 100644 --- a/src/compas_model/models/elementnode.py +++ b/src/compas_model/models/elementnode.py @@ -2,7 +2,6 @@ from typing import Optional from compas.datastructures import TreeNode - from compas_model.elements import Element if TYPE_CHECKING: diff --git a/src/compas_model/models/elementtree.py b/src/compas_model/models/elementtree.py index b5e71391..f66e6771 100644 --- a/src/compas_model/models/elementtree.py +++ b/src/compas_model/models/elementtree.py @@ -1,7 +1,6 @@ from typing import Optional from compas.datastructures import Tree - from compas_model.elements import Element from .elementnode import ElementNode diff --git a/src/compas_model/models/interactiongraph.py b/src/compas_model/models/interactiongraph.py index 07059a83..44036a12 100644 --- a/src/compas_model/models/interactiongraph.py +++ b/src/compas_model/models/interactiongraph.py @@ -2,7 +2,6 @@ from typing import Optional from compas.datastructures import Graph - from compas_model.elements import Element # noqa: F401 from compas_model.interactions import Interaction # noqa: F401 diff --git a/src/compas_model/models/model.py b/src/compas_model/models/model.py index 73c1e834..d4e04fce 100644 --- a/src/compas_model/models/model.py +++ b/src/compas_model/models/model.py @@ -7,7 +7,6 @@ from compas.datastructures import Datastructure from compas.geometry import Frame from compas.geometry import Transformation - from compas_model.elements import Element from compas_model.interactions import Interaction from compas_model.materials import Material @@ -380,6 +379,8 @@ def add_interaction(self, a: Element, b: Element, interaction: Optional[Interact interactions.append(interaction) self.graph.edge_attribute(edge, name="interactions", value=interactions) + self._guid_element[str(b.guid)].is_dirty = True + return edge def remove_element(self, element: Element) -> None: @@ -398,6 +399,9 @@ def remove_element(self, element: Element) -> None: guid = str(element.guid) if guid not in self._guid_element: raise Exception("Element not in the model.") + + self._guid_element[guid].is_dirty = True + del self._guid_element[guid] self.graph.delete_node(element.graphnode) @@ -420,6 +424,9 @@ def remove_interaction(self, a: Element, b: Element, interaction: Optional[Inter if interaction: raise NotImplementedError + elements = list(self.elements()) + elements[b.graphnode]._is_dirty = True + edge = a.graphnode, b.graphnode if self.graph.has_edge(edge): self.graph.delete_edge(edge) diff --git a/src/compas_model/notebook/__init__.py b/src/compas_model/notebook/__init__.py index e69de29b..ad8b196b 100644 --- a/src/compas_model/notebook/__init__.py +++ b/src/compas_model/notebook/__init__.py @@ -0,0 +1,6 @@ +from .scene import ThreeBlockObject, ThreeModelObject + +__all__ = [ + "ThreeBlockObject", + "ThreeModelObject", +] diff --git a/src/compas_model/notebook/scene/__init__.py b/src/compas_model/notebook/scene/__init__.py index e219c18d..3696e440 100644 --- a/src/compas_model/notebook/scene/__init__.py +++ b/src/compas_model/notebook/scene/__init__.py @@ -20,9 +20,3 @@ def register_scene_objects(): register(Model, ThreeModelObject, context="Notebook") # print("PyThreeJS Model elements registered.") - - -__all__ = [ - "ThreeBlockObject", - "ThreeModelObjec", -] diff --git a/src/compas_model/notebook/scene/blockobject.py b/src/compas_model/notebook/scene/blockobject.py index d40dbfa2..1360ed45 100644 --- a/src/compas_model/notebook/scene/blockobject.py +++ b/src/compas_model/notebook/scene/blockobject.py @@ -1,11 +1,11 @@ -import compas.datastructures # noqa: F401 -import compas.geometry # noqa: F401 import numpy import pythreejs as three -from compas.geometry import Polygon -from compas.geometry import earclip_polygon from compas_notebook.scene import ThreeSceneObject +import compas.datastructures # noqa: F401 +import compas.geometry # noqa: F401 +from compas.geometry import Polygon +from compas.geometry import earclip_polygon from compas_model.scene import BlockObject diff --git a/src/compas_model/notebook/scene/modelobject.py b/src/compas_model/notebook/scene/modelobject.py index 0bf80aca..fb157bd6 100644 --- a/src/compas_model/notebook/scene/modelobject.py +++ b/src/compas_model/notebook/scene/modelobject.py @@ -1,7 +1,7 @@ -import compas.datastructures # noqa: F401 -import compas.geometry # noqa: F401 from compas_notebook.scene import ThreeSceneObject +import compas.datastructures # noqa: F401 +import compas.geometry # noqa: F401 from compas_model.scene import ElementObject from compas_model.scene import ModelObject diff --git a/src/compas_model/scene/elementobject.py b/src/compas_model/scene/elementobject.py index a59fea20..8f3c5a6f 100644 --- a/src/compas_model/scene/elementobject.py +++ b/src/compas_model/scene/elementobject.py @@ -3,12 +3,11 @@ from __future__ import print_function import compas.geometry # noqa: F401 +import compas_model.elements # noqa: F401 from compas.colors import Color from compas.scene import SceneObject from compas.scene.descriptors.colordict import ColorDictAttribute -import compas_model.elements # noqa: F401 - class ElementObject(SceneObject): """Base class for all element scene objects. diff --git a/src/compas_model/scene/modelobject.py b/src/compas_model/scene/modelobject.py index fafb2355..7b72c1f6 100644 --- a/src/compas_model/scene/modelobject.py +++ b/src/compas_model/scene/modelobject.py @@ -3,9 +3,8 @@ from __future__ import print_function import compas.geometry # noqa: F401 -from compas.scene import SceneObject - import compas_model.models # noqa: F401 +from compas.scene import SceneObject class ModelObject(SceneObject): diff --git a/src/compas_model/viewers/blockmodelviewer.py b/src/compas_model/viewers/blockmodelviewer.py index ab7e1056..1de2ae0b 100644 --- a/src/compas_model/viewers/blockmodelviewer.py +++ b/src/compas_model/viewers/blockmodelviewer.py @@ -2,16 +2,19 @@ from compas.datastructures import Mesh from compas.geometry import Line from compas.itertools import remap_values -from compas_viewer import Viewer -from compas_viewer.components import Button -from compas_viewer.components.slider import Slider -from compas_viewer.scene import GroupObject - from compas_model.elements import BlockElement from compas_model.elements import BlockGeometry from compas_model.interactions import ContactInterface from compas_model.models import Model +try: + from compas_viewer import Viewer + from compas_viewer.components import Button + from compas_viewer.components.slider import Slider + from compas_viewer.scene import GroupObject +except ImportError: + raise print("compas_viewer is required for this functionality. Please install it via conda.") + def toggle_supports(): viewer = BlockModelViewer() diff --git a/tests/test_element.py b/tests/test_element.py new file mode 100644 index 00000000..4edc0c11 --- /dev/null +++ b/tests/test_element.py @@ -0,0 +1,163 @@ +from compas_model.models import Model +from compas_model.elements import Element +from compas_model.interactions import Interaction +from compas.datastructures import Mesh +from typing import Optional + +from compas.geometry import Frame +from compas.geometry import Box +from compas.geometry import Transformation + + +class MyElement(Element): + """Class representing an element for testing.""" + + def __init__( + self, + size: float = 0.1, + frame: Frame = Frame.worldXY(), + transformation: Optional[Transformation] = None, + name: Optional[str] = None, + ) -> "MyElement": + super().__init__(frame=frame, transformation=transformation, name=name) + + self.size: float = size + + def compute_elementgeometry(self) -> Mesh: + """Element geometry in the local frame. + + Returns + ------- + :class:`compas.datastructures.Mesh` + + """ + + return Box(self.size, self.size, self.size, self.frame).to_mesh() + + +def test_is_dirty_setter(): + model = Model() + a = MyElement(name="a") + b = MyElement(name="b") + c = MyElement(name="c") + d = MyElement(name="d") + model.add_element(a) + model.add_element(b) + model.add_element(c) + model.add_element(d) + model.add_interaction(a, c, interaction=Interaction(name="i_a_c")) # a affects c + model.add_interaction(a, b, interaction=Interaction(name="i_b_c")) # a affects b + a.is_dirty = False + b.is_dirty = False + c.is_dirty = False + d.is_dirty = False + + elements = list(model.elements()) + + assert not elements[0].is_dirty + assert not elements[1].is_dirty + assert not elements[2].is_dirty + assert not elements[3].is_dirty + + elements[0].is_dirty = True + + assert elements[0].is_dirty + assert elements[1].is_dirty + assert elements[2].is_dirty + assert not elements[3].is_dirty + + +def test_is_dirty_add_interaction(): + model = Model() + a = MyElement(name="a") + b = MyElement(name="b") + c = MyElement(name="c") + d = MyElement(name="d") + model.add_element(a) + model.add_element(b) + model.add_element(c) + model.add_element(d) + + model.add_interaction(a, b, interaction=Interaction(name="i_a_b")) # c affects a + for element in model.elements(): + element.modelgeometry # All elements is_dirty is set to False + model.add_interaction(a, c, interaction=Interaction(name="i_a_c")) # c affects b + + elements = list(model.elements()) + assert not elements[0].is_dirty + assert not elements[1].is_dirty + assert elements[2].is_dirty + assert not elements[3].is_dirty + + +def test_is_dirty_remove_interaction(): + model = Model() + a = MyElement(name="a") + b = MyElement(name="b") + c = MyElement(name="c") + d = MyElement(name="d") + model.add_element(a) + model.add_element(b) + model.add_element(c) + model.add_element(d) + model.add_interaction(a, b, interaction=Interaction(name="i_a_b")) # a affects b + model.add_interaction(a, c, interaction=Interaction(name="i_a_c")) # a affects c + + for element in model.elements(): + element.is_dirty = False + model.remove_interaction(a, b) # a affects b + model.remove_interaction(a, c) # a affects c + + elements = list(model.elements()) + assert not elements[0].is_dirty + assert elements[1].is_dirty + assert elements[2].is_dirty + assert not elements[3].is_dirty + + +def test_is_dirty_remove_element_0(): + model = Model() + a = MyElement(name="a") + b = MyElement(name="b") + c = MyElement(name="c") + d = MyElement(name="d") + model.add_element(a) + model.add_element(b) + model.add_element(c) + model.add_element(d) + model.add_interaction(a, b, interaction=Interaction(name="i_a_b")) # a affects b + model.add_interaction(a, c, interaction=Interaction(name="i_a_c")) # a affects c + + for element in model.elements(): + element.modelgeometry # All element is_dirty is set to False + + model.remove_element(a) # b and c is_dirty is set to True + + elements = list(model.elements()) + assert elements[0].is_dirty + assert elements[1].is_dirty + assert not elements[2].is_dirty + + +def test_is_dirty_remove_element_1(): + model = Model() + a = MyElement(name="a") + b = MyElement(name="b") + c = MyElement(name="c") + d = MyElement(name="d") + model.add_element(a) + model.add_element(b) + model.add_element(c) + model.add_element(d) + model.add_interaction(a, b, interaction=Interaction(name="i_a_b")) # a affects b + model.add_interaction(a, c, interaction=Interaction(name="i_a_c")) # a affects c + + for element in model.elements(): + element.modelgeometry # All element is_dirty is set to False + + model.remove_element(b) # b and c is_dirty is set to True + + elements = list(model.elements()) + assert not elements[0].is_dirty + assert not elements[1].is_dirty + assert not elements[2].is_dirty