99from threading import Thread
1010from itertools import chain
1111from webbrowser import open_new_tab
12+ from uuid import uuid1
1213
1314from typish import instance_of
1415
1516from trame .app import get_server
1617from trame .app .core import Server
17- from trame .widgets import html , vtk as vtk_widgets , client
18- from trame .ui .html import DivLayout
19-
18+ from trame .widgets import vtk as vtk_widgets , client , trame , vuetify3 as v3
19+ from trame .ui .vuetify3 import SinglePageWithDrawerLayout
2020from . import Shape
2121from .vis import style , Showable , ShapeLike , _split_showables
2222
@@ -43,7 +43,7 @@ class Figure:
4343 ren : vtkRenderer
4444 view : vtk_widgets .VtkRemoteView
4545 shapes : dict [ShapeLike , list [vtkProp3D ]]
46- actors : list [ vtkProp3D ]
46+ actors : dict [ str , tuple [ vtkProp3D , ...] ]
4747 loop : AbstractEventLoop
4848 thread : Thread
4949 empty : bool
@@ -107,24 +107,104 @@ def __init__(self, port: int = 18081):
107107 self .ren = renderer
108108
109109 self .shapes = {}
110- self .actors = []
110+ self .actors = {}
111+ self .active = None
111112
112113 # server
113- server = get_server ("CQ-server" )
114- server .client_type = "vue3"
114+ server = get_server ("CQ-server" , client_type = "vue3" )
115+ self .server = server
116+
117+ # state
118+ self .state = self .server .state
119+
120+ self .state .actors = []
121+ self .state .selected = None
115122
116123 # layout
117- with DivLayout (server ):
124+ self .layout = SinglePageWithDrawerLayout (server , show_drawer = False )
125+ with self .layout as layout :
118126 client .Style ("body { margin: 0; }" )
119127
120- with html .Div (style = FULL_SCREEN ):
121- self .view = vtk_widgets .VtkRemoteView (
122- win , interactive_ratio = 1 , interactive_quality = 100
128+ layout .title .set_text ("CQ viewer" )
129+ layout .footer .hide ()
130+
131+ with layout .toolbar :
132+
133+ BSTYLE = "display: block;"
134+
135+ v3 .VBtn (
136+ click = lambda : self ._fit (),
137+ flat = True ,
138+ density = "compact" ,
139+ icon = "mdi-crop-free" ,
140+ style = BSTYLE ,
141+ )
142+
143+ v3 .VBtn (
144+ click = lambda : self ._view ((0 , 0 , 0 ), (1 , 1 , 1 ), (0 , 0 , 1 )),
145+ flat = True ,
146+ density = "compact" ,
147+ icon = "mdi-axis-arrow" ,
148+ style = BSTYLE ,
149+ )
150+
151+ v3 .VBtn (
152+ click = lambda : self ._view ((0 , 0 , 0 ), (1 , 0 , 0 ), (0 , 0 , 1 )),
153+ flat = True ,
154+ density = "compact" ,
155+ icon = "mdi-axis-x-arrow" ,
156+ style = BSTYLE ,
157+ )
158+
159+ v3 .VBtn (
160+ click = lambda : self ._view ((0 , 0 , 0 ), (0 , 1 , 0 ), (0 , 0 , 1 )),
161+ flat = True ,
162+ density = "compact" ,
163+ icon = "mdi-axis-y-arrow" ,
164+ style = BSTYLE ,
165+ )
166+
167+ v3 .VBtn (
168+ click = lambda : self ._view ((0 , 0 , 0 ), (0 , 0 , 1 ), (0 , 1 , 0 )),
169+ flat = True ,
170+ density = "compact" ,
171+ icon = "mdi-axis-z-arrow" ,
172+ style = BSTYLE ,
173+ )
174+
175+ v3 .VBtn (
176+ click = lambda : self ._pop (),
177+ flat = True ,
178+ density = "compact" ,
179+ icon = "mdi-file-document-remove-outline" ,
180+ style = BSTYLE ,
181+ )
182+
183+ v3 .VBtn (
184+ click = lambda : self ._clear ([]),
185+ flat = True ,
186+ density = "compact" ,
187+ icon = "mdi-delete-outline" ,
188+ style = BSTYLE ,
189+ )
190+
191+ with layout .content :
192+ with v3 .VContainer (
193+ fluid = True , classes = "pa-0 fill-height" ,
194+ ):
195+ self .view = vtk_widgets .VtkRemoteView (
196+ win , interactive_ratio = 1 , interactive_quality = 100
197+ )
198+
199+ with layout .drawer :
200+ self .tree = trame .GitTree (
201+ sources = ("actors" ,),
202+ visibility_change = (self .onVisibility , "[$event]" ),
203+ actives_change = (self .onSelection , "[$event]" ),
123204 )
124205
125206 server .state .flush ()
126207
127- self .server = server
128208 self .loop = new_event_loop ()
129209
130210 def _run_loop ():
@@ -159,7 +239,20 @@ def _run(self, coro) -> Future:
159239
160240 return run_coroutine_threadsafe (coro , self .loop )
161241
162- def show (self , * showables : Showable | vtkProp3D | list [vtkProp3D ], ** kwargs ):
242+ def _update_state (self , name : str ):
243+ async def _ ():
244+
245+ self .state .dirty (name )
246+ self .state .flush ()
247+
248+ self ._run (_ ())
249+
250+ def show (
251+ self ,
252+ * showables : Showable | vtkProp3D | list [vtkProp3D ],
253+ name : Optional [str ] = None ,
254+ ** kwargs ,
255+ ):
163256 """
164257 Show objects.
165258 """
@@ -170,6 +263,9 @@ def show(self, *showables: Showable | vtkProp3D | list[vtkProp3D], **kwargs):
170263 pts = style (vecs , ** kwargs )
171264 axs = style (locs , ** kwargs )
172265
266+ # to be added to state
267+ new_actors = []
268+
173269 for s in shapes :
174270 # do not show markers by default
175271 if "markersize" not in kwargs :
@@ -181,14 +277,18 @@ def show(self, *showables: Showable | vtkProp3D | list[vtkProp3D], **kwargs):
181277 for actor in actors :
182278 self .ren .AddActor (actor )
183279
280+ new_actors .extend (actors )
281+
184282 for prop in chain (props , axs ):
185- self .actors .append (prop )
186283 self .ren .AddActor (prop )
187284
285+ new_actors .append (prop )
286+
188287 if vecs :
189- self .actors .append (* pts )
190288 self .ren .AddActor (* pts )
191289
290+ new_actors .append (* pts )
291+
192292 # store to enable pop
193293 self .last = (shapes , axs , pts if vecs else None , props )
194294
@@ -202,76 +302,155 @@ async def _show():
202302 self .fit ()
203303 self .empty = False
204304
305+ # update actors
306+ uuid = str (uuid1 ())
307+ self .state .actors .append (
308+ {
309+ "id" : uuid ,
310+ "parent" : "0" ,
311+ "visible" : 1 ,
312+ "name" : f"{ name if name else type (showables [0 ]).__name__ } at { id (showables [0 ]):x} " ,
313+ }
314+ )
315+ self ._update_state ("actors" )
316+
317+ self .actors [uuid ] = tuple (new_actors )
318+
205319 return self
206320
321+ async def _fit (self ):
322+ self .ren .ResetCamera ()
323+ self .view .update ()
324+
207325 def fit (self ):
208326 """
209327 Update view to fit all objects.
210328 """
211329
212- async def _show ():
213- self .ren .ResetCamera ()
214- self .view .update ()
330+ self ._run (self ._fit ())
215331
216- self ._run (_show ())
332+ return self
333+
334+ async def _view (self , foc , pos , up ):
335+
336+ cam = self .ren .GetActiveCamera ()
337+
338+ cam .SetViewUp (* up )
339+ cam .SetFocalPoint (* foc )
340+ cam .SetPosition (* pos )
341+
342+ self .ren .ResetCamera ()
343+
344+ self .view .update ()
345+
346+ def iso (self ):
347+
348+ self ._run (self ._view ((0 , 0 , 0 ), (1 , 1 , 1 ), (0 , 0 , 1 )))
217349
218350 return self
219351
220- def clear (self , * shapes : Shape | vtkProp3D ):
221- """
222- Clear specified objects. If no arguments are passed, clears all objects.
223- """
352+ def up (self ):
224353
225- async def _clear ():
354+ self . _run ( self . _view (( 0 , 0 , 0 ), ( 0 , 0 , 1 ), ( 0 , 1 , 0 )))
226355
227- if len (shapes ) == 0 :
228- self .ren .RemoveAllViewProps ()
356+ return self
229357
230- self .actors .clear ()
231- self .shapes .clear ()
358+ pass
232359
233- for s in shapes :
234- if instance_of (s , ShapeLike ):
235- for a in self .shapes [s ]:
236- self .ren .RemoveActor (a )
360+ def front (self ):
237361
238- del self .shapes [s ]
239- else :
240- self .actors .remove (s )
241- self .ren .RemoveActor (s )
362+ self ._run (self ._view ((0 , 0 , 0 ), (1 , 0 , 0 ), (0 , 0 , 1 )))
242363
243- self .view .update ()
364+ return self
365+
366+ def side (self ):
367+
368+ self ._run (self ._view ((0 , 0 , 0 ), (0 , 1 , 0 ), (0 , 0 , 1 )))
369+
370+ return self
371+
372+ async def _clear (self , shapes ):
373+
374+ if len (shapes ) == 0 :
375+ self .ren .RemoveAllViewProps ()
376+
377+ self .actors .clear ()
378+ self .shapes .clear ()
379+
380+ self .state .actors = []
381+
382+ for s in shapes :
383+ if instance_of (s , ShapeLike ):
384+ for a in self .shapes [s ]:
385+ self .ren .RemoveActor (a )
386+
387+ del self .shapes [s ]
388+ else :
389+ for k , v in self .actors .items ():
390+ if s in v :
391+ for el in self .actors .pop (k ):
392+ self .ren .RemoveActor (el )
393+
394+ break
395+
396+ self ._update_state ("actors" )
397+ self .view .update ()
398+
399+ def clear (self , * shapes : Shape | vtkProp3D ):
400+ """
401+ Clear specified objects. If no arguments are passed, clears all objects.
402+ """
244403
245404 # reset last, bc we don't want to keep track of what was removed
246405 self .last = None
247- future = self ._run (_clear ())
406+ future = self ._run (self . _clear (shapes ))
248407 future .result ()
249408
250409 return self
251410
411+ async def _pop (self ):
412+
413+ if self .active is None :
414+ self .active = self .actors [- 1 ]["id" ]
415+
416+ if self .active in self .actors :
417+ for act in self .actors [self .active ]:
418+ self .ren .RemoveActor (act )
419+
420+ self .actors .pop (self .active )
421+
422+ # update corresponding state
423+ for i , el in enumerate (self .state .actors ):
424+ if el ["id" ] == self .active :
425+ self .state .actors .pop (i )
426+ self ._update_state ("actors" )
427+ break
428+
429+ self .active = None
430+
431+ else :
432+ return
433+
434+ self .view .update ()
435+
252436 def pop (self ):
253437 """
254- Clear the last showable.
438+ Clear the selected showable.
255439 """
256440
257- async def _pop ():
441+ self . _run ( self . _pop ())
258442
259- ( shapes , axs , pts , props ) = self . last
443+ return self
260444
261- for s in shapes :
262- for act in self .shapes .pop (s ):
263- self .ren .RemoveActor (act )
445+ def onVisibility (self , event ):
264446
265- for act in chain (axs , props ):
266- self .ren .RemoveActor (act )
267- self .actors .remove (act )
447+ actors = self .actors [event ["id" ]]
268448
269- if pts :
270- self .ren .RemoveActor (* pts )
271- self .actors .remove (* pts )
449+ for act in actors :
450+ act .SetVisibility (event ["visible" ])
272451
273- self .view .update ()
452+ self .view .update ()
274453
275- self . _run ( _pop ())
454+ def onSelection ( self , event ):
276455
277- return self
456+ self . active = event [ 0 ]
0 commit comments