2828from gsqlite import create_sqlite_database
2929from gutils import GColors
3030
31+ def _is_jupyter () -> bool :
32+ try :
33+ from IPython import get_ipython
34+ shell = get_ipython ()
35+ return shell is not None and "IPKernelApp" in shell .config
36+ except ImportError :
37+ return False
38+
39+ _in_jupyter = _is_jupyter ()
40+
3141has_pyvista : bool = False
3242pv : Optional [object ] = None
3343BackgroundPlotterCls = None
@@ -147,13 +157,16 @@ def plotter(self):
147157
148158 if self ._plotter is None :
149159 if self .use_background_plotter and BackgroundPlotterCls is not None :
150- # Non-blocking background window
151160 self ._plotter = BackgroundPlotterCls (show = True )
152161 else :
153- # Normal blocking Plotter
154- self ._plotter = self .pv .Plotter ()
162+ if _in_jupyter :
163+ self ._plotter = self .pv .Plotter (
164+ notebook = True ,
165+ window_size = (self .args .width , self .args .height ),
166+ )
167+ else :
168+ self ._plotter = self .pv .Plotter ()
155169
156- # One-time scene setup
157170 self ._plotter .add_axes ()
158171 self ._plotter .set_background ("#303048" , top = "#000020" )
159172 self ._plotter .camera_position = "iso"
@@ -304,71 +317,92 @@ def show(self, block: bool = True):
304317 p .app .exec_ ()
305318 else :
306319 # Normal Plotter path
307- if block :
320+ if _in_jupyter :
321+
322+ cpos = self ._configure_camera_from_bounds (
323+ margin = - 12.8 ,
324+ distance_scale = 3.0 ,
325+ )
326+ return p .show (
327+ jupyter_backend = "html" ,
328+ cpos = cpos ,
329+ return_viewer = True ,
330+ )
331+
332+
333+ elif block :
308334 p .show ()
309335
310- def _configure_camera_from_bounds (self ):
336+ def _configure_camera_from_bounds (self , margin : float = 0.8 , distance_scale : float = 4.0 ):
311337 if not self .use_pyvista :
312- return
338+ return None
313339
314340 p = self .plotter
315341 if p is None :
316- return
342+ return None
317343
318- # If nothing is added yet, bounds can be degenerate
319344 try :
320345 xmin , xmax , ymin , ymax , zmin , zmax = p .bounds
321346 except Exception :
322- return
347+ return None
323348
324- # Guard against empty scene
325- if any (not np .isfinite (v ) for v in (xmin , xmax , ymin , ymax , zmin , zmax )):
326- return
327- if xmax == xmin and ymax == ymin and zmax == zmin :
328- return
349+ vals = (xmin , xmax , ymin , ymax , zmin , zmax )
350+ if any (not np .isfinite (v ) for v in vals ):
351+ return None
352+
353+ dx = xmax - xmin
354+ dy = ymax - ymin
355+ dz = zmax - zmin
329356
330- center = np .array ([(xmin + xmax ) / 2 ,
331- (ymin + ymax ) / 2 ,
332- (zmin + zmax ) / 2 ])
333- scene_len = np .linalg .norm ([xmax - xmin ,
334- ymax - ymin ,
335- zmax - zmin ])
357+ if dx <= 0 and dy <= 0 and dz <= 0 :
358+ return None
336359
337- # iso direction
338- direction = np .array ([1.0 , 1.0 , 1.0 ]) / np .sqrt (3.0 )
339- distance = 2.5 * scene_len if scene_len > 0 else 1.0
360+ center = np .array ([
361+ 0.5 * (xmin + xmax ),
362+ 0.5 * (ymin + ymax ),
363+ 0.5 * (zmin + zmax ),
364+ ])
340365
341- pos = center + direction * distance
342- p .camera_position = [tuple (pos ), tuple (center ), (0 , 0 , 1 )]
366+ scene_len = np .linalg .norm ([dx , dy , dz ])
367+ if scene_len <= 0 :
368+ scene_len = max (dx , dy , dz , 1.0 )
343369
344- if self .args .add_axes_at_zero :
345- # if you have this helper on the plotter; otherwise use your own
346- try :
347- p .add_axes_at_origin (xlabel = "X" , ylabel = "Y" , zlabel = "Z" )
348- except AttributeError :
349- pass
370+ direction = np .array ([1.0 , 1.0 , 1.0 ])
371+ direction /= np .linalg .norm (direction )
372+
373+ # Larger distance_scale = more zoomed out
374+ distance = distance_scale * scene_len
375+ position = center + direction * distance
376+
377+ cpos = [
378+ tuple (position ),
379+ tuple (center ),
380+ (0 , 0 , 1 ),
381+ ]
382+
383+ p .camera_position = cpos
350384
351- # Optional: keep your original “nice” orientation
385+ # Important for html/local backend: avoid parallel projection here.
352386 try :
353- p .view_zy () # or view_isometric, etc.
354- except AttributeError :
387+ p .disable_parallel_projection ()
388+ except Exception :
355389 pass
356390
391+ # Larger view_angle = more zoomed out in perspective projection.
357392 try :
358- p .enable_anti_aliasing ()
359- except AttributeError :
393+ p .camera . view_angle = 45.0
394+ except Exception :
360395 pass
361396
362397 try :
363- p .enable_parallel_projection ()
364- except AttributeError :
398+ p .reset_camera_clipping_range ()
399+ except Exception :
365400 pass
366401
367- # Sometimes helps with clipping issues
368- if hasattr (p , "reset_camera_clipped_range" ):
369- p .reset_camera_clipped_range ()
370-
371402 self ._camera_initialized = True
403+ return cpos
404+
405+
372406
373407
374408# autogeometry utility to executes show() at exit
@@ -386,6 +420,11 @@ def autogeometry(
386420 enable_pyvista : Optional [bool ] = None ,
387421 use_background_plotter : Optional [bool ] = None ,
388422):
423+ # in jupyter: always enable pyvista, never use atexit
424+ if _in_jupyter :
425+ enable_pyvista = True
426+ auto_show = False
427+
389428 cfg = GConfiguration (
390429 experiment ,
391430 application ,
0 commit comments