From 7daaeacbae681f4ecef2e276b71975a6087bd3b6 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Mon, 27 Jul 2026 13:28:18 +0200 Subject: [PATCH 01/24] feat(clipping_planes): Using vtkPlane to set clipping planes --- .../rpc/viewer/schemas/__init__.py | 1 + .../rpc/viewer/schemas/clipping_planes.json | 46 +++++++++++++++++++ .../rpc/viewer/schemas/clipping_planes.py | 17 +++++++ .../rpc/viewer/viewer_protocols.py | 11 +++++ src/opengeodeweb_viewer/vtk_protocol.py | 14 ++++++ tests/test_viewer_protocols.py | 22 +++++++++ 6 files changed, 111 insertions(+) create mode 100644 src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.json create mode 100644 src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.py diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py b/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py index 0fe2b748..ea1ce54a 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py @@ -13,3 +13,4 @@ from .grid_scale import * from .get_point_position import * from .axes import * +from .clipping_planes import * diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.json b/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.json new file mode 100644 index 00000000..56533eec --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.json @@ -0,0 +1,46 @@ +{ + "rpc": "clipping_planes", + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "planes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "origin": { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 + }, + "normal": { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 + } + }, + "required": [ + "origin", + "normal" + ], + "additionalProperties": false + } + } + }, + "required": [ + "ids", + "planes" + ], + "additionalProperties": false +} diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.py b/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.py new file mode 100644 index 00000000..6c5d0118 --- /dev/null +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.py @@ -0,0 +1,17 @@ +from dataclasses_json import DataClassJsonMixin +from dataclasses import dataclass + + +@dataclass +class PlaneData(DataClassJsonMixin): + origin: list[float] + normal: list[float] + + +@dataclass +class ClippingPlanes(DataClassJsonMixin): + def __post_init__(self) -> None: + print(self, flush=True) + + ids: list[str] + planes: list[PlaneData] diff --git a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py index 408fca26..63742dca 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py +++ b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py @@ -362,6 +362,17 @@ def setHighlight( "attributes": data_attributes, } + + @exportRpc(viewer_prefix + viewer_schemas_dict["clipping_planes"]["rpc"]) + def setClippingPlanes(self, rpc_params: RpcParams) -> None: + validate_schema( + rpc_params, self.viewer_schemas_dict["clipping_planes"], self.viewer_prefix + ) + params = schemas.ClippingPlanes.from_dict(rpc_params) + self.SetClippingPlanes(params.ids, params.planes) + 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 ef8a6f36..d9c3a07d 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -28,6 +28,7 @@ vtkBoundingBox, vtkSelection, vtkSelectionNode, + vtkPlane, ) from vtkmodules.vtkFiltersExtraction import vtkExtractSelection from vtkmodules.vtkCommonCore import vtkStringArray, vtkIdTypeArray @@ -42,6 +43,7 @@ from opengeodeweb_microservice.database.connection import get_session, init_database from opengeodeweb_microservice.database.data import Data from opengeodeweb_microservice.database.data_types import ViewerType, ViewerElementsType +from opengeodeweb_viewer.rpc.viewer.schemas import PlaneData @dataclass @@ -198,6 +200,18 @@ def clear_highlights(self, data_ids: list[str]) -> None: pipeline = self.get_vtk_pipeline(data_id) pipeline.highlight.actor.VisibilityOff() + def SetClippingPlanes( + self, data_ids: list[str], planes_data: list[PlaneData] + ) -> None: + for data_id in data_ids: + pipeline = self.get_vtk_pipeline(data_id) + pipeline.mapper.RemoveAllClippingPlanes() + for plane_info in planes_data: + plane = vtkPlane() + plane.SetOrigin(plane_info.origin) + plane.SetNormal(plane_info.normal) + pipeline.mapper.AddClippingPlane(plane) + 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: diff --git a/tests/test_viewer_protocols.py b/tests/test_viewer_protocols.py index 7ebcb0df..d3bf6911 100644 --- a/tests/test_viewer_protocols.py +++ b/tests/test_viewer_protocols.py @@ -361,3 +361,25 @@ def test_combined_scaling_and_grid( [{"z_scale": 2.5}], ) assert server.compare_image("viewer/combined_scaling_and_grid.jpeg") == True + + +def test_clipping_planes( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + test_register_mesh(server, dataset_factory) + + server.call( + VtkViewerView.viewer_prefix + + VtkViewerView.viewer_schemas_dict["clipping_planes"]["rpc"], + [ + { + "ids": ["123456789"], + "planes": [ + { + "origin": [0.0, 0.0, 0.0], + "normal": [1.0, 0.0, 0.0], + } + ], + } + ], + ) From 57f8a933542abbbef297a93c713dc92fc004f445 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Mon, 27 Jul 2026 15:08:18 +0200 Subject: [PATCH 02/24] clipping_plane and crinkle clip --- .../rpc/viewer/schemas/clipping_planes.json | 1 + .../rpc/viewer/schemas/clipping_planes.py | 3 --- .../rpc/viewer/viewer_protocols.py | 2 +- src/opengeodeweb_viewer/vtk_protocol.py | 22 ++++++++++++++++--- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.json b/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.json index 56533eec..96e14215 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.json +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.json @@ -10,6 +10,7 @@ }, "planes": { "type": "array", + "minItems": 1, "items": { "type": "object", "properties": { diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.py b/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.py index 6c5d0118..ddddce9d 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.py +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.py @@ -10,8 +10,5 @@ class PlaneData(DataClassJsonMixin): @dataclass class ClippingPlanes(DataClassJsonMixin): - def __post_init__(self) -> None: - print(self, flush=True) - ids: list[str] planes: list[PlaneData] diff --git a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py index 63742dca..5b7e3eee 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py +++ b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py @@ -369,7 +369,7 @@ def setClippingPlanes(self, rpc_params: RpcParams) -> None: rpc_params, self.viewer_schemas_dict["clipping_planes"], self.viewer_prefix ) params = schemas.ClippingPlanes.from_dict(rpc_params) - self.SetClippingPlanes(params.ids, params.planes) + self.set_clipping_planes(params.ids, params.planes) self.render(-1) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index d9c3a07d..8cfa3712 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -29,8 +29,13 @@ vtkSelection, vtkSelectionNode, vtkPlane, + vtkImplicitBoolean, ) -from vtkmodules.vtkFiltersExtraction import vtkExtractSelection +from vtkmodules.vtkFiltersExtraction import ( + vtkExtractSelection, + vtkExtractGeometry, +) +from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter from vtkmodules.vtkCommonCore import vtkStringArray, vtkIdTypeArray from vtkmodules.vtkRenderingAnnotation import ( vtkCubeAxesActor, @@ -78,6 +83,7 @@ class VtkPipeline: mapper: vtkMapper filter: vtkAlgorithm | None = None actor: vtkActor = field(default_factory=vtkActor) + clipping_filter: vtkExtractGeometry | None = None highlight: HighlightPipeline = field(default_factory=HighlightPipeline) blockDataSets: list[vtkDataObject | None] = field(default_factory=list) blockGeodeIds: list[str] = field(default_factory=list) @@ -200,17 +206,27 @@ def clear_highlights(self, data_ids: list[str]) -> None: pipeline = self.get_vtk_pipeline(data_id) pipeline.highlight.actor.VisibilityOff() - def SetClippingPlanes( + def set_clipping_planes( self, data_ids: list[str], planes_data: list[PlaneData] ) -> None: for data_id in data_ids: pipeline = self.get_vtk_pipeline(data_id) pipeline.mapper.RemoveAllClippingPlanes() + implicit_boolean = vtkImplicitBoolean() + implicit_boolean.SetOperationTypeToIntersection() for plane_info in planes_data: plane = vtkPlane() plane.SetOrigin(plane_info.origin) plane.SetNormal(plane_info.normal) - pipeline.mapper.AddClippingPlane(plane) + implicit_boolean.AddFunction(plane) + clipping_filter = vtkExtractGeometry() + clipping_filter.SetInputConnection(pipeline.reader.GetOutputPort()) + clipping_filter.SetImplicitFunction(implicit_boolean) + pipeline.clipping_filter = clipping_filter + geom_filter = vtkGeometryFilter() + geom_filter.SetInputConnection(clipping_filter.GetOutputPort()) + pipeline.mapper.SetInputConnection(geom_filter.GetOutputPort()) + 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). From 661baa1f762708f6471aafb7f99f699d4d01697c Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Mon, 27 Jul 2026 13:29:03 +0000 Subject: [PATCH 03/24] Apply prepare changes --- opengeodeweb_viewer_schemas.json | 48 +++++++++++++++++++ requirements.txt | 1 - .../rpc/viewer/schemas/__init__.py | 2 +- .../rpc/viewer/schemas/clipping_planes.py | 17 +++++-- .../rpc/viewer/viewer_protocols.py | 2 - src/opengeodeweb_viewer/vtk_protocol.py | 1 - 6 files changed, 61 insertions(+), 10 deletions(-) diff --git a/opengeodeweb_viewer_schemas.json b/opengeodeweb_viewer_schemas.json index be483a47..83fbf13b 100644 --- a/opengeodeweb_viewer_schemas.json +++ b/opengeodeweb_viewer_schemas.json @@ -2350,6 +2350,54 @@ ], "additionalProperties": false }, + "clipping_planes": { + "$id": "opengeodeweb_viewer.viewer.clipping_planes", + "rpc": "clipping_planes", + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "planes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "origin": { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 + }, + "normal": { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 + } + }, + "required": [ + "origin", + "normal" + ], + "additionalProperties": false + } + } + }, + "required": [ + "ids", + "planes" + ], + "additionalProperties": false + }, "axes": { "$id": "opengeodeweb_viewer.viewer.axes", "rpc": "axes", diff --git a/requirements.txt b/requirements.txt index 344c705a..9108a15f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -63,4 +63,3 @@ wslink==1.12.4 yarl>=1 # via aiohttp -opengeodeweb-microservice==1.*,>=1.1.4 diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py b/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py index ea1ce54a..ee2b4ce8 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/__init__.py @@ -12,5 +12,5 @@ from .highlight import * from .grid_scale import * from .get_point_position import * -from .axes import * from .clipping_planes import * +from .axes import * diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.py b/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.py index ddddce9d..eb81c8e5 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.py +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.py @@ -1,14 +1,21 @@ from dataclasses_json import DataClassJsonMixin from dataclasses import dataclass +from typing import List @dataclass -class PlaneData(DataClassJsonMixin): - origin: list[float] - normal: list[float] +class Plane(DataClassJsonMixin): + def __post_init__(self) -> None: + print(self, flush=True) + + normal: List[float] + origin: List[float] @dataclass class ClippingPlanes(DataClassJsonMixin): - ids: list[str] - planes: list[PlaneData] + def __post_init__(self) -> None: + print(self, flush=True) + + ids: List[str] + planes: List[Plane] diff --git a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py index 5b7e3eee..57c5411f 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py +++ b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py @@ -362,7 +362,6 @@ def setHighlight( "attributes": data_attributes, } - @exportRpc(viewer_prefix + viewer_schemas_dict["clipping_planes"]["rpc"]) def setClippingPlanes(self, rpc_params: RpcParams) -> None: validate_schema( @@ -372,7 +371,6 @@ def setClippingPlanes(self, rpc_params: RpcParams) -> None: self.set_clipping_planes(params.ids, params.planes) 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 8cfa3712..0010af8c 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -227,7 +227,6 @@ def set_clipping_planes( geom_filter.SetInputConnection(clipping_filter.GetOutputPort()) pipeline.mapper.SetInputConnection(geom_filter.GetOutputPort()) - 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: From 4c207ed3257b5bf090b24e4c28bd0f499ba83c16 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Mon, 27 Jul 2026 15:32:25 +0200 Subject: [PATCH 04/24] plane --- src/opengeodeweb_viewer/vtk_protocol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 0010af8c..91e4eb91 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -48,7 +48,7 @@ from opengeodeweb_microservice.database.connection import get_session, init_database from opengeodeweb_microservice.database.data import Data from opengeodeweb_microservice.database.data_types import ViewerType, ViewerElementsType -from opengeodeweb_viewer.rpc.viewer.schemas import PlaneData +from opengeodeweb_viewer.rpc.viewer.schemas import Plane @dataclass @@ -207,7 +207,7 @@ def clear_highlights(self, data_ids: list[str]) -> None: pipeline.highlight.actor.VisibilityOff() def set_clipping_planes( - self, data_ids: list[str], planes_data: list[PlaneData] + self, data_ids: list[str], planes_data: list[Plane] ) -> None: for data_id in data_ids: pipeline = self.get_vtk_pipeline(data_id) From f57ed4f14141a2114f0659c14f45cea629817339 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Mon, 27 Jul 2026 15:35:41 +0200 Subject: [PATCH 05/24] imports --- src/opengeodeweb_viewer/vtk_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 91e4eb91..7d9197e5 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -48,7 +48,7 @@ from opengeodeweb_microservice.database.connection import get_session, init_database from opengeodeweb_microservice.database.data import Data from opengeodeweb_microservice.database.data_types import ViewerType, ViewerElementsType -from opengeodeweb_viewer.rpc.viewer.schemas import Plane +from opengeodeweb_viewer.rpc.viewer.schemas.clipping_planes import Plane @dataclass From 5813c1cf0d8d97c79706101a71098f5524d6ef3f Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Mon, 27 Jul 2026 15:36:45 +0200 Subject: [PATCH 06/24] revert --- src/opengeodeweb_viewer/vtk_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 7d9197e5..91e4eb91 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -48,7 +48,7 @@ from opengeodeweb_microservice.database.connection import get_session, init_database from opengeodeweb_microservice.database.data import Data from opengeodeweb_microservice.database.data_types import ViewerType, ViewerElementsType -from opengeodeweb_viewer.rpc.viewer.schemas.clipping_planes import Plane +from opengeodeweb_viewer.rpc.viewer.schemas import Plane @dataclass From 35fe0303f14334d99a923aa0a62cb3d57f916107 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Mon, 27 Jul 2026 15:50:50 +0200 Subject: [PATCH 07/24] comments --- .../rpc/viewer/schemas/clipping_planes.json | 5 +++-- src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py | 3 +-- src/opengeodeweb_viewer/vtk_protocol.py | 8 ++++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.json b/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.json index 96e14215..83b38706 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.json +++ b/src/opengeodeweb_viewer/rpc/viewer/schemas/clipping_planes.json @@ -5,12 +5,13 @@ "ids": { "type": "array", "items": { - "type": "string" + "type": "string", + "minLength": 32, + "maxLength": 32 } }, "planes": { "type": "array", - "minItems": 1, "items": { "type": "object", "properties": { diff --git a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py index 57c5411f..7450b2bd 100644 --- a/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py +++ b/src/opengeodeweb_viewer/rpc/viewer/viewer_protocols.py @@ -31,7 +31,7 @@ RpcParams, ) from opengeodeweb_viewer.vtk_protocol import VtkView -from . import schemas +from opengeodeweb_viewer.rpc.viewer import schemas class VtkViewerView(VtkView): @@ -369,7 +369,6 @@ def setClippingPlanes(self, rpc_params: RpcParams) -> None: ) params = schemas.ClippingPlanes.from_dict(rpc_params) self.set_clipping_planes(params.ids, params.planes) - self.render(-1) @exportRpc(viewer_prefix + viewer_schemas_dict["set_z_scaling"]["rpc"]) def setZScaling(self, rpc_params: RpcParams) -> None: diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 91e4eb91..385fad8a 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -48,7 +48,7 @@ from opengeodeweb_microservice.database.connection import get_session, init_database from opengeodeweb_microservice.database.data import Data from opengeodeweb_microservice.database.data_types import ViewerType, ViewerElementsType -from opengeodeweb_viewer.rpc.viewer.schemas import Plane +from opengeodeweb_viewer.rpc.viewer.schemas.clipping_planes import Plane @dataclass @@ -211,7 +211,11 @@ def set_clipping_planes( ) -> None: for data_id in data_ids: pipeline = self.get_vtk_pipeline(data_id) - pipeline.mapper.RemoveAllClippingPlanes() + if not planes_data: + if pipeline.clipping_filter is not None: + pipeline.mapper.SetInputConnection(pipeline.reader.GetOutputPort()) + pipeline.clipping_filter = None + continue implicit_boolean = vtkImplicitBoolean() implicit_boolean.SetOperationTypeToIntersection() for plane_info in planes_data: From 125de264eb3140c3f2264c1c33e3534aed972036 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Mon, 27 Jul 2026 13:51:25 +0000 Subject: [PATCH 08/24] Apply prepare changes --- opengeodeweb_viewer_schemas.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/opengeodeweb_viewer_schemas.json b/opengeodeweb_viewer_schemas.json index 83fbf13b..e406edce 100644 --- a/opengeodeweb_viewer_schemas.json +++ b/opengeodeweb_viewer_schemas.json @@ -2358,12 +2358,13 @@ "ids": { "type": "array", "items": { - "type": "string" + "type": "string", + "minLength": 32, + "maxLength": 32 } }, "planes": { "type": "array", - "minItems": 1, "items": { "type": "object", "properties": { From ea37d5923724bc2a7b08d6642d2844d9f5c807ea Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Mon, 27 Jul 2026 15:52:48 +0200 Subject: [PATCH 09/24] planes = [] --- src/opengeodeweb_viewer/vtk_protocol.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 385fad8a..1d9d4ce5 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -212,9 +212,8 @@ def set_clipping_planes( for data_id in data_ids: pipeline = self.get_vtk_pipeline(data_id) if not planes_data: - if pipeline.clipping_filter is not None: - pipeline.mapper.SetInputConnection(pipeline.reader.GetOutputPort()) - pipeline.clipping_filter = None + pipeline.mapper.SetInputConnection(pipeline.reader.GetOutputPort()) + pipeline.clipping_filter = None continue implicit_boolean = vtkImplicitBoolean() implicit_boolean.SetOperationTypeToIntersection() From ca9c2f15e27e477c9a4bd753b160bb82aeca924e Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Tue, 28 Jul 2026 17:20:30 +0200 Subject: [PATCH 10/24] near model attribute still --- .../object/object_methods.py | 23 +-- .../rpc/mesh/mesh_protocols.py | 20 +-- .../rpc/model/model_protocols.py | 3 + src/opengeodeweb_viewer/vtk_protocol.py | 136 ++++++++++++++++-- 4 files changed, 137 insertions(+), 45 deletions(-) diff --git a/src/opengeodeweb_viewer/object/object_methods.py b/src/opengeodeweb_viewer/object/object_methods.py index 1f2de7cf..37cb1a0b 100644 --- a/src/opengeodeweb_viewer/object/object_methods.py +++ b/src/opengeodeweb_viewer/object/object_methods.py @@ -133,27 +133,6 @@ def SetPointsColor( actor = self.get_vtk_pipeline(data_id).actor actor.GetProperty().SetVertexColor([red / 255, green / 255, blue / 255]) - def _prune_hidden_blocks( - self, - 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()): - block = dataset.GetBlock(index) - if block is None: - continue - if not visibility_attributes.GetBlockVisibility(block): - continue - if isinstance(block, vtkMultiBlockDataSet): - pruned.SetBlock( - index, self._prune_hidden_blocks(block, visibility_attributes) - ) - else: - pruned.SetBlock(index, block) - return pruned def SetBlocksVisibility( self, data_id: str, block_ids: list[int], visibility: bool @@ -166,7 +145,7 @@ def SetBlocksVisibility( visibility_attributes = mapper.GetCompositeDataDisplayAttributes() for block_id in block_ids: visibility_attributes.SetBlockVisibility(blocks[block_id], visibility) - dataset = (pipeline.filter or pipeline.reader).GetOutputDataObject(0) + dataset = mapper.GetInputDataObject(0, 0) if not isinstance(dataset, vtkMultiBlockDataSet): return # Re-build a pruned dataset for the dedicated pick mapper diff --git a/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py b/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py index 0834a2d7..bca3b75f 100644 --- a/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py +++ b/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py @@ -127,10 +127,12 @@ def displayAttributeOnVertices( minimum: float, maximum: float, ) -> None: - reader = self.get_vtk_pipeline(data_id).reader - points = reader.GetOutputAsDataSet().GetPointData() - points.SetActiveScalars(name) - mapper = self.get_vtk_pipeline(data_id).mapper + pipeline = self.get_vtk_pipeline(data_id) + pipeline.reader.GetOutputAsDataSet().GetPointData().SetActiveScalars(name) + active_ds = pipeline.mapper.GetInputDataObject(0, 0) + if active_ds: + active_ds.GetPointData().SetActiveScalars(name) + mapper = pipeline.mapper mapper.ScalarVisibilityOn() mapper.SetScalarModeToUsePointData() mapper.SetArrayComponent(item) @@ -145,10 +147,12 @@ def displayAttributeOnCells( minimum: float, maximum: float, ) -> None: - reader = self.get_vtk_pipeline(data_id).reader - cells = reader.GetOutputAsDataSet().GetCellData() - cells.SetActiveScalars(name) - mapper = self.get_vtk_pipeline(data_id).mapper + pipeline = self.get_vtk_pipeline(data_id) + pipeline.reader.GetOutputAsDataSet().GetCellData().SetActiveScalars(name) + active_ds = pipeline.mapper.GetInputDataObject(0, 0) + if active_ds: + active_ds.GetCellData().SetActiveScalars(name) + mapper = pipeline.mapper mapper.ScalarVisibilityOn() mapper.SetScalarModeToUseCellData() mapper.SetArrayComponent(item) diff --git a/src/opengeodeweb_viewer/rpc/model/model_protocols.py b/src/opengeodeweb_viewer/rpc/model/model_protocols.py index 9753616b..035a3877 100644 --- a/src/opengeodeweb_viewer/rpc/model/model_protocols.py +++ b/src/opengeodeweb_viewer/rpc/model/model_protocols.py @@ -127,6 +127,9 @@ def updateBlockColors(self, pipeline: VtkPipeline, block_id: int) -> None: other_field_data.SetActiveScalars("") mapper = pipeline.mapper + attr = mapper.GetCompositeDataDisplayAttributes() + if attr: + attr.RemoveBlockColor(block) mapper.ScalarVisibilityOn() mapper.SetColorModeToDirectScalars() mapper.SetScalarModeToDefault() diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 1d9d4ce5..3fc07f62 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -11,7 +11,10 @@ vtkXMLReader, ) from vtkmodules.vtkWebCore import vtkWebApplication -from vtkmodules.vtkCommonExecutionModel import vtkAlgorithm +from vtkmodules.vtkCommonExecutionModel import ( + vtkAlgorithm, + vtkCompositeDataPipeline, +) from vtkmodules.vtkRenderingCore import ( vtkActor, vtkMapper, @@ -25,6 +28,7 @@ from vtkmodules.vtkCommonDataModel import ( vtkDataObject, vtkDataSet, + vtkMultiBlockDataSet, vtkBoundingBox, vtkSelection, vtkSelectionNode, @@ -195,8 +199,8 @@ def update_highlight( selection_list.SetNumberOfComponents(1) selection_list.InsertNextValue(id_to_select) node.SetSelectionList(selection_list) - if dataset is not None: - pipeline.highlight.extractSelection.SetInputData(0, dataset) + target_dataset = dataset or pipeline.mapper.GetInputDataObject(0, 0) + pipeline.highlight.extractSelection.SetInputData(0, target_dataset) pipeline.highlight.extractSelection.Modified() pipeline.highlight.extractSelection.Update() pipeline.highlight.actor.VisibilityOn() @@ -206,29 +210,131 @@ def clear_highlights(self, data_ids: list[str]) -> None: pipeline = self.get_vtk_pipeline(data_id) pipeline.highlight.actor.VisibilityOff() + def _extract_blocks( + self, multiblock: vtkDataObject | None + ) -> list[vtkDataObject | None]: + if not isinstance(multiblock, vtkMultiBlockDataSet): + return [] + blocks: list[vtkDataObject | None] = [] + iterator = multiblock.NewTreeIterator() + iterator.InitTraversal() + while not iterator.IsDoneWithTraversal(): + flat_index = iterator.GetCurrentFlatIndex() + blocks.extend([None] * (flat_index + 1 - len(blocks))) + blocks[flat_index] = iterator.GetCurrentDataObject() + iterator.GoToNextItem() + return blocks + + def _prune_hidden_blocks( + self, dataset: vtkMultiBlockDataSet, attributes: Any + ) -> vtkMultiBlockDataSet: + pruned = vtkMultiBlockDataSet() + pruned.SetNumberOfBlocks(dataset.GetNumberOfBlocks()) + for index in range(dataset.GetNumberOfBlocks()): + block = dataset.GetBlock(index) + if block and attributes.GetBlockVisibility(block): + child = ( + self._prune_hidden_blocks(block, attributes) + if isinstance(block, vtkMultiBlockDataSet) + else block + ) + pruned.SetBlock(index, child) + return pruned + + def _transfer_composite_attributes( + self, + attributes: Any, + source_blocks: list[vtkDataObject | None], + destination_blocks: list[vtkDataObject | None], + ) -> None: + color_rgb = [0.0, 0.0, 0.0] + for source_block, destination_block in zip(source_blocks, destination_blocks): + if source_block and destination_block: + if attributes.HasBlockColor(source_block): + attributes.GetBlockColor(source_block, color_rgb) + attributes.SetBlockColor(destination_block, color_rgb) + else: + attributes.RemoveBlockColor(destination_block) + if attributes.HasBlockVisibility(source_block): + attributes.SetBlockVisibility( + destination_block, attributes.GetBlockVisibility(source_block) + ) + if attributes.HasBlockOpacity(source_block): + attributes.SetBlockOpacity( + destination_block, attributes.GetBlockOpacity(source_block) + ) + + def _create_implicit_boolean(self, planes_data: list[Plane]) -> vtkImplicitBoolean: + implicit_boolean = vtkImplicitBoolean() + implicit_boolean.SetOperationTypeToIntersection() + for plane_info in planes_data: + plane = vtkPlane() + plane.SetOrigin(plane_info.origin) + plane.SetNormal(plane_info.normal) + implicit_boolean.AddFunction(plane) + return implicit_boolean + + def _sync_composite_pipeline( + self, pipeline: VtkPipeline, dataset: vtkDataObject + ) -> None: + blocks = self._extract_blocks(dataset) + attributes = pipeline.mapper.GetCompositeDataDisplayAttributes() + self._transfer_composite_attributes(attributes, pipeline.blockDataSets, blocks) + pipeline.blockDataSets = blocks + pipeline.mapper.SetInputDataObject(dataset) + if pipeline.pick_mapper and isinstance(dataset, vtkMultiBlockDataSet): + pipeline.pick_mapper.SetInputDataObject( + self._prune_hidden_blocks(dataset, attributes) + ) + if hasattr(self, "updateBlockColors"): + for block_id in pipeline.block_styles: + self.updateBlockColors(pipeline, block_id) + def set_clipping_planes( self, data_ids: list[str], planes_data: list[Plane] ) -> None: for data_id in data_ids: pipeline = self.get_vtk_pipeline(data_id) + is_composite = isinstance(pipeline.mapper, vtkCompositePolyDataMapper) if not planes_data: - pipeline.mapper.SetInputConnection(pipeline.reader.GetOutputPort()) + if is_composite: + original_dataset = ( + pipeline.filter.GetOutputDataObject(0) + if pipeline.filter + else pipeline.reader.GetOutputDataObject(0) + ) + self._sync_composite_pipeline(pipeline, original_dataset) + else: + pipeline.mapper.SetInputConnection(pipeline.reader.GetOutputPort()) + reader_dataset = pipeline.reader.GetOutputAsDataSet() + if active_points := reader_dataset.GetPointData().GetScalars(): + reader_dataset.GetPointData().SetActiveScalars(active_points.GetName()) + if active_cells := reader_dataset.GetCellData().GetScalars(): + reader_dataset.GetCellData().SetActiveScalars(active_cells.GetName()) pipeline.clipping_filter = None continue - implicit_boolean = vtkImplicitBoolean() - implicit_boolean.SetOperationTypeToIntersection() - for plane_info in planes_data: - plane = vtkPlane() - plane.SetOrigin(plane_info.origin) - plane.SetNormal(plane_info.normal) - implicit_boolean.AddFunction(plane) clipping_filter = vtkExtractGeometry() + geometry_filter = vtkGeometryFilter() + if is_composite: + clipping_filter.SetExecutive(vtkCompositeDataPipeline()) + geometry_filter.SetExecutive(vtkCompositeDataPipeline()) clipping_filter.SetInputConnection(pipeline.reader.GetOutputPort()) - clipping_filter.SetImplicitFunction(implicit_boolean) + clipping_filter.SetImplicitFunction(self._create_implicit_boolean(planes_data)) + geometry_filter.SetInputConnection(clipping_filter.GetOutputPort()) + geometry_filter.Update() + if is_composite: + self._sync_composite_pipeline( + pipeline, geometry_filter.GetOutputDataObject(0) + ) + else: + pipeline.mapper.SetInputConnection(geometry_filter.GetOutputPort()) + reader_dataset = pipeline.reader.GetOutputAsDataSet() + filtered_dataset = geometry_filter.GetOutputDataObject(0) + if active_points := reader_dataset.GetPointData().GetScalars(): + filtered_dataset.GetPointData().SetActiveScalars(active_points.GetName()) + if active_cells := reader_dataset.GetCellData().GetScalars(): + filtered_dataset.GetCellData().SetActiveScalars(active_cells.GetName()) pipeline.clipping_filter = clipping_filter - geom_filter = vtkGeometryFilter() - geom_filter.SetInputConnection(clipping_filter.GetOutputPort()) - pipeline.mapper.SetInputConnection(geom_filter.GetOutputPort()) 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). From cfc552f240bfd74fb5293eefb98331c0073e238e Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:21:08 +0000 Subject: [PATCH 11/24] Apply prepare changes --- .../object/object_methods.py | 1 - src/opengeodeweb_viewer/vtk_protocol.py | 20 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/opengeodeweb_viewer/object/object_methods.py b/src/opengeodeweb_viewer/object/object_methods.py index 37cb1a0b..78ba07af 100644 --- a/src/opengeodeweb_viewer/object/object_methods.py +++ b/src/opengeodeweb_viewer/object/object_methods.py @@ -133,7 +133,6 @@ def SetPointsColor( actor = self.get_vtk_pipeline(data_id).actor actor.GetProperty().SetVertexColor([red / 255, green / 255, blue / 255]) - def SetBlocksVisibility( self, data_id: str, block_ids: list[int], visibility: bool ) -> None: diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 3fc07f62..2a60e015 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -308,9 +308,13 @@ def set_clipping_planes( pipeline.mapper.SetInputConnection(pipeline.reader.GetOutputPort()) reader_dataset = pipeline.reader.GetOutputAsDataSet() if active_points := reader_dataset.GetPointData().GetScalars(): - reader_dataset.GetPointData().SetActiveScalars(active_points.GetName()) + reader_dataset.GetPointData().SetActiveScalars( + active_points.GetName() + ) if active_cells := reader_dataset.GetCellData().GetScalars(): - reader_dataset.GetCellData().SetActiveScalars(active_cells.GetName()) + reader_dataset.GetCellData().SetActiveScalars( + active_cells.GetName() + ) pipeline.clipping_filter = None continue clipping_filter = vtkExtractGeometry() @@ -319,7 +323,9 @@ def set_clipping_planes( clipping_filter.SetExecutive(vtkCompositeDataPipeline()) geometry_filter.SetExecutive(vtkCompositeDataPipeline()) clipping_filter.SetInputConnection(pipeline.reader.GetOutputPort()) - clipping_filter.SetImplicitFunction(self._create_implicit_boolean(planes_data)) + clipping_filter.SetImplicitFunction( + self._create_implicit_boolean(planes_data) + ) geometry_filter.SetInputConnection(clipping_filter.GetOutputPort()) geometry_filter.Update() if is_composite: @@ -331,9 +337,13 @@ def set_clipping_planes( reader_dataset = pipeline.reader.GetOutputAsDataSet() filtered_dataset = geometry_filter.GetOutputDataObject(0) if active_points := reader_dataset.GetPointData().GetScalars(): - filtered_dataset.GetPointData().SetActiveScalars(active_points.GetName()) + filtered_dataset.GetPointData().SetActiveScalars( + active_points.GetName() + ) if active_cells := reader_dataset.GetCellData().GetScalars(): - filtered_dataset.GetCellData().SetActiveScalars(active_cells.GetName()) + filtered_dataset.GetCellData().SetActiveScalars( + active_cells.GetName() + ) pipeline.clipping_filter = clipping_filter def swap_pick_mappers(self, data_ids: list[str], use_pick_mapper: bool) -> None: From 6a4816d3e37e16591aebbdaf07886c605c98291f Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Wed, 29 Jul 2026 11:40:32 +0200 Subject: [PATCH 12/24] temp need refacto --- .../object/object_methods.py | 1 + .../rpc/model/model_protocols.py | 76 +---------- src/opengeodeweb_viewer/vtk_protocol.py | 128 +++++++++++++----- 3 files changed, 99 insertions(+), 106 deletions(-) diff --git a/src/opengeodeweb_viewer/object/object_methods.py b/src/opengeodeweb_viewer/object/object_methods.py index 78ba07af..17c0a5f8 100644 --- a/src/opengeodeweb_viewer/object/object_methods.py +++ b/src/opengeodeweb_viewer/object/object_methods.py @@ -142,6 +142,7 @@ def SetBlocksVisibility( raise Exception("Mapper is not a vtkCompositePolyDataMapper") blocks = pipeline.blockDataSets visibility_attributes = mapper.GetCompositeDataDisplayAttributes() + print(f"{visibility_attributes=}", flush=True) for block_id in block_ids: visibility_attributes.SetBlockVisibility(blocks[block_id], visibility) dataset = mapper.GetInputDataObject(0, 0) diff --git a/src/opengeodeweb_viewer/rpc/model/model_protocols.py b/src/opengeodeweb_viewer/rpc/model/model_protocols.py index 035a3877..15f8a49f 100644 --- a/src/opengeodeweb_viewer/rpc/model/model_protocols.py +++ b/src/opengeodeweb_viewer/rpc/model/model_protocols.py @@ -60,81 +60,6 @@ class VtkModelView(VtkObjectView): def __init__(self) -> None: super().__init__() - def _get_block_style(self, pipeline: VtkPipeline, block_id: int) -> BlockStyle: - if block_id not in pipeline.block_styles: - style = BlockStyle( - name="", - attribute_location="point", - points=[], - minimum=0.0, - maximum=1.0, - item=0, - ) - pipeline.block_styles[block_id] = style - return pipeline.block_styles[block_id] - - def updateBlockColors(self, pipeline: VtkPipeline, block_id: int) -> None: - block = pipeline.blockDataSets[block_id] - if not isinstance(block, vtkDataSet): - return - - style = self._get_block_style(pipeline, block_id) - if not style["name"]: - block.GetPointData().SetActiveScalars("") - block.GetCellData().SetActiveScalars("") - return - - field_data = ( - block.GetPointData() - if style["attribute_location"] == "point" - else block.GetCellData() - ) - scalar_array = field_data.GetArray(style["name"]) - if not scalar_array: - return - - lut = vtkColorTransferFunction() - points = style["points"] - minimum = style["minimum"] - maximum = style["maximum"] - if points: - x_min, x_max = points[0], points[-4] - span = x_max - x_min - for i in range(0, len(points), 4): - x, r, g, b = points[i : i + 4] - new_x = ( - minimum + (x - x_min) / span * (maximum - minimum) - if span - else minimum - ) - lut.AddRGBPoint(new_x, r, g, b) - else: - lut.AddRGBPoint(minimum, 0, 0, 0) - lut.AddRGBPoint(maximum, 1, 1, 1) - - lut.SetRange(minimum, maximum) - rgba_colors = lut.MapScalars(scalar_array, 0, style.get("item", 0)) - rgba_colors.SetName(f"__colors_{style['name']}") - - field_data.AddArray(rgba_colors) - field_data.SetActiveScalars(rgba_colors.GetName()) - - other_field_data = ( - block.GetCellData() - if style["attribute_location"] == "point" - else block.GetPointData() - ) - other_field_data.SetActiveScalars("") - - mapper = pipeline.mapper - attr = mapper.GetCompositeDataDisplayAttributes() - if attr: - attr.RemoveBlockColor(block) - mapper.ScalarVisibilityOn() - mapper.SetColorModeToDirectScalars() - mapper.SetScalarModeToDefault() - mapper.Modified() - def apply_color( self, pipeline: VtkPipeline, @@ -146,6 +71,7 @@ def apply_color( if not isinstance(mapper, vtkCompositePolyDataMapper): return [] attr = mapper.GetCompositeDataDisplayAttributes() + print(f"{attr=}", flush=True) colors: list[ColorResult] = [] for block_id in block_ids: block_dataset = pipeline.blockDataSets[block_id] diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 2a60e015..294f8f1c 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -1,7 +1,7 @@ # Standard library imports import math import os -from typing import cast, Any, Literal, TypedDict +from typing import cast, Any, Literal, TypedDict, Callable from dataclasses import dataclass, field # Third party imports @@ -22,8 +22,10 @@ vtkRenderWindow, vtkDataSetMapper, vtkCompositePolyDataMapper, + vtkCompositeDataDisplayAttributes, vtkCellPicker, vtkHardwarePicker, + vtkColorTransferFunction, ) from vtkmodules.vtkCommonDataModel import ( vtkDataObject, @@ -226,7 +228,7 @@ def _extract_blocks( return blocks def _prune_hidden_blocks( - self, dataset: vtkMultiBlockDataSet, attributes: Any + self, dataset: vtkMultiBlockDataSet, attributes: vtkCompositeDataDisplayAttributes ) -> vtkMultiBlockDataSet: pruned = vtkMultiBlockDataSet() pruned.SetNumberOfBlocks(dataset.GetNumberOfBlocks()) @@ -241,14 +243,90 @@ def _prune_hidden_blocks( pruned.SetBlock(index, child) return pruned - def _transfer_composite_attributes( - self, - attributes: Any, - source_blocks: list[vtkDataObject | None], - destination_blocks: list[vtkDataObject | None], + def _get_block_style(self, pipeline: VtkPipeline, block_id: int) -> BlockStyle: + if block_id not in pipeline.block_styles: + style = BlockStyle( + name="", + attribute_location="point", + points=[], + minimum=0.0, + maximum=1.0, + item=0, + ) + pipeline.block_styles[block_id] = style + return pipeline.block_styles[block_id] + + def updateBlockColors(self, pipeline: VtkPipeline, block_id: int) -> None: + block = pipeline.blockDataSets[block_id] + if not isinstance(block, vtkDataSet): + return + + style = self._get_block_style(pipeline, block_id) + if not style["name"]: + block.GetPointData().SetActiveScalars("") + block.GetCellData().SetActiveScalars("") + return + + field_data = ( + block.GetPointData() + if style["attribute_location"] == "point" + else block.GetCellData() + ) + scalar_array = field_data.GetArray(style["name"]) + if not scalar_array: + return + + lut = vtkColorTransferFunction() + points = style["points"] + minimum = style["minimum"] + maximum = style["maximum"] + if points: + x_min, x_max = points[0], points[-4] + span = x_max - x_min + for i in range(0, len(points), 4): + x, r, g, b = points[i : i + 4] + new_x = ( + minimum + (x - x_min) / span * (maximum - minimum) + if span + else minimum + ) + lut.AddRGBPoint(new_x, r, g, b) + else: + lut.AddRGBPoint(minimum, 0, 0, 0) + lut.AddRGBPoint(maximum, 1, 1, 1) + + lut.SetRange(minimum, maximum) + rgba_colors = lut.MapScalars(scalar_array, 0, style.get("item", 0)) + rgba_colors.SetName(f"__colors_{style['name']}") + + field_data.AddArray(rgba_colors) + field_data.SetActiveScalars(rgba_colors.GetName()) + + other_field_data = ( + block.GetCellData() + if style["attribute_location"] == "point" + else block.GetPointData() + ) + other_field_data.SetActiveScalars("") + + mapper = pipeline.mapper + attributes = mapper.GetCompositeDataDisplayAttributes() + if attributes: + attributes.RemoveBlockColor(block) + mapper.ScalarVisibilityOn() + mapper.SetColorModeToDirectScalars() + mapper.SetScalarModeToDefault() + mapper.Modified() + + def _sync_composite_pipeline( + self, pipeline: VtkPipeline, dataset: vtkDataObject ) -> None: + blocks = self._extract_blocks(dataset) + attributes = pipeline.mapper.GetCompositeDataDisplayAttributes() + print(f"[_sync_composite_pipeline] {attributes=}", flush=True) + print(f"[_sync_composite_pipeline] {pipeline.block_styles=}", flush=True) color_rgb = [0.0, 0.0, 0.0] - for source_block, destination_block in zip(source_blocks, destination_blocks): + for source_block, destination_block in zip(pipeline.blockDataSets, blocks): if source_block and destination_block: if attributes.HasBlockColor(source_block): attributes.GetBlockColor(source_block, color_rgb) @@ -263,32 +341,15 @@ def _transfer_composite_attributes( attributes.SetBlockOpacity( destination_block, attributes.GetBlockOpacity(source_block) ) - - def _create_implicit_boolean(self, planes_data: list[Plane]) -> vtkImplicitBoolean: - implicit_boolean = vtkImplicitBoolean() - implicit_boolean.SetOperationTypeToIntersection() - for plane_info in planes_data: - plane = vtkPlane() - plane.SetOrigin(plane_info.origin) - plane.SetNormal(plane_info.normal) - implicit_boolean.AddFunction(plane) - return implicit_boolean - - def _sync_composite_pipeline( - self, pipeline: VtkPipeline, dataset: vtkDataObject - ) -> None: - blocks = self._extract_blocks(dataset) - attributes = pipeline.mapper.GetCompositeDataDisplayAttributes() - self._transfer_composite_attributes(attributes, pipeline.blockDataSets, blocks) pipeline.blockDataSets = blocks pipeline.mapper.SetInputDataObject(dataset) if pipeline.pick_mapper and isinstance(dataset, vtkMultiBlockDataSet): pipeline.pick_mapper.SetInputDataObject( self._prune_hidden_blocks(dataset, attributes) ) - if hasattr(self, "updateBlockColors"): - for block_id in pipeline.block_styles: - self.updateBlockColors(pipeline, block_id) + for block_id in pipeline.block_styles: + print(f"[_sync_composite_pipeline] Updating block colors for {block_id=}", flush=True) + self.updateBlockColors(pipeline, block_id) def set_clipping_planes( self, data_ids: list[str], planes_data: list[Plane] @@ -323,9 +384,14 @@ def set_clipping_planes( clipping_filter.SetExecutive(vtkCompositeDataPipeline()) geometry_filter.SetExecutive(vtkCompositeDataPipeline()) clipping_filter.SetInputConnection(pipeline.reader.GetOutputPort()) - clipping_filter.SetImplicitFunction( - self._create_implicit_boolean(planes_data) - ) + implicit_boolean = vtkImplicitBoolean() + implicit_boolean.SetOperationTypeToIntersection() + for plane_info in planes_data: + plane = vtkPlane() + plane.SetOrigin(plane_info.origin) + plane.SetNormal(plane_info.normal) + implicit_boolean.AddFunction(plane) + clipping_filter.SetImplicitFunction(implicit_boolean) geometry_filter.SetInputConnection(clipping_filter.GetOutputPort()) geometry_filter.Update() if is_composite: From cffb3dfba38a0a1970aacb5fe67f8fb295c62d8b Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:41:17 +0000 Subject: [PATCH 13/24] Apply prepare changes --- src/opengeodeweb_viewer/vtk_protocol.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 294f8f1c..af3bd73d 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -228,7 +228,9 @@ def _extract_blocks( return blocks def _prune_hidden_blocks( - self, dataset: vtkMultiBlockDataSet, attributes: vtkCompositeDataDisplayAttributes + self, + dataset: vtkMultiBlockDataSet, + attributes: vtkCompositeDataDisplayAttributes, ) -> vtkMultiBlockDataSet: pruned = vtkMultiBlockDataSet() pruned.SetNumberOfBlocks(dataset.GetNumberOfBlocks()) @@ -348,7 +350,10 @@ def _sync_composite_pipeline( self._prune_hidden_blocks(dataset, attributes) ) for block_id in pipeline.block_styles: - print(f"[_sync_composite_pipeline] Updating block colors for {block_id=}", flush=True) + print( + f"[_sync_composite_pipeline] Updating block colors for {block_id=}", + flush=True, + ) self.updateBlockColors(pipeline, block_id) def set_clipping_planes( From bd52885c6b463260267c72125e57598e4b05d7cd Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Wed, 29 Jul 2026 11:54:50 +0200 Subject: [PATCH 14/24] specific mapper --- src/opengeodeweb_viewer/vtk_protocol.py | 47 +++++++++++-------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 294f8f1c..60988a0d 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -1,7 +1,7 @@ # Standard library imports import math import os -from typing import cast, Any, Literal, TypedDict, Callable +from typing import cast, Any, Literal, TypedDict from dataclasses import dataclass, field # Third party imports @@ -309,20 +309,21 @@ def updateBlockColors(self, pipeline: VtkPipeline, block_id: int) -> None: ) other_field_data.SetActiveScalars("") - mapper = pipeline.mapper - attributes = mapper.GetCompositeDataDisplayAttributes() - if attributes: - attributes.RemoveBlockColor(block) - mapper.ScalarVisibilityOn() - mapper.SetColorModeToDirectScalars() - mapper.SetScalarModeToDefault() - mapper.Modified() + if isinstance(pipeline.mapper, vtkCompositePolyDataMapper): + attributes = pipeline.mapper.GetCompositeDataDisplayAttributes() + if attributes: + attributes.RemoveBlockColor(block) + pipeline.mapper.ScalarVisibilityOn() + pipeline.mapper.SetColorModeToDirectScalars() + pipeline.mapper.SetScalarModeToDefault() + pipeline.mapper.Modified() def _sync_composite_pipeline( self, pipeline: VtkPipeline, dataset: vtkDataObject ) -> None: blocks = self._extract_blocks(dataset) - attributes = pipeline.mapper.GetCompositeDataDisplayAttributes() + mapper = cast(vtkCompositePolyDataMapper, pipeline.mapper) + attributes = mapper.GetCompositeDataDisplayAttributes() print(f"[_sync_composite_pipeline] {attributes=}", flush=True) print(f"[_sync_composite_pipeline] {pipeline.block_styles=}", flush=True) color_rgb = [0.0, 0.0, 0.0] @@ -351,6 +352,14 @@ def _sync_composite_pipeline( print(f"[_sync_composite_pipeline] Updating block colors for {block_id=}", flush=True) self.updateBlockColors(pipeline, block_id) + def _restore_active_scalars( + self, source: vtkDataSet, target: vtkDataSet + ) -> None: + if active_points := source.GetPointData().GetScalars(): + target.GetPointData().SetActiveScalars(active_points.GetName()) + if active_cells := source.GetCellData().GetScalars(): + target.GetCellData().SetActiveScalars(active_cells.GetName()) + def set_clipping_planes( self, data_ids: list[str], planes_data: list[Plane] ) -> None: @@ -368,14 +377,7 @@ def set_clipping_planes( else: pipeline.mapper.SetInputConnection(pipeline.reader.GetOutputPort()) reader_dataset = pipeline.reader.GetOutputAsDataSet() - if active_points := reader_dataset.GetPointData().GetScalars(): - reader_dataset.GetPointData().SetActiveScalars( - active_points.GetName() - ) - if active_cells := reader_dataset.GetCellData().GetScalars(): - reader_dataset.GetCellData().SetActiveScalars( - active_cells.GetName() - ) + self._restore_active_scalars(reader_dataset, reader_dataset) pipeline.clipping_filter = None continue clipping_filter = vtkExtractGeometry() @@ -402,14 +404,7 @@ def set_clipping_planes( pipeline.mapper.SetInputConnection(geometry_filter.GetOutputPort()) reader_dataset = pipeline.reader.GetOutputAsDataSet() filtered_dataset = geometry_filter.GetOutputDataObject(0) - if active_points := reader_dataset.GetPointData().GetScalars(): - filtered_dataset.GetPointData().SetActiveScalars( - active_points.GetName() - ) - if active_cells := reader_dataset.GetCellData().GetScalars(): - filtered_dataset.GetCellData().SetActiveScalars( - active_cells.GetName() - ) + self._restore_active_scalars(reader_dataset, filtered_dataset) pipeline.clipping_filter = clipping_filter def swap_pick_mappers(self, data_ids: list[str], use_pick_mapper: bool) -> None: From f177172eb065f0a21533fc140301c111e0a03902 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:55:32 +0000 Subject: [PATCH 15/24] Apply prepare changes --- src/opengeodeweb_viewer/vtk_protocol.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 796c1b38..1bbb0453 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -357,9 +357,7 @@ def _sync_composite_pipeline( ) self.updateBlockColors(pipeline, block_id) - def _restore_active_scalars( - self, source: vtkDataSet, target: vtkDataSet - ) -> None: + def _restore_active_scalars(self, source: vtkDataSet, target: vtkDataSet) -> None: if active_points := source.GetPointData().GetScalars(): target.GetPointData().SetActiveScalars(active_points.GetName()) if active_cells := source.GetCellData().GetScalars(): From 810b92aae408d96ca6ed58ad06ad4f1362275bb2 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Wed, 29 Jul 2026 15:27:19 +0200 Subject: [PATCH 16/24] vtk_pipeline --- .../object/object_methods.py | 22 +- .../rpc/mesh/mesh_protocols.py | 18 +- .../rpc/model/model_protocols.py | 38 ++- src/opengeodeweb_viewer/vtk_pipeline.py | 225 ++++++++++++++++ src/opengeodeweb_viewer/vtk_protocol.py | 243 ++---------------- 5 files changed, 277 insertions(+), 269 deletions(-) create mode 100644 src/opengeodeweb_viewer/vtk_pipeline.py diff --git a/src/opengeodeweb_viewer/object/object_methods.py b/src/opengeodeweb_viewer/object/object_methods.py index 17c0a5f8..84747c4d 100644 --- a/src/opengeodeweb_viewer/object/object_methods.py +++ b/src/opengeodeweb_viewer/object/object_methods.py @@ -1,25 +1,17 @@ -# Standard library imports -import os - # Third party imports -from vtkmodules.vtkIOXML import vtkXMLDataReader, vtkXMLImageDataReader -from vtkmodules.vtkCommonExecutionModel import vtkAlgorithm +from vtkmodules.vtkCommonDataModel import ( + vtkDataSet, + vtkMultiBlockDataSet, +) from vtkmodules.vtkRenderingCore import ( - vtkMapper, vtkActor, - vtkTexture, vtkCompositePolyDataMapper, - vtkCompositeDataDisplayAttributes, vtkDataSetMapper, ) -from vtkmodules.vtkCommonDataModel import ( - vtkDataSet, - vtkMultiBlockDataSet, -) -from vtkmodules.vtkFiltersExtraction import vtkExtractSelection # Local application imports -from opengeodeweb_viewer.vtk_protocol import VtkView, VtkPipeline +from opengeodeweb_viewer.vtk_pipeline import VtkPipeline +from opengeodeweb_viewer.vtk_protocol import VtkView class VtkObjectView(VtkView): @@ -152,7 +144,7 @@ def SetBlocksVisibility( if pipeline.pick_mapper is None: pipeline.pick_mapper = vtkCompositePolyDataMapper() pipeline.pick_mapper.SetInputDataObject( - self._prune_hidden_blocks(dataset, visibility_attributes) + pipeline.prune_hidden_blocks(dataset, visibility_attributes) ) def SetBlocksColor( diff --git a/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py b/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py index bca3b75f..46648a76 100644 --- a/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py +++ b/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py @@ -2,27 +2,23 @@ import os # Third party imports -from wslink import register as exportRpc # type: ignore +from opengeodeweb_microservice.database.data import Data +from opengeodeweb_microservice.schemas import get_schemas_dict from vtkmodules.vtkIOXML import vtkXMLGenericDataObjectReader, vtkXMLImageDataReader from vtkmodules.vtkRenderingCore import ( + vtkColorTransferFunction, vtkDataSetMapper, - vtkActor, vtkTexture, - vtkColorTransferFunction, ) -from vtkmodules.vtkCommonDataModel import vtkDataSet, vtkCellTypes -from vtkmodules.vtkCommonExecutionModel import vtkAlgorithm -from vtkmodules.vtkCommonDataModel import vtkPolyData -from opengeodeweb_microservice.database.data import Data -from opengeodeweb_microservice.schemas import get_schemas_dict +from wslink import register as exportRpc # type: ignore # Local application imports +from opengeodeweb_viewer.object.object_methods import VtkObjectView from opengeodeweb_viewer.utils_functions import ( - validate_schema, RpcParams, + validate_schema, ) -from opengeodeweb_viewer.object.object_methods import VtkObjectView -from opengeodeweb_viewer.vtk_protocol import VtkPipeline +from opengeodeweb_viewer.vtk_pipeline import VtkPipeline from . import schemas diff --git a/src/opengeodeweb_viewer/rpc/model/model_protocols.py b/src/opengeodeweb_viewer/rpc/model/model_protocols.py index 15f8a49f..62408a60 100644 --- a/src/opengeodeweb_viewer/rpc/model/model_protocols.py +++ b/src/opengeodeweb_viewer/rpc/model/model_protocols.py @@ -1,33 +1,31 @@ # Standard library imports import os +from typing import Optional, Protocol, TypedDict # Third party imports +from opengeodeweb_microservice.schemas import get_schemas_dict from vtkmodules.vtkCommonDataModel import ( - vtkCompositeDataSet, vtkBoundingBox, + vtkCompositeDataSet, vtkDataSet, - vtkSelectionNode, ) +from vtkmodules.vtkFiltersCore import vtkAppendDataSets +from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter +from vtkmodules.vtkIOXML import vtkXMLMultiBlockDataReader from vtkmodules.vtkRenderingCore import ( - vtkCompositePolyDataMapper, vtkCompositeDataDisplayAttributes, - vtkColorTransferFunction, + vtkCompositePolyDataMapper, ) -from vtkmodules.vtkFiltersCore import vtkAppendDataSets -from vtkmodules.vtkIOXML import vtkXMLMultiBlockDataReader -from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter from wslink import register as exportRpc # type: ignore -from opengeodeweb_microservice.schemas import get_schemas_dict # Local application imports +from opengeodeweb_viewer.object.object_methods import VtkObjectView from opengeodeweb_viewer.utils_functions import ( - validate_schema, - RpcParams, deterministic_color, + RpcParams, + validate_schema, ) -from opengeodeweb_viewer.object.object_methods import VtkObjectView -from opengeodeweb_viewer.vtk_protocol import VtkPipeline, BlockStyle -from typing import Optional, List, TypedDict, Protocol +from opengeodeweb_viewer.vtk_pipeline import VtkPipeline from . import schemas @@ -78,7 +76,7 @@ def apply_color( if isinstance(block_dataset, vtkDataSet): block_dataset.GetPointData().SetActiveScalars("") block_dataset.GetCellData().SetActiveScalars("") - self._get_block_style(pipeline, block_id)["name"] = "" + pipeline.get_block_style(block_id)["name"] = "" if color_mode == "random": geode_id = pipeline.blockGeodeIds[block_id] red, green, blue = deterministic_color(str(geode_id)) @@ -120,14 +118,14 @@ def displayAttributeOnVertices( ) -> None: pipeline = self.get_vtk_pipeline(data_id) for block_id in block_ids: - style = self._get_block_style(pipeline, block_id) + style = pipeline.get_block_style(block_id) style["name"] = name style["item"] = item style["attribute_location"] = "point" style["points"] = color_map style["minimum"] = minimum style["maximum"] = maximum - self.updateBlockColors(pipeline, block_id) + pipeline.update_block_colors(block_id) def displayAttributeOnCells( self, @@ -141,14 +139,14 @@ def displayAttributeOnCells( ) -> None: pipeline = self.get_vtk_pipeline(data_id) for block_id in block_ids: - style = self._get_block_style(pipeline, block_id) + style = pipeline.get_block_style(block_id) style["name"] = name style["item"] = item style["attribute_location"] = "cell" style["points"] = color_map style["minimum"] = minimum style["maximum"] = maximum - self.updateBlockColors(pipeline, block_id) + pipeline.update_block_colors(block_id) def setupColorMap( self, @@ -159,11 +157,11 @@ def setupColorMap( maximum: float, ) -> None: for block_id in block_ids: - style = self._get_block_style(pipeline, block_id) + style = pipeline.get_block_style(block_id) style["points"] = points style["minimum"] = minimum style["maximum"] = maximum - self.updateBlockColors(pipeline, block_id) + pipeline.update_block_colors(block_id) @exportRpc(model_prefix + model_schemas_dict["register"]["rpc"]) def registerModel(self, rpc_params: RpcParams) -> None: diff --git a/src/opengeodeweb_viewer/vtk_pipeline.py b/src/opengeodeweb_viewer/vtk_pipeline.py new file mode 100644 index 00000000..90c7186d --- /dev/null +++ b/src/opengeodeweb_viewer/vtk_pipeline.py @@ -0,0 +1,225 @@ +# Standard library imports +from dataclasses import dataclass, field +from typing import cast, Literal, TypedDict + +# Third party imports +from vtkmodules.vtkRenderingCore import ( + vtkActor, + vtkDataSetMapper, + vtkMapper, + vtkCompositePolyDataMapper, + vtkCompositeDataDisplayAttributes, + vtkColorTransferFunction, +) +from vtkmodules.vtkRenderingAnnotation import ( + vtkScalarBarActor, +) +from vtkmodules.vtkCommonDataModel import ( + vtkDataObject, + vtkDataSet, + vtkMultiBlockDataSet, + vtkSelection, + vtkSelectionNode, +) +from vtkmodules.vtkCommonExecutionModel import vtkAlgorithm +from vtkmodules.vtkFiltersExtraction import ( + vtkExtractGeometry, + vtkExtractSelection, +) +from vtkmodules.vtkIOXML import vtkXMLReader + +# Local application imports +from opengeodeweb_microservice.database.data_types import ( + ViewerElementsType, + ViewerType, +) + + +@dataclass +class ViewerData: + id: str + viewable_file: str | None + viewer_object: ViewerType + 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) + + +class BlockStyle(TypedDict): + name: str + attribute_location: Literal["point", "cell"] + points: list[float] + minimum: float + maximum: float + item: int + + +@dataclass +class VtkPipeline: + reader: vtkXMLReader + mapper: vtkMapper + filter: vtkAlgorithm | None = None + actor: vtkActor = field(default_factory=vtkActor) + clipping_filter: vtkExtractGeometry | None = None + highlight: HighlightPipeline = field(default_factory=HighlightPipeline) + blockDataSets: list[vtkDataObject | None] = field(default_factory=list) + blockGeodeIds: list[str] = field(default_factory=list) + scalarBar: vtkScalarBarActor = field(default_factory=vtkScalarBarActor) + block_styles: dict[int, BlockStyle] = field(default_factory=dict) + pick_mapper: vtkMapper | None = None + + def extract_blocks( + self, multiblock: vtkDataObject | None + ) -> list[vtkDataObject | None]: + if not isinstance(multiblock, vtkMultiBlockDataSet): + return [] + blocks: list[vtkDataObject | None] = [] + iterator = multiblock.NewTreeIterator() + iterator.InitTraversal() + while not iterator.IsDoneWithTraversal(): + flat_index = iterator.GetCurrentFlatIndex() + blocks.extend([None] * (flat_index + 1 - len(blocks))) + blocks[flat_index] = iterator.GetCurrentDataObject() + iterator.GoToNextItem() + print( + f"[extract_blocks] Total slots={len(blocks)} (None count={blocks.count(None)}): " + f"{[(index, type(obj).__name__ if obj else None) for index, obj in enumerate(blocks)]}", + flush=True, + ) + return blocks + + def prune_hidden_blocks( + self, + dataset: vtkMultiBlockDataSet, + attributes: vtkCompositeDataDisplayAttributes, + ) -> vtkMultiBlockDataSet: + pruned = vtkMultiBlockDataSet() + pruned.SetNumberOfBlocks(dataset.GetNumberOfBlocks()) + for index in range(dataset.GetNumberOfBlocks()): + block = dataset.GetBlock(index) + if block and attributes.GetBlockVisibility(block): + child = ( + self.prune_hidden_blocks(block, attributes) + if isinstance(block, vtkMultiBlockDataSet) + else block + ) + pruned.SetBlock(index, child) + return pruned + + def get_block_style(self, block_id: int) -> BlockStyle: + if block_id not in self.block_styles: + style = BlockStyle( + name="", + attribute_location="point", + points=[], + minimum=0.0, + maximum=1.0, + item=0, + ) + self.block_styles[block_id] = style + return self.block_styles[block_id] + + def update_block_colors(self, block_id: int) -> None: + block = self.blockDataSets[block_id] + if not isinstance(block, vtkDataSet): + return + + style = self.get_block_style(block_id) + if not style["name"]: + block.GetPointData().SetActiveScalars("") + block.GetCellData().SetActiveScalars("") + return + + field_data = ( + block.GetPointData() + if style["attribute_location"] == "point" + else block.GetCellData() + ) + scalar_array = field_data.GetArray(style["name"]) + if not scalar_array: + return + + lut = vtkColorTransferFunction() + points = style["points"] + minimum = style["minimum"] + maximum = style["maximum"] + if points: + x_min, x_max = points[0], points[-4] + span = x_max - x_min + for i in range(0, len(points), 4): + x, r, g, b = points[i : i + 4] + new_x = ( + minimum + (x - x_min) / span * (maximum - minimum) + if span + else minimum + ) + lut.AddRGBPoint(new_x, r, g, b) + else: + lut.AddRGBPoint(minimum, 0, 0, 0) + lut.AddRGBPoint(maximum, 1, 1, 1) + + lut.SetRange(minimum, maximum) + rgba_colors = lut.MapScalars(scalar_array, 0, style.get("item", 0)) + rgba_colors.SetName(f"__colors_{style['name']}") + + field_data.AddArray(rgba_colors) + field_data.SetActiveScalars(rgba_colors.GetName()) + + other_field_data = ( + block.GetCellData() + if style["attribute_location"] == "point" + else block.GetPointData() + ) + other_field_data.SetActiveScalars("") + + if isinstance(self.mapper, vtkCompositePolyDataMapper): + attributes = self.mapper.GetCompositeDataDisplayAttributes() + if attributes: + attributes.RemoveBlockColor(block) + self.mapper.ScalarVisibilityOn() + self.mapper.SetColorModeToDirectScalars() + self.mapper.SetScalarModeToDefault() + self.mapper.Modified() + + def sync_block_display_attributes( + self, new_blocks: list[vtkDataObject | None] + ) -> None: + mapper = cast(vtkCompositePolyDataMapper, self.mapper) + attributes = mapper.GetCompositeDataDisplayAttributes() + color_rgb = [0.0, 0.0, 0.0] + for source_block, destination_block in zip(self.blockDataSets, new_blocks): + if source_block and destination_block: + if attributes.HasBlockColor(source_block): + attributes.GetBlockColor(source_block, color_rgb) + attributes.SetBlockColor(destination_block, color_rgb) + else: + attributes.RemoveBlockColor(destination_block) + if attributes.HasBlockVisibility(source_block): + attributes.SetBlockVisibility( + destination_block, attributes.GetBlockVisibility(source_block) + ) + if attributes.HasBlockOpacity(source_block): + attributes.SetBlockOpacity( + destination_block, attributes.GetBlockOpacity(source_block) + ) + self.blockDataSets = new_blocks + for block_id in self.block_styles: + self.update_block_colors(block_id) + + def sync_composite_pipeline(self, dataset: vtkDataObject) -> None: + new_blocks = self.extract_blocks(dataset) + self.sync_block_display_attributes(new_blocks) + self.mapper.SetInputDataObject(dataset) + if self.pick_mapper and isinstance(dataset, vtkMultiBlockDataSet): + mapper = cast(vtkCompositePolyDataMapper, self.mapper) + attributes = mapper.GetCompositeDataDisplayAttributes() + self.pick_mapper.SetInputDataObject( + self.prune_hidden_blocks(dataset, attributes) + ) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 1bbb0453..6d5a1e02 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -1,101 +1,44 @@ # Standard library imports import math import os -from typing import cast, Any, Literal, TypedDict -from dataclasses import dataclass, field +from typing import Any, cast # Third party imports import vtkmodules.vtkRenderingOpenGL2 from vtkmodules.web import protocols as vtk_protocols -from vtkmodules.vtkIOXML import ( - vtkXMLReader, -) from vtkmodules.vtkWebCore import vtkWebApplication -from vtkmodules.vtkCommonExecutionModel import ( - vtkAlgorithm, - vtkCompositeDataPipeline, -) from vtkmodules.vtkRenderingCore import ( vtkActor, - vtkMapper, + vtkCellPicker, + vtkCompositePolyDataMapper, vtkRenderer, vtkRenderWindow, - vtkDataSetMapper, - vtkCompositePolyDataMapper, - vtkCompositeDataDisplayAttributes, - vtkCellPicker, - vtkHardwarePicker, - vtkColorTransferFunction, ) from vtkmodules.vtkCommonDataModel import ( + vtkBoundingBox, vtkDataObject, vtkDataSet, - vtkMultiBlockDataSet, - vtkBoundingBox, - vtkSelection, - vtkSelectionNode, - vtkPlane, vtkImplicitBoolean, + vtkPlane, + vtkSelectionNode, ) -from vtkmodules.vtkFiltersExtraction import ( - vtkExtractSelection, - vtkExtractGeometry, -) +from vtkmodules.vtkFiltersExtraction import vtkExtractGeometry from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter -from vtkmodules.vtkCommonCore import vtkStringArray, vtkIdTypeArray +from vtkmodules.vtkCommonCore import vtkIdTypeArray, vtkStringArray from vtkmodules.vtkRenderingAnnotation import ( - vtkCubeAxesActor, vtkAxesActor, - vtkScalarBarActor, + vtkCubeAxesActor, ) from vtkmodules.vtkInteractionWidgets import vtkOrientationMarkerWidget # Local application imports -from opengeodeweb_microservice.database.connection import get_session, init_database +from opengeodeweb_microservice.database.connection import get_session from opengeodeweb_microservice.database.data import Data -from opengeodeweb_microservice.database.data_types import ViewerType, ViewerElementsType from opengeodeweb_viewer.rpc.viewer.schemas.clipping_planes import Plane - - -@dataclass -class ViewerData: - id: str - viewable_file: str | None - viewer_object: ViewerType - 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) - - -class BlockStyle(TypedDict): - name: str - attribute_location: Literal["point", "cell"] - points: list[float] - minimum: float - maximum: float - item: int - - -@dataclass -class VtkPipeline: - reader: vtkXMLReader - mapper: vtkMapper - filter: vtkAlgorithm | None = None - actor: vtkActor = field(default_factory=vtkActor) - clipping_filter: vtkExtractGeometry | None = None - highlight: HighlightPipeline = field(default_factory=HighlightPipeline) - blockDataSets: list[vtkDataObject | None] = field(default_factory=list) - blockGeodeIds: list[str] = field(default_factory=list) - scalarBar: vtkScalarBarActor = field(default_factory=vtkScalarBarActor) - block_styles: dict[int, BlockStyle] = field(default_factory=dict) - pick_mapper: vtkMapper | None = None +from opengeodeweb_viewer.vtk_pipeline import ( + ViewerData, + VtkPipeline, +) class VtkTypingMixin: @@ -185,7 +128,7 @@ def reset_camera_clipping_range(self) -> None: else: renderer.ResetCameraClippingRange() - def update_highlight( + def highlight_selection( self, pipeline: VtkPipeline, id_to_select: int, @@ -212,151 +155,6 @@ def clear_highlights(self, data_ids: list[str]) -> None: pipeline = self.get_vtk_pipeline(data_id) pipeline.highlight.actor.VisibilityOff() - def _extract_blocks( - self, multiblock: vtkDataObject | None - ) -> list[vtkDataObject | None]: - if not isinstance(multiblock, vtkMultiBlockDataSet): - return [] - blocks: list[vtkDataObject | None] = [] - iterator = multiblock.NewTreeIterator() - iterator.InitTraversal() - while not iterator.IsDoneWithTraversal(): - flat_index = iterator.GetCurrentFlatIndex() - blocks.extend([None] * (flat_index + 1 - len(blocks))) - blocks[flat_index] = iterator.GetCurrentDataObject() - iterator.GoToNextItem() - return blocks - - def _prune_hidden_blocks( - self, - dataset: vtkMultiBlockDataSet, - attributes: vtkCompositeDataDisplayAttributes, - ) -> vtkMultiBlockDataSet: - pruned = vtkMultiBlockDataSet() - pruned.SetNumberOfBlocks(dataset.GetNumberOfBlocks()) - for index in range(dataset.GetNumberOfBlocks()): - block = dataset.GetBlock(index) - if block and attributes.GetBlockVisibility(block): - child = ( - self._prune_hidden_blocks(block, attributes) - if isinstance(block, vtkMultiBlockDataSet) - else block - ) - pruned.SetBlock(index, child) - return pruned - - def _get_block_style(self, pipeline: VtkPipeline, block_id: int) -> BlockStyle: - if block_id not in pipeline.block_styles: - style = BlockStyle( - name="", - attribute_location="point", - points=[], - minimum=0.0, - maximum=1.0, - item=0, - ) - pipeline.block_styles[block_id] = style - return pipeline.block_styles[block_id] - - def updateBlockColors(self, pipeline: VtkPipeline, block_id: int) -> None: - block = pipeline.blockDataSets[block_id] - if not isinstance(block, vtkDataSet): - return - - style = self._get_block_style(pipeline, block_id) - if not style["name"]: - block.GetPointData().SetActiveScalars("") - block.GetCellData().SetActiveScalars("") - return - - field_data = ( - block.GetPointData() - if style["attribute_location"] == "point" - else block.GetCellData() - ) - scalar_array = field_data.GetArray(style["name"]) - if not scalar_array: - return - - lut = vtkColorTransferFunction() - points = style["points"] - minimum = style["minimum"] - maximum = style["maximum"] - if points: - x_min, x_max = points[0], points[-4] - span = x_max - x_min - for i in range(0, len(points), 4): - x, r, g, b = points[i : i + 4] - new_x = ( - minimum + (x - x_min) / span * (maximum - minimum) - if span - else minimum - ) - lut.AddRGBPoint(new_x, r, g, b) - else: - lut.AddRGBPoint(minimum, 0, 0, 0) - lut.AddRGBPoint(maximum, 1, 1, 1) - - lut.SetRange(minimum, maximum) - rgba_colors = lut.MapScalars(scalar_array, 0, style.get("item", 0)) - rgba_colors.SetName(f"__colors_{style['name']}") - - field_data.AddArray(rgba_colors) - field_data.SetActiveScalars(rgba_colors.GetName()) - - other_field_data = ( - block.GetCellData() - if style["attribute_location"] == "point" - else block.GetPointData() - ) - other_field_data.SetActiveScalars("") - - if isinstance(pipeline.mapper, vtkCompositePolyDataMapper): - attributes = pipeline.mapper.GetCompositeDataDisplayAttributes() - if attributes: - attributes.RemoveBlockColor(block) - pipeline.mapper.ScalarVisibilityOn() - pipeline.mapper.SetColorModeToDirectScalars() - pipeline.mapper.SetScalarModeToDefault() - pipeline.mapper.Modified() - - def _sync_composite_pipeline( - self, pipeline: VtkPipeline, dataset: vtkDataObject - ) -> None: - blocks = self._extract_blocks(dataset) - mapper = cast(vtkCompositePolyDataMapper, pipeline.mapper) - attributes = mapper.GetCompositeDataDisplayAttributes() - print(f"[_sync_composite_pipeline] {attributes=}", flush=True) - print(f"[_sync_composite_pipeline] {pipeline.block_styles=}", flush=True) - color_rgb = [0.0, 0.0, 0.0] - for source_block, destination_block in zip(pipeline.blockDataSets, blocks): - if source_block and destination_block: - if attributes.HasBlockColor(source_block): - attributes.GetBlockColor(source_block, color_rgb) - attributes.SetBlockColor(destination_block, color_rgb) - else: - attributes.RemoveBlockColor(destination_block) - if attributes.HasBlockVisibility(source_block): - attributes.SetBlockVisibility( - destination_block, attributes.GetBlockVisibility(source_block) - ) - if attributes.HasBlockOpacity(source_block): - attributes.SetBlockOpacity( - destination_block, attributes.GetBlockOpacity(source_block) - ) - pipeline.blockDataSets = blocks - pipeline.mapper.SetInputDataObject(dataset) - if pipeline.pick_mapper and isinstance(dataset, vtkMultiBlockDataSet): - pipeline.pick_mapper.SetInputDataObject( - self._prune_hidden_blocks(dataset, attributes) - ) - for block_id in pipeline.block_styles: - print( - f"[_sync_composite_pipeline] Updating block colors for {block_id=}", - flush=True, - ) - self.updateBlockColors(pipeline, block_id) - def _restore_active_scalars(self, source: vtkDataSet, target: vtkDataSet) -> None: if active_points := source.GetPointData().GetScalars(): target.GetPointData().SetActiveScalars(active_points.GetName()) @@ -376,18 +174,17 @@ def set_clipping_planes( if pipeline.filter else pipeline.reader.GetOutputDataObject(0) ) - self._sync_composite_pipeline(pipeline, original_dataset) + pipeline.sync_composite_pipeline(original_dataset) else: pipeline.mapper.SetInputConnection(pipeline.reader.GetOutputPort()) reader_dataset = pipeline.reader.GetOutputAsDataSet() self._restore_active_scalars(reader_dataset, reader_dataset) pipeline.clipping_filter = None continue + # vtkExtractGeometry does not support vtkUnstructuredGrid clipping_filter = vtkExtractGeometry() + # transform data from vtkUnstructuredGrid to vtkPolyData geometry_filter = vtkGeometryFilter() - if is_composite: - clipping_filter.SetExecutive(vtkCompositeDataPipeline()) - geometry_filter.SetExecutive(vtkCompositeDataPipeline()) clipping_filter.SetInputConnection(pipeline.reader.GetOutputPort()) implicit_boolean = vtkImplicitBoolean() implicit_boolean.SetOperationTypeToIntersection() @@ -400,8 +197,8 @@ def set_clipping_planes( geometry_filter.SetInputConnection(clipping_filter.GetOutputPort()) geometry_filter.Update() if is_composite: - self._sync_composite_pipeline( - pipeline, geometry_filter.GetOutputDataObject(0) + pipeline.sync_composite_pipeline( + geometry_filter.GetOutputDataObject(0) ) else: pipeline.mapper.SetInputConnection(geometry_filter.GetOutputPort()) From 5d231b4618db6fe3ca49959aada2e98cf7283c79 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:28:11 +0000 Subject: [PATCH 17/24] Apply prepare changes --- src/opengeodeweb_viewer/vtk_protocol.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 6d5a1e02..e7b7c7c9 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -197,9 +197,7 @@ def set_clipping_planes( geometry_filter.SetInputConnection(clipping_filter.GetOutputPort()) geometry_filter.Update() if is_composite: - pipeline.sync_composite_pipeline( - geometry_filter.GetOutputDataObject(0) - ) + pipeline.sync_composite_pipeline(geometry_filter.GetOutputDataObject(0)) else: pipeline.mapper.SetInputConnection(geometry_filter.GetOutputPort()) reader_dataset = pipeline.reader.GetOutputAsDataSet() From 98575428e2e50a37b871a62177c3a36db69c4ab6 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Wed, 29 Jul 2026 16:59:24 +0200 Subject: [PATCH 18/24] vtkGeometryFilter in pipeline --- .../object/object_methods.py | 6 +--- .../rpc/mesh/mesh_protocols.py | 2 ++ .../rpc/model/model_protocols.py | 16 ++++----- src/opengeodeweb_viewer/vtk_pipeline.py | 12 +++++-- src/opengeodeweb_viewer/vtk_protocol.py | 33 ++++++------------- 5 files changed, 29 insertions(+), 40 deletions(-) diff --git a/src/opengeodeweb_viewer/object/object_methods.py b/src/opengeodeweb_viewer/object/object_methods.py index 84747c4d..e9a90bab 100644 --- a/src/opengeodeweb_viewer/object/object_methods.py +++ b/src/opengeodeweb_viewer/object/object_methods.py @@ -199,11 +199,7 @@ def _apply_highlight_style(self, actor: vtkActor, mapper: vtkDataSetMapper) -> N def highlight(self, pipeline: VtkPipeline) -> None: highlight = pipeline.highlight self._apply_highlight_style(highlight.actor, highlight.mapper) - input_port = ( - pipeline.filter.GetOutputPort() - if pipeline.filter - else pipeline.reader.GetOutputPort() - ) + input_port = pipeline.filter.GetOutputPort() highlight.selection.AddNode(highlight.selectionNode) highlight.extractSelection.SetInputConnection(0, input_port) highlight.extractSelection.SetInputData(1, highlight.selection) diff --git a/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py b/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py index 46648a76..b2650b74 100644 --- a/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py +++ b/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py @@ -125,6 +125,7 @@ def displayAttributeOnVertices( ) -> None: pipeline = self.get_vtk_pipeline(data_id) pipeline.reader.GetOutputAsDataSet().GetPointData().SetActiveScalars(name) + pipeline.filter.Update() active_ds = pipeline.mapper.GetInputDataObject(0, 0) if active_ds: active_ds.GetPointData().SetActiveScalars(name) @@ -145,6 +146,7 @@ def displayAttributeOnCells( ) -> None: pipeline = self.get_vtk_pipeline(data_id) pipeline.reader.GetOutputAsDataSet().GetCellData().SetActiveScalars(name) + pipeline.filter.Update() active_ds = pipeline.mapper.GetInputDataObject(0, 0) if active_ds: active_ds.GetCellData().SetActiveScalars(name) diff --git a/src/opengeodeweb_viewer/rpc/model/model_protocols.py b/src/opengeodeweb_viewer/rpc/model/model_protocols.py index 62408a60..72c42a2f 100644 --- a/src/opengeodeweb_viewer/rpc/model/model_protocols.py +++ b/src/opengeodeweb_viewer/rpc/model/model_protocols.py @@ -10,7 +10,6 @@ vtkDataSet, ) from vtkmodules.vtkFiltersCore import vtkAppendDataSets -from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter from vtkmodules.vtkIOXML import vtkXMLMultiBlockDataReader from vtkmodules.vtkRenderingCore import ( vtkCompositeDataDisplayAttributes, @@ -177,17 +176,16 @@ def registerModel(self, rpc_params: RpcParams) -> None: reader = vtkXMLMultiBlockDataReader() reader.SetFileName(os.path.join(self.DATA_FOLDER_PATH, data_id, file_name)) reader.Update() - filter = vtkGeometryFilter() - filter.SetInputConnection(reader.GetOutputPort()) - filter.Update() - geometry_output = filter.GetOutputDataObject(0) - if geometry_output: - geometry_output.SetObjectName(params.name) mapper = vtkCompositePolyDataMapper() - mapper.SetInputDataObject(geometry_output) attributes = vtkCompositeDataDisplayAttributes() mapper.SetCompositeDataDisplayAttributes(attributes) - data = VtkPipeline(reader, mapper, filter) + data = VtkPipeline(reader, mapper) + data.filter.SetInputConnection(reader.GetOutputPort()) + data.filter.Update() + geometry_output = data.filter.GetOutputDataObject(0) + if geometry_output: + geometry_output.SetObjectName(params.name) + mapper.SetInputDataObject(geometry_output) self.highlight(data) iterator = geometry_output.NewTreeIterator() iterator.InitTraversal() diff --git a/src/opengeodeweb_viewer/vtk_pipeline.py b/src/opengeodeweb_viewer/vtk_pipeline.py index 90c7186d..2723aa99 100644 --- a/src/opengeodeweb_viewer/vtk_pipeline.py +++ b/src/opengeodeweb_viewer/vtk_pipeline.py @@ -26,6 +26,7 @@ vtkExtractGeometry, vtkExtractSelection, ) +from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter from vtkmodules.vtkIOXML import vtkXMLReader # Local application imports @@ -65,7 +66,7 @@ class BlockStyle(TypedDict): class VtkPipeline: reader: vtkXMLReader mapper: vtkMapper - filter: vtkAlgorithm | None = None + filter: vtkGeometryFilter = field(default_factory=vtkGeometryFilter) actor: vtkActor = field(default_factory=vtkActor) clipping_filter: vtkExtractGeometry | None = None highlight: HighlightPipeline = field(default_factory=HighlightPipeline) @@ -130,13 +131,11 @@ def update_block_colors(self, block_id: int) -> None: block = self.blockDataSets[block_id] if not isinstance(block, vtkDataSet): return - style = self.get_block_style(block_id) if not style["name"]: block.GetPointData().SetActiveScalars("") block.GetCellData().SetActiveScalars("") return - field_data = ( block.GetPointData() if style["attribute_location"] == "point" @@ -213,6 +212,13 @@ def sync_block_display_attributes( for block_id in self.block_styles: self.update_block_colors(block_id) + def restore_active_scalars(self, target: vtkDataSet) -> None: + source = self.reader.GetOutputAsDataSet() + if active_points := source.GetPointData().GetScalars(): + target.GetPointData().SetActiveScalars(active_points.GetName()) + if active_cells := source.GetCellData().GetScalars(): + target.GetCellData().SetActiveScalars(active_cells.GetName()) + def sync_composite_pipeline(self, dataset: vtkDataObject) -> None: new_blocks = self.extract_blocks(dataset) self.sync_block_display_attributes(new_blocks) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index e7b7c7c9..895cb37f 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -155,12 +155,6 @@ def clear_highlights(self, data_ids: list[str]) -> None: pipeline = self.get_vtk_pipeline(data_id) pipeline.highlight.actor.VisibilityOff() - def _restore_active_scalars(self, source: vtkDataSet, target: vtkDataSet) -> None: - if active_points := source.GetPointData().GetScalars(): - target.GetPointData().SetActiveScalars(active_points.GetName()) - if active_cells := source.GetCellData().GetScalars(): - target.GetCellData().SetActiveScalars(active_cells.GetName()) - def set_clipping_planes( self, data_ids: list[str], planes_data: list[Plane] ) -> None: @@ -169,22 +163,16 @@ def set_clipping_planes( is_composite = isinstance(pipeline.mapper, vtkCompositePolyDataMapper) if not planes_data: if is_composite: - original_dataset = ( - pipeline.filter.GetOutputDataObject(0) - if pipeline.filter - else pipeline.reader.GetOutputDataObject(0) - ) + pipeline.filter.SetInputConnection(pipeline.reader.GetOutputPort()) + pipeline.filter.Update() + original_dataset = pipeline.filter.GetOutputDataObject(0) pipeline.sync_composite_pipeline(original_dataset) else: pipeline.mapper.SetInputConnection(pipeline.reader.GetOutputPort()) - reader_dataset = pipeline.reader.GetOutputAsDataSet() - self._restore_active_scalars(reader_dataset, reader_dataset) + pipeline.restore_active_scalars(pipeline.reader.GetOutputAsDataSet()) pipeline.clipping_filter = None continue - # vtkExtractGeometry does not support vtkUnstructuredGrid clipping_filter = vtkExtractGeometry() - # transform data from vtkUnstructuredGrid to vtkPolyData - geometry_filter = vtkGeometryFilter() clipping_filter.SetInputConnection(pipeline.reader.GetOutputPort()) implicit_boolean = vtkImplicitBoolean() implicit_boolean.SetOperationTypeToIntersection() @@ -194,15 +182,14 @@ def set_clipping_planes( plane.SetNormal(plane_info.normal) implicit_boolean.AddFunction(plane) clipping_filter.SetImplicitFunction(implicit_boolean) - geometry_filter.SetInputConnection(clipping_filter.GetOutputPort()) - geometry_filter.Update() + pipeline.filter.SetInputConnection(clipping_filter.GetOutputPort()) + pipeline.filter.Update() + filtered_dataset = pipeline.filter.GetOutputDataObject(0) if is_composite: - pipeline.sync_composite_pipeline(geometry_filter.GetOutputDataObject(0)) + pipeline.sync_composite_pipeline(filtered_dataset) else: - pipeline.mapper.SetInputConnection(geometry_filter.GetOutputPort()) - reader_dataset = pipeline.reader.GetOutputAsDataSet() - filtered_dataset = geometry_filter.GetOutputDataObject(0) - self._restore_active_scalars(reader_dataset, filtered_dataset) + pipeline.mapper.SetInputConnection(pipeline.filter.GetOutputPort()) + pipeline.restore_active_scalars(filtered_dataset) pipeline.clipping_filter = clipping_filter def swap_pick_mappers(self, data_ids: list[str], use_pick_mapper: bool) -> None: From 60850d0b9864884207867b437e156f0a617b502b Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:00:18 +0000 Subject: [PATCH 19/24] Apply prepare changes --- src/opengeodeweb_viewer/vtk_protocol.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 895cb37f..f4a632fa 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -169,7 +169,9 @@ def set_clipping_planes( pipeline.sync_composite_pipeline(original_dataset) else: pipeline.mapper.SetInputConnection(pipeline.reader.GetOutputPort()) - pipeline.restore_active_scalars(pipeline.reader.GetOutputAsDataSet()) + pipeline.restore_active_scalars( + pipeline.reader.GetOutputAsDataSet() + ) pipeline.clipping_filter = None continue clipping_filter = vtkExtractGeometry() From c6ccf30369d605cb86c8dc365d36773649a11ec0 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 30 Jul 2026 09:45:37 +0200 Subject: [PATCH 20/24] filter for highlight pipeline --- .../object/object_methods.py | 2 + src/opengeodeweb_viewer/vtk_protocol.py | 50 +++++++++---------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/opengeodeweb_viewer/object/object_methods.py b/src/opengeodeweb_viewer/object/object_methods.py index e9a90bab..afde5cc5 100644 --- a/src/opengeodeweb_viewer/object/object_methods.py +++ b/src/opengeodeweb_viewer/object/object_methods.py @@ -199,6 +199,8 @@ def _apply_highlight_style(self, actor: vtkActor, mapper: vtkDataSetMapper) -> N def highlight(self, pipeline: VtkPipeline) -> None: highlight = pipeline.highlight self._apply_highlight_style(highlight.actor, highlight.mapper) + if pipeline.filter.GetNumberOfInputConnections(0) == 0: + pipeline.filter.SetInputConnection(pipeline.reader.GetOutputPort()) input_port = pipeline.filter.GetOutputPort() highlight.selection.AddNode(highlight.selectionNode) highlight.extractSelection.SetInputConnection(0, input_port) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index f4a632fa..942cc0af 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -128,7 +128,7 @@ def reset_camera_clipping_range(self) -> None: else: renderer.ResetCameraClippingRange() - def highlight_selection( + def update_highlight( self, pipeline: VtkPipeline, id_to_select: int, @@ -161,38 +161,34 @@ def set_clipping_planes( for data_id in data_ids: pipeline = self.get_vtk_pipeline(data_id) is_composite = isinstance(pipeline.mapper, vtkCompositePolyDataMapper) - if not planes_data: - if is_composite: - pipeline.filter.SetInputConnection(pipeline.reader.GetOutputPort()) - pipeline.filter.Update() - original_dataset = pipeline.filter.GetOutputDataObject(0) - pipeline.sync_composite_pipeline(original_dataset) - else: - pipeline.mapper.SetInputConnection(pipeline.reader.GetOutputPort()) - pipeline.restore_active_scalars( - pipeline.reader.GetOutputAsDataSet() - ) + if planes_data: + clipping_filter = vtkExtractGeometry() + clipping_filter.SetInputConnection(pipeline.reader.GetOutputPort()) + implicit_boolean = vtkImplicitBoolean() + implicit_boolean.SetOperationTypeToIntersection() + for plane_info in planes_data: + plane = vtkPlane() + plane.SetOrigin(plane_info.origin) + plane.SetNormal(plane_info.normal) + implicit_boolean.AddFunction(plane) + clipping_filter.SetImplicitFunction(implicit_boolean) + pipeline.clipping_filter = clipping_filter + input_port = clipping_filter.GetOutputPort() + else: pipeline.clipping_filter = None - continue - clipping_filter = vtkExtractGeometry() - clipping_filter.SetInputConnection(pipeline.reader.GetOutputPort()) - implicit_boolean = vtkImplicitBoolean() - implicit_boolean.SetOperationTypeToIntersection() - for plane_info in planes_data: - plane = vtkPlane() - plane.SetOrigin(plane_info.origin) - plane.SetNormal(plane_info.normal) - implicit_boolean.AddFunction(plane) - clipping_filter.SetImplicitFunction(implicit_boolean) - pipeline.filter.SetInputConnection(clipping_filter.GetOutputPort()) + input_port = pipeline.reader.GetOutputPort() + pipeline.filter.SetInputConnection(input_port) pipeline.filter.Update() filtered_dataset = pipeline.filter.GetOutputDataObject(0) if is_composite: pipeline.sync_composite_pipeline(filtered_dataset) else: - pipeline.mapper.SetInputConnection(pipeline.filter.GetOutputPort()) - pipeline.restore_active_scalars(filtered_dataset) - pipeline.clipping_filter = clipping_filter + pipeline.mapper.SetInputConnection(input_port) + pipeline.restore_active_scalars( + cast(vtkDataSet, filtered_dataset) + if planes_data + else pipeline.reader.GetOutputAsDataSet() + ) 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). From a7d4b5daedd535ab57e279d1b65f25735a63a27d Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 30 Jul 2026 09:48:34 +0200 Subject: [PATCH 21/24] trigger From 45ee70c5e4ffb7f5ac84cd80e618b1803e75c7bc Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 30 Jul 2026 10:07:46 +0200 Subject: [PATCH 22/24] no need to ask filter --- src/opengeodeweb_viewer/vtk_protocol.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 942cc0af..7ac1943e 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -400,11 +400,7 @@ def update_scalar_bars_layout(self) -> None: row_height = 0.12 for i, (data_id, bar) in enumerate(visible_bars): - pipeline = self.get_vtk_pipeline(data_id) - if pipeline.filter: - dataset = pipeline.filter.GetOutputDataObject(0) - else: - dataset = pipeline.reader.GetOutputDataObject(0) + dataset = pipeline.reader.GetOutputDataObject(0) attr_name = "" if dataset: From 7bb1f1f833ad804b59906178b90e46cf4a99e0f7 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 30 Jul 2026 13:10:32 +0200 Subject: [PATCH 23/24] add tests for clipping plane on each datas --- .../rpc/mesh/mesh_protocols.py | 23 +++----- .../rpc/model/model_protocols.py | 12 ++-- src/opengeodeweb_viewer/vtk_protocol.py | 55 +++++++++--------- .../images/mesh/cells/clipping_plane.jpeg | Bin 0 -> 4009 bytes .../images/mesh/edges/clipping_plane.jpeg | Bin 0 -> 6779 bytes .../images/mesh/polygons/clipping_plane.jpeg | Bin 0 -> 6779 bytes .../images/mesh/polyhedra/clipping_plane.jpeg | Bin 0 -> 3588 bytes .../images/model/blocks/clipping_plane.jpeg | Bin 0 -> 19716 bytes .../images/model/lines/clipping_plane.jpeg | Bin 0 -> 4787 bytes .../images/model/surfaces/clipping_plane.jpeg | Bin 0 -> 6197 bytes .../test_cells_attribute_cell_protocols.py | 2 +- .../test_cells_attribute_vertex_protocols.py | 2 +- tests/mesh/cells/test_mesh_cells_protocols.py | 27 ++++++++- .../test_edges_attribute_edge_protocols.py | 2 +- .../test_edges_attribute_vertex_protocols.py | 2 +- tests/mesh/edges/test_mesh_edges_protocols.py | 39 +++++++++++-- .../test_points_attribute_vertex_protocols.py | 2 +- .../mesh/points/test_mesh_points_protocols.py | 6 +- ...st_polygons_attribute_polygon_protocols.py | 2 +- ...est_polygons_attribute_vertex_protocols.py | 2 +- .../polygons/test_mesh_polygons_protocols.py | 32 +++++++++- ...olyhedra_attribute_polyhedron_protocols.py | 2 +- ...st_polyhedra_attribute_vertex_protocols.py | 2 +- .../test_mesh_polyhedra_protocols.py | 36 ++++++++++-- tests/mesh/test_mesh_protocols.py | 15 +++-- ...l_blocks_attribute_polyhedron_protocols.py | 3 +- ...model_blocks_attribute_vertex_protocols.py | 3 +- .../blocks/test_model_blocks_protocols.py | 34 ++++++++++- ...odel_corners_attribute_vertex_protocols.py | 3 +- .../corners/test_model_corners_protocols.py | 11 ++-- .../model/edges/test_model_edges_protocols.py | 5 +- ...st_model_lines_attribute_edge_protocols.py | 3 +- ..._model_lines_attribute_vertex_protocols.py | 3 +- .../model/lines/test_model_lines_protocols.py | 34 ++++++++++- .../points/test_model_points_protocols.py | 7 ++- ...el_surfaces_attribute_polygon_protocols.py | 3 +- ...del_surfaces_attribute_vertex_protocols.py | 3 +- .../surfaces/test_model_surfaces_protocols.py | 40 +++++++++++-- tests/model/test_model_protocols.py | 17 +++--- tests/test_viewer_protocols.py | 9 ++- 40 files changed, 327 insertions(+), 114 deletions(-) create mode 100644 tests/data/images/mesh/cells/clipping_plane.jpeg create mode 100644 tests/data/images/mesh/edges/clipping_plane.jpeg create mode 100644 tests/data/images/mesh/polygons/clipping_plane.jpeg create mode 100644 tests/data/images/mesh/polyhedra/clipping_plane.jpeg create mode 100644 tests/data/images/model/blocks/clipping_plane.jpeg create mode 100644 tests/data/images/model/lines/clipping_plane.jpeg create mode 100644 tests/data/images/model/surfaces/clipping_plane.jpeg diff --git a/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py b/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py index b2650b74..b9b3720c 100644 --- a/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py +++ b/src/opengeodeweb_viewer/rpc/mesh/mesh_protocols.py @@ -46,12 +46,9 @@ def registerMesh(self, rpc_params: RpcParams) -> None: reader = vtkXMLGenericDataObjectReader() reader.SetFileName(os.path.join(self.DATA_FOLDER_PATH, data_id, file_name)) reader.Update() - dataset = reader.GetOutputDataObject(0) - if dataset: - dataset.SetObjectName(params.name) mapper = vtkDataSetMapper() - mapper.SetInputConnection(reader.GetOutputPort()) data = VtkPipeline(reader, mapper) + self.setup_pipeline(data, params.name) self.highlight(data) self.registerObject(data_id, file_name, data) except Exception as e: @@ -103,16 +100,14 @@ def meshApplyTextures(self, rpc_params: RpcParams) -> None: texture = vtkTexture() texture.SetInputConnection(texture_reader.GetOutputPort()) texture.InterpolateOn() - reader = self.get_vtk_pipeline(mesh_id).reader - output = reader.GetOutputAsDataSet() - point_data = output.GetPointData() - for i in range(point_data.GetNumberOfArrays()): - array = point_data.GetArray(i) - if array.GetName() == tex_info.texture_name: - point_data.SetTCoords(array) - break - actor = self.get_vtk_pipeline(mesh_id).actor - actor.SetTexture(texture) + pipeline = self.get_vtk_pipeline(mesh_id) + output = pipeline.reader.GetOutputAsDataSet() + array = output.GetPointData().GetArray(tex_info.texture_name) + if array: + output.GetPointData().SetTCoords(array) + pipeline.filter.Modified() + pipeline.filter.Update() + pipeline.actor.SetTexture(texture) def displayAttributeOnVertices( self, diff --git a/src/opengeodeweb_viewer/rpc/model/model_protocols.py b/src/opengeodeweb_viewer/rpc/model/model_protocols.py index 72c42a2f..cdc0a028 100644 --- a/src/opengeodeweb_viewer/rpc/model/model_protocols.py +++ b/src/opengeodeweb_viewer/rpc/model/model_protocols.py @@ -1,6 +1,6 @@ # Standard library imports import os -from typing import Optional, Protocol, TypedDict +from typing import Optional, Protocol, TypedDict, cast # Third party imports from opengeodeweb_microservice.schemas import get_schemas_dict @@ -9,6 +9,7 @@ vtkCompositeDataSet, vtkDataSet, ) +from vtkmodules.vtkCommonDataModel import vtkMultiBlockDataSet from vtkmodules.vtkFiltersCore import vtkAppendDataSets from vtkmodules.vtkIOXML import vtkXMLMultiBlockDataReader from vtkmodules.vtkRenderingCore import ( @@ -180,12 +181,9 @@ def registerModel(self, rpc_params: RpcParams) -> None: attributes = vtkCompositeDataDisplayAttributes() mapper.SetCompositeDataDisplayAttributes(attributes) data = VtkPipeline(reader, mapper) - data.filter.SetInputConnection(reader.GetOutputPort()) - data.filter.Update() - geometry_output = data.filter.GetOutputDataObject(0) - if geometry_output: - geometry_output.SetObjectName(params.name) - mapper.SetInputDataObject(geometry_output) + geometry_output = cast( + vtkMultiBlockDataSet, self.setup_pipeline(data, params.name) + ) self.highlight(data) iterator = geometry_output.NewTreeIterator() iterator.InitTraversal() diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index 7ac1943e..baf53d65 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -124,10 +124,21 @@ def reset_camera_clipping_range(self) -> None: if grid_scale is not None and grid_scale.GetVisibility(): grid_scale.SetUseBounds(True) renderer.ResetCameraClippingRange() - grid_scale.SetUseBounds(False) else: renderer.ResetCameraClippingRange() + def setup_pipeline(self, pipeline: VtkPipeline, name: str) -> vtkDataObject | None: + pipeline.filter.SetInputConnection(pipeline.reader.GetOutputPort()) + pipeline.filter.Update() + geometry_output = pipeline.filter.GetOutputDataObject(0) + if geometry_output: + geometry_output.SetObjectName(name) + if isinstance(pipeline.mapper, vtkCompositePolyDataMapper): + pipeline.mapper.SetInputDataObject(geometry_output) + else: + pipeline.mapper.SetInputConnection(pipeline.filter.GetOutputPort()) + return geometry_output + def update_highlight( self, pipeline: VtkPipeline, @@ -378,16 +389,13 @@ def get_dist(axis: int) -> float: self.reset_camera_clipping_range() def update_scalar_bars_layout(self) -> None: - visible_bars = [] - for data_id, pipeline in self.get_data_base().items(): - if ( - pipeline.scalarBar.GetVisibility() - and pipeline.scalarBar.GetLookupTable() is not None - ): - visible_bars.append((data_id, pipeline.scalarBar)) - - n = len(visible_bars) - if n == 0: + visible_bars = [ + (data_id, pipeline) + for data_id, pipeline in self.get_data_base().items() + if pipeline.scalarBar.GetVisibility() + and pipeline.scalarBar.GetLookupTable() is not None + ] + if not visible_bars: return start_x = 0.22 @@ -399,21 +407,16 @@ def update_scalar_bars_layout(self) -> None: actual_width = 0.10 row_height = 0.12 - for i, (data_id, bar) in enumerate(visible_bars): - dataset = pipeline.reader.GetOutputDataObject(0) - - attr_name = "" - if dataset: - pd = dataset.GetPointData().GetScalars() - cd = dataset.GetCellData().GetScalars() - if pd: - attr_name = pd.GetName() - elif cd: - attr_name = cd.GetName() - - if not attr_name: - attr_name = "Attribute" - + for i, (data_id, pipeline) in enumerate(visible_bars): + bar = pipeline.scalarBar + dataset = pipeline.filter.GetOutputDataObject(0) + scalars = ( + dataset.GetPointData().GetScalars() + or dataset.GetCellData().GetScalars() + if dataset + else None + ) + attr_name = scalars.GetName() if scalars else "Attribute" data_name = ( dataset.GetObjectName() if dataset and dataset.GetObjectName() diff --git a/tests/data/images/mesh/cells/clipping_plane.jpeg b/tests/data/images/mesh/cells/clipping_plane.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..917361bf2e9ab921ab765a4c0089ef0275f4c657 GIT binary patch literal 4009 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 literal 0 HcmV?d00001 diff --git a/tests/data/images/mesh/edges/clipping_plane.jpeg b/tests/data/images/mesh/edges/clipping_plane.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..164d73d1e17e0b2bb178f8b4dd85b7109f9351bd GIT binary patch literal 6779 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 O!c=$Q&eG?8`o91WAZdI6 literal 0 HcmV?d00001 diff --git a/tests/data/images/mesh/polygons/clipping_plane.jpeg b/tests/data/images/mesh/polygons/clipping_plane.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..164d73d1e17e0b2bb178f8b4dd85b7109f9351bd GIT binary patch literal 6779 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 O!c=$Q&eG?8`o91WAZdI6 literal 0 HcmV?d00001 diff --git a/tests/data/images/mesh/polyhedra/clipping_plane.jpeg b/tests/data/images/mesh/polyhedra/clipping_plane.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..08a839794aac276656ea7213a59be78271863ad5 GIT binary patch literal 3588 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 literal 0 HcmV?d00001 diff --git a/tests/data/images/model/lines/clipping_plane.jpeg b/tests/data/images/model/lines/clipping_plane.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..7ef71b2dc5b2171dc3b8d1e02f6775757f91f7d6 GIT binary patch literal 4787 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 Xs@jWlq^h%H4~w78(C&XawDs~IXmm;_ literal 0 HcmV?d00001 diff --git a/tests/data/images/model/surfaces/clipping_plane.jpeg b/tests/data/images/model/surfaces/clipping_plane.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..713d813f080e0e4503c9a9d6920b9488fa651a8f GIT binary patch literal 6197 zcmbtY2UJsSlfFslH3^YkLhrpJC3FPA(2J;mbO8YYMNsLzN)c%$bP$jxAVqpd1W}|) zZ_jE+&l02`1u?F)z-MA0f0aN0K$EM^C>_T z=l%cI&m{l947lj?b^t~M5&#K6Kx_aQ27 zHhaH9E(+fsdtWdv5blK}QRSk{+Aj{~XxU80s0YG}fP}fm?tG{qiEu}E+ z9}4KZe4tNfF914Szv-=bgID7(3b<>kePsI}R%bJwZBL7|Y6ArT#LsTUuZ9l;@-3e! zg_CoCrLx&tb?otNWl8KqcCTlmP67a?Is-AK9raHE{}m6A$*cqxwHE>a(D<9ZPR!0H zr-f|H{`a@)+JEH(%o=?sML)R%fdAf+`iqgq5A3V>Ll=N7>MrAMFLV9tyc6{enXUtj z=?cI2_|bJ)%Adwx+l1MiMA1dB@YoAKjbB^2DDOKo7x0VnFMJc?1SO0U4IVxOf(Hf& ze}#e}c!U55pMVG{jiMHjgu$ne08VQ}I0b_6!RJ666&JcvnQWsZ$5bK1 z%RH*3t&Sm_BG*pSt1Qxbpo-_4%eLmIgQsG~#l*vj=|Jm_256~9N%o7hX*ZS}I~KBb zWSoUKe!&l72hsXzC4*QxLyp-r=8}=TA}%W1sW{;=SaU;}T$$hlcwwKLG^Gm5=I24K zj8Duu9`*NO^uU0zN_JF9;Id1qq-h;%{j>r{iAYiMIUx4#$4kzJTG=JWwC#&HYTy}t z7MXsXC4asKc{y2DcWTY>$9rYn^PiCm-4xv><2FqULi{*hzloE(d7y;cOpIyuPVo%& zhwDT2GOpe8N2C(!M7g%^l1pJnyHl|pn>pf>_DvZMJ9#wmaxa0}B(MktC(TVI)sHp)WGuV%A8AeqT9ygkCl>16>3v@){q?FQOT(m$PgX&H z`GmX3qt;u{2W?P$n2%5f^ZRC~Tx^P6*{gaprMZiAF`qI+FVQ?0Pbc!We7~!+y~7=H zBJ>4Dy#MMG7u5lp2o^e}BI`%pXscTaMxmtZzEcOX-X=y;dk z?^`rt)9rM-H;MI2*o1Z1RT7WYY>!K>1sU3@3-93xMyURyuZk~=M;bR7v^SQG#z$R@y*(ko2l7$B!e6A zqndcW5rmF({%pP8gR}Ght>snT;HZLYsoWCV6A1iN$iz$3S*A+=F5~+$sE`-UEwX7| zVcSAsYx)j-=%k-zC`Ri|sLf>ep0^I}m46AjKZQSfQFx!rm_(ukwsqV$BnlkqEy^-WT)z{;qsS-&MVwT^asMbo|V#W32afdZPN# z#?dqnqK@frueMw_PUmFqo#2-?O+lm^J3nzK_UeN=3nXkB%d%yW7jrSh<%vrs7-vQ; zVvw%_A`kE9Ik3KWy2F9Wx-jB7o^s)X*nz`R8P_reNofBiUgZAsUv*s^T{8cc)Z*Z+ zjZpg()1^3$P4FnDVN>9%&zlL!FBN=OD0F(4FAX!95P3zq-{sezwPO^@w_tp)#iXq9 zB)LD?-hT8m>_(^*M+kl;mq67-JQ&*-AHH8Ei8XmbpZ7r{Gd+Ya zS!%{vyj6Y?&OYxK_GD3zScu~|zN!-IpWfy*H1KVo;*6MtHVD=3#RU2B(2et34bQ~4 z=IDfy4Sbr_SdO~%@`^wY{7PDJLoauuhhNyiUut$Ze`}}H@}ARp*(0%+XVS}&N;0d2 z)(ada0rITZxT?rF7fJ{nU%F!26l`SeUte_Pc|!X^isz-d*N1ddlQGEOi5`v)i#FB&#-e z6Q*`9>-!ruisPn-E2RyDVO?2J@LwZCY4{zY$AYUXHd23qdJ?Mwdpu#2Yh^6YgPA=ecp{;iLz z9}=}gJ3TlFI~ytcCJr9XZ2u?bbeVtuD$77$D!y;qq|_eqcsc0g%fpvbGyn0TorJQ( zZb}?y;TsFW6OZ$>?n+EK;dcZ&clZ+Tpi_9Fhcc1VG8%Klecc>x!FIF{@)2?aA0lGq zCP%Sa9xqD;NiGfJcIWEAUaYO$KF!NNwtr@nH+6eps7SBsOT_Secd4q6+Ea=S55cMd zr#n<}_2necY+R<8()7#WdI%1U{3_wA z-RFRxMYKBe9586|S=SK=kbxUwz^Hw=*sHSPG7Jy|f39op0bbAxkrj2SPim*Jd_jL2 zsNP&wvh!9*BHQ0WEn+*=>Gpi-i*Bs4 zeKoT8^s?7-H1!l{EX$WIT^Ucw2oB1(=&T>c-{ibCUy^0!^!D5HzS~5jCrF>?!VkuLx#!G3XYpI)nQv_BRa*Mk*djOTzSdX&R6ZcimE(q^Q;^*d{BFxvD7UZq*?BLXxIr{Px$=FDAX(;MqVkCCnqTG1OOk3(RlMF~ICP=oVhL=f$?=vioz-Py5p9p79NlhJ~jY_|LW) zxy&30J`3Lrtj>XdNECg{G{nKe9KOO~q&5vLRE{L~n@Tm8-@YX-j`_aRE2m$gB6-si zB|<^#CDzJx)WhEKeYS4w`KHFA3<~ad4vdT|OO+N$73ZX0EBt6MO53!uz#Vd1_g-k2 z`c6mWu}etNiRNqnY(P9m8c)yaN=b zZjC_CVNzk+5c>%XLy131+|a;@=xzpA7_*uNA<5H3CWiF@QjhRE%G~}^D#B^lQxQhT z#lCX@&*kiK|C&&0k;Sq)k>;95`zx^y)5Yi=jrQSJiV=(3wwme$Lh6N7g8S?S>;#JB zsWA=J3j10$}$?7Yiq{OFe0av>|^tp_u$Y3GBy>%e3EDv zbrZd=W2;~@wvk0bU1nO3!f0`|jspc9yN-MH9SgO3bbi=UURkr~D^>L=s`QC-#LNv# z_Qr6krXa=Fdho35tY^W;QW3j)Lz`m^C)QzwvxgSb-ZGxUZ;AZ4h1BCxYOQ;Js4^-~ zT|vm+7}rR$Z>%;$Yq>Zva#h4D)qVT!rDmO%=c&LzGH}GcDeuy$Fki}Dx#@>y9w`jW z;TmG;oo9NS9jzp!$@f+F2&8^DtfG^Lw)%MacJMr&Is`3p0EuC9iS%8w7v1AL6ZuiDKMA4WVY zxFR=L|InGgqmEm{Y9>NfMDb~Es}G74OhHYYec-~%z&3s;>zbGjUI@Z#WvAsp;|m2} zXfp^&hBoTbJ9l%I_Y)OZ4h7Xiot0gY7I|ip2={h1WOZj!W(ZN%zBL;!xJm>h#t-ed zZFsYm-`+eci^%OpGhFZ-V0UmN8ARDqF_>r}2`*Eslf@ak46!OKnK>6E^@?ln>Wq<= zwh1ITl1xG%jU0pHYU~I)bP(q{J_&*cja3Pa1vQQkBTew2k6yR@IS>LMxV?!R@Bi=1 z3POhbJDLY*1^#wPey09Mc5ncY0078<-)ziZNWyQI2S>#HME(N%aYh0F5WhT-3eFM% z_`f^=Oo}^>-~pk2yFU#vp4A@@1SUeF{x|^eGxT3vkbg30LjU=o`J2K8{dq_S{@DqC z-#o4j0-`8o5hOYgs_$X#9?-V?=e$G-dJa&aS^+{CvbH&hE?zMzE!d1FTfM%#L4efxRN^xdE%=Qt7TA%m7c4 z08V%b)9aVZ7x4(!bAdA{_~0WY4Z?BLi(6eV2Gm+{K{qzk*;oreDPPFJT-^LohK#&b z6j8Gh{NwB`uAwZi`-{F6FGVvuz$;ej zNjd2x3}Op`VxOUJn?^fL7a&m=YE@R+x7zOCP^i(Q)qGYFMVJd73DgI201aN{7=yzW zV+<^sW>5C$KFORGSx^l`L1fA;2SQxo4)S16CnSUG$E+w&lY=8Ync= zo)~jZz$k+}>5KvqC>idRnP%@UcncW z%FsHZe!U|!@B1hdy|YM~I<$0B9RW5pRwStJGALTKR| zL~P>)#X}Xl2V0?>SzF&l6aWw=`*IL}LidtrPmU^^r&uCl1$z4 zo%ldd6poJph`>Q?GLaDrH{bShmrWj>0}_4eZvY+?N)$H2sHw>k8qHAW22tT*vYpt2 z;i)OZa^%=-xUW1#MwHqFB1AJ#Knwr}vU-6x%AGh$Op?ZAL&<*7mP z!=(|S1$pg$U<(PGM7yH!zl^seg19RrgyxpBy@Et2G2hX`b204Ygsm$%(vHwqYD{w! z@NGv-R3)?a=Fv$-SzsB_v(iZ1qy1>Q8RX*kLm9d{Oic>TFoFRQ!L6Q%EOhb!NrSTJ zv8CRHPt% z#n3qeLcx|(RT1R3gQ^F6Pvc!Yv+4TivYUK!(L|25ZYHl!9!rY|$|nhufR68f+4%V4 Vdh`9bL|B^WQyW^suN&tR{|1Jm1Zn^P literal 0 HcmV?d00001 diff --git a/tests/mesh/cells/attribute/cell/test_cells_attribute_cell_protocols.py b/tests/mesh/cells/attribute/cell/test_cells_attribute_cell_protocols.py index 711a7fd4..ecc1133e 100644 --- a/tests/mesh/cells/attribute/cell/test_cells_attribute_cell_protocols.py +++ b/tests/mesh/cells/attribute/cell/test_cells_attribute_cell_protocols.py @@ -11,7 +11,7 @@ from tests.conftest import ServerMonitor # Local constants -mesh_id = "123456789" +mesh_id = "12345678901234567890123456789012" def test_register(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: diff --git a/tests/mesh/cells/attribute/vertex/test_cells_attribute_vertex_protocols.py b/tests/mesh/cells/attribute/vertex/test_cells_attribute_vertex_protocols.py index 58bd66d2..7a9baa69 100644 --- a/tests/mesh/cells/attribute/vertex/test_cells_attribute_vertex_protocols.py +++ b/tests/mesh/cells/attribute/vertex/test_cells_attribute_vertex_protocols.py @@ -11,7 +11,7 @@ from tests.conftest import ServerMonitor # Local constants -mesh_id = "123456789" +mesh_id = "12345678901234567890123456789012" def test_register(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: diff --git a/tests/mesh/cells/test_mesh_cells_protocols.py b/tests/mesh/cells/test_mesh_cells_protocols.py index 9da03dbe..47aa5e45 100644 --- a/tests/mesh/cells/test_mesh_cells_protocols.py +++ b/tests/mesh/cells/test_mesh_cells_protocols.py @@ -4,13 +4,14 @@ # Third party imports from opengeodeweb_viewer.rpc.mesh.mesh_protocols import VtkMeshView from opengeodeweb_viewer.rpc.mesh.cells.cells_protocols import VtkMeshCellsView +from opengeodeweb_viewer.rpc.viewer.viewer_protocols import VtkViewerView # Local application imports from tests.mesh.test_mesh_protocols import test_register_mesh from tests.conftest import ServerMonitor # Local constants -mesh_id = "123456789" +mesh_id = "12345678901234567890123456789012" def test_register(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: @@ -52,3 +53,27 @@ def test_cells_visibility( [{"id": mesh_id, "visibility": False}], ) assert server.compare_image("mesh/cells/visibility.jpeg") == True + + +def test_cells_clipping_plane( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + test_register(server, dataset_factory) + + server.call( + VtkViewerView.viewer_prefix + + VtkViewerView.viewer_schemas_dict["clipping_planes"]["rpc"], + [ + { + "ids": [mesh_id], + "planes": [ + { + "origin": [262.0, 387.0, 0.0], + "normal": [1.0, 1.0, 0.0], + } + ], + } + ], + ) + assert server.compare_image("mesh/cells/clipping_plane.jpeg") == True diff --git a/tests/mesh/edges/attribute/edge/test_edges_attribute_edge_protocols.py b/tests/mesh/edges/attribute/edge/test_edges_attribute_edge_protocols.py index dab93f18..92a20c72 100644 --- a/tests/mesh/edges/attribute/edge/test_edges_attribute_edge_protocols.py +++ b/tests/mesh/edges/attribute/edge/test_edges_attribute_edge_protocols.py @@ -11,7 +11,7 @@ from tests.conftest import ServerMonitor # Local constants -mesh_id = "123456789" +mesh_id = "12345678901234567890123456789012" def test_register(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: diff --git a/tests/mesh/edges/attribute/vertex/test_edges_attribute_vertex_protocols.py b/tests/mesh/edges/attribute/vertex/test_edges_attribute_vertex_protocols.py index ac75fdb6..5de92b82 100644 --- a/tests/mesh/edges/attribute/vertex/test_edges_attribute_vertex_protocols.py +++ b/tests/mesh/edges/attribute/vertex/test_edges_attribute_vertex_protocols.py @@ -11,7 +11,7 @@ from tests.conftest import ServerMonitor # Local constants -mesh_id = "123456789" +mesh_id = "12345678901234567890123456789012" def test_register(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: diff --git a/tests/mesh/edges/test_mesh_edges_protocols.py b/tests/mesh/edges/test_mesh_edges_protocols.py index b6b3ed77..d123b957 100644 --- a/tests/mesh/edges/test_mesh_edges_protocols.py +++ b/tests/mesh/edges/test_mesh_edges_protocols.py @@ -4,11 +4,15 @@ # Third party imports from opengeodeweb_viewer.rpc.mesh.mesh_protocols import VtkMeshView from opengeodeweb_viewer.rpc.mesh.edges.edges_protocols import VtkMeshEdgesView +from opengeodeweb_viewer.rpc.viewer.viewer_protocols import VtkViewerView # Local application imports from tests.mesh.test_mesh_protocols import test_register_mesh from tests.conftest import ServerMonitor +# Local constants +mesh_id = "12345678901234567890123456789012" + def test_edges_visibility( server: ServerMonitor, dataset_factory: Callable[..., str] @@ -18,7 +22,7 @@ def test_edges_visibility( server.call( VtkMeshEdgesView.mesh_edges_prefix + VtkMeshEdgesView.mesh_edges_schemas_dict["visibility"]["rpc"], - [{"id": "123456789", "visibility": True}], + [{"id": mesh_id, "visibility": True}], ) assert server.compare_image("mesh/edges/visibility.jpeg") == True @@ -33,7 +37,7 @@ def test_edges_color( + VtkMeshEdgesView.mesh_edges_schemas_dict["color"]["rpc"], [ { - "id": "123456789", + "id": mesh_id, "color": {"red": 255, "green": 0, "blue": 0, "alpha": 0.5}, } ], @@ -45,25 +49,48 @@ def test_edges_with_edged_curve( server: ServerMonitor, dataset_factory: Callable[..., str] ) -> None: dataset_factory( - id="123456789", viewable_file="edged_curve.vtp", viewer_elements_type="edges" + id=mesh_id, viewable_file="edged_curve.vtp", viewer_elements_type="edges" ) server.call( VtkMeshView.mesh_prefix + VtkMeshView.mesh_schemas_dict["register"]["rpc"], - [{"id": "123456789", "name": "edged_curve.vtp"}], + [{"id": mesh_id, "name": "edged_curve.vtp"}], ) assert server.compare_image("mesh/edges/register_edged_curve.jpeg") == True server.call( VtkMeshEdgesView.mesh_edges_prefix + VtkMeshEdgesView.mesh_edges_schemas_dict["color"]["rpc"], - [{"id": "123456789", "color": {"red": 255, "green": 0, "blue": 0, "alpha": 1}}], + [{"id": mesh_id, "color": {"red": 255, "green": 0, "blue": 0, "alpha": 1}}], ) assert server.compare_image("mesh/edges/edged_curve_color.jpeg") == True server.call( VtkMeshEdgesView.mesh_edges_prefix + VtkMeshEdgesView.mesh_edges_schemas_dict["visibility"]["rpc"], - [{"id": "123456789", "visibility": False}], + [{"id": mesh_id, "visibility": False}], ) assert server.compare_image("mesh/edges/edged_curve_visibility.jpeg") == True + + +def test_edges_clipping_plane( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + test_register_mesh(server, dataset_factory) + + server.call( + VtkViewerView.viewer_prefix + + VtkViewerView.viewer_schemas_dict["clipping_planes"]["rpc"], + [ + { + "ids": [mesh_id], + "planes": [ + { + "origin": [0.0, 2.5, 0.0], + "normal": [1.0, 0.0, 1.0], + } + ], + } + ], + ) + assert server.compare_image("mesh/edges/clipping_plane.jpeg") == True diff --git a/tests/mesh/points/attribute/vertex/test_points_attribute_vertex_protocols.py b/tests/mesh/points/attribute/vertex/test_points_attribute_vertex_protocols.py index 20da3916..91f74233 100644 --- a/tests/mesh/points/attribute/vertex/test_points_attribute_vertex_protocols.py +++ b/tests/mesh/points/attribute/vertex/test_points_attribute_vertex_protocols.py @@ -11,7 +11,7 @@ from tests.conftest import ServerMonitor # Local constants -mesh_id = "123456789" +mesh_id = "12345678901234567890123456789012" def test_register(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: diff --git a/tests/mesh/points/test_mesh_points_protocols.py b/tests/mesh/points/test_mesh_points_protocols.py index 256ad2e4..8100052a 100644 --- a/tests/mesh/points/test_mesh_points_protocols.py +++ b/tests/mesh/points/test_mesh_points_protocols.py @@ -15,7 +15,7 @@ def test_points_visibility( server: ServerMonitor, dataset_factory: Callable[..., str] ) -> None: - mesh_id = "123456789" + mesh_id = "12345678901234567890123456789012" test_register_mesh(server, dataset_factory) server.call( @@ -29,7 +29,7 @@ def test_points_visibility( def test_points_size( server: ServerMonitor, dataset_factory: Callable[..., str] ) -> None: - mesh_id = "123456789" + mesh_id = "12345678901234567890123456789012" test_points_visibility(server, dataset_factory) server.call( @@ -43,7 +43,7 @@ def test_points_size( def test_points_color( server: ServerMonitor, dataset_factory: Callable[..., str] ) -> None: - mesh_id = "123456789" + mesh_id = "12345678901234567890123456789012" test_points_size(server, dataset_factory) server.call( diff --git a/tests/mesh/polygons/attribute/polygon/test_polygons_attribute_polygon_protocols.py b/tests/mesh/polygons/attribute/polygon/test_polygons_attribute_polygon_protocols.py index 84739dee..aa161317 100644 --- a/tests/mesh/polygons/attribute/polygon/test_polygons_attribute_polygon_protocols.py +++ b/tests/mesh/polygons/attribute/polygon/test_polygons_attribute_polygon_protocols.py @@ -11,7 +11,7 @@ from tests.conftest import ServerMonitor # Local constants -mesh_id = "123456789" +mesh_id = "12345678901234567890123456789012" def test_register(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: diff --git a/tests/mesh/polygons/attribute/vertex/test_polygons_attribute_vertex_protocols.py b/tests/mesh/polygons/attribute/vertex/test_polygons_attribute_vertex_protocols.py index 00f0b2c8..2ec8c4bf 100644 --- a/tests/mesh/polygons/attribute/vertex/test_polygons_attribute_vertex_protocols.py +++ b/tests/mesh/polygons/attribute/vertex/test_polygons_attribute_vertex_protocols.py @@ -11,7 +11,7 @@ from tests.conftest import ServerMonitor # Local constants -mesh_id = "123456789" +mesh_id = "12345678901234567890123456789012" def test_register(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: diff --git a/tests/mesh/polygons/test_mesh_polygons_protocols.py b/tests/mesh/polygons/test_mesh_polygons_protocols.py index f74acb28..e9fa585d 100644 --- a/tests/mesh/polygons/test_mesh_polygons_protocols.py +++ b/tests/mesh/polygons/test_mesh_polygons_protocols.py @@ -4,11 +4,15 @@ # Third party imports from opengeodeweb_viewer.rpc.mesh.mesh_protocols import VtkMeshView from opengeodeweb_viewer.rpc.mesh.polygons.polygons_protocols import VtkMeshPolygonsView +from opengeodeweb_viewer.rpc.viewer.viewer_protocols import VtkViewerView # Local application imports from tests.mesh.test_mesh_protocols import test_register_mesh from tests.conftest import ServerMonitor +# Local constants +mesh_id = "12345678901234567890123456789012" + def test_polygons_color( server: ServerMonitor, dataset_factory: Callable[..., str] @@ -21,7 +25,7 @@ def test_polygons_color( + VtkMeshPolygonsView.mesh_polygons_schemas_dict["color"]["rpc"], [ { - "id": "123456789", + "id": mesh_id, "color": {"red": 255, "green": 0, "blue": 0, "alpha": 1.0}, } ], @@ -38,6 +42,30 @@ def test_polygons_visibility( server.call( VtkMeshPolygonsView.mesh_polygons_prefix + VtkMeshPolygonsView.mesh_polygons_schemas_dict["visibility"]["rpc"], - [{"id": "123456789", "visibility": False}], + [{"id": mesh_id, "visibility": False}], ) assert server.compare_image("mesh/polygons/visibility.jpeg") == True + + +def test_polygons_clipping_plane( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + test_register_mesh(server, dataset_factory) + + server.call( + VtkViewerView.viewer_prefix + + VtkViewerView.viewer_schemas_dict["clipping_planes"]["rpc"], + [ + { + "ids": [mesh_id], + "planes": [ + { + "origin": [0.0, 2.5, 0.0], + "normal": [1.0, 0.0, 1.0], + } + ], + } + ], + ) + assert server.compare_image("mesh/polygons/clipping_plane.jpeg") == True diff --git a/tests/mesh/polyhedra/attribute/polyhedron/test_polyhedra_attribute_polyhedron_protocols.py b/tests/mesh/polyhedra/attribute/polyhedron/test_polyhedra_attribute_polyhedron_protocols.py index 0a6d9b5a..f60da143 100644 --- a/tests/mesh/polyhedra/attribute/polyhedron/test_polyhedra_attribute_polyhedron_protocols.py +++ b/tests/mesh/polyhedra/attribute/polyhedron/test_polyhedra_attribute_polyhedron_protocols.py @@ -11,7 +11,7 @@ from tests.conftest import ServerMonitor # Local constants -mesh_id = "123456789" +mesh_id = "12345678901234567890123456789012" def test_register(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: diff --git a/tests/mesh/polyhedra/attribute/vertex/test_polyhedra_attribute_vertex_protocols.py b/tests/mesh/polyhedra/attribute/vertex/test_polyhedra_attribute_vertex_protocols.py index a467cf29..1412c18f 100644 --- a/tests/mesh/polyhedra/attribute/vertex/test_polyhedra_attribute_vertex_protocols.py +++ b/tests/mesh/polyhedra/attribute/vertex/test_polyhedra_attribute_vertex_protocols.py @@ -11,7 +11,7 @@ from tests.conftest import ServerMonitor # Local constants -mesh_id = "123456789" +mesh_id = "12345678901234567890123456789012" def test_register(server: ServerMonitor, dataset_factory: Callable[..., str]) -> None: diff --git a/tests/mesh/polyhedra/test_mesh_polyhedra_protocols.py b/tests/mesh/polyhedra/test_mesh_polyhedra_protocols.py index ebc3b2b8..2aa0e11d 100644 --- a/tests/mesh/polyhedra/test_mesh_polyhedra_protocols.py +++ b/tests/mesh/polyhedra/test_mesh_polyhedra_protocols.py @@ -6,23 +6,27 @@ from opengeodeweb_viewer.rpc.mesh.polyhedra.polyhedra_protocols import ( VtkMeshPolyhedraView, ) +from opengeodeweb_viewer.rpc.viewer.viewer_protocols import VtkViewerView # Local application imports from tests.conftest import ServerMonitor +# Local constants +mesh_id = "12345678901234567890123456789012" + def test_register_mesh( server: ServerMonitor, dataset_factory: Callable[..., str] ) -> None: dataset_factory( - id="123456789", + id=mesh_id, viewable_file="polyhedron_attribute.vtu", viewer_elements_type="polyhedra", ) server.call( VtkMeshView.mesh_prefix + VtkMeshView.mesh_schemas_dict["register"]["rpc"], - [{"id": "123456789", "name": "polyhedron_attribute.vtu"}], + [{"id": mesh_id, "name": "polyhedron_attribute.vtu"}], ) assert server.compare_image("mesh/polyhedra/register.jpeg") == True @@ -37,7 +41,7 @@ def test_polyhedra_color( + VtkMeshPolyhedraView.mesh_polyhedra_schemas_dict["color"]["rpc"], [ { - "id": "123456789", + "id": mesh_id, "color": {"red": 255, "green": 0, "blue": 0, "alpha": 1.0}, } ], @@ -53,6 +57,30 @@ def test_polyhedra_visibility( server.call( VtkMeshPolyhedraView.mesh_polyhedra_prefix + VtkMeshPolyhedraView.mesh_polyhedra_schemas_dict["visibility"]["rpc"], - [{"id": "123456789", "visibility": False}], + [{"id": mesh_id, "visibility": False}], ) assert server.compare_image("mesh/polyhedra/visibility.jpeg") == True + + +def test_polyhedra_clipping_plane( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + test_register_mesh(server, dataset_factory) + + server.call( + VtkViewerView.viewer_prefix + + VtkViewerView.viewer_schemas_dict["clipping_planes"]["rpc"], + [ + { + "ids": [mesh_id], + "planes": [ + { + "origin": [0.5, 0.5, 0.5], + "normal": [-1.0, 0.0, 0.0], + } + ], + } + ], + ) + assert server.compare_image("mesh/polyhedra/clipping_plane.jpeg") == True diff --git a/tests/mesh/test_mesh_protocols.py b/tests/mesh/test_mesh_protocols.py index e696cdb4..a751ffdc 100644 --- a/tests/mesh/test_mesh_protocols.py +++ b/tests/mesh/test_mesh_protocols.py @@ -2,17 +2,20 @@ from opengeodeweb_viewer.rpc.mesh.mesh_protocols import VtkMeshView from tests.conftest import ServerMonitor +# Local constants +mesh_id = "12345678901234567890123456789012" + def test_register_mesh( server: ServerMonitor, dataset_factory: Callable[..., str] ) -> None: dataset_factory( - id="123456789", viewable_file="hat.vtp", viewer_elements_type="polygons" + id=mesh_id, viewable_file="hat.vtp", viewer_elements_type="polygons" ) server.call( VtkMeshView.mesh_prefix + VtkMeshView.mesh_schemas_dict["register"]["rpc"], - [{"id": "123456789", "name": "hat.vtp"}], + [{"id": mesh_id, "name": "hat.vtp"}], ) assert server.compare_image("mesh/register.jpeg") == True @@ -24,7 +27,7 @@ def test_deregister_mesh( server.call( VtkMeshView.mesh_prefix + VtkMeshView.mesh_schemas_dict["deregister"]["rpc"], - [{"id": "123456789"}], + [{"id": mesh_id}], ) assert server.compare_image("mesh/deregister.jpeg") == True @@ -35,7 +38,7 @@ def test_visibility(server: ServerMonitor, dataset_factory: Callable[..., str]) server.call( VtkMeshView.mesh_prefix + VtkMeshView.mesh_schemas_dict["visibility"]["rpc"], - [{"id": "123456789", "visibility": False}], + [{"id": mesh_id, "visibility": False}], ) assert server.compare_image("mesh/visibility.jpeg") == True @@ -47,7 +50,7 @@ def test_color(server: ServerMonitor, dataset_factory: Callable[..., str]) -> No VtkMeshView.mesh_prefix + VtkMeshView.mesh_schemas_dict["color"]["rpc"], [ { - "id": "123456789", + "id": mesh_id, "color": {"red": 50, "green": 2, "blue": 250, "alpha": 1.0}, } ], @@ -70,7 +73,7 @@ def test_apply_textures( + VtkMeshView.mesh_schemas_dict["apply_textures"]["rpc"], [ { - "id": "123456789", + "id": mesh_id, "textures": [ { "texture_name": "lambert2SG", diff --git a/tests/model/blocks/attribute/polyhedron/test_model_blocks_attribute_polyhedron_protocols.py b/tests/model/blocks/attribute/polyhedron/test_model_blocks_attribute_polyhedron_protocols.py index ec292c96..9ffd7816 100644 --- a/tests/model/blocks/attribute/polyhedron/test_model_blocks_attribute_polyhedron_protocols.py +++ b/tests/model/blocks/attribute/polyhedron/test_model_blocks_attribute_polyhedron_protocols.py @@ -13,7 +13,8 @@ from tests.model.test_model_protocols import test_register_model_cube from tests.conftest import ServerMonitor -model_id = "123456789" +# Local constants +model_id = "12345678901234567890123456789012" def test_blocks_polyhedron_attribute( diff --git a/tests/model/blocks/attribute/vertex/test_model_blocks_attribute_vertex_protocols.py b/tests/model/blocks/attribute/vertex/test_model_blocks_attribute_vertex_protocols.py index 5906802d..3b899a3b 100644 --- a/tests/model/blocks/attribute/vertex/test_model_blocks_attribute_vertex_protocols.py +++ b/tests/model/blocks/attribute/vertex/test_model_blocks_attribute_vertex_protocols.py @@ -13,7 +13,8 @@ from tests.model.test_model_protocols import test_register_model_cube from tests.conftest import ServerMonitor -model_id = "123456789" +# Local constants +model_id = "12345678901234567890123456789012" def test_blocks_vertex_attribute( diff --git a/tests/model/blocks/test_model_blocks_protocols.py b/tests/model/blocks/test_model_blocks_protocols.py index 9b824fd5..c9393a0e 100644 --- a/tests/model/blocks/test_model_blocks_protocols.py +++ b/tests/model/blocks/test_model_blocks_protocols.py @@ -5,11 +5,15 @@ from opengeodeweb_viewer.rpc.model.blocks.model_blocks_protocols import ( VtkModelBlocksView, ) +from opengeodeweb_viewer.rpc.viewer.viewer_protocols import VtkViewerView # Local application imports from tests.model.test_model_protocols import test_register_model_cube from tests.conftest import ServerMonitor +# Local constants +model_id = "12345678901234567890123456789012" + def test_blocks_polyhedra_visibility( server: ServerMonitor, dataset_factory: Callable[..., str] @@ -22,7 +26,7 @@ def test_blocks_polyhedra_visibility( + VtkModelBlocksView.model_blocks_schemas_dict["visibility"]["rpc"], [ { - "id": "123456789", + "id": model_id, "block_ids": list(range(1, 50)), "visibility": False, } @@ -36,7 +40,7 @@ def test_blocks_polyhedra_visibility( + VtkModelBlocksView.model_blocks_schemas_dict["visibility"]["rpc"], [ { - "id": "123456789", + "id": model_id, "block_ids": list(range(48, 50)), "visibility": True, } @@ -57,7 +61,7 @@ def test_blocks_polyhedra_color( + VtkModelBlocksView.model_blocks_schemas_dict["color"]["rpc"], [ { - "id": "123456789", + "id": model_id, "block_ids": list(range(48, 50)), "color_mode": "constant", "color": {"red": 255, "green": 0, "blue": 0, "alpha": 1.0}, @@ -65,3 +69,27 @@ def test_blocks_polyhedra_color( ], ) assert server.compare_image("model/blocks/color.jpeg") == True + + +def test_blocks_clipping_plane( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + test_register_model_cube(server, dataset_factory) + + server.call( + VtkViewerView.viewer_prefix + + VtkViewerView.viewer_schemas_dict["clipping_planes"]["rpc"], + [ + { + "ids": [model_id], + "planes": [ + { + "origin": [5.0, 5.0, 5.0], + "normal": [1.0, 1.0, 1.0], + } + ], + } + ], + ) + assert server.compare_image("model/blocks/clipping_plane.jpeg") == True diff --git a/tests/model/corners/attribute/vertex/test_model_corners_attribute_vertex_protocols.py b/tests/model/corners/attribute/vertex/test_model_corners_attribute_vertex_protocols.py index 6f0ded94..e047dc17 100644 --- a/tests/model/corners/attribute/vertex/test_model_corners_attribute_vertex_protocols.py +++ b/tests/model/corners/attribute/vertex/test_model_corners_attribute_vertex_protocols.py @@ -13,7 +13,8 @@ from tests.model.test_model_protocols import test_register_model_cube from tests.conftest import ServerMonitor -model_id = "123456789" +# Local constants +model_id = "12345678901234567890123456789012" def test_corners_vertex_attribute( diff --git a/tests/model/corners/test_model_corners_protocols.py b/tests/model/corners/test_model_corners_protocols.py index f19ea28e..eaef6704 100644 --- a/tests/model/corners/test_model_corners_protocols.py +++ b/tests/model/corners/test_model_corners_protocols.py @@ -10,6 +10,9 @@ from tests.model.test_model_protocols import test_register_model_cube from tests.conftest import ServerMonitor +# Local constants +model_id = "12345678901234567890123456789012" + def test_corners_points_visibility( server: ServerMonitor, dataset_factory: Callable[..., str] @@ -22,7 +25,7 @@ def test_corners_points_visibility( + VtkModelCornersView.model_corners_schemas_dict["visibility"]["rpc"], [ { - "id": "123456789", + "id": model_id, "block_ids": list(range(1, 50)), "visibility": False, } @@ -35,7 +38,7 @@ def test_corners_points_visibility( + VtkModelCornersView.model_corners_schemas_dict["visibility"]["rpc"], [ { - "id": "123456789", + "id": model_id, "block_ids": list(range(1, 13)), "visibility": True, } @@ -55,7 +58,7 @@ def test_corners_points_color( + VtkModelCornersView.model_corners_schemas_dict["color"]["rpc"], [ { - "id": "123456789", + "id": model_id, "block_ids": list(range(1, 13)), "color_mode": "constant", "color": {"red": 255, "green": 0, "blue": 0, "alpha": 1.0}, @@ -74,6 +77,6 @@ def test_corners_points_random_color( server.call( VtkModelCornersView.model_corners_prefix + VtkModelCornersView.model_corners_schemas_dict["color"]["rpc"], - [{"id": "123456789", "block_ids": list(range(1, 13)), "color_mode": "random"}], + [{"id": model_id, "block_ids": list(range(1, 13)), "color_mode": "random"}], ) assert server.compare_image("model/corners/random_color.jpeg") == True diff --git a/tests/model/edges/test_model_edges_protocols.py b/tests/model/edges/test_model_edges_protocols.py index f10dbc48..a9584124 100644 --- a/tests/model/edges/test_model_edges_protocols.py +++ b/tests/model/edges/test_model_edges_protocols.py @@ -10,6 +10,9 @@ from tests.model.test_model_protocols import test_register_model from tests.conftest import ServerMonitor +# Local constants +model_id = "12345678901234567890123456789012" + def test_edges_visibility( server: ServerMonitor, dataset_factory: Callable[..., str] @@ -20,6 +23,6 @@ def test_edges_visibility( server.call( VtkModelEdgesView.model_edges_prefix + VtkModelEdgesView.model_edges_schemas_dict["visibility"]["rpc"], - [{"id": "123456789", "visibility": True}], + [{"id": model_id, "visibility": True}], ) assert server.compare_image("model/edges/visibility.jpeg") == True diff --git a/tests/model/lines/attribute/edge/test_model_lines_attribute_edge_protocols.py b/tests/model/lines/attribute/edge/test_model_lines_attribute_edge_protocols.py index 76b99fef..e1a6f417 100644 --- a/tests/model/lines/attribute/edge/test_model_lines_attribute_edge_protocols.py +++ b/tests/model/lines/attribute/edge/test_model_lines_attribute_edge_protocols.py @@ -13,7 +13,8 @@ from tests.model.test_model_protocols import test_register_model_cube from tests.conftest import ServerMonitor -model_id = "123456789" +# Local constants +model_id = "12345678901234567890123456789012" def test_lines_edge_attribute( diff --git a/tests/model/lines/attribute/vertex/test_model_lines_attribute_vertex_protocols.py b/tests/model/lines/attribute/vertex/test_model_lines_attribute_vertex_protocols.py index c84cd74c..004a91f0 100644 --- a/tests/model/lines/attribute/vertex/test_model_lines_attribute_vertex_protocols.py +++ b/tests/model/lines/attribute/vertex/test_model_lines_attribute_vertex_protocols.py @@ -13,7 +13,8 @@ from tests.model.test_model_protocols import test_register_model_cube from tests.conftest import ServerMonitor -model_id = "123456789" +# Local constants +model_id = "12345678901234567890123456789012" def test_lines_vertex_attribute( diff --git a/tests/model/lines/test_model_lines_protocols.py b/tests/model/lines/test_model_lines_protocols.py index d06ef1a7..3f2bdacf 100644 --- a/tests/model/lines/test_model_lines_protocols.py +++ b/tests/model/lines/test_model_lines_protocols.py @@ -5,11 +5,15 @@ from opengeodeweb_viewer.rpc.model.lines.model_lines_protocols import ( VtkModelLinesView, ) +from opengeodeweb_viewer.rpc.viewer.viewer_protocols import VtkViewerView # Local application imports from tests.model.test_model_protocols import test_register_model_cube from tests.conftest import ServerMonitor +# Local constants +model_id = "12345678901234567890123456789012" + def test_lines_edges_visibility( server: ServerMonitor, dataset_factory: Callable[..., str] @@ -22,7 +26,7 @@ def test_lines_edges_visibility( + VtkModelLinesView.model_lines_schemas_dict["visibility"]["rpc"], [ { - "id": "123456789", + "id": model_id, "block_ids": list(range(1, 50)), "visibility": False, } @@ -35,7 +39,7 @@ def test_lines_edges_visibility( + VtkModelLinesView.model_lines_schemas_dict["visibility"]["rpc"], [ { - "id": "123456789", + "id": model_id, "block_ids": list(range(14, 35)), "visibility": True, } @@ -55,7 +59,7 @@ def test_lines_edges_color( + VtkModelLinesView.model_lines_schemas_dict["color"]["rpc"], [ { - "id": "123456789", + "id": model_id, "block_ids": list(range(14, 35)), "color_mode": "constant", "color": {"red": 255, "green": 0, "blue": 0, "alpha": 0.5}, @@ -63,3 +67,27 @@ def test_lines_edges_color( ], ) assert server.compare_image("model/lines/color.jpeg") == True + + +def test_lines_clipping_plane( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + test_lines_edges_visibility(server, dataset_factory) + + server.call( + VtkViewerView.viewer_prefix + + VtkViewerView.viewer_schemas_dict["clipping_planes"]["rpc"], + [ + { + "ids": [model_id], + "planes": [ + { + "origin": [5.0, 5.0, 5.0], + "normal": [1.0, 1.0, 1.0], + } + ], + } + ], + ) + assert server.compare_image("model/lines/clipping_plane.jpeg") == True diff --git a/tests/model/points/test_model_points_protocols.py b/tests/model/points/test_model_points_protocols.py index 95b7eac0..d3f81c17 100644 --- a/tests/model/points/test_model_points_protocols.py +++ b/tests/model/points/test_model_points_protocols.py @@ -10,6 +10,9 @@ from tests.model.test_model_protocols import test_register_model from tests.conftest import ServerMonitor +# Local constants +model_id = "12345678901234567890123456789012" + def test_points_visibility( server: ServerMonitor, dataset_factory: Callable[..., str] @@ -20,7 +23,7 @@ def test_points_visibility( server.call( VtkModelPointsView.model_points_prefix + VtkModelPointsView.model_points_schemas_dict["visibility"]["rpc"], - [{"id": "123456789", "visibility": True}], + [{"id": model_id, "visibility": True}], ) assert server.compare_image("model/points/visibility.jpeg") == True @@ -34,6 +37,6 @@ def test_points_size( server.call( VtkModelPointsView.model_points_prefix + VtkModelPointsView.model_points_schemas_dict["size"]["rpc"], - [{"id": "123456789", "size": 20}], + [{"id": model_id, "size": 20}], ) assert server.compare_image("model/points/size.jpeg") == True diff --git a/tests/model/surfaces/attribute/polygon/test_model_surfaces_attribute_polygon_protocols.py b/tests/model/surfaces/attribute/polygon/test_model_surfaces_attribute_polygon_protocols.py index 7a1ddf33..807f6997 100644 --- a/tests/model/surfaces/attribute/polygon/test_model_surfaces_attribute_polygon_protocols.py +++ b/tests/model/surfaces/attribute/polygon/test_model_surfaces_attribute_polygon_protocols.py @@ -18,7 +18,8 @@ # Local application imports from tests.conftest import ServerMonitor -model_id = "123456789" +# Local constants +model_id = "12345678901234567890123456789012" def add_polygon_attribute_to_cube(model_id: str) -> None: diff --git a/tests/model/surfaces/attribute/vertex/test_model_surfaces_attribute_vertex_protocols.py b/tests/model/surfaces/attribute/vertex/test_model_surfaces_attribute_vertex_protocols.py index 7e6d1c0a..5d1b5e6b 100644 --- a/tests/model/surfaces/attribute/vertex/test_model_surfaces_attribute_vertex_protocols.py +++ b/tests/model/surfaces/attribute/vertex/test_model_surfaces_attribute_vertex_protocols.py @@ -13,7 +13,8 @@ from tests.model.test_model_protocols import test_register_model_cube from tests.conftest import ServerMonitor -model_id = "123456789" +# Local constants +model_id = "12345678901234567890123456789012" def test_surfaces_vertex_attribute( diff --git a/tests/model/surfaces/test_model_surfaces_protocols.py b/tests/model/surfaces/test_model_surfaces_protocols.py index 4edead99..af74bca4 100644 --- a/tests/model/surfaces/test_model_surfaces_protocols.py +++ b/tests/model/surfaces/test_model_surfaces_protocols.py @@ -5,11 +5,15 @@ from opengeodeweb_viewer.rpc.model.surfaces.model_surfaces_protocols import ( VtkModelSurfacesView, ) +from opengeodeweb_viewer.rpc.viewer.viewer_protocols import VtkViewerView # Local application imports from tests.model.test_model_protocols import test_register_model_cube from tests.conftest import ServerMonitor +# Local constants +model_id = "12345678901234567890123456789012" + def test_surfaces_polygons_visibility( server: ServerMonitor, dataset_factory: Callable[..., str] @@ -22,7 +26,7 @@ def test_surfaces_polygons_visibility( + VtkModelSurfacesView.model_surfaces_schemas_dict["visibility"]["rpc"], [ { - "id": "123456789", + "id": model_id, "block_ids": list(range(1, 50)), "visibility": False, } @@ -35,8 +39,8 @@ def test_surfaces_polygons_visibility( + VtkModelSurfacesView.model_surfaces_schemas_dict["visibility"]["rpc"], [ { - "id": "123456789", - "block_ids": list(range(36, 49)), + "id": model_id, + "block_ids": list(range(36, 47)), "visibility": True, } ], @@ -56,8 +60,8 @@ def test_surfaces_polygons_color( + VtkModelSurfacesView.model_surfaces_schemas_dict["color"]["rpc"], [ { - "id": "123456789", - "block_ids": list(range(36, 49)), + "id": model_id, + "block_ids": list(range(36, 47)), "color_mode": "constant", "color": {"red": 255, "green": 0, "blue": 0, "alpha": 0.5}, } @@ -75,6 +79,30 @@ def test_surfaces_polygons_random_color( server.call( VtkModelSurfacesView.model_surfaces_prefix + VtkModelSurfacesView.model_surfaces_schemas_dict["color"]["rpc"], - [{"id": "123456789", "block_ids": list(range(36, 49)), "color_mode": "random"}], + [{"id": model_id, "block_ids": list(range(36, 47)), "color_mode": "random"}], ) assert server.compare_image("model/surfaces/random_color.jpeg") == True + + +def test_surfaces_clipping_plane( + server: ServerMonitor, dataset_factory: Callable[..., str] +) -> None: + + test_surfaces_polygons_visibility(server, dataset_factory) + + server.call( + VtkViewerView.viewer_prefix + + VtkViewerView.viewer_schemas_dict["clipping_planes"]["rpc"], + [ + { + "ids": [model_id], + "planes": [ + { + "origin": [5.0, 5.0, 5.0], + "normal": [1.0, 1.0, 1.0], + } + ], + } + ], + ) + assert server.compare_image("model/surfaces/clipping_plane.jpeg") == True diff --git a/tests/model/test_model_protocols.py b/tests/model/test_model_protocols.py index 6eebc86b..323074b5 100644 --- a/tests/model/test_model_protocols.py +++ b/tests/model/test_model_protocols.py @@ -2,15 +2,18 @@ from opengeodeweb_viewer.rpc.model.model_protocols import VtkModelView from tests.conftest import ServerMonitor +# Local constants +model_id = "12345678901234567890123456789012" + def test_register_model( server: ServerMonitor, dataset_factory: Callable[..., str] ) -> None: - dataset_factory(id="123456789", viewable_file="CrossSection.vtm") + dataset_factory(id=model_id, viewable_file="CrossSection.vtm") server.call( VtkModelView.model_prefix + VtkModelView.model_schemas_dict["register"]["rpc"], - [{"id": "123456789", "name": "cube.vtm"}], + [{"id": model_id, "name": "cube.vtm"}], ) assert server.compare_image("model/register.jpeg") == True @@ -19,10 +22,10 @@ def test_register_model_cube( server: ServerMonitor, dataset_factory: Callable[..., str] ) -> None: - dataset_factory(id="123456789", viewable_file="cube.vtm") + dataset_factory(id=model_id, viewable_file="cube.vtm") server.call( VtkModelView.model_prefix + VtkModelView.model_schemas_dict["register"]["rpc"], - [{"id": "123456789", "name": "cube.vtm"}], + [{"id": model_id, "name": "cube.vtm"}], ) assert server.compare_image("model/cube_register.jpeg") == True @@ -36,7 +39,7 @@ def test_visibility_model( server.call( VtkModelView.model_prefix + VtkModelView.model_schemas_dict["visibility"]["rpc"], - [{"id": "123456789", "visibility": False}], + [{"id": model_id, "visibility": False}], ) assert server.compare_image("model/visibility.jpeg") == True @@ -50,7 +53,7 @@ def test_deregister_model( server.call( VtkModelView.model_prefix + VtkModelView.model_schemas_dict["deregister"]["rpc"], - [{"id": "123456789"}], + [{"id": model_id}], ) assert server.compare_image("model/deregister.jpeg") == True @@ -62,7 +65,7 @@ def test_get_blocks_bounds( test_register_model(server, dataset_factory) rpc = VtkModelView.model_prefix + "get_blocks_bounds" - server.call(rpc, [{"id": "123456789", "block_ids": [2]}]) + server.call(rpc, [{"id": "12345678901234567890123456789012", "block_ids": [2]}]) response = server.get_response() while isinstance(response, bytes) or response.get("id") != f"rpc:{rpc}": diff --git a/tests/test_viewer_protocols.py b/tests/test_viewer_protocols.py index d3bf6911..14d6fca4 100644 --- a/tests/test_viewer_protocols.py +++ b/tests/test_viewer_protocols.py @@ -12,6 +12,9 @@ from tests.mesh.test_mesh_protocols import test_register_mesh from tests.conftest import ServerMonitor +# Local constants +mesh_id = "12345678901234567890123456789012" + def test_reset_visualization(server: ServerMonitor) -> None: server.call( @@ -342,11 +345,11 @@ def test_combined_scaling_and_grid( ) assert server.compare_image("viewer/reset_visualization.jpeg") == True dataset_factory( - id="123456789", viewable_file="hat.vtp", viewer_elements_type="polygons" + id=mesh_id, viewable_file="hat.vtp", viewer_elements_type="polygons" ) server.call( VtkMeshView.mesh_prefix + VtkMeshView.mesh_schemas_dict["register"]["rpc"], - [{"id": "123456789", "name": "hat.vtp"}], + [{"id": mesh_id, "name": "hat.vtp"}], ) assert server.compare_image("viewer/register_hat.jpeg") == True server.call( @@ -373,7 +376,7 @@ def test_clipping_planes( + VtkViewerView.viewer_schemas_dict["clipping_planes"]["rpc"], [ { - "ids": ["123456789"], + "ids": [mesh_id], "planes": [ { "origin": [0.0, 0.0, 0.0], From d1ffc89698cbedc7dd4a534f523b9761f65196ce Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 30 Jul 2026 13:23:00 +0200 Subject: [PATCH 24/24] mypy --- src/opengeodeweb_viewer/vtk_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/opengeodeweb_viewer/vtk_protocol.py b/src/opengeodeweb_viewer/vtk_protocol.py index baf53d65..d6c959ba 100644 --- a/src/opengeodeweb_viewer/vtk_protocol.py +++ b/src/opengeodeweb_viewer/vtk_protocol.py @@ -130,7 +130,7 @@ def reset_camera_clipping_range(self) -> None: def setup_pipeline(self, pipeline: VtkPipeline, name: str) -> vtkDataObject | None: pipeline.filter.SetInputConnection(pipeline.reader.GetOutputPort()) pipeline.filter.Update() - geometry_output = pipeline.filter.GetOutputDataObject(0) + geometry_output: vtkDataObject | None = pipeline.filter.GetOutputDataObject(0) if geometry_output: geometry_output.SetObjectName(name) if isinstance(pipeline.mapper, vtkCompositePolyDataMapper):