1313from ansys .visor .viewer .core .visor_logging import VisorDefaultLogger
1414from ansys .visor .viewer .models .common .visor_camera_state import VisorCameraState
1515from ansys .visor .viewer .models .runtime .requests .sync_camera_payload import SyncCameraPayload
16+ from ansys .visor .viewer .models .runtime .requests .widget_state_payloads import (
17+ SetBoundingBoxVisibilityPayload ,
18+ SetCrossSectionVisibilityPayload ,
19+ SetEdgesVisiblePayload ,
20+ SetProjectionPayload ,
21+ )
1622
1723logger = VisorDefaultLogger (__name__ )
1824
1925# Disable VTK warning display
2026vtkObject .GlobalWarningDisplayOff ()
2127
2228
23- class ScenePartStateApi (Protocol ):
24- """Structural type of the per-part coordinator surface LocalApp calls.
29+ class SceneMutationApi (Protocol ):
30+ """Structural type of the coordinator surface LocalApp calls.
2531
2632 Typing only: there is no ``runtime_checkable`` decoration and no
2733 ``isinstance`` check anywhere against it. Declaring it here rather than
2834 importing the scene keeps this module free of any scene type, so the
2935 injected object remains LocalApp's only route to the scene.
3036
31- ``sync_camera`` is not per-part, and it is declared here anyway: the one
32- production injection site passes the whole scene coordinator, so a second
33- protocol would be the same object under a second name.
37+ Covers both per-part mutations (visibility, opacity, colour, selection)
38+ and scene-wide ones (camera sync, the widget-state toggles, projection,
39+ cross-section plane). The one production injection site passes the
40+ whole scene coordinator, so this protocol describes that whole surface.
3441 """
3542
3643 def set_part_visibility (self , node_id : int , visible : bool ) -> None : ...
@@ -56,6 +63,14 @@ def clear_part_color_variable(self, node_id: int) -> None: ...
5663
5764 def sync_camera (self , camera_state : VisorCameraState ) -> None : ...
5865
66+ def set_cross_section_visibility (self , visible : bool ) -> None : ...
67+
68+ def set_edges_visible (self , visible : bool ) -> None : ...
69+
70+ def set_bounding_box_visibility (self , visible : bool ) -> None : ...
71+
72+ def set_projection (self , parallel : bool ) -> None : ...
73+
5974
6075# ----------------------------------------------------------------------
6176# Trigger payload models
@@ -184,6 +199,10 @@ class LocalApp:
184199 set_part_color_variable: colours one part by a scalar variable
185200 clear_part_color_variable: stops colouring one part by a scalar variable
186201 sync_camera: records a settled camera reported by the frontend
202+ set_cross_section_visibility: shows or hides the cross-section plane
203+ set_edges_visible: shows or hides edges on every part
204+ set_bounding_box_visibility: shows or hides the bounding-box outline
205+ set_projection: sets parallel or perspective projection on the camera record
187206 set_only_cookie: sets a cookie on the server (note: Trame server only allows a single cookie header)
188207 Protected Methods:
189208 _cleanup(): Cleans up the active actor in the visualization pipeline.
@@ -196,7 +215,7 @@ def __init__(
196215 standalone : bool = True ,
197216 trame_logger : Logger | None = None ,
198217 pick_geometry = None ,
199- scene_part_state_api : ScenePartStateApi | None = None ,
218+ scene_mutation_api : SceneMutationApi | None = None ,
200219 ):
201220 self .server = server
202221 # Callable to get the scene details in JSON format
@@ -205,10 +224,10 @@ def __init__(
205224 self ._handle_save_state_response = handle_save_state_response
206225 # Callable for sub-geometry picking (optional)
207226 self ._pick_geometry = pick_geometry
208- # Per-part visual state coordinator (see ScenePartStateApi ). The one
227+ # Per-part visual state coordinator (see SceneMutationApi ). The one
209228 # production construction site always supplies it; it is optional so
210229 # that the class stays constructible without a scene.
211- self ._scene_part_state_api = scene_part_state_api
230+ self ._scene_mutation_api = scene_mutation_api
212231 # logger for logging trame server lifecycle info
213232 self .__trame_logger = trame_logger
214233
@@ -333,18 +352,19 @@ def perf_report_server_update(self, payload: dict):
333352 # body.
334353 # ------------------------------------------------------------------
335354
336- def _part_state_api (self , trigger_name : str ) -> ScenePartStateApi | None :
355+ def _mutation_api (self , trigger_name : str , payload : BaseModel ) -> SceneMutationApi | None :
337356 """Return the injected coordinator, or ``None`` after logging."""
338- if self ._scene_part_state_api is None :
357+ logger .debug ("[trigger] %s arrived: %s." , trigger_name , payload )
358+ if self ._scene_mutation_api is None :
339359 logger .debug ("%s: no scene part-state API injected; ignoring." , trigger_name )
340360 return None
341- return self ._scene_part_state_api
361+ return self ._scene_mutation_api
342362
343363 @trigger ("set_part_visibility" )
344364 @parse_payload (SetPartVisibilityPayload )
345365 def set_part_visibility (self , payload ) -> None :
346366 """Frontend -> Backend: set whether one part is visible."""
347- api = self ._part_state_api ("set_part_visibility" )
367+ api = self ._mutation_api ("set_part_visibility" , payload )
348368 if api is None :
349369 return
350370 api .set_part_visibility (payload .node_id , payload .visible )
@@ -357,7 +377,7 @@ def set_part_opacity(self, payload) -> None:
357377 An opacity outside ``[0.0, 1.0]`` fails validation and is a logged
358378 no-op; it does not reach VTK to be clamped.
359379 """
360- api = self ._part_state_api ("set_part_opacity" )
380+ api = self ._mutation_api ("set_part_opacity" , payload )
361381 if api is None :
362382 return
363383 api .set_part_opacity (payload .node_id , payload .opacity )
@@ -372,7 +392,7 @@ def set_part_diffuse_color(self, payload) -> None:
372392 or a colour that is not exactly three components, is a logged
373393 no-op -- nothing is delegated, so nothing is written to the store.
374394 """
375- api = self ._part_state_api ("set_part_diffuse_color" )
395+ api = self ._mutation_api ("set_part_diffuse_color" , payload )
376396 if api is None :
377397 return
378398 api .set_part_diffuse_color (payload .node_id , payload .diffuse_rgb )
@@ -385,7 +405,7 @@ def set_part_selected(self, payload) -> None:
385405 No colour crosses this trigger: the server reads the part's stored
386406 diffuse colour from its own record.
387407 """
388- api = self ._part_state_api ("set_part_selected" )
408+ api = self ._mutation_api ("set_part_selected" , payload )
389409 if api is None :
390410 return
391411 api .set_part_selected (payload .node_id , payload .selected )
@@ -403,7 +423,7 @@ def set_part_color_variable(self, payload) -> None:
403423 name. ``variableId`` is forwarded verbatim and is never parsed by
404424 the server.
405425 """
406- api = self ._part_state_api ("set_part_color_variable" )
426+ api = self ._mutation_api ("set_part_color_variable" , payload )
407427 if api is None :
408428 return
409429 api .set_part_color_variable (
@@ -420,7 +440,7 @@ def set_part_color_variable(self, payload) -> None:
420440 @parse_payload (ClearPartColorVariablePayload )
421441 def clear_part_color_variable (self , payload ) -> None :
422442 """Frontend -> Backend: stop colouring one part by a scalar variable."""
423- api = self ._part_state_api ("clear_part_color_variable" )
443+ api = self ._mutation_api ("clear_part_color_variable" , payload )
424444 if api is None :
425445 return
426446 api .clear_part_color_variable (payload .node_id )
@@ -457,7 +477,7 @@ def sync_camera(self, payload) -> None:
457477 if payload .origin != "gesture" :
458478 logger .debug ("sync_camera: origin=%s; dropping." , payload .origin )
459479 return
460- api = self ._part_state_api ("sync_camera" )
480+ api = self ._mutation_api ("sync_camera" , payload )
461481 if api is None :
462482 return
463483 logger .debug (
@@ -467,6 +487,60 @@ def sync_camera(self, payload) -> None:
467487 )
468488 api .sync_camera (payload .camera )
469489
490+ # ------------------------------------------------------------------
491+ # Widget-state triggers
492+ #
493+ # Frontend -> Backend. One trigger per server-tracked toggle. Each
494+ # carries the absolute target value, not a delta, so a redundant
495+ # message is indistinguishable from a no-op one, and both are fine.
496+ # No ``origin`` field: unlike the camera, a toggle echo is idempotent.
497+ #
498+ # ``set_projection`` lives here too: it is delivered the same way, but
499+ # it writes the camera record's projection field rather than a toggle
500+ # of its own.
501+ # ------------------------------------------------------------------
502+
503+ @trigger ("set_cross_section_visibility" )
504+ @parse_payload (SetCrossSectionVisibilityPayload )
505+ def set_cross_section_visibility (self , payload ) -> None :
506+ """Frontend -> Backend: show or hide the cross-section plane."""
507+ api = self ._mutation_api ("set_cross_section_visibility" , payload )
508+ if api is None :
509+ return
510+ api .set_cross_section_visibility (payload .visible )
511+
512+ @trigger ("set_edges_visible" )
513+ @parse_payload (SetEdgesVisiblePayload )
514+ def set_edges_visible (self , payload ) -> None :
515+ """Frontend -> Backend: show or hide edges on every part."""
516+ api = self ._mutation_api ("set_edges_visible" , payload )
517+ if api is None :
518+ return
519+ api .set_edges_visible (payload .visible )
520+
521+ @trigger ("set_bounding_box_visibility" )
522+ @parse_payload (SetBoundingBoxVisibilityPayload )
523+ def set_bounding_box_visibility (self , payload ) -> None :
524+ """Frontend -> Backend: show or hide the bounding-box outline."""
525+ api = self ._mutation_api ("set_bounding_box_visibility" , payload )
526+ if api is None :
527+ return
528+ api .set_bounding_box_visibility (payload .visible )
529+
530+ @trigger ("set_projection" )
531+ @parse_payload (SetProjectionPayload )
532+ def set_projection (self , payload ) -> None :
533+ """Frontend -> Backend: set parallel or perspective projection.
534+
535+ The projection is the camera record's field, not a toggle of its
536+ own: the coordinator writes the record and re-serialises the
537+ camera in one critical section.
538+ """
539+ api = self ._mutation_api ("set_projection" , payload )
540+ if api is None :
541+ return
542+ api .set_projection (payload .parallel )
543+
470544 def set_only_cookie (self , key : str , value : str ):
471545 """
472546 Sets a cookie on the server. NOTE: there is a limitation
0 commit comments