diff --git a/CHANGELOG.md b/CHANGELOG.md index 37b6917d..4c98226e 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/src/compas_model/elements/element.py b/src/compas_model/elements/element.py index e217128e..17f2d4ee 100644 --- a/src/compas_model/elements/element.py +++ b/src/compas_model/elements/element.py @@ -92,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. """ @@ -140,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})" @@ -177,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 # ========================================================================== @@ -290,7 +307,18 @@ 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()) # noqa: F841 + 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)): + pass # TODO: apply interaction + + 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/models/model.py b/src/compas_model/models/model.py index 831d6d55..efafaba9 100644 --- a/src/compas_model/models/model.py +++ b/src/compas_model/models/model.py @@ -379,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: @@ -397,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) @@ -419,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/viewers/blockmodelviewer.py b/src/compas_model/viewers/blockmodelviewer.py index a94e9426..b4925102 100644 --- a/src/compas_model/viewers/blockmodelviewer.py +++ b/src/compas_model/viewers/blockmodelviewer.py @@ -6,6 +6,10 @@ from compas_model.elements import BlockGeometry from compas_model.interactions import ContactInterface from compas_model.models import Model +from compas_viewer import Viewer +from compas_viewer.components import Button +from compas_viewer.components.slider import Slider +from compas_viewer.scene import GroupObject try: from compas_viewer import Viewer 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