diff --git a/opengeodeweb_viewer_schemas.json b/opengeodeweb_viewer_schemas.json index 47126516..e29f0e60 100644 --- a/opengeodeweb_viewer_schemas.json +++ b/opengeodeweb_viewer_schemas.json @@ -1927,6 +1927,39 @@ ], "additionalProperties": false }, + "hover_highlight": { + "$id": "opengeodeweb_viewer.viewer.hover_highlight", + "rpc": "hover_highlight", + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "field_type": { + "enum": [ + "CELL", + "POINT" + ] + }, + "ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": [ + "x", + "y", + "field_type", + "ids" + ], + "additionalProperties": false + }, "grid_scale": { "$id": "opengeodeweb_viewer.viewer.grid_scale", "rpc": "grid_scale", diff --git a/requirements.txt b/requirements.txt index 74d61ff7..7819587c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -61,4 +61,3 @@ wslink==1.12.4 yarl>=1 # via aiohttp -opengeodeweb-microservice==1.*,>=1.1.3 diff --git a/src/opengeodeweb_viewer/object/object_methods.py b/src/opengeodeweb_viewer/object/object_methods.py index 6e6899b3..a6c27db8 100644 --- a/src/opengeodeweb_viewer/object/object_methods.py +++ b/src/opengeodeweb_viewer/object/object_methods.py @@ -13,9 +13,9 @@ vtkDataSetMapper, ) from vtkmodules.vtkCommonDataModel import ( - vtkDataObject, vtkDataSet, ) +from vtkmodules.vtkFiltersExtraction import vtkExtractSelection # Local application imports from opengeodeweb_viewer.vtk_protocol import VtkView, VtkPipeline @@ -47,7 +47,7 @@ def registerObject( if actor.visibility == True: resetCamara = False renderer.AddActor(data.actor) - renderer.AddActor(data.highlightActor) + renderer.AddActor(data.hoverHighlight.actor) if resetCamara: renderer.ResetCamera() @@ -56,7 +56,7 @@ def deregisterObject(self, data_id: str) -> None: renderWindow = self.getView("-1") renderer = renderWindow.GetRenderers().GetFirstRenderer() renderer.RemoveActor(pipeline.actor) - renderer.RemoveActor(pipeline.highlightActor) + renderer.RemoveActor(pipeline.hoverHighlight.actor) self.deregister_object(data_id) def SetVisibility(self, data_id: str, visibility: bool) -> None: @@ -162,19 +162,31 @@ def clearColors(self, data_id: str) -> None: output.GetCellData().SetActiveScalars("") mapper.ScalarVisibilityOff() - def highlight( - self, actor: vtkActor, mapper: vtkMapper, input_dataset: vtkDataObject - ) -> None: - mapper.SetInputDataObject(input_dataset) + def _apply_highlight_style(self, actor: vtkActor, mapper: vtkDataSetMapper) -> None: mapper.ScalarVisibilityOff() mapper.SetResolveCoincidentTopologyToPolygonOffset() + mapper.SetRelativeCoincidentTopologyPolygonOffsetParameters(-2, -2) prop = actor.GetProperty() prop.SetColor(0.235, 0.6, 0.514) - prop.SetLineWidth(3) - prop.SetPointSize(14) + prop.SetLineWidth(4) + prop.SetPointSize(15) prop.SetRenderPointsAsSpheres(True) - prop.SetLighting(True) + prop.SetLighting(False) prop.SetEdgeVisibility(True) prop.SetEdgeColor(0.12, 0.35, 0.30) actor.SetMapper(mapper) actor.VisibilityOff() + actor.SetUseBounds(False) + + def highlight(self, pipeline: VtkPipeline) -> None: + highlight = pipeline.hoverHighlight + self._apply_highlight_style(highlight.actor, highlight.mapper) + input_port = ( + pipeline.filter.GetOutputPort() + if pipeline.filter + else pipeline.reader.GetOutputPort() + ) + highlight.selection.AddNode(highlight.selectionNode) + highlight.extractSelection.SetInputConnection(0, input_port) + highlight.extractSelection.SetInputData(1, highlight.selection) + highlight.mapper.SetInputConnection(highlight.extractSelection.GetOutputPort()) diff --git a/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py b/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py index ce7e3a67..7bccb2f4 100644 --- a/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py +++ b/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py @@ -50,11 +50,8 @@ def registerMesh(self, rpc_params: RpcParams) -> None: reader.Update() mapper = vtkDataSetMapper() mapper.SetInputConnection(reader.GetOutputPort()) - highlight_mapper = vtkDataSetMapper() - data = VtkPipeline(reader, highlight_mapper, mapper) - self.highlight( - data.highlightActor, data.highlightMapper, reader.GetOutputDataObject(0) - ) + data = VtkPipeline(reader, mapper) + self.highlight(data) self.registerObject(data_id, file_name, data) except Exception as e: print(f"Error registering mesh {data_id}: {str(e)}", flush=True) @@ -177,5 +174,12 @@ def setMeshhighlight(self, rpc_params: RpcParams) -> None: ) params = schemas.Highlight.from_dict(rpc_params) pipeline = self.get_vtk_pipeline(params.id) - pipeline.highlightActor.SetVisibility(params.visibility) + if params.visibility: + dataset = pipeline.reader.GetOutputDataObject(0) + pipeline.hoverHighlight.mapper.SetInputDataObject(dataset) + else: + pipeline.hoverHighlight.mapper.SetInputConnection( + pipeline.hoverHighlight.extractSelection.GetOutputPort() + ) + pipeline.hoverHighlight.actor.SetVisibility(params.visibility) self.render(-1) diff --git a/src/opengeodeweb_viewer/rpc/model/model_protocols.py b/src/opengeodeweb_viewer/rpc/model/model_protocols.py index 29e41c8f..ca0a8086 100644 --- a/src/opengeodeweb_viewer/rpc/model/model_protocols.py +++ b/src/opengeodeweb_viewer/rpc/model/model_protocols.py @@ -6,11 +6,13 @@ vtkCompositeDataSet, vtkBoundingBox, vtkDataSet, + vtkSelectionNode, ) from vtkmodules.vtkRenderingCore import ( - vtkCompositeDataDisplayAttributes, vtkCompositePolyDataMapper, + vtkCompositeDataDisplayAttributes, ) +from vtkmodules.vtkFiltersCore import vtkAppendDataSets from vtkmodules.vtkIOXML import vtkXMLMultiBlockDataReader from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter from wslink import register as exportRpc # type: ignore @@ -120,12 +122,8 @@ def registerModel(self, rpc_params: RpcParams) -> None: mapper.SetInputDataObject(geometry_output) attributes = vtkCompositeDataDisplayAttributes() mapper.SetCompositeDataDisplayAttributes(attributes) - highlight_mapper = vtkCompositePolyDataMapper() - highlight_mapper.SetCompositeDataDisplayAttributes( - vtkCompositeDataDisplayAttributes() - ) - data = VtkPipeline(reader, highlight_mapper, mapper, filter) - self.highlight(data.highlightActor, data.highlightMapper, geometry_output) + data = VtkPipeline(reader, mapper, filter) + self.highlight(data) iterator = geometry_output.NewTreeIterator() iterator.InitTraversal() while not iterator.IsDoneWithTraversal(): @@ -136,9 +134,6 @@ def registerModel(self, rpc_params: RpcParams) -> None: data.blockDataSets.append(None) data.blockGeodeIds.append("") data.blockDataSets.append(block) - highlight_mapper.GetCompositeDataDisplayAttributes().SetBlockVisibility( - block, False - ) meta = iterator.GetCurrentMetaData() name = meta.Get(vtkCompositeDataSet.NAME()) data.blockGeodeIds.append(name) @@ -171,23 +166,23 @@ def setModelhighlight(self, rpc_params: RpcParams) -> None: ) params = schemas.Highlight.from_dict(rpc_params) pipeline = self.get_vtk_pipeline(params.id) - pipeline.highlightActor.SetVisibility(params.visibility) - mapper = pipeline.highlightMapper - assert isinstance(mapper, vtkCompositePolyDataMapper) - attributes = mapper.GetCompositeDataDisplayAttributes() - - for i in pipeline.activeHighlightIds: - attributes.SetBlockVisibility(pipeline.blockDataSets[i], False) - - pipeline.activeHighlightIds = [] - if params.visibility: - pipeline.activeHighlightIds = [ - i for i in params.block_ids if pipeline.blockDataSets[i] - ] - for i in pipeline.activeHighlightIds: - attributes.SetBlockVisibility(pipeline.blockDataSets[i], True) - - mapper.Modified() + if params.visibility and params.block_ids: + append = vtkAppendDataSets() + for i in params.block_ids: + block = ( + pipeline.blockDataSets[i] + if i < len(pipeline.blockDataSets) + else None + ) + if isinstance(block, vtkDataSet): + append.AddInputData(block) + append.Update() + pipeline.hoverHighlight.mapper.SetInputDataObject(append.GetOutput()) + else: + pipeline.hoverHighlight.mapper.SetInputConnection( + pipeline.hoverHighlight.extractSelection.GetOutputPort() + ) + pipeline.hoverHighlight.actor.SetVisibility(params.visibility) self.render(-1) @exportRpc(model_prefix + model_schemas_dict["get_blocks_bounds"]["rpc"]) diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py b/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py index aa88dcf8..dc64d6ce 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py @@ -7,6 +7,7 @@ from .reset_camera import * from .render import * from .picked_ids import * +from .hover_highlight import * from .grid_scale import * from .get_point_position import * from .axes import * diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/hover_highlight.json b/src/opengeodeweb_viewer/rpc/viewer/schemas/hover_highlight.json new file mode 100644 index 00000000..365cf0fd --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/hover_highlight.json @@ -0,0 +1,24 @@ +{ + "rpc": "hover_highlight", + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "field_type": { + "enum": ["CELL", "POINT"] + }, + "ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": ["x", "y", "field_type", "ids"], + "additionalProperties": false +} diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/hover_highlight.py b/src/opengeodeweb_viewer/rpc/viewer/schemas/hover_highlight.py new file mode 100644 index 00000000..f256a026 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/hover_highlight.py @@ -0,0 +1,20 @@ +from dataclasses_json import DataClassJsonMixin +from enum import Enum +from dataclasses import dataclass +from typing import List + + +class FieldType(Enum): + CELL = "CELL" + POINT = "POINT" + + +@dataclass +class HoverHighlight(DataClassJsonMixin): + def __post_init__(self) -> None: + print(self, flush=True) + + field_type: FieldType + ids: List[str] + x: float + y: float diff --git a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py index c2153d96..bfc76454 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py +++ b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py @@ -19,8 +19,8 @@ vtkCompositePolyDataMapper, ) from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackball -from vtkmodules.vtkCommonCore import reference -from vtkmodules.vtkCommonDataModel import vtkBoundingBox, vtkDataSet +from vtkmodules.vtkCommonCore import reference, vtkIdTypeArray +from vtkmodules.vtkCommonDataModel import vtkBoundingBox, vtkDataSet, vtkSelectionNode from vtkmodules.vtkCommonTransforms import vtkTransform from vtkmodules.vtkInteractionWidgets import vtkOrientationMarkerWidget from opengeodeweb_microservice.schemas import get_schemas_dict @@ -303,6 +303,45 @@ def renderNow(self, rpc_params: RpcParams) -> None: ) self.render() + @exportRpc(viewer_prefix + viewer_schemas_dict["hover_highlight"]["rpc"]) + def setHoverHighlight(self, rpc_params: RpcParams) -> None: + validate_schema( + rpc_params, self.viewer_schemas_dict["hover_highlight"], self.viewer_prefix + ) + params = schemas.HoverHighlight.from_dict(rpc_params) + picker = vtkCellPicker(tolerance=0.005) + picker.Pick(params.x, params.y, 0, self.get_renderer()) + self.clearHoverHighlights(params.ids) + actor = picker.GetActor() + pipeline = next( + ( + self.get_vtk_pipeline(id) + for id in params.ids + if self.get_vtk_pipeline(id).actor == actor + ), + None, + ) + if pipeline: + id_to_select = ( + picker.GetCellId() + if params.field_type == schemas.FieldType.CELL + else picker.GetPointId() + ) + if id_to_select != -1: + dataset = None + if isinstance(pipeline.mapper, vtkCompositePolyDataMapper): + flat_index = picker.GetFlatBlockIndex() + block = ( + pipeline.blockDataSets[flat_index] + if 0 <= flat_index < len(pipeline.blockDataSets) + else None + ) + dataset = block if isinstance(block, vtkDataSet) else None + self.updateHoverHighlight( + pipeline, id_to_select, params.field_type.value, dataset + ) + self.render(-1) + @exportRpc(viewer_prefix + viewer_schemas_dict["set_z_scaling"]["rpc"]) def setZScaling(self, rpc_params: RpcParams) -> None: validate_schema( diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 603fb5ea..786806e8 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -17,10 +17,17 @@ vtkMapper, vtkRenderer, vtkRenderWindow, - vtkCompositePolyDataMapper, + vtkDataSetMapper, ) -from vtkmodules.vtkCommonDataModel import vtkDataObject, vtkBoundingBox -from vtkmodules.vtkCommonCore import vtkStringArray +from vtkmodules.vtkCommonDataModel import ( + vtkDataObject, + vtkDataSet, + vtkBoundingBox, + vtkSelection, + vtkSelectionNode, +) +from vtkmodules.vtkFiltersExtraction import vtkExtractSelection +from vtkmodules.vtkCommonCore import vtkStringArray, vtkIdTypeArray from vtkmodules.vtkRenderingAnnotation import vtkCubeAxesActor, vtkAxesActor from vtkmodules.vtkInteractionWidgets import vtkOrientationMarkerWidget @@ -38,17 +45,24 @@ class ViewerData: viewer_elements_type: ViewerElementsType +@dataclass +class HighlightPipeline: + actor: vtkActor = field(default_factory=vtkActor) + mapper: vtkDataSetMapper = field(default_factory=vtkDataSetMapper) + selectionNode: vtkSelectionNode = field(default_factory=vtkSelectionNode) + selection: vtkSelection = field(default_factory=vtkSelection) + extractSelection: vtkExtractSelection = field(default_factory=vtkExtractSelection) + + @dataclass class VtkPipeline: reader: vtkXMLReader - highlightMapper: vtkMapper mapper: vtkMapper filter: vtkAlgorithm | None = None actor: vtkActor = field(default_factory=vtkActor) - highlightActor: vtkActor = field(default_factory=vtkActor) + hoverHighlight: HighlightPipeline = field(default_factory=HighlightPipeline) blockDataSets: list[vtkDataObject | None] = field(default_factory=list) blockGeodeIds: list[str] = field(default_factory=list) - activeHighlightIds: list[int] = field(default_factory=list) class VtkTypingMixin: @@ -138,6 +152,33 @@ def reset_camera_clipping_range(self) -> None: else: renderer.ResetCameraClippingRange() + def updateHoverHighlight( + self, + pipeline: VtkPipeline, + id_to_select: int, + field_type: str, + dataset: vtkDataSet | None = None, + ) -> None: + node = pipeline.hoverHighlight.selectionNode + node.SetContentType(vtkSelectionNode.INDICES) + node.SetFieldType( + vtkSelectionNode.CELL if field_type == "CELL" else vtkSelectionNode.POINT + ) + selection_list = vtkIdTypeArray() + selection_list.SetNumberOfComponents(1) + selection_list.InsertNextValue(id_to_select) + node.SetSelectionList(selection_list) + if dataset is not None: + pipeline.hoverHighlight.extractSelection.SetInputData(0, dataset) + pipeline.hoverHighlight.extractSelection.Modified() + pipeline.hoverHighlight.extractSelection.Update() + pipeline.hoverHighlight.actor.VisibilityOn() + + def clearHoverHighlights(self, ids: list[str]) -> None: + for data_id in ids: + pipeline = self.get_vtk_pipeline(data_id) + pipeline.hoverHighlight.actor.VisibilityOff() + def update_grid_scale_and_clipping_range(self) -> None: grid_scale = self.get_grid_scale() if grid_scale is not None: