So, it’s all working fairly well except for these 2 issues:
- The tree view is not in leaf selection mode, that is I cannot select deselect the root nodes of the tree no matter which options I choose
- Using the card, the select and tree control are rendered twice. Once in the card, and again at the bottom of the screen. Here’s the code
My dataset is a pvd file that consists of multiblock data
from pathlib import Path
from trame.app import TrameApp
from trame.ui.vuetify3 import VAppLayout
from trame.widgets import vuetify3 as v3, vtklocal, html
from trame.decorators import change
from paraview.modules.vtkPVVTKExtensionsIOCore import vtkPVDReader
import vtk
OVERLAY_TOP_LEFT = "position: absolute; top: 1rem; left: 1rem;z-index: 1; width: 20rem; max-width: 22rem; background: rgba(255,255,255,0.1);"
v3.enable_lab()
class PVDViewer(TrameApp):
def _build_multiblock_tree(self, dataset, id_counter, renderer, lut, actors, block_id_to_actor, block_ids, fields):
import vtk
tree = []
if hasattr(dataset, "GetNumberOfBlocks") and dataset.GetNumberOfBlocks() > 0:
for i in range(dataset.GetNumberOfBlocks()):
child = dataset.GetBlock(i)
name = dataset.GetMetaData(i).Get(vtk.vtkCompositeDataSet.NAME()) if dataset.GetMetaData(i) and dataset.GetMetaData(i).Has(vtk.vtkCompositeDataSet.NAME()) else f"Block {i}"
node_id = next(id_counter)
if hasattr(child, "GetNumberOfBlocks") and child.GetNumberOfBlocks() > 0:
children = self._build_multiblock_tree(child, id_counter, renderer, lut, actors, block_id_to_actor, block_ids, fields)
tree.append({"id": node_id, "name": name, "children": children})
elif child and child.IsA('vtkDataSet'):
# Only create actor for leaf
mapper = vtk.vtkDataSetMapper()
mapper.SetInputData(child)
mapper.SetLookupTable(lut)
actor = vtk.vtkActor(mapper=mapper)
renderer.AddActor(actor)
actors.append(actor)
block_id_to_actor[node_id] = actor
block_ids.append(node_id)
# Collect point data arrays
pd = child.GetPointData()
arr_names = [pd.GetArrayName(j) for j in range(pd.GetNumberOfArrays())]
for name_field in arr_names:
if name_field:
fields.add(name_field)
tree.append({"id": node_id, "name": name})
else:
tree.append({"id": node_id, "name": name})
return tree
def __init__(self, server=None):
super().__init__(server)
# CLI handling
self.server.cli.add_argument("--data", required=True)
args, _ = self.server.cli.parse_known_args()
# Init app
self._setup_vtk(args.data)
self._build_ui()
def _setup_vtk(self, file_to_load):
reader = vtkPVDReader(
file_name=str(Path(file_to_load).resolve())
)
reader.Update()
renderer = vtk.vtkRenderer(background=(0.5, 0.5, 0.5))
render_window = vtk.vtkRenderWindow()
render_window.AddRenderer(renderer)
render_window.OffScreenRenderingOn()
interactor = vtk.vtkRenderWindowInteractor()
interactor.SetRenderWindow(render_window)
interactor.GetInteractorStyle().SetCurrentStyleToTrackballCamera()
# Create a shared 12-band rainbow lookup table
lut = vtk.vtkLookupTable()
lut.SetNumberOfTableValues(12)
lut.SetHueRange(0.66667, 0.0) # VTK rainbow: blue to red
lut.SetTableRange(0, 1) # Will be set to data range later
lut.SetRampToLinear()
lut.SetNumberOfColors(12)
lut.Build()
self.lut = lut
# Add all blocks as actors and collect all unique point data arrays
output = reader.GetOutputDataObject(0)
fields = set()
actors = []
self._block_id_to_actor = {}
self._tree_data = []
self._block_ids = []
def id_counter_gen():
n = 1
while True:
yield n
n += 1
id_counter = id_counter_gen()
if output and output.IsA('vtkMultiBlockDataSet'):
print("MultiBlockDataSet with blocks")
self._tree_data = self._build_multiblock_tree(output, id_counter, renderer, lut, actors, self._block_id_to_actor, self._block_ids, fields)
elif output and output.IsA('vtkDataSet'):
print("Single vtkDataSet")
node_id = 1
mapper = vtk.vtkDataSetMapper()
mapper.SetInputData(output)
mapper.SetLookupTable(lut)
actor = vtk.vtkActor(mapper=mapper)
renderer.AddActor(actor)
actors.append(actor)
self._block_id_to_actor[node_id] = actor
self._block_ids = [node_id]
self._tree_data = [{"id": node_id, "name": "Block 0"}]
pd = output.GetPointData()
arr_names = [pd.GetArrayName(i) for i in range(pd.GetNumberOfArrays())]
print(f"Single block arrays: {arr_names}")
for name in arr_names:
if name:
fields.add(name)
renderer.ResetCamera()
self.reader = reader
self.actors = actors
self.render_window = render_window
self.fields = sorted(fields)
self.server.state.tree_data = self._tree_data
self.server.state.visible_blocks = list(self._block_ids)
@change("visible_blocks")
def _on_visible_blocks(self, visible_blocks, **_):
# Set actor visibility based on selected block ids
for block_id, actor in self._block_id_to_actor.items():
actor.SetVisibility(block_id in visible_blocks)
self.ctx.view.update()
@change("color_by")
def _on_color_by(self, color_by, **_):
if not color_by:
for actor in getattr(self, 'actors', []):
mapper = actor.GetMapper()
mapper.SetScalarVisibility(0)
return
# First, find the global min/max for the selected array across all blocks
global_min = None
global_max = None
for idx, actor in enumerate(getattr(self, 'actors', [])):
mapper = actor.GetMapper()
ds = mapper.GetInput()
pd = ds.GetPointData() if ds else None
arr = pd.GetArray(color_by) if pd else None
if arr is not None:
arr_range = arr.GetRange()
if global_min is None or arr_range[0] < global_min:
global_min = arr_range[0]
if global_max is None or arr_range[1] > global_max:
global_max = arr_range[1]
print(f"Global range for '{color_by}': {global_min}, {global_max}")
# Set the LUT range globally
if hasattr(self, 'lut') and global_min is not None and global_max is not None:
self.lut.SetTableRange(global_min, global_max)
self.lut.Build()
# Now, apply this range to all actors that have the array
for idx, actor in enumerate(getattr(self, 'actors', [])):
mapper = actor.GetMapper()
ds = mapper.GetInput()
pd = ds.GetPointData() if ds else None
arr_names = [pd.GetArrayName(i) for i in range(pd.GetNumberOfArrays())] if pd else []
print(f"Block {idx} arrays: {arr_names}")
arr = pd.GetArray(color_by) if pd else None
print(f"Trying to color by: '{color_by}'")
if arr is not None and global_min is not None and global_max is not None:
mapper.SetScalarVisibility(1)
mapper.SetColorModeToMapScalars()
mapper.SelectColorArray(color_by)
mapper.SetScalarModeToUsePointFieldData()
mapper.SetScalarRange(global_min, global_max)
print(f"{color_by} set to global range: {global_min}, {global_max}")
else:
mapper.SetScalarVisibility(0)
print(f"Block missing array '{color_by}', not colored.")
# Update view
self.ctx.view.update()
def _build_ui(self):
print(self._block_ids)
print(self._tree_data)
with VAppLayout(self.server, full_height=True) as self.ui:
with v3.VMain():
# 3D view
vtklocal.LocalView(
self.render_window,
ctx_name="view",
)
# Floating card with treeview and select
v3.VCard(
style=OVERLAY_TOP_LEFT,
children=[
v3.VSelect(
v_model=("color_by", None),
items=("fields", self.fields),
variant="outlined",
hide_details=True,
density="compact",
style="background: rgba(255,255,255,0.1);",
),
v3.VTreeview(
v_model_selected="visible_blocks",
items=("tree_data", self._tree_data),
selectable=True,
active_strategy ="leaf",
select_strategy="leaf",
open_all=True,
item_title="name",
item_value="id",
variant="outlined",
hide_details=True,
density="compact",
style="margin-bottom: 1rem; background: rgba(255,255,255,0.1);",
),
],
)
def main():
app = PVDViewer()
app.server.start()
if __name__ == "__main__":
main()