Skip to content
Draft
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
1 change: 1 addition & 0 deletions doc/changelog.d/199.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add geometry picker for server mode
19 changes: 19 additions & 0 deletions src/ansys/visor/viewer/app/trame/scene_trigger_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
from ansys.visor.viewer.core.visor_enums import RenderingMode, VisorVtkVariableType
from ansys.visor.viewer.core.visor_logging import VisorDefaultLogger
from ansys.visor.viewer.models.common.visor_camera_state import VisorCameraState
from ansys.visor.viewer.models.runtime.picking.geometry_pick import GeometryPick
from ansys.visor.viewer.models.runtime.requests.geometry_pick_payloads import (
SetGeometryHighlightPayload,
)
from ansys.visor.viewer.models.runtime.requests.sync_camera_payload import SyncCameraPayload
from ansys.visor.viewer.models.runtime.requests.variable_range_payload import SetVariableRangePayload
from ansys.visor.viewer.models.runtime.requests.widget_state_payloads import (
Expand Down Expand Up @@ -88,6 +92,8 @@ def set_panel_top_right_legend_collapsed(self, collapsed: bool) -> None: ...

def set_panel_top_right_tab_index(self, tab_index: int) -> None: ...

def set_geometry_highlight(self, pick: GeometryPick) -> None: ...


# ----------------------------------------------------------------------
# Trigger payload models
Expand Down Expand Up @@ -564,6 +570,19 @@ def set_panel_top_right_tab_index(self, payload) -> None:
return
api.set_panel_top_right_tab_index(payload.tab_index)

@trigger("set_geometry_highlight")
@parse_payload(SetGeometryHighlightPayload)
def set_geometry_highlight(self, payload) -> None:
"""Frontend -> Backend: highlight the picked feature, or clear the highlight on a not-found pick.

Transient: the scene stores nothing. The renderer pushes the
highlight to the client itself, without rebuilding the frontend.
"""
api = self._mutation_api("set_geometry_highlight", payload)
if api is None:
return
api.set_geometry_highlight(payload.pick)

def set_only_cookie(self, key: str, value: str):
"""
Sets a cookie on the server. NOTE: there is a limitation
Expand Down
19 changes: 15 additions & 4 deletions src/ansys/visor/viewer/app/trame/server_app.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""
ServerApp
=========
Trame app for server-side rendering: the shared scene triggers, part picking,
the client theme report, and the trame-rca area the rendered frames stream
through.
Trame app for server-side rendering: the shared scene triggers, part and
geometry picking, the client theme report, and the trame-rca area the rendered
frames stream through.
"""

from __future__ import annotations
Expand All @@ -23,6 +23,8 @@
)
from ansys.visor.viewer.app.trame.server_loop_bridge import ServerLoopBridge
from ansys.visor.viewer.core.visor_enums import RenderingMode
from ansys.visor.viewer.models.runtime.picking.geometry_pick import GeometryPick
from ansys.visor.viewer.models.runtime.requests.geometry_pick_payloads import PickGeometryAtPayload
from ansys.visor.viewer.renderer.server_window import VisorRcaWindow

RCA_AREA_NAME: str = "view"
Expand All @@ -44,7 +46,7 @@ class SetDarkThemePayload(BaseModel):

@TrameApp()
class ServerApp(SceneTriggerApp):
"""Serves the scene triggers, part picking and the streamed view for server-side rendering."""
"""Serves the scene triggers, part and geometry picking and the streamed view for server-side rendering."""

def __init__(
self,
Expand All @@ -54,12 +56,14 @@ def __init__(
trame_logger: Logger | None,
scene_mutation_api: SceneMutationApi,
pick_part_at: Callable[[int, int], int | None],
pick_geometry_at: Callable[[int, int, str], GeometryPick],
set_dark_mode: Callable[[bool], None],
rca_window: VisorRcaWindow,
bridge: ServerLoopBridge,
) -> None:
"""Create the area before start; the view handler is built when the server is ready."""
self._pick_part_at = pick_part_at
self._pick_geometry_at = pick_geometry_at
self._set_dark_mode = set_dark_mode
self._rca_window = rca_window
self._bridge = bridge
Expand Down Expand Up @@ -96,6 +100,7 @@ def _bind_stream(self) -> None:
interactive_quality=60,
still_quality=90,
)
self._rca_window.bind_pixel_scale(lambda: view_handler.scale_factor)
self._bridge.bind(view_handler)

@trigger("pick_part_at")
Expand All @@ -104,6 +109,12 @@ def pick_part_at(self, payload) -> int | None:
"""Frontend -> Backend: the node under a render-window pixel, or None; selects nothing."""
return self._pick_part_at(payload.x, payload.y)

