diff --git a/requirements.txt b/requirements.txt index 50aaa5f..9108a15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -63,4 +63,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 c2ec3ee..1f2de7c 100644 --- a/src/opengeodeweb_viewer/object/object_methods.py +++ b/src/opengeodeweb_viewer/object/object_methods.py @@ -138,6 +138,7 @@ def _prune_hidden_blocks( dataset: vtkMultiBlockDataSet, visibility_attributes: vtkCompositeDataDisplayAttributes, ) -> vtkMultiBlockDataSet: + # Recursively construct a new multi-block dataset excluding hidden blocks. pruned = vtkMultiBlockDataSet() pruned.SetNumberOfBlocks(dataset.GetNumberOfBlocks()) for index in range(dataset.GetNumberOfBlocks()): @@ -165,13 +166,10 @@ def SetBlocksVisibility( visibility_attributes = mapper.GetCompositeDataDisplayAttributes() for block_id in block_ids: visibility_attributes.SetBlockVisibility(blocks[block_id], visibility) - dataset = ( - pipeline.filter.GetOutputDataObject(0) - if pipeline.filter - else pipeline.reader.GetOutputDataObject(0) - ) + dataset = (pipeline.filter or pipeline.reader).GetOutputDataObject(0) if not isinstance(dataset, vtkMultiBlockDataSet): return + # Re-build a pruned dataset for the dedicated pick mapper if pipeline.pick_mapper is None: pipeline.pick_mapper = vtkCompositePolyDataMapper() pipeline.pick_mapper.SetInputDataObject( diff --git a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py index 39089c1..07ae72d 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py +++ b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py @@ -15,7 +15,6 @@ vtkWorldPointPicker, vtkCellPicker, vtkPropPicker, - vtkCompositePolyDataMapper, vtkDataSetMapper, vtkActor, ) @@ -262,52 +261,26 @@ def pickedIds(self, rpc_params: RpcParams) -> dict[str, list[str] | int | None]: rpc_params, self.viewer_schemas_dict["picked_ids"], self.viewer_prefix ) params = schemas.PickedIDS.from_dict(rpc_params) - renderer = self.getView("-1").GetRenderers().GetFirstRenderer() - - pipelines_to_restore = [ - pipeline - for pipeline_id in params.ids - if (pipeline := self.get_vtk_pipeline(pipeline_id)).pick_mapper is not None - ] - for pipeline in pipelines_to_restore: - pick_mapper = pipeline.pick_mapper - if pick_mapper is not None: - pipeline.actor.SetMapper(pick_mapper) - - actors = [] picker = vtkCellPicker(tolerance=0.005) - picker.Pick(params.x, params.y, 0, renderer) - actor = picker.GetActor() - viewer_id = picker.GetFlatBlockIndex() - - while actor: - actors.append(actor) - actor.SetPickable(False) - picker.Pick(params.x, params.y, 0, renderer) - actor = picker.GetActor() - - for actor in actors: - actor.SetPickable(True) - for pipeline in pipelines_to_restore: - pipeline.actor.SetMapper(pipeline.mapper) - + # Retrieve all actors under the clicked coordinates + actors, flat_index = self.pick_actors_under_coordinate( + params.ids, params.x, params.y, picker + ) + # Filter pipeline IDs whose actors are in the picked list array_ids = [ - id for id in params.ids if self.get_vtk_pipeline(id).actor in actors + data_id + for data_id in params.ids + if self.get_vtk_pipeline(data_id).actor in actors ] if not array_ids: return {"array_ids": [], "viewer_id": None} - if array_ids and viewer_id != -1: + viewer_id = flat_index if flat_index != -1 else None + if viewer_id is not None: pipeline = self.get_vtk_pipeline(array_ids[0]) - mapper = pipeline.mapper - if isinstance(mapper, vtkCompositePolyDataMapper): - attr = mapper.GetCompositeDataDisplayAttributes() - if attr and not attr.GetBlockVisibility( - pipeline.blockDataSets[viewer_id] - ): - array_ids, viewer_id = [], -1 + dataset, geode_id = self.get_composite_block_info(pipeline, picker) return { "array_ids": array_ids, - "viewer_id": viewer_id if viewer_id != -1 else None, + "viewer_id": viewer_id, } @exportRpc(viewer_prefix + viewer_schemas_dict["grid_scale"]["rpc"]) @@ -362,89 +335,27 @@ def setHighlight( rpc_params, self.viewer_schemas_dict["highlight"], self.viewer_prefix ) params = schemas.Highlight.from_dict(rpc_params) - picker = vtkCellPicker(tolerance=0.005) - - pipelines_to_restore = [ - pipeline - for pipeline_id in params.ids - if (pipeline := self.get_vtk_pipeline(pipeline_id)).pick_mapper is not None - ] - for pipeline in pipelines_to_restore: - pick_mapper = pipeline.pick_mapper - if pick_mapper is not None: - pipeline.actor.SetMapper(pick_mapper) - try: - picker.Pick(params.x, params.y, 0, self.get_renderer()) - finally: - for pipeline in pipelines_to_restore: - pipeline.actor.SetMapper(pipeline.mapper) - + # Clear previous highlights self.clear_highlights(params.ids) - actor = picker.GetActor() - pipeline_id = next( - (id for id in params.ids if self.get_vtk_pipeline(id).actor == actor), None - ) - id_to_select = ( - picker.GetCellId() - if params.field_type == schemas.FieldType.CELL - else picker.GetPointId() + picker = vtkCellPicker(tolerance=0.005) + # Perform pick operation to identify clicked pipeline and primitive ID + data_id, id_to_select = self.pick_cell_or_point( + params.ids, params.x, params.y, params.field_type.value, picker ) - - if not pipeline_id or id_to_select == -1: + if not data_id or id_to_select == -1: self.render(-1) return {} - - pipeline = self.get_vtk_pipeline(pipeline_id) - dataset = None - geode_id = None - if isinstance(pipeline.mapper, vtkCompositePolyDataMapper): - flat_index = picker.GetFlatBlockIndex() - dataset = ( - pipeline.blockDataSets[flat_index] - if 0 <= flat_index < len(pipeline.blockDataSets) - else None - ) - if dataset: - attr = pipeline.mapper.GetCompositeDataDisplayAttributes() - if attr and not attr.GetBlockVisibility(dataset): - self.render(-1) - return {} - geode_id = ( - pipeline.blockGeodeIds[flat_index] - if 0 <= flat_index < len(pipeline.blockGeodeIds) - else None - ) - + # Retrieve picked composite block information + pipeline = self.get_vtk_pipeline(data_id) + dataset, geode_id = self.get_composite_block_info(pipeline, picker) + # Update highlight visibility and extract attributes from the picked element self.update_highlight(pipeline, id_to_select, params.field_type.value, dataset) self.render(-1) - - data_obj = dataset or pipeline.reader.GetOutputDataObject(0) - data_attributes = {} - if isinstance(data_obj, vtkDataSet): - field_data = ( - data_obj.GetCellData() - if params.field_type == schemas.FieldType.CELL - else data_obj.GetPointData() - ) - for array_index in range(field_data.GetNumberOfArrays()): - array = field_data.GetArray(array_index) - if array and array.GetName(): - num_comps = array.GetNumberOfComponents() - component_values = [ - array.GetComponent(id_to_select, component_index) - for component_index in range(num_comps) - ] - data_attributes[array.GetName()] = ( - component_values[0] if num_comps == 1 else component_values - ) - - if params.field_type == schemas.FieldType.POINT: - point_coordinates = data_obj.GetPoint(id_to_select) - if point_coordinates: - data_attributes["coordinates"] = list(point_coordinates) - + data_attributes = self.extract_picked_attributes( + pipeline, id_to_select, params.field_type.value, dataset + ) return { - "id": pipeline_id, + "id": data_id, "picked_id": id_to_select, "field_type": params.field_type.value, "geode_id": geode_id, diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 32169f9..09a6926 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -18,6 +18,9 @@ vtkRenderer, vtkRenderWindow, vtkDataSetMapper, + vtkCompositePolyDataMapper, + vtkCellPicker, + vtkHardwarePicker, ) from vtkmodules.vtkCommonDataModel import ( vtkDataObject, @@ -189,11 +192,114 @@ def update_highlight( pipeline.highlight.extractSelection.Update() pipeline.highlight.actor.VisibilityOn() - def clear_highlights(self, ids: list[str]) -> None: - for data_id in ids: + def clear_highlights(self, data_ids: list[str]) -> None: + for data_id in data_ids: pipeline = self.get_vtk_pipeline(data_id) pipeline.highlight.actor.VisibilityOff() + def swap_pick_mappers(self, data_ids: list[str], use_pick_mapper: bool) -> None: + # Swap actor mappers between the default and the pick_mapper (where hidden blocks are pruned). + for data_id in data_ids: + pipeline = self.get_vtk_pipeline(data_id) + if pipeline.pick_mapper: + mapper = pipeline.pick_mapper if use_pick_mapper else pipeline.mapper + pipeline.actor.SetMapper(mapper) + + def pick_cell_or_point( + self, + data_ids: list[str], + x: float, + y: float, + field_type: str, + picker: vtkCellPicker, + ) -> tuple[str | None, int]: + self.swap_pick_mappers(data_ids, use_pick_mapper=True) + try: + picker.Pick(x, y, 0, self.get_renderer()) + finally: + self.swap_pick_mappers(data_ids, use_pick_mapper=False) + actor = picker.GetActor() + # Find which pipeline owns the picked actor + data_id = next( + ( + current_data_id + for current_data_id in data_ids + if self.get_vtk_pipeline(current_data_id).actor == actor + ), + None, + ) + id_to_select = ( + picker.GetCellId() if field_type == "CELL" else picker.GetPointId() + ) + return data_id, id_to_select + + def pick_actors_under_coordinate( + self, data_ids: list[str], x: float, y: float, picker: vtkCellPicker + ) -> tuple[list[vtkActor], int]: + renderer = self.get_renderer() + self.swap_pick_mappers(data_ids, use_pick_mapper=True) + actors = [] + viewer_id = -1 + try: + picker.Pick(x, y, 0, renderer) + viewer_id = picker.GetFlatBlockIndex() + while actor := picker.GetActor(): + actors.append(actor) + actor.SetPickable(False) + picker.Pick(x, y, 0, renderer) + finally: + for actor in actors: + actor.SetPickable(True) + self.swap_pick_mappers(data_ids, use_pick_mapper=False) + return actors, viewer_id + + def get_composite_block_info( + self, pipeline: VtkPipeline, picker: vtkCellPicker + ) -> tuple[vtkDataObject | None, str | None]: + # Extract the specific block dataset and metadata from a picked composite flat index + if not isinstance(pipeline.mapper, vtkCompositePolyDataMapper): + return None, None + flat_index = picker.GetFlatBlockIndex() + if not (0 <= flat_index < len(pipeline.blockDataSets)): + return None, None + dataset = pipeline.blockDataSets[flat_index] + geode_id = ( + pipeline.blockGeodeIds[flat_index] + if flat_index < len(pipeline.blockGeodeIds) + else None + ) + return dataset, geode_id + + def get_array_values(self, array: Any, id_to_select: int) -> list[float] | float: + components = array.GetNumberOfComponents() + if components == 1: + return float(array.GetComponent(id_to_select, 0)) + return [float(array.GetComponent(id_to_select, i)) for i in range(components)] + + def extract_picked_attributes( + self, + pipeline: VtkPipeline, + id_to_select: int, + field_type: str, + dataset: vtkDataObject | None, + ) -> dict[str, list[float] | float]: + data_object = dataset or pipeline.reader.GetOutputDataObject(0) + if not isinstance(data_object, vtkDataSet): + return {} + field_data = ( + data_object.GetCellData() + if field_type == "CELL" + else data_object.GetPointData() + ) + attributes = {} + for i in range(field_data.GetNumberOfArrays()): + array = field_data.GetArray(i) + if array and array.GetName(): + attributes[array.GetName()] = self.get_array_values(array, id_to_select) + if field_type == "POINT" and (coords := data_object.GetPoint(id_to_select)): + attributes["coordinates"] = list(coords) + return attributes + def update_grid_scale_and_clipping_range(self) -> None: grid_scale = self.get_grid_scale() if grid_scale is not None: