Skip to content

Commit d5b9ad4

Browse files
committed
feat(viewer): add support for intersected cell from Box
1 parent ffe81fb commit d5b9ad4

File tree

5 files changed

+172
-2
lines changed

5 files changed

+172
-2
lines changed

geos-trame/geos_trame/app/data_types/renderable.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
class Renderable( Enum ):
88
"""Enum class for renderable types and their ids."""
9+
BOX = "Box"
910
VTKMESH = "VTKMesh"
1011
INTERNALMESH = "InternalMesh"
1112
INTERNALWELL = "InternalWell"
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
3+
# SPDX-FileContributor: Lucas Givord - Kitware
4+
import pyvista as pv
5+
6+
from geos_trame.schema_generated.schema_mod import Box
7+
8+
import re
9+
10+
11+
class BoxViewer:
12+
"""A BoxViewer represents a Box and its intersected cell in a mesh.
13+
14+
This mesh is represented in GEOS with a Box.
15+
"""
16+
17+
def __init__( self, mesh: pv.UnstructuredGrid, box: Box ) -> None:
18+
"""Initialize the BoxViewer with a mesh and a box."""
19+
self._mesh: pv.UnstructuredGrid = mesh
20+
21+
self._box: Box = box
22+
self._box_polydata: pv.PolyData = None
23+
self._box_polydata_actor: pv.Actor = None
24+
25+
self._extracted_cell: pv.UnstructuredGrid = None
26+
self._extracted_cell_actor: pv.Actor = None
27+
28+
self._compute_box_as_polydata()
29+
self._compute_intersected_cell()
30+
31+
def append_to_plotter( self, plotter: pv.Plotter ) -> None:
32+
"""Append the box and the intersected cell to the plotter.
33+
34+
The box is represented as a polydata with a low opacity.
35+
"""
36+
self._box_polydata_actor = plotter.add_mesh( self._box_polydata, opacity=0.2 )
37+
38+
if self._extracted_cell is not None:
39+
self._extracted_cell_actor = plotter.add_mesh( self._extracted_cell, show_edges=True )
40+
41+
def reset( self, plotter: pv.Plotter ) -> None:
42+
"""Reset the box viewer by removing the box and the intersected cell from the plotter."""
43+
if self._box_polydata_actor is not None:
44+
plotter.remove_actor( self._box_polydata_actor )
45+
46+
if self._extracted_cell_actor is not None:
47+
plotter.remove_actor( self._extracted_cell_actor )
48+
49+
self._box_polydata = None
50+
self._extracted_cell = None
51+
52+
def _compute_box_as_polydata( self ) -> None:
53+
"""Create a polydata reresenting a BBox using pyvista and coordinates from the Geos Box."""
54+
bounding_box: list[ float ] = self._retrieve_bounding_box()
55+
self._box_polydata = pv.Box( bounds=bounding_box )
56+
57+
def _retrieve_bounding_box( self ) -> list[ float ]:
58+
"""This method converts bounding box information from Box into a list of coordinates readable by pyvista.
59+
60+
e.g., this Box:
61+
62+
<Box name="box_1"
63+
xMin="{ 1150, 700, 62 }"
64+
xMax="{ 1250, 800, 137 }"/>
65+
66+
will return [1150, 1250, 700, 800, 62, 137].
67+
"""
68+
# split str and remove brackets
69+
min_point_str = re.findall( r"-?\d+\.\d+|-?\d+", self._box.x_min )
70+
max_point_str = re.findall( r"-?\d+\.\d+|-?\d+", self._box.x_max )
71+
72+
min_point = list( map( float, min_point_str ) )
73+
max_point = list( map( float, max_point_str ) )
74+
75+
return [
76+
min_point[ 0 ],
77+
max_point[ 0 ],
78+
min_point[ 1 ],
79+
max_point[ 1 ],
80+
min_point[ 2 ],
81+
max_point[ 2 ],
82+
]
83+
84+
def _compute_intersected_cell( self ) -> None:
85+
"""Extract the cells from the mesh that are inside the box."""
86+
ids = self._mesh.find_cells_within_bounds( self._box_polydata.bounds )
87+
88+
saved_ids: list[ int ] = []
89+
90+
for id in ids:
91+
cell: pv.vtkCell = self._mesh.GetCell( id )
92+
93+
is_inside = self._check_cell_inside_box( cell, self._box_polydata.bounds )
94+
if is_inside:
95+
saved_ids.append( id )
96+
97+
if len( saved_ids ) > 0:
98+
self._extracted_cell = self._mesh.extract_cells( saved_ids )
99+
100+
def _check_cell_inside_box( self, cell: pv.Cell, box_bounds: list[ float ] ) -> bool:
101+
"""Check if the cell is inside the box bounds.
102+
103+
A cell is considered inside the box if his bounds are completely
104+
inside the box bounds.
105+
"""
106+
cell_bounds = cell.GetBounds()
107+
is_inside_in_x = cell_bounds[ 0 ] >= box_bounds[ 0 ] and cell_bounds[ 1 ] <= box_bounds[ 1 ]
108+
is_inside_in_y = cell_bounds[ 2 ] >= box_bounds[ 2 ] and cell_bounds[ 3 ] <= box_bounds[ 3 ]
109+
is_inside_in_z = cell_bounds[ 4 ] >= box_bounds[ 4 ] and cell_bounds[ 5 ] <= box_bounds[ 5 ]
110+
111+
return is_inside_in_x and is_inside_in_y and is_inside_in_z