@trigger("pick_geometry_at")
@parse_payload(PickGeometryAtPayload)
def pick_geometry_at(self, payload) -> dict:
"""Frontend -> Backend: the geometry feature under a render-window pixel, dumped by alias; highlights nothing."""
return self._pick_geometry_at(payload.x, payload.y, payload.mode).model_dump(by_alias=True)

@trigger("set_dark_theme")
@parse_payload(SetDarkThemePayload)
def set_dark_theme(self, payload) -> None:
Expand Down
1 change: 1 addition & 0 deletions src/ansys/visor/viewer/app/visor_vtk_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def _initialize_rendering(self, standalone: bool, trame_log_dir: str | None) ->
trame_logger=get_trame_logger(trame_log_dir),
scene_mutation_api=self._scene,
pick_part_at=self._scene.pick_part_at,
pick_geometry_at=self._scene.pick_geometry_at,
set_dark_mode=self._scene.set_dark_mode,
rca_window=self._scene.rca_window,
bridge=bridge,
Expand Down
Empty file.
79 changes: 79 additions & 0 deletions src/ansys/visor/viewer/models/runtime/picking/geometry_pick.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
Geometry pick results
=====================
The four results a geometry pick can report. Each dumps by alias to the
wire shape the client's result panel reads.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, List, Literal, Union

from pydantic import BaseModel, ConfigDict, Field

if TYPE_CHECKING:
from ansys.visor.viewer.vtk.widgets.geometry_highlight import GeometryHighlight

GeometryPickMode = Literal["vertex", "edge", "face"]


class NoGeometryPick(BaseModel):
"""No part cell under the pick."""

model_config = ConfigDict(populate_by_name=True)

found: Literal[False] = False

def highlight_with(self, highlight: "GeometryHighlight") -> None:
"""Hide the highlight."""
highlight.hide()


class VertexPick(BaseModel):
"""The picked cell's vertex nearest the pick position."""

model_config = ConfigDict(populate_by_name=True)

found: Literal[True] = True
mode: Literal["vertex"] = "vertex"
position: List[float]

def highlight_with(self, highlight: "GeometryHighlight") -> None:
"""Draw a dot at the vertex."""
highlight.show_vertex(self.position)


class EdgePick(BaseModel):
"""The picked cell's edge nearest the pick position, with its length."""

model_config = ConfigDict(populate_by_name=True)

found: Literal[True] = True
mode: Literal["edge"] = "edge"
point_a: List[float] = Field(alias="pointA")
point_b: List[float] = Field(alias="pointB")
# Not "length" on the wire: trame's trigger decorators read an object with a
# numeric length as a file list and send it as an array.
length: float = Field(alias="edgeLength")

def highlight_with(self, highlight: "GeometryHighlight") -> None:
"""Draw a line along the edge."""
highlight.show_edge(self.point_a, self.point_b)


class FacePick(BaseModel):
"""The picked cell as a whole, with its area."""

model_config = ConfigDict(populate_by_name=True)

found: Literal[True] = True
mode: Literal["face"] = "face"
area: float
points: List[List[float]]

def highlight_with(self, highlight: "GeometryHighlight") -> None:
"""Draw the cell as a filled outline."""
highlight.show_face(self.points)


GeometryPick = Union[VertexPick, EdgePick, FacePick, NoGeometryPick]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Models for the geometry-pick trigger payloads."""

from pydantic import BaseModel

from ansys.visor.viewer.models.runtime.picking.geometry_pick import GeometryPick, GeometryPickMode


class PickGeometryAtPayload(BaseModel):
"""pick_geometry_at payload: a render-window pixel, origin bottom-left, and the mode."""

x: int
y: int
mode: GeometryPickMode


class SetGeometryHighlightPayload(BaseModel):
"""set_geometry_highlight payload: the pick whose feature to highlight; a not-found pick clears it."""

pick: GeometryPick
13 changes: 13 additions & 0 deletions src/ansys/visor/viewer/renderer/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

from ansys.visor.viewer.core.visor_enums import VisorVtkVariableType
from ansys.visor.viewer.models.common.visor_camera_state import VisorCameraState
from ansys.visor.viewer.models.runtime.picking.geometry_pick import GeometryPick
from ansys.visor.viewer.models.runtime.vtk.renderer_annotation import RendererAnnotation
from ansys.visor.viewer.vtk.scene_graph import VisorSceneGraphPartNode

Expand Down Expand Up @@ -316,6 +317,18 @@ def update_bounds(self, bounds: list[float]) -> None:
def update_actor_count(self, count: int) -> None:
"""Propagate the current part-node count to widgets."""

# ------------------------------------------------------------------------
# Geometry highlight
# ------------------------------------------------------------------------

@abstractmethod
def show_geometry_highlight(self, pick: "GeometryPick") -> None:
"""Draw pick on the scene's one highlight, or hide it when not found.

On return, the client has been sent what it needs to show the
highlight, without rebuilding its frontend.
"""


# ------------------------------------------------------------------------
# Render
Expand Down
Loading
Loading