Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.1.13
current_version = 0.1.14
commit = True
tag = True

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "quickview"
version = "0.1.13"
version = "0.1.14"
description = "An application to explore/analyze data for atmosphere component for E3SM"
authors = [
{name = "Kitware Inc."},
Expand Down
2 changes: 1 addition & 1 deletion quickview/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""QuickView: Visual Analysis for E3SM Atmosphere Data."""

__version__ = "0.1.13"
__version__ = "0.1.14"
__author__ = "Kitware Inc."
__license__ = "Apache-2.0"
4 changes: 3 additions & 1 deletion quickview/ui/projection_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,7 @@ def update_pipeline_interactive(self, **kwargs):
projection = self.state.projection
self.source.UpdateProjection(projection)
self.source.UpdatePipeline()
self.views.update_views_for_timestep()
# For projection changes, we need to fit viewports to new bounds
self.views.update_views_for_timestep(fit_viewport=True)
# Render once after all updates
self.views.render_all_views()
4 changes: 4 additions & 0 deletions quickview/ui/slice_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,15 @@ def update_pipeline_interactive(self, **kwargs):
tstamp = self.state.tstamp
long = self.state.cliplong
lat = self.state.cliplat

self.source.UpdateLev(lev, ilev)
self.source.UpdateTimeStep(tstamp)
self.source.ApplyClipping(long, lat)
self.source.UpdatePipeline()

# update_views_for_timestep will handle fitting and rendering
self.views.update_views_for_timestep()
# Render once after all updates
self.views.render_all_views()

def on_click_advance_middle(self, diff):
Expand Down
65 changes: 0 additions & 65 deletions quickview/utils/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,71 +39,6 @@ def calculate_weighted_average(
return np.mean(data)


def calculate_aspect_ratio_scale(
bounds: List[float], viewport_size: Tuple[float, float], margin: float = 1.05
) -> Optional[float]:
"""
Calculate optimal ParallelScale for ParaView camera based on data bounds and viewport.

This function determines the appropriate camera scale to fit objects within
the viewport while maintaining aspect ratio.

Args:
bounds: Data bounds [xmin, xmax, ymin, ymax, zmin, zmax]
viewport_size: Viewport dimensions (width, height) in pixels
margin: Margin factor (1.05 = 5% margin around objects)

Returns:
Optimal parallel scale value for the camera, or None if calculation fails
"""
if not bounds or len(bounds) < 4:
return None

# Calculate data dimensions
width = bounds[1] - bounds[0]
height = bounds[3] - bounds[2]

if width <= 0 or height <= 0:
return None

if viewport_size[0] <= 0 or viewport_size[1] <= 0:
return None

viewport_aspect = viewport_size[0] / viewport_size[1]
data_aspect = width / height

# Calculate optimal parallel scale
# The parallel scale represents half the height of the view in world coordinates
if data_aspect > viewport_aspect:
# Data is wider than viewport - fit to width
parallel_scale = (width / (2.0 * viewport_aspect)) * margin
else:
# Data is taller than viewport - fit to height
parallel_scale = (height / 2.0) * margin

return parallel_scale


def calculate_data_center(bounds: List[float]) -> List[float]:
"""
Calculate the center point of data bounds.

Args:
bounds: Data bounds [xmin, xmax, ymin, ymax, zmin, zmax]

Returns:
Center point [x, y, z]
"""
if not bounds or len(bounds) < 6:
return [0, 0, 0]

return [
(bounds[0] + bounds[1]) / 2,
(bounds[2] + bounds[3]) / 2,
(bounds[4] + bounds[5]) / 2,
]


def calculate_data_range(bounds: List[float]) -> Tuple[float, float, float]:
"""
Calculate the range (width, height, depth) from data bounds.
Expand Down
138 changes: 39 additions & 99 deletions quickview/view_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,11 @@
from quickview.pipeline import EAMVisSource
from quickview.utils.color import get_cached_colorbar_image
from quickview.utils.geometry import generate_annotations as generate_map_annotations
from quickview.utils.math import (
calculate_weighted_average,
calculate_aspect_ratio_scale,
calculate_data_center,
)
from quickview.utils.math import calculate_weighted_average
from quickview.utils.state import ViewContext, ViewRegistry

# Constants for camera and display
LABEL_OFFSET_FACTOR = 0.075 # Factor for offsetting labels from map edge
CAMERA_Z_OFFSET = 1000 # Z-axis offset for 2D camera positioning
ZOOM_IN_FACTOR = 0.95 # Scale factor for zooming in
ZOOM_OUT_FACTOR = 1.05 # Scale factor for zooming out
DEFAULT_MARGIN = 1.05 # Default margin for viewport fitting (5% margin)
Expand Down Expand Up @@ -103,11 +98,19 @@ def get_color_range(self, var, index):
else:
return self.compute_range(var)

def update_views_for_timestep(self):
def update_views_for_timestep(self, fit_viewport=True):
"""Update views for timestep changes.

Args:
fit_viewport: Whether to fit viewport after update (default True).
Set to False to avoid redundant fits when caller will do it.
"""
if len(self.registry) == 0:
return
data = sm.Fetch(self.source.views["atmosphere_data"])

first_view = None

for var, context in self.registry.items():
varavg = self.compute_average(var, vtkdata=data)
# Directly set average in trame state
Expand All @@ -128,9 +131,14 @@ def update_views_for_timestep(self):
self.state.dirty("varmax")

self.generate_colorbar_image(context.index)
# Fit objects optimally after geometry changes
if context.view_proxy:
self.fit_to_viewport(context.view_proxy)

# Track the first view for camera fitting
if first_view is None and context.view_proxy:
first_view = context.view_proxy

# Only fit the first view since cameras are linked
if fit_viewport and first_view:
self.fit_to_viewport(first_view)

def refresh_view_display(self, context: ViewContext):
if not self.should_use_manual_range(context.index):
Expand Down Expand Up @@ -204,8 +212,8 @@ def configure_new_view(self, var, context: ViewContext, sources):
rep.SetScalarBarVisibility(rview, False)
rview.CameraParallelProjection = 1

# Skip individual fit - will be handled by view0 after all views are created
Render(rview)
# ResetCamera(rview)

# This function is no longer needed - we work directly with state arrays

Expand Down Expand Up @@ -244,103 +252,33 @@ def generate_colorbar_image(self, index):
except Exception as e:
print(f"Error getting cached colorbar image for {var}: {e}")

def calculate_parallel_scale(self, view, margin=DEFAULT_MARGIN):
"""
Calculate optimal ParallelScale for a view based on GridProj bounds.

Args:
view: The render view to calculate scale for
margin: Margin factor (1.05 = 5% margin around objects)

Returns:
Optimal parallel scale value, or None if calculation fails
"""
from paraview.simple import UpdatePipeline, FindSource

try:
# Ensure pipeline is up to date
UpdatePipeline()

# Get GridProj bounds - it encompasses the full map extent
grid_source = FindSource("GridProj")
if not grid_source:
return None

bounds = grid_source.GetDataInformation().GetBounds()
if not bounds or bounds[0] > bounds[1]:
return None

# Use the utility function for calculation
view_size = view.ViewSize
return calculate_aspect_ratio_scale(bounds, view_size, margin)

except Exception as e:
print(f"Error calculating parallel scale: {e}")
return None

def fit_to_viewport(self, view, margin=DEFAULT_MARGIN):
def fit_to_viewport(self, view, margin=DEFAULT_MARGIN, use_largest_viewport=False):
"""
Dynamically calculate and set optimal ParallelScale to fit objects in viewport.
Reset camera to fit objects in viewport.

Args:
view: The render view to fit
margin: Margin factor (1.05 = 5% margin around objects)
margin: Not used (kept for compatibility)
use_largest_viewport: Not used (kept for compatibility)
"""
from paraview.simple import SetActiveView, FindSource
from paraview.simple import SetActiveView

try:
# Set this as the active view to ensure camera operations work correctly
# Set this view as active and reset camera
SetActiveView(view)

# Calculate the optimal parallel scale
parallel_scale = self.calculate_parallel_scale(view, margin)
if parallel_scale is None:
return

# Get GridProj bounds for centering the camera
grid_source = FindSource("GridProj")
if not grid_source:
return

combined_bounds = grid_source.GetDataInformation().GetBounds()
if not combined_bounds:
return

# Get the view's camera directly
camera = view.GetActiveCamera()
camera.SetParallelProjection(True)
camera.SetParallelScale(parallel_scale)

# Center camera on data using utility function
center = calculate_data_center(combined_bounds)
camera.SetFocalPoint(*center)

# For 2D projections, position camera perpendicular to XY plane
camera_pos = [center[0], center[1], center[2] + CAMERA_Z_OFFSET]
camera.SetPosition(*camera_pos)
camera.SetViewUp(0, 1, 0)

# Apply the camera settings to the view
view.CameraParallelScale = parallel_scale
view.ResetCamera(True, 0.9)

except Exception as e:
print(f"Error in fit_to_viewport: {e}")
# Fallback to simple reset if our calculation fails
try:
from paraview.simple import ResetCamera

ResetCamera(view)
except Exception:
pass

def reset_camera(self, **kwargs):
"""Reset camera for all views to optimally fit objects."""
for i, widget in enumerate(self.widgets):
if i < len(self.state.variables):
var = self.state.variables[i]
context = self.registry.get_view(var)
if context and context.view_proxy:
self.fit_to_viewport(context.view_proxy)
# Only reset the first view since cameras are linked
if len(self.widgets) > 0 and len(self.state.variables) > 0:
var = self.state.variables[0]
context = self.registry.get_view(var)
if context and context.view_proxy:
self.fit_to_viewport(context.view_proxy)
self.render_all_views()

def render_all_views(self, **kwargs):
Expand Down Expand Up @@ -493,6 +431,7 @@ def rebuild_visualization_layout(self, cached_layout=None):
self.configure_new_view(var, context, self.source.views)
else:
self.refresh_view_display(context)
# Skip individual viewport fitting - will be done once for view0
else:
view = CreateRenderView()
view.UseColorPaletteForBackground = 0
Expand Down Expand Up @@ -579,23 +518,24 @@ def rebuild_visualization_layout(self, cached_layout=None):
# Use index as identifier to maintain compatibility with grid expectations
layout.append({"x": x, "y": y, "w": wdt, "h": hgt, "i": index})

view0.CameraParallelScale = 100
# Only fit view0 since all cameras are linked
if view0 is not None:
self.fit_to_viewport(view0)

self.state.views = sWidgets
self.state.layout = layout
self.state.dirty("views")
self.state.dirty("layout")
# from trame.app import asynchronous
# asynchronous.create_task(self.flushViews())

# Single render after all updates
self.render_all_views()

"""
async def flushViews(self):
await self.server.network_completion
print("Flushing views")
self.render_all_views()
import asyncio
await asyncio.sleep(1)
print("Resetting views after sleep")
self.render_all_views()
"""

Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "app"
version = "0.1.13"
version = "0.1.14"
description = "QuickView: Visual Analyis for E3SM Atmosphere Data"
authors = ["Kitware"]
license = ""
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"package": {
"productName": "QuickView",
"version": "0.1.13"
"version": "0.1.14"
},
"tauri": {
"allowlist": {
Expand Down
Loading