diff --git a/geos-trame/src/geos/trame/app/components/alertHandler.py b/geos-trame/src/geos/trame/app/components/alertHandler.py
index 230b041..ea206d6 100644
--- a/geos-trame/src/geos/trame/app/components/alertHandler.py
+++ b/geos-trame/src/geos/trame/app/components/alertHandler.py
@@ -5,6 +5,19 @@
from trame.widgets import vuetify3
+from enum import Enum
+
+
+class AlertType( str, Enum ):
+ """Enum representing the type of VAlert.
+
+ For more information, see the uetify documentation:
+ https://vuetifyjs.com/en/api/VAlert/#props-type
+ """
+ SUCCESS = 'success'
+ WARNING = 'warning'
+ ERROR = 'error'
+
class AlertHandler( vuetify3.VContainer ):
"""Vuetify component used to display an alert status.
@@ -26,8 +39,9 @@ def __init__( self ) -> None:
self.state.alerts = []
- self.ctrl.on_add_error.add_task( self.add_error )
- self.ctrl.on_add_warning.add_task( self.add_warning )
+ self.server.controller.on_add_success.add_task( self.add_success )
+ self.server.controller.on_add_warning.add_task( self.add_warning )
+ self.server.controller.on_add_error.add_task( self.add_error )
self.generate_alert_ui()
@@ -75,7 +89,7 @@ def add_alert( self, type: str, title: str, message: str ) -> None:
self.state.dirty( "alerts" )
self.state.flush()
- if type == "warning":
+ if type == AlertType.WARNING:
asyncio.get_event_loop().call_later( self.__lifetime_of_alert, self.on_close, alert_id )
async def add_warning( self, title: str, message: str ) -> None:
@@ -86,6 +100,10 @@ async def add_error( self, title: str, message: str ) -> None:
"""Add an alert of type 'error'."""
self.add_alert( "error", title, message )
+ async def add_success( self, title: str, message: str ) -> None:
+ """Add an alert of type 'success'."""
+ self.add_alert( AlertType.SUCCESS, title, message )
+
def on_close( self, alert_id: int ) -> None:
"""Remove in the state the alert associated to the given id."""
self.state.alerts = list( filter( lambda i: i[ "id" ] != alert_id, self.state.alerts ) )
diff --git a/geos-trame/src/geos/trame/app/core.py b/geos-trame/src/geos/trame/app/core.py
index 1f66b96..0a8f409 100644
--- a/geos-trame/src/geos/trame/app/core.py
+++ b/geos-trame/src/geos/trame/app/core.py
@@ -61,7 +61,7 @@ def __init__( self, server: Server, file_name: str ) -> None:
self.ctrl.simput_reload_data = self.simput_widget.reload_data
# Tree
- self.tree = DeckTree( self.state.sm_id )
+ self.tree = DeckTree( self.state.sm_id, self.ctrl )
# Viewers
self.region_viewer = RegionViewer()
diff --git a/geos-trame/src/geos/trame/app/data_types/renderable.py b/geos-trame/src/geos/trame/app/data_types/renderable.py
index e031240..af26a62 100644
--- a/geos-trame/src/geos/trame/app/data_types/renderable.py
+++ b/geos-trame/src/geos/trame/app/data_types/renderable.py
@@ -6,6 +6,7 @@
class Renderable( Enum ):
"""Enum class for renderable types and their ids."""
+ BOX = "Box"
VTKMESH = "VTKMesh"
INTERNALMESH = "InternalMesh"
INTERNALWELL = "InternalWell"
diff --git a/geos-trame/src/geos/trame/app/deck/tree.py b/geos-trame/src/geos/trame/app/deck/tree.py
index 4979f3b..b732dde 100644
--- a/geos-trame/src/geos/trame/app/deck/tree.py
+++ b/geos-trame/src/geos/trame/app/deck/tree.py
@@ -4,26 +4,28 @@
import os
from collections import defaultdict
from typing import Any
-
import dpath
import funcy
from pydantic import BaseModel
-from trame_simput import get_simput_manager
+
from xsdata.formats.dataclass.parsers.config import ParserConfig
from xsdata.formats.dataclass.serializers.config import SerializerConfig
from xsdata.utils import text
from xsdata_pydantic.bindings import DictDecoder, XmlContext, XmlSerializer
+from trame_server.controller import Controller
+from trame_simput import get_simput_manager
+
from geos.trame.app.deck.file import DeckFile
from geos.trame.app.geosTrameException import GeosTrameException
-from geos.trame.app.utils.file_utils import normalize_path, format_xml
from geos.trame.schema_generated.schema_mod import Problem, Included, File, Functions
+from geos.trame.app.utils.file_utils import normalize_path, format_xml
class DeckTree( object ):
"""A tree that represents a deck file along with all the available blocks and parameters."""
- def __init__( self, sm_id: str | None = None, **kwargs: Any ) -> None:
+ def __init__( self, sm_id: str | None = None, ctrl: Controller = None, **kwargs: Any ) -> None:
"""Constructor."""
super( DeckTree, self ).__init__( **kwargs )
@@ -33,6 +35,7 @@ def __init__( self, sm_id: str | None = None, **kwargs: Any ) -> None:
self.root = None
self.input_has_errors = False
self._sm_id = sm_id
+ self._ctrl = ctrl
def set_input_file( self, input_filename: str ) -> None:
"""Set a new input file.
@@ -172,6 +175,8 @@ def write_files( self ) -> None:
file.write( model_as_xml )
file.close()
+ self._ctrl.on_add_success( title="File saved", message=f"File {basename} has been saved." )
+
@staticmethod
def _append_include_file( model: Problem, included_file_path: str ) -> None:
"""Append an Included object which follows this structure according to the documentation.
diff --git a/geos-trame/src/geos/trame/app/ui/viewer/boxViewer.py b/geos-trame/src/geos/trame/app/ui/viewer/boxViewer.py
new file mode 100644
index 0000000..05b59f1
--- /dev/null
+++ b/geos-trame/src/geos/trame/app/ui/viewer/boxViewer.py
@@ -0,0 +1,111 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
+# SPDX-FileContributor: Lucas Givord - Kitware
+import pyvista as pv
+
+from geos.trame.schema_generated.schema_mod import Box
+
+import re
+
+
+class BoxViewer:
+ """A BoxViewer represents a Box and its intersected cell in a mesh.
+
+ This mesh is represented in GEOS with a Box.
+ """
+
+ def __init__( self, mesh: pv.UnstructuredGrid, box: Box ) -> None:
+ """Initialize the BoxViewer with a mesh and a box."""
+ self._mesh: pv.UnstructuredGrid = mesh
+
+ self._box: Box = box
+ self._box_polydata: pv.PolyData = None
+ self._box_polydata_actor: pv.Actor = None
+
+ self._extracted_cell: pv.UnstructuredGrid = None
+ self._extracted_cell_actor: pv.Actor = None
+
+ self._compute_box_as_polydata()
+ self._compute_intersected_cell()
+
+ def append_to_plotter( self, plotter: pv.Plotter ) -> None:
+ """Append the box and the intersected cell to the plotter.
+
+ The box is represented as a polydata with a low opacity.
+ """
+ self._box_polydata_actor = plotter.add_mesh( self._box_polydata, opacity=0.2 )
+
+ if self._extracted_cell is not None:
+ self._extracted_cell_actor = plotter.add_mesh( self._extracted_cell, show_edges=True )
+
+ def reset( self, plotter: pv.Plotter ) -> None:
+ """Reset the box viewer by removing the box and the intersected cell from the plotter."""
+ if self._box_polydata_actor is not None:
+ plotter.remove_actor( self._box_polydata_actor )
+
+ if self._extracted_cell_actor is not None:
+ plotter.remove_actor( self._extracted_cell_actor )
+
+ self._box_polydata = None
+ self._extracted_cell = None
+
+ def _compute_box_as_polydata( self ) -> None:
+ """Create a polydata reresenting a BBox using pyvista and coordinates from the Geos Box."""
+ bounding_box: list[ float ] = self._retrieve_bounding_box()
+ self._box_polydata = pv.Box( bounds=bounding_box )
+
+ def _retrieve_bounding_box( self ) -> list[ float ]:
+ """This method converts bounding box information from Box into a list of coordinates readable by pyvista.
+
+ e.g., this Box:
+
+
+
+ will return [1150, 1250, 700, 800, 62, 137].
+ """
+ # split str and remove brackets
+ min_point_str = re.findall( r"-?\d+\.\d+|-?\d+", self._box.x_min )
+ max_point_str = re.findall( r"-?\d+\.\d+|-?\d+", self._box.x_max )
+
+ min_point = list( map( float, min_point_str ) )
+ max_point = list( map( float, max_point_str ) )
+
+ return [
+ min_point[ 0 ],
+ max_point[ 0 ],
+ min_point[ 1 ],
+ max_point[ 1 ],
+ min_point[ 2 ],
+ max_point[ 2 ],
+ ]
+
+ def _compute_intersected_cell( self ) -> None:
+ """Extract the cells from the mesh that are inside the box."""
+ ids = self._mesh.find_cells_within_bounds( self._box_polydata.bounds )
+
+ saved_ids: list[ int ] = []
+
+ for id in ids:
+ cell: pv.vtkCell = self._mesh.GetCell( id )
+
+ is_inside = self._check_cell_inside_box( cell, self._box_polydata.bounds )
+ if is_inside:
+ saved_ids.append( id )
+
+ if len( saved_ids ) > 0:
+ self._extracted_cell = self._mesh.extract_cells( saved_ids )
+
+ def _check_cell_inside_box( self, cell: pv.Cell, box_bounds: list[ float ] ) -> bool:
+ """Check if the cell is inside the box bounds.
+
+ A cell is considered inside the box if his bounds are completely
+ inside the box bounds.
+ """
+ cell_bounds = cell.GetBounds()
+ is_inside_in_x = cell_bounds[ 0 ] >= box_bounds[ 0 ] and cell_bounds[ 1 ] <= box_bounds[ 1 ]
+ is_inside_in_y = cell_bounds[ 2 ] >= box_bounds[ 2 ] and cell_bounds[ 3 ] <= box_bounds[ 3 ]
+ is_inside_in_z = cell_bounds[ 4 ] >= box_bounds[ 4 ] and cell_bounds[ 5 ] <= box_bounds[ 5 ]
+
+ return is_inside_in_x and is_inside_in_y and is_inside_in_z
diff --git a/geos-trame/src/geos/trame/app/ui/viewer/viewer.py b/geos-trame/src/geos/trame/app/ui/viewer/viewer.py
index 8064204..fea98c0 100644
--- a/geos-trame/src/geos/trame/app/ui/viewer/viewer.py
+++ b/geos-trame/src/geos/trame/app/ui/viewer/viewer.py
@@ -11,10 +11,11 @@
from vtkmodules.vtkRenderingCore import vtkActor
from geos.trame.app.deck.tree import DeckTree
+from geos.trame.app.ui.viewer.boxViewer import BoxViewer
from geos.trame.app.ui.viewer.perforationViewer import PerforationViewer
from geos.trame.app.ui.viewer.regionViewer import RegionViewer
from geos.trame.app.ui.viewer.wellViewer import WellViewer
-from geos.trame.schema_generated.schema_mod import Vtkmesh, Vtkwell, InternalWell, Perforation
+from geos.trame.schema_generated.schema_mod import Box, Vtkmesh, Vtkwell, InternalWell, Perforation
pv.OFF_SCREEN = True
@@ -59,6 +60,7 @@ def __init__(
self.SELECTED_DATA_ARRAY = "viewer_selected_data_array"
self.state.change( self.SELECTED_DATA_ARRAY )( self._update_actor_array )
+ self.box_engine: BoxViewer | None = None
self.region_engine = region_viewer
self.well_engine = well_viewer
self._perforations: dict[ str, PerforationViewer ] = {}
@@ -138,6 +140,24 @@ def update_viewer( self, active_block: BaseModel, path: str, show_obj: bool ) ->
if isinstance( active_block, Perforation ):
self._update_perforation( active_block, show_obj, path )
+ if isinstance( active_block, Box ):
+ if self.region_engine.input.number_of_cells == 0 and show_obj:
+ self.ctrl.on_add_warning(
+ "Can't display " + active_block.name,
+ "Please display the mesh before creating a well",
+ )
+ return
+
+ if self.box_engine is not None:
+ self.box_engine.reset( self.plotter )
+
+ if not show_obj:
+ return
+
+ box: Box = active_block
+ self.box_engine = BoxViewer( self.region_engine.input, box )
+ self.box_engine.append_to_plotter( self.plotter )
+
def _on_clip_visibility_change( self, **kwargs: Any ) -> None:
"""Toggle cut plane visibility for all actors.
@@ -215,6 +235,7 @@ def _update_internalwell( self, path: str, show: bool ) -> None:
"""
if not show:
self.plotter.remove_actor( self.well_engine.get_actor( path ) ) # type: ignore
+ self.well_engine.remove_actor( path )
return
tube_actor = self.plotter.add_mesh( self.well_engine.get_tube( self.well_engine.get_last_mesh_idx() ) )
@@ -229,6 +250,7 @@ def _update_vtkwell( self, path: str, show: bool ) -> None:
"""
if not show:
self.plotter.remove_actor( self.well_engine.get_actor( path ) ) # type: ignore
+ self.well_engine.remove_actor( path )
return
tube_actor = self.plotter.add_mesh( self.well_engine.get_tube( self.well_engine.get_last_mesh_idx() ) )
diff --git a/geos-trame/src/geos/trame/app/ui/viewer/wellViewer.py b/geos-trame/src/geos/trame/app/ui/viewer/wellViewer.py
index 4196f7b..237d50c 100644
--- a/geos-trame/src/geos/trame/app/ui/viewer/wellViewer.py
+++ b/geos-trame/src/geos/trame/app/ui/viewer/wellViewer.py
@@ -17,6 +17,12 @@ class Well:
well_path: str
polyline: pv.PolyData
tube: pv.PolyData
+
+
+@dataclass
+class WellActor:
+ """A WellActor stores the VTK Actor representing a well."""
+ well_path: str
actor: pv.Actor
@@ -28,6 +34,7 @@ def __init__( self, size: float, amplification: float ) -> None:
A Well in GEOS could a InternalWell or a Vtkwell.
"""
self._wells: list[ Well ] = []
+ self._wells_actors: list[ WellActor ] = []
self.size: float = size
self.amplification: float = amplification
@@ -51,7 +58,8 @@ def add_mesh( self, mesh: pv.PolyData, mesh_path: str ) -> int:
radius = self.size * ( self.STARTING_VALUE / 100 )
tube = mesh.tube( radius=radius, n_sides=50 )
- self._wells.append( Well( mesh_path, mesh, tube, pv.Actor() ) )
+ self._wells.append( Well( mesh_path, mesh, tube ) )
+ self._wells_actors.append( WellActor( mesh_path, pv.Actor() ) )
return len( self._wells ) - 1
@@ -78,21 +86,21 @@ def get_tube_size( self ) -> float:
def append_actor( self, perforation_path: str, tube_actor: pv.Actor ) -> None:
"""Append a given actor, typically the Actor returned by the pv.Plotter() when a given mes is added."""
- index = self._get_index_from_perforation( perforation_path )
+ index = self._get_actor_index_from_perforation( perforation_path )
if index == -1:
print( "Cannot found the well to remove from path: ", perforation_path )
return
- self._wells[ index ].actor = tube_actor
+ self._wells_actors[ index ].actor = tube_actor
def get_actor( self, perforation_path: str ) -> pv.Actor | None:
"""Retrieve the polyline linked to a given perforation path."""
- index = self._get_index_from_perforation( perforation_path )
+ index = self._get_actor_index_from_perforation( perforation_path )
if index == -1:
print( "Cannot found the well to remove from path: ", perforation_path )
return None
- return self._wells[ index ].actor
+ return self._wells_actors[ index ].actor
def update( self, value: float ) -> None:
"""Update the radius of the tubes."""
@@ -108,6 +116,14 @@ def remove( self, perforation_path: str ) -> None:
self._wells.remove( self._wells[ index ] )
+ def remove_actor( self, perforation_path: str ) -> None:
+ """Clear all data stored in this class."""
+ index = self._get_actor_index_from_perforation( perforation_path )
+ if index == -1:
+ print( "Cannot found the well to remove from path: ", perforation_path )
+
+ self._wells_actors.remove( self._wells_actors[ index ] )
+
def _get_index_from_perforation( self, perforation_path: str ) -> int:
"""Retrieve the well associated to a given perforation, otherwise return -1."""
index = -1
@@ -121,6 +137,19 @@ def _get_index_from_perforation( self, perforation_path: str ) -> int:
return index
+ def _get_actor_index_from_perforation( self, perforation_path: str ) -> int:
+ """Retrieve the well actor associated to a given perforation, otherwise return -1."""
+ index = -1
+ if len( self._wells_actors ) == 0:
+ return index
+
+ for i in range( 0, len( self._wells_actors ) ):
+ if self._wells_actors[ i ].well_path in perforation_path:
+ index = i
+ break
+
+ return index
+
def get_number_of_wells( self ) -> int:
"""Get the number of wells in the viewer."""
return len( self._wells )
diff --git a/geos-trame/tests/data/geosDeck/geosDeck.xml b/geos-trame/tests/data/geosDeck/geosDeck.xml
index a4a7de8..efff1d4 100644
--- a/geos-trame/tests/data/geosDeck/geosDeck.xml
+++ b/geos-trame/tests/data/geosDeck/geosDeck.xml
@@ -127,8 +127,8 @@
+ xMin="{ 3509, 4117, -596 }"
+ xMax="{ 4482, 5041, -500 }"/>
diff --git a/geos-trame/tests/test_box_intersection.py b/geos-trame/tests/test_box_intersection.py
new file mode 100644
index 0000000..22b39f0
--- /dev/null
+++ b/geos-trame/tests/test_box_intersection.py
@@ -0,0 +1,36 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
+# SPDX-FileContributor: Kitware
+# ruff: noqa
+from pathlib import Path
+
+from trame_server import Server
+from trame_server.state import State
+from trame_vuetify.ui.vuetify3 import VAppLayout
+
+from geos.trame.app.core import GeosTrame
+from tests.trame_fixtures import trame_state, trame_server_layout
+
+
+def test_box_intersection( trame_server_layout: tuple[ Server, VAppLayout ] ) -> None:
+ """Test box intersection."""
+ root_path = Path( __file__ ).parent.absolute().__str__()
+ file_name = root_path + "/data/geosDeck/geosDeck.xml"
+
+ app = GeosTrame( trame_server_layout[ 0 ], file_name )
+ app.state.ready()
+
+ app.deckInspector.state.object_state = [ "Problem/Mesh/0/VTKMesh/0", True ]
+ app.deckInspector.state.flush()
+
+ app.deckInspector.state.object_state = [ "Problem/Geometry/0/Box/0", True ]
+ app.deckInspector.state.flush()
+
+ box = app.deckViewer.box_engine._box
+ cells = app.deckViewer.box_engine._extracted_cell
+
+ assert box is not None
+ assert box.x_min == '{ 3509, 4117, -596 }'
+ assert box.x_max == '{ 4482, 5041, -500 }'
+ assert cells is not None
+ assert cells.number_of_cells == 1