geos-trame/geos_trame/app/ui/viewer/viewer.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
from vtkmodules.vtkRenderingCore import vtkActor
1212

1313
from geos_trame.app.deck.tree import DeckTree
14+
from geos_trame.app.ui.viewer.boxViewer import BoxViewer
1415
from geos_trame.app.ui.viewer.perforationViewer import PerforationViewer
1516
from geos_trame.app.ui.viewer.regionViewer import RegionViewer
1617
from geos_trame.app.ui.viewer.wellViewer import WellViewer
1718
from geos_trame.schema_generated.schema_mod import (
19+
Box,
1820
Vtkmesh,
1921
Vtkwell,
2022
Perforation,
@@ -40,6 +42,7 @@ def __init__(
4042
- Vtkwell,
4143
- Perforation,
4244
- InternalWell
45+
- Box
4346
4447
Everything is handle in the method 'update_viewer()' which is trigger when the
4548
'state.object_state' changed (see DeckTree).
@@ -57,6 +60,7 @@ def __init__(
5760
self.server.state[ self.CUT_PLANE ] = True
5861
self.server.state[ self.ZAMPLIFICATION ] = 1
5962

63+
self.box_engine: BoxViewer | None = None
6064
self.region_engine = region_viewer
6165
self.well_engine = well_viewer
6266
self._perforations: dict[ str, PerforationViewer ] = {}
@@ -123,6 +127,24 @@ def update_viewer( self, active_block: BaseModel, path: str, show_obj: bool ) ->
123127
if isinstance( active_block, Perforation ):
124128
self._update_perforation( active_block, show_obj, path )
125129

130+
if isinstance( active_block, Box ):
131+
if self.region_engine.input.number_of_cells == 0 and show_obj:
132+
self.ctrl.on_add_warning(
133+
"Can't display " + active_block.name,
134+
"Please display the mesh before creating a well",
135+
)
136+
return
137+
138+
if self.box_engine is not None:
139+
self.box_engine.reset( self.plotter )
140+
141+
if not show_obj:
142+
return
143+
144+
box: Box = active_block
145+
self.box_engine = BoxViewer( self.region_engine.input, box )
146+
self.box_engine.append_to_plotter( self.plotter )
147+
126148
def _on_clip_visibility_change( self, **kwargs: Any ) -> None:
127149
"""Toggle cut plane visibility for all actors.
128150

geos-trame/tests/data/geosDeck/geosDeck.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@
127127
<Geometry>
128128
<Box
129129
name="box_1"
130-
xMin="{ 1150, 700, 62 }"
131-
xMax="{ 1250, 800, 137 }"/>
130+
xMin="{ 3509, 4117, -596 }"
131+
xMax="{ 4482, 5041, -500 }"/>
132132
</Geometry>
133133
<!-- End Block to comment to deactivate box definition -->
134134

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright 2023-2024 TotalEnergies.
3+
# SPDX-FileContributor: Kitware
4+
# ruff: noqa
5+
from pathlib import Path
6+
7+
from trame_server import Server
8+
from trame_server.state import State
9+
from trame_vuetify.ui.vuetify3 import VAppLayout
10+
11+
from geos_trame.app.core import GeosTrame
12+
from tests.trame_fixtures import trame_state, trame_server_layout
13+
14+
15+
def test_box_intersection( trame_server_layout: tuple[ Server, VAppLayout ] ) -> None:
16+
"""Test box intersection."""
17+
root_path = Path( __file__ ).parent.absolute().__str__()
18+
file_name = root_path + "/data/geosDeck/geosDeck.xml"
19+
20+
app = GeosTrame( trame_server_layout[ 0 ], file_name )
21+
app.state.ready()
22+
23+
app.deckInspector.state.object_state = [ "Problem/Mesh/0/VTKMesh/0", True ]
24+
app.deckInspector.state.flush()
25+
26+
app.deckInspector.state.object_state = [ "Problem/Geometry/0/Box/0", True ]
27+
app.deckInspector.state.flush()
28+
29+
box = app.deckViewer.box_engine._box
30+
cells = app.deckViewer.box_engine._extracted_cell
31+
32+
assert box is not None
33+
assert box.x_min == '{ 3509, 4117, -596 }'
34+
assert box.x_max == '{ 4482, 5041, -500 }'
35+
assert cells is not None
36+
assert cells.number_of_cells == 1

0 commit comments

Comments
 (0)