diff --git a/python/README.md b/python/README.md index e2d55befb..7a8eb5a90 100644 --- a/python/README.md +++ b/python/README.md @@ -111,6 +111,9 @@ Note: integration testing can be disable using the pytest --without-integration ```json { + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, "python.analysis.typeCheckingMode": "basic", "python.testing.pytestArgs": [ "tests" diff --git a/python/brayns/__init__.py b/python/brayns/__init__.py index 7f29de6fd..cb5dc72d8 100644 --- a/python/brayns/__init__.py +++ b/python/brayns/__init__.py @@ -24,8 +24,61 @@ This package provides an API to interact with Brayns service. """ +from .api.core.camera import ( + Camera, + CameraSettings, + DepthOfField, + OrthographicCamera, + OrthographicSettings, + PanoramicCamera, + PanoramicSettings, + PerspectiveCamera, + PerspectiveSettings, + Stereo, + StereoMode, + create_orthographic_camera, + create_panoramic_camera, + create_perspective_camera, + get_camera, + get_orthographic_camera, + get_panoramic_camera, + get_perspective_camera, + update_camera, + update_orthographic_camera, + update_panoramic_camera, + update_perspective_camera, +) +from .api.core.framebuffer import ( + Accumulation, + Framebuffer, + FramebufferChannel, + FramebufferFormat, + FramebufferSettings, + Size2, + create_framebuffer, + get_framebuffer, + update_framebuffer, +) +from .api.core.image import ( + JpegChannel, + PngChannel, + read_framebuffer, + read_framebuffer_as_exr, + read_framebuffer_as_jpeg, + read_framebuffer_as_png, +) +from .api.core.image_operation import ( + ImageOperation, + ToneMapper, + ToneMapperSettings, + create_tone_mapper, + get_tone_mapper, + update_tone_mapper, +) from .api.core.objects import ( + EmptyObject, Object, + ObjectInfo, clear_objects, create_empty_object, get_all_objects, @@ -39,6 +92,7 @@ TaskInfo, TaskOperation, Version, + cancel_all_tasks, cancel_task, get_endpoint, get_methods, @@ -48,6 +102,27 @@ get_version, stop_service, ) +from .api.core.transfer_function import ( + LinearTransferFunction, + LinearTransferFunctionSettings, + TransferFunction, + create_linear_transfer_function, + get_linear_transfer_function, + update_linear_transfer_function, +) +from .api.core.volume import ( + RegularVolume, + RegularVolumeSettings, + RegularVolumeUpdate, + Size3, + Volume, + VolumeFilter, + VolumeType, + VoxelType, + create_regular_volume, + get_regular_volume, + update_regular_volume, +) from .network.connection import Connection, FutureResponse, Request, Response, connect from .network.json_rpc import ( JsonRpcError, @@ -58,45 +133,137 @@ JsonRpcSuccessResponse, ) from .network.websocket import ServiceUnavailable, WebSocketError +from .utils.box import Box1, Box2, Box3 +from .utils.color import Color3, Color4 from .utils.logger import create_logger +from .utils.quaternion import Quaternion +from .utils.rotation import Rotation, axis_angle, euler, get_rotation_between +from .utils.vector import Vector2, Vector3, Vector4 +from .utils.view import View, X, Y, Z from .version import VERSION __version__ = VERSION """Version tag of brayns Python package (major.minor.patch).""" __all__ = [ + "Accumulation", + "axis_angle", + "Box1", + "Box2", + "Box3", + "Camera", + "CameraSettings", + "cancel_all_tasks", "cancel_task", "clear_objects", + "Color3", + "Color4", "connect", "Connection", "create_empty_object", + "create_framebuffer", + "create_linear_transfer_function", "create_logger", + "create_orthographic_camera", + "create_panoramic_camera", + "create_perspective_camera", + "create_regular_volume", + "create_tone_mapper", + "DepthOfField", + "EmptyObject", "Endpoint", + "euler", + "Framebuffer", + "FramebufferChannel", + "FramebufferFormat", + "FramebufferSettings", "FutureResponse", "get_all_objects", + "get_camera", "get_endpoint", + "get_framebuffer", + "get_framebuffer", + "get_linear_transfer_function", + "get_linear_transfer_function", "get_methods", "get_object", + "get_orthographic_camera", + "get_orthographic_camera", + "get_panoramic_camera", + "get_panoramic_camera", + "get_perspective_camera", + "get_perspective_camera", + "get_regular_volume", + "get_rotation_between", "get_task_result", "get_task", "get_tasks", + "get_tone_mapper", + "get_tone_mapper", "get_version", + "ImageOperation", + "JpegChannel", "JsonRpcError", "JsonRpcErrorResponse", "JsonRpcId", "JsonRpcRequest", "JsonRpcResponse", "JsonRpcSuccessResponse", + "LinearTransferFunction", + "LinearTransferFunctionSettings", "Object", + "ObjectInfo", + "OrthographicCamera", + "OrthographicSettings", + "PanoramicCamera", + "PanoramicSettings", + "PerspectiveCamera", + "PerspectiveSettings", + "PngChannel", + "Quaternion", + "read_framebuffer_as_exr", + "read_framebuffer_as_jpeg", + "read_framebuffer_as_png", + "read_framebuffer", + "RegularVolume", + "RegularVolumeSettings", + "RegularVolumeUpdate", "remove_objects", "Request", "Response", + "Rotation", "ServiceUnavailable", + "Size2", + "Size3", + "Stereo", + "StereoMode", "stop_service", "Task", "TaskInfo", "TaskOperation", + "ToneMapper", + "ToneMapperSettings", + "TransferFunction", + "update_camera", + "update_framebuffer", + "update_linear_transfer_function", "update_object", + "update_orthographic_camera", + "update_panoramic_camera", + "update_perspective_camera", + "update_regular_volume", + "update_tone_mapper", + "Vector2", + "Vector3", + "Vector4", "Version", + "View", + "Volume", + "VolumeFilter", + "VolumeType", + "VoxelType", "WebSocketError", + "X", + "Y", + "Z", ] diff --git a/python/brayns/api/core/camera.py b/python/brayns/api/core/camera.py new file mode 100644 index 000000000..33fcc59d5 --- /dev/null +++ b/python/brayns/api/core/camera.py @@ -0,0 +1,276 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import math +from dataclasses import dataclass, field +from enum import Enum +from typing import Any + +from brayns.network.connection import Connection +from brayns.utils.box import Box2 +from brayns.utils.parsing import deserialize_box, deserialize_vector, get, serialize_box, try_get +from brayns.utils.vector import Vector2, Vector3 +from brayns.utils.view import View + +from .objects import Object, create_composed_object, get_specific_object, update_specific_object + + +class Camera(Object): ... + + +@dataclass +class CameraSettings: + view: View = field(default_factory=View) + near_clip: float = 0.0 + image_region: Box2 = Box2(Vector2(0, 0), Vector2(1, 1)) + + +def serialize_camera_settings(settings: CameraSettings) -> dict[str, Any]: + return { + "view": { + "position": settings.view.position, + "direction": settings.view.direction, + "up": settings.view.up, + }, + "nearClip": settings.near_clip, + "imageRegion": serialize_box(settings.image_region), + } + + +def deserialize_camera_settings(message: dict[str, Any]) -> CameraSettings: + view = get(message, "view", dict[str, Any]) + + return CameraSettings( + view=View( + position=deserialize_vector(view, "position", Vector3), + direction=deserialize_vector(view, "direction", Vector3), + up=deserialize_vector(view, "up", Vector3), + ), + near_clip=get(message, "nearClip", float), + image_region=deserialize_box(get(message, "imageRegion", dict[str, Any]), Box2), + ) + + +async def create_camera( + connection: Connection, typename: str, settings: CameraSettings, derived: dict[str, Any] +) -> Camera: + base = serialize_camera_settings(settings) + object = await create_composed_object(connection, typename, base, derived) + return Camera(object.id) + + +async def get_camera(connection: Connection, camera: Camera) -> CameraSettings: + result = await get_specific_object(connection, "Camera", camera) + return deserialize_camera_settings(result) + + +async def update_camera(connection: Connection, camera: Camera, settings: CameraSettings) -> None: + params = serialize_camera_settings(settings) + await update_specific_object(connection, "Camera", camera, params) + + +class PerspectiveCamera(Camera): ... + + +@dataclass +class DepthOfField: + aperture_radius: float + focus_distance: float = 1.0 + + +class StereoMode(Enum): + LEFT = "Left" + RIGHT = "Right" + SIDE_BY_SIDE = "SideBySide" + TOP_BOTTOM = "TopBottom" + + +@dataclass +class Stereo: + mode: StereoMode + interpupillary_distance: float = 0.0635 + + +def serialize_stereo(stereo: Stereo) -> dict[str, Any]: + return { + "mode": stereo.mode.value, + "interpupillaryDistance": stereo.interpupillary_distance, + } + + +def deserialize_stereo(message: dict[str, Any]) -> Stereo: + return Stereo( + mode=StereoMode(get(message, "mode", str)), + interpupillary_distance=get(message, "interpupillaryDistance", float), + ) + + +@dataclass +class PerspectiveSettings: + fovy: float = math.radians(45) + depth_of_field: DepthOfField | None = None + architectural: bool = False + stereo: Stereo | None = None + + +def serialize_perspective_settings(settings: PerspectiveSettings) -> dict[str, Any]: + depth_of_field = None + + if settings.depth_of_field is not None: + depth_of_field = { + "apertureRadius": settings.depth_of_field.aperture_radius, + "focusDistance": settings.depth_of_field.focus_distance, + } + + stereo = None + + if settings.stereo is not None: + stereo = serialize_stereo(settings.stereo) + + return { + "fovy": settings.fovy, + "depthOfField": depth_of_field, + "architectural": settings.architectural, + "stereo": stereo, + } + + +def deserialize_perspective_settings(message: dict[str, Any]) -> PerspectiveSettings: + depth_of_field = try_get(message, "depthOfField", dict[str, Any]) + + if depth_of_field is not None: + depth_of_field = DepthOfField( + aperture_radius=get(depth_of_field, "apertureRadius", float), + focus_distance=get(depth_of_field, "focusDistance", float), + ) + + stereo = try_get(message, "stereo", dict[str, Any]) + + if stereo is not None: + stereo = deserialize_stereo(stereo) + + return PerspectiveSettings( + fovy=get(message, "fovy", float), + depth_of_field=depth_of_field, + architectural=get(message, "architectural", bool), + stereo=stereo, + ) + + +async def create_perspective_camera( + connection: Connection, settings: CameraSettings, perspective: PerspectiveSettings +) -> PerspectiveCamera: + derived = serialize_perspective_settings(perspective) + object = await create_camera(connection, "PerspectiveCamera", settings, derived) + return PerspectiveCamera(object.id) + + +async def get_perspective_camera(connection: Connection, camera: PerspectiveCamera) -> PerspectiveSettings: + result = await get_specific_object(connection, "PerspectiveCamera", camera) + return deserialize_perspective_settings(result) + + +async def update_perspective_camera( + connection: Connection, camera: PerspectiveCamera, settings: PerspectiveSettings +) -> None: + params = serialize_perspective_settings(settings) + await update_specific_object(connection, "PerspectiveCamera", camera, params) + + +class OrthographicCamera(Camera): ... + + +@dataclass +class OrthographicSettings: + height: float = 1.0 + + +def serialize_orthographic_settings(value: OrthographicSettings) -> dict[str, Any]: + return {"height": value.height} + + +def deserialize_orthographic_settings(message: dict[str, Any]) -> OrthographicSettings: + return OrthographicSettings(height=get(message, "height", float)) + + +async def create_orthographic_camera( + connection: Connection, settings: CameraSettings, orthographic: OrthographicSettings +) -> OrthographicCamera: + derived = serialize_orthographic_settings(orthographic) + camera = await create_camera(connection, "OrthographicCamera", settings, derived) + return OrthographicCamera(camera.id) + + +async def get_orthographic_camera(connection: Connection, camera: OrthographicCamera) -> OrthographicSettings: + result = await get_specific_object(connection, "OrthographicCamera", camera) + return deserialize_orthographic_settings(result) + + +async def update_orthographic_camera( + connection: Connection, camera: OrthographicCamera, settings: OrthographicSettings +) -> None: + params = serialize_orthographic_settings(settings) + await update_specific_object(connection, "OrthographicCamera", camera, params) + + +class PanoramicCamera(Camera): ... + + +@dataclass +class PanoramicSettings: + stereo: Stereo | None = None + + +def serialize_panoramic_settings(settings: PanoramicSettings) -> dict[str, Any]: + stereo = None + + if settings.stereo is not None: + stereo = serialize_stereo(settings.stereo) + + return {"stereo": stereo} + + +def deserialize_panoramic_settings(message: dict[str, Any]) -> PanoramicSettings: + stereo = try_get(message, "stereo", dict[str, Any]) + + if stereo is not None: + stereo = deserialize_stereo(stereo) + + return PanoramicSettings(stereo) + + +async def create_panoramic_camera( + connection: Connection, settings: CameraSettings, panoramic: PanoramicSettings +) -> PanoramicCamera: + derived = serialize_panoramic_settings(panoramic) + camera = await create_camera(connection, "PanoramicCamera", settings, derived) + return PanoramicCamera(camera.id) + + +async def get_panoramic_camera(connection: Connection, camera: PanoramicCamera) -> PanoramicSettings: + result = await get_specific_object(connection, "PanoramicCamera", camera) + return deserialize_panoramic_settings(result) + + +async def update_panoramic_camera( + connection: Connection, camera: PanoramicCamera, settings: PanoramicSettings +) -> None: + params = serialize_panoramic_settings(settings) + await update_specific_object(connection, "PanoramicCamera", camera, params) diff --git a/python/brayns/api/core/framebuffer.py b/python/brayns/api/core/framebuffer.py new file mode 100644 index 000000000..dd7cd29d4 --- /dev/null +++ b/python/brayns/api/core/framebuffer.py @@ -0,0 +1,125 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from collections.abc import Iterable +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, NamedTuple + +from brayns.network.connection import Connection +from brayns.utils.parsing import get, get_tuple, try_get + +from .image_operation import ImageOperation +from .objects import Object, create_specific_object, get_specific_object, update_specific_object + + +class Framebuffer(Object): ... + + +class Size2(NamedTuple): + width: int + height: int + + +class FramebufferFormat(Enum): + RGBA8 = "Rgba8" + SRGBA8 = "Srgba8" + RGBA32F = "Rgba32F" + + +class FramebufferChannel(Enum): + COLOR = "Color" + DEPTH = "Depth" + NORMAL = "Normal" + ALBEDO = "Albedo" + PRIMITIVE_ID = "PrimitiveId" + MODEL_ID = "ModelId" + INSTANCE_ID = "InstanceId" + + +@dataclass +class Accumulation: + variance: bool = False + + +@dataclass +class FramebufferSettings: + resolution: Size2 = Size2(1920, 1080) + format: FramebufferFormat = FramebufferFormat.SRGBA8 + channels: set[FramebufferChannel] = field(default_factory=lambda: {FramebufferChannel.COLOR}) + accumulation: Accumulation | None = None + operations: list[ImageOperation] = field(default_factory=list) + + +def serialize_framebuffer_settings(settings: FramebufferSettings) -> dict[str, Any]: + accumulation = settings.accumulation + accumulation = None if accumulation is None else {"variance": accumulation.variance} + + return { + "resolution": list(settings.resolution), + "format": settings.format.value, + "channels": [channel.value for channel in settings.channels], + "accumulation": accumulation, + "operations": [operation.id for operation in settings.operations], + } + + +def deserialize_framebuffer_settings(message: dict[str, Any]) -> FramebufferSettings: + accumulation = try_get(message, "accumulation", dict[str, Any]) + accumulation = None if accumulation is None else Accumulation(get(accumulation, "variance", bool)) + + return FramebufferSettings( + resolution=Size2(*get_tuple(message, "resolution", int, 2)), + format=FramebufferFormat(get(message, "format", str)), + channels={FramebufferChannel(value) for value in get(message, "channels", list[str])}, + accumulation=accumulation, + operations=[ImageOperation(id) for id in get(message, "operations", list[int])], + ) + + +@dataclass +class FramebufferInfo: + settings: FramebufferSettings + variance: float | None = None + + +def deserialize_framebuffer_info(message: dict[str, Any]) -> FramebufferInfo: + return FramebufferInfo( + settings=deserialize_framebuffer_settings(get(message, "params", dict[str, Any])), + variance=try_get(message, "variance", float), + ) + + +async def create_framebuffer(connection: Connection, settings: FramebufferSettings) -> Framebuffer: + params = serialize_framebuffer_settings(settings) + object = await create_specific_object(connection, "Framebuffer", params) + return Framebuffer(object.id) + + +async def get_framebuffer(connection: Connection, framebuffer: Framebuffer) -> FramebufferInfo: + result = await get_specific_object(connection, "Framebuffer", framebuffer) + return deserialize_framebuffer_info(result) + + +async def update_framebuffer( + connection: Connection, framebuffer: Framebuffer, operations: Iterable[ImageOperation] +) -> None: + properties = {"operations": [operation.id for operation in operations]} + await update_specific_object(connection, "Framebuffer", framebuffer, properties) diff --git a/python/brayns/api/core/image.py b/python/brayns/api/core/image.py new file mode 100644 index 000000000..530b2ec4f --- /dev/null +++ b/python/brayns/api/core/image.py @@ -0,0 +1,61 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from enum import Enum + +from brayns.network.connection import Connection + +from .framebuffer import Framebuffer, FramebufferChannel + + +async def read_framebuffer(connection: Connection, framebuffer: Framebuffer, channel: FramebufferChannel) -> bytes: + params = {"id": framebuffer.id, "settings": {"channel": channel.value}} + response = await connection.request("readFramebuffer", params) + return response.binary + + +class JpegChannel(Enum): + COLOR = FramebufferChannel.COLOR.value + ALBEDO = FramebufferChannel.ALBEDO.value + + +async def read_framebuffer_as_jpeg( + connection: Connection, framebuffer: Framebuffer, channel: JpegChannel, quality: int = 100 +) -> bytes: + params = {"id": framebuffer.id, "settings": {"channel": channel.value, "settings": {"quality": quality}}} + response = await connection.request("readFramebufferAsJpeg", params) + return response.binary + + +PngChannel = JpegChannel + + +async def read_framebuffer_as_png(connection: Connection, framebuffer: Framebuffer, channel: PngChannel) -> bytes: + params = {"id": framebuffer.id, "settings": {"channel": channel.value}} + response = await connection.request("readFramebufferAsPng", params) + return response.binary + + +async def read_framebuffer_as_exr( + connection: Connection, framebuffer: Framebuffer, channels: set[FramebufferChannel] +) -> bytes: + params = {"id": framebuffer.id, "settings": {"channels": [channel.value for channel in channels]}} + response = await connection.request("readFramebufferAsExr", params) + return response.binary diff --git a/python/brayns/api/core/image_operation.py b/python/brayns/api/core/image_operation.py new file mode 100644 index 000000000..94b2347e4 --- /dev/null +++ b/python/brayns/api/core/image_operation.py @@ -0,0 +1,89 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from dataclasses import dataclass +from typing import Any + +from brayns.network.connection import Connection +from brayns.utils.parsing import get + +from .objects import Object, create_composed_object, get_specific_object, update_specific_object + + +class ImageOperation(Object): ... + + +async def create_image_operation(connection: Connection, typename: str, derived: dict[str, Any]) -> ImageOperation: + object = await create_composed_object(connection, typename, None, derived) + return ImageOperation(object.id) + + +class ToneMapper(ImageOperation): ... + + +@dataclass +class ToneMapperSettings: + exposure: float = 1.0 + contrast: float = 1.6773 + shoulder: float = 0.9714 + mid_in: float = 0.18 + mid_out: float = 0.18 + hdr_max: float = 11.0785 + aces_color: bool = True + + +def serialize_tone_mapper_settings(settings: ToneMapperSettings) -> dict[str, Any]: + return { + "exposure": settings.exposure, + "contrast": settings.contrast, + "shoulder": settings.shoulder, + "midIn": settings.mid_in, + "midOut": settings.mid_out, + "hdrMax": settings.hdr_max, + "acesColor": settings.aces_color, + } + + +def deserialize_tone_mapper_settings(message: dict[str, Any]) -> ToneMapperSettings: + return ToneMapperSettings( + exposure=get(message, "exposure", float), + contrast=get(message, "contrast", float), + shoulder=get(message, "shoulder", float), + mid_in=get(message, "midIn", float), + mid_out=get(message, "midOut", float), + hdr_max=get(message, "hdrMax", float), + aces_color=get(message, "acesColor", float), + ) + + +async def create_tone_mapper(connection: Connection, settings: ToneMapperSettings) -> ToneMapper: + derived = serialize_tone_mapper_settings(settings) + object = await create_image_operation(connection, "ToneMapper", derived) + return ToneMapper(object.id) + + +async def get_tone_mapper(connection: Connection, tone_mapper: ToneMapper) -> ToneMapperSettings: + result = await get_specific_object(connection, "ToneMapper", tone_mapper) + return deserialize_tone_mapper_settings(result) + + +async def update_tone_mapper(connection: Connection, tone_mapper: ToneMapper, settings: ToneMapperSettings) -> None: + params = serialize_tone_mapper_settings(settings) + await update_specific_object(connection, "ToneMapper", tone_mapper, params) diff --git a/python/brayns/api/core/objects.py b/python/brayns/api/core/objects.py index 7a8ad0b56..92dc51e49 100644 --- a/python/brayns/api/core/objects.py +++ b/python/brayns/api/core/objects.py @@ -18,55 +18,106 @@ # along with this library; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from collections.abc import Iterable from dataclasses import dataclass -from typing import Any +from typing import Any, NamedTuple from brayns.network.connection import Connection from brayns.utils.parsing import check_type, get, try_get +class Object(NamedTuple): + id: int + + @dataclass -class Object: +class ObjectInfo: id: int type: str user_data: Any -def parse_object(message: dict[str, Any]) -> Object: - return Object( +def deserialize_object_info(message: dict[str, Any]) -> ObjectInfo: + return ObjectInfo( id=get(message, "id", int), type=get(message, "type", str), - user_data=try_get(message, "user_data", Any, None), + user_data=try_get(message, "userData", Any, None), ) -async def get_all_objects(connection: Connection) -> list[Object]: - result = await connection.get_result("get-all-objects") +async def get_all_objects(connection: Connection) -> list[ObjectInfo]: + result = await connection.get_result("getAllObjects") check_type(result, dict[str, Any]) objects = get(result, "objects", list[dict[str, Any]]) - return [parse_object(item) for item in objects] + return [deserialize_object_info(item) for item in objects] -async def get_object(connection: Connection, id: int) -> Object: - result = await connection.get_result("get-object", {"id": id}) +async def get_object(connection: Connection, object: Object) -> ObjectInfo: + result = await connection.get_result("getObject", {"id": object.id}) check_type(result, dict[str, Any]) - return parse_object(result) + return deserialize_object_info(result) -async def update_object(connection: Connection, id: int, user_data: Any) -> None: - properties = {"user_data": user_data} - await connection.get_result("update-object", {"id": id, "properties": properties}) +async def update_object(connection: Connection, object: Object, user_data: Any) -> None: + settings = {"userData": user_data} + await connection.get_result("updateObject", {"id": object.id, "settings": settings}) -async def remove_objects(connection: Connection, ids: list[int]) -> None: - await connection.get_result("remove-objects", {"ids": ids}) +async def remove_objects(connection: Connection, objects: Iterable[Object]) -> None: + await connection.get_result("removeObjects", {"ids": [object.id for object in objects]}) async def clear_objects(connection: Connection) -> None: - await connection.get_result("clear-objects") + await connection.get_result("clearObjects") -async def create_empty_object(connection: Connection) -> int: - result = await connection.get_result("create-empty-object") +async def create_specific_object( + connection: Connection, typename: str, params: dict[str, Any] | None = None, binary: bytes = b"" +) -> Object: + method = "create" + typename + result = await connection.get_result(method, params, binary) check_type(result, dict[str, Any]) - return get(result, "id", int) + id = get(result, "id", int) + return Object(id) + + +async def create_composed_object( + connection: Connection, + typename: str, + base: dict[str, Any] | None = None, + derived: dict[str, Any] | None = None, + binary: bytes = b"", +) -> Object: + params = {} + + if base is not None: + params["base"] = base + + if derived is not None: + params["derived"] = derived + + return await create_specific_object(connection, typename, params, binary) + + +async def get_specific_object(connection: Connection, typename: str, object: Object) -> dict[str, Any]: + method = "get" + typename + params = {"id": object.id} + result = await connection.get_result(method, params) + check_type(result, dict[str, Any]) + return result + + +async def update_specific_object( + connection: Connection, typename: str, object: Object, settings: dict[str, Any] +) -> None: + method = "update" + typename + params = {"id": object.id, "settings": settings} + await connection.get_result(method, params) + + +class EmptyObject(Object): ... + + +async def create_empty_object(connection: Connection) -> EmptyObject: + object = await create_specific_object(connection, "EmptyObject", None) + return EmptyObject(object.id) diff --git a/python/brayns/api/core/service.py b/python/brayns/api/core/service.py index 0664664a8..551abcc80 100644 --- a/python/brayns/api/core/service.py +++ b/python/brayns/api/core/service.py @@ -36,19 +36,19 @@ class Version: async def get_version(connection: Connection) -> Version: - result = await connection.get_result("get-version") + result = await connection.get_result("getVersion") return Version( major=get(result, "major", int), minor=get(result, "minor", int), patch=get(result, "patch", int), - pre_release=get(result, "pre_release", int), + pre_release=get(result, "preRelease", int), tag=get(result, "tag", str), ) async def get_methods(connection: Connection) -> list[str]: - result = await connection.get_result("get-methods") + result = await connection.get_result("getMethods") return get(result, "methods", list[str]) @@ -63,7 +63,7 @@ class Endpoint: async def get_endpoint(connection: Connection, method: str) -> Endpoint: - result = await connection.get_result("get-schema", {"method": method}) + result = await connection.get_result("getSchema", {"method": method}) return Endpoint( method=get(result, "method", str), @@ -74,6 +74,15 @@ async def get_endpoint(connection: Connection, method: str) -> Endpoint: ) +class Task: + def __init__(self, id: int) -> None: + self._id = id + + @property + def id(self) -> int: + return self._id + + @dataclass class TaskOperation: description: str @@ -94,15 +103,12 @@ def done(self) -> bool: return index == self.operation_count - 1 and completion == 1.0 -T = TypeVar("T") - - def deserialize_task(message: dict[str, Any]) -> TaskInfo: - operation = get(message, "current_operation", dict[str, Any]) + operation = get(message, "currentOperation", dict[str, Any]) return TaskInfo( id=get(message, "id", int), - operation_count=get(message, "operation_count", int), + operation_count=get(message, "operationCount", int), current_operation=TaskOperation( description=get(operation, "description", str), index=get(operation, "index", int), @@ -112,51 +118,54 @@ def deserialize_task(message: dict[str, Any]) -> TaskInfo: async def get_tasks(connection: Connection) -> list[TaskInfo]: - result = await connection.get_result("get-tasks") + result = await connection.get_result("getTasks") + tasks = get(result, "tasks", list[dict[str, Any]]) + return [deserialize_task(task) for task in tasks] - tasks: list[dict[str, Any]] = get(result, "tasks", list[dict[str, Any]]) - return [deserialize_task(task) for task in tasks] +async def get_task(connection: Connection, task: Task) -> TaskInfo: + result = await connection.get_result("getTask", {"taskId": task.id}) + return deserialize_task(result) -async def get_task(connection: Connection, task_id: int) -> TaskInfo: - result = await connection.get_result("get-task", {"task_id": task_id}) +async def cancel_task(connection: Connection, task: Task) -> None: + await connection.get_result("cancelTask", {"taskId": task.id}) - return deserialize_task(result) +async def cancel_all_tasks(connection: Connection) -> None: + await connection.get_result("cancelAllTasks") -async def cancel_task(connection: Connection, task_id: int) -> None: - await connection.get_result("cancel-task", {"task_id": task_id}) +async def get_task_result(connection: Connection, task: Task) -> Response: + return await connection.request("getTaskResult", {"taskId": task.id}) -async def get_task_result(connection: Connection, task_id: int) -> Response: - return await connection.request("get-task-result", {"task_id": task_id}) +async def stop_service(connection: Connection) -> None: + await connection.get_result("stop") -class Task(Generic[T]): - def __init__(self, connection: Connection, id: int, parser: Callable[[Response], T]) -> None: - self._connection = connection - self._id = id + +T = TypeVar("T") + + +class TaskWrapper(Generic[T]): + def __init__(self, task: Task, parser: Callable[[Response], T]) -> None: + self._task = task self._parser = parser @property def id(self) -> int: - return self._id + return self._task.id - async def get_status(self) -> TaskInfo: - return await get_task(self._connection, self._id) + async def get_status(self, connection: Connection) -> TaskInfo: + return await get_task(connection, self._task) - async def is_done(self) -> bool: - status = await self.get_status() + async def is_done(self, connection: Connection) -> bool: + status = await self.get_status(connection) return status.done - async def cancel(self) -> None: - await cancel_task(self._connection, self._id) + async def cancel(self, connection: Connection) -> None: + await cancel_task(connection, self._task) - async def wait(self) -> T: - result = await get_task_result(self._connection, self._id) + async def wait(self, connection: Connection) -> T: + result = await get_task_result(connection, self._task) return self._parser(result) - - -async def stop_service(connection: Connection) -> None: - await connection.get_result("stop") diff --git a/python/brayns/api/core/transfer_function.py b/python/brayns/api/core/transfer_function.py new file mode 100644 index 000000000..363ce36a4 --- /dev/null +++ b/python/brayns/api/core/transfer_function.py @@ -0,0 +1,82 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from dataclasses import dataclass, field +from typing import Any + +from brayns.network.connection import Connection +from brayns.utils.box import Box1 +from brayns.utils.color import Color4 +from brayns.utils.parsing import deserialize_box1, get, serialize_box1 + +from .objects import Object, create_composed_object, get_specific_object, update_specific_object + + +class TransferFunction(Object): ... + + +async def create_transfer_function(connection: Connection, typename: str, derived: dict[str, Any]) -> TransferFunction: + object = await create_composed_object(connection, typename, None, derived) + return TransferFunction(object.id) + + +class LinearTransferFunction(TransferFunction): ... + + +@dataclass +class LinearTransferFunctionSettings: + scalar_range: Box1 = Box1(0, 1) + colors: list[Color4] = field(default_factory=lambda: [Color4(0, 0, 0), Color4(1, 1, 1)]) + + +def serialize_linear_transfer_function_settings(settings: LinearTransferFunctionSettings) -> dict[str, Any]: + return { + "scalarRange": serialize_box1(settings.scalar_range), + "colors": [list(color) for color in settings.colors], + } + + +def deserialize_linear_transfer_function_settings(message: dict[str, Any]) -> LinearTransferFunctionSettings: + return LinearTransferFunctionSettings( + scalar_range=deserialize_box1(get(message, "scalarRange", dict[str, Any])), + colors=[Color4(*color) for color in get(message, "colors", list[list[float]])], + ) + + +async def create_linear_transfer_function( + connection: Connection, settings: LinearTransferFunctionSettings +) -> LinearTransferFunction: + derived = serialize_linear_transfer_function_settings(settings) + object = await create_transfer_function(connection, "LinearTransferFunction", derived) + return LinearTransferFunction(object.id) + + +async def get_linear_transfer_function( + connection: Connection, transfer_function: LinearTransferFunction +) -> LinearTransferFunctionSettings: + result = await get_specific_object(connection, "LinearTransferFunction", transfer_function) + return deserialize_linear_transfer_function_settings(result) + + +async def update_linear_transfer_function( + connection: Connection, transfer_function: LinearTransferFunction, settings: LinearTransferFunctionSettings +) -> None: + params = serialize_linear_transfer_function_settings(settings) + await update_specific_object(connection, "LinearTransferFunction", transfer_function, params) diff --git a/python/brayns/api/core/volume.py b/python/brayns/api/core/volume.py new file mode 100644 index 000000000..75d815b2a --- /dev/null +++ b/python/brayns/api/core/volume.py @@ -0,0 +1,127 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, NamedTuple + +from brayns.network.connection import Connection +from brayns.utils.parsing import deserialize_vector, get, get_tuple, try_get +from brayns.utils.vector import Vector3 + +from .objects import Object, create_composed_object, get_specific_object, update_specific_object + + +class Volume(Object): ... + + +class VoxelType(Enum): + U8 = "U8" + U16 = "U16" + F32 = "F32" + F64 = "F64" + + +class VolumeFilter(Enum): + NEAREST = "Nearest" + LINEAR = "Linear" + CUBIC = "Cubic" + + +class VolumeType(Enum): + CELL_CENTERED = "CellCentered" + VERTEX_CENTERED = "VertexCentered" + + +@dataclass +class RegularVolumeUpdate: + origin: Vector3 = Vector3() + spacing: Vector3 = Vector3.full(1.0) + type: VolumeType = VolumeType.VERTEX_CENTERED + filter: VolumeFilter = VolumeFilter.LINEAR + background: float | None = None + + +def serialize_volume_update(settings: RegularVolumeUpdate) -> dict[str, Any]: + return { + "origin": list(settings.origin), + "spacing": list(settings.spacing), + "type": settings.type.value, + "filter": settings.filter.value, + "background": settings.background, + } + + +def deserialize_volume_update(message: dict[str, Any]) -> RegularVolumeUpdate: + return RegularVolumeUpdate( + origin=deserialize_vector(message, "origin", Vector3), + spacing=deserialize_vector(message, "spacing", Vector3), + type=VolumeType(get(message, "type", str)), + filter=VolumeFilter(get(message, "filter", str)), + background=try_get(message, "background", float | None, None), + ) + + +class Size3(NamedTuple): + x: int + y: int + z: int + + +@dataclass +class RegularVolumeSettings: + voxel_type: VoxelType + voxel_count: Size3 + update: RegularVolumeUpdate = field(default_factory=RegularVolumeUpdate) + + +def serialize_volume_settings(settings: RegularVolumeSettings) -> dict[str, Any]: + return { + "voxelType": settings.voxel_type.value, + "voxelCount": list(settings.voxel_count), + "settings": serialize_volume_update(settings.update), + } + + +def deserialize_volume_settings(message: dict[str, Any]) -> RegularVolumeSettings: + return RegularVolumeSettings( + voxel_type=VoxelType(get(message, "voxelType", str)), + voxel_count=Size3(*get_tuple(message, "voxelCount", int, 3)), + update=deserialize_volume_update(get(message, "settings", dict[str, Any])), + ) + + +class RegularVolume(Volume): ... + + +async def create_regular_volume(connection: Connection, settings: RegularVolumeSettings, data: bytes) -> RegularVolume: + params = serialize_volume_settings(settings) + object = await create_composed_object(connection, "RegularVolume", None, params, data) + return RegularVolume(object.id) + + +async def get_regular_volume(connection: Connection, volume: RegularVolume) -> RegularVolumeSettings: + result = await get_specific_object(connection, "RegularVolume", volume) + return deserialize_volume_settings(result) + + +async def update_regular_volume(connection: Connection, volume: RegularVolume, settings: RegularVolumeUpdate) -> None: + params = serialize_volume_update(settings) + await update_specific_object(connection, "RegularVolume", volume, params) diff --git a/python/brayns/network/connection.py b/python/brayns/network/connection.py index f78573b7e..0bd81cd85 100644 --- a/python/brayns/network/connection.py +++ b/python/brayns/network/connection.py @@ -115,7 +115,7 @@ async def poll(self) -> None: if self._request_id is None: raise ValueError("Cannot poll requests without ID") - if self.done: + if self._buffer.is_done(self._request_id): return data = await self._websocket.receive() @@ -155,6 +155,9 @@ async def __aenter__(self) -> Self: async def __aexit__(self, *args) -> None: await self._websocket.close() + async def close(self) -> None: + await self._websocket.close() + async def send_json_rpc(self, request: JsonRpcRequest) -> FutureResponse: if request.id is not None and self._buffer.is_running(request.id): raise ValueError(f"A request with ID {request.id} is already running") @@ -181,12 +184,10 @@ async def send(self, request: Request) -> FutureResponse: async def task(self, method: str, params: Any = None, binary: bytes = b"") -> FutureResponse: request = Request(method, params, binary) - return await self.send(request) async def request(self, method: str, params: Any = None, binary: bytes = b"") -> Response: future = await self.task(method, params, binary) - return await future.wait() async def get_result(self, method: str, params: Any = None, binary: bytes = b"") -> Any: diff --git a/python/brayns/network/json_rpc.py b/python/brayns/network/json_rpc.py index 449602985..e784ddaf5 100644 --- a/python/brayns/network/json_rpc.py +++ b/python/brayns/network/json_rpc.py @@ -48,6 +48,9 @@ class JsonRpcError(Exception): message: str data: Any = None + def __str__(self) -> str: + return f"{self.message} (code = {self.code})" + @dataclass class JsonRpcErrorResponse: diff --git a/python/brayns/utils/box.py b/python/brayns/utils/box.py new file mode 100644 index 000000000..70e75f8da --- /dev/null +++ b/python/brayns/utils/box.py @@ -0,0 +1,210 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from collections.abc import Sequence +from typing import Generic, NamedTuple, Self, TypeVar + +from .vector import Vector, Vector2, Vector3 + +T = TypeVar("T", bound=Vector) + + +class Box1(NamedTuple): + min: float + max: float + + def __contains__(self, key: float) -> bool: + return key >= self.min and key <= self.max + + @property + def center(self) -> float: + return (self.min + self.max) / 2 + + @property + def size(self) -> float: + return self.max - self.min + + def translate(self, translation: float) -> Self: + return type(self)( + min=self.min + translation, + max=self.max + translation, + ) + + def scale(self, value: float) -> Self: + return type(self)( + min=value * self.min, + max=value * self.max, + ) + + +class Box(NamedTuple, Generic[T]): + min: T + max: T + + @classmethod + def vector_type(cls) -> type[T]: + raise NotImplementedError() + + @classmethod + def full(cls, value: float) -> Self: + t = cls.vector_type() + upper = t.full(value) + lower = -upper + + return cls(lower, upper) + + @classmethod + def zeros(cls) -> Self: + return cls.full(0) + + @classmethod + def ones(cls) -> Self: + return cls.full(1) + + @classmethod + def at(cls, center: T, size: T) -> Self: + half_size = size / 2 + + return cls( + min=center - half_size, + max=center + half_size, + ) + + @classmethod + def of(cls, size: T) -> Self: + t = cls.vector_type() + center = t.zeros() + + return cls.at(center, size) + + @classmethod + def around(cls, points: Sequence[T]) -> Self: + t = cls.vector_type() + + return cls( + min=t.componentwise_min(points), + max=t.componentwise_max(points), + ) + + @classmethod + def merge(cls, boxes: Sequence[Self]) -> Self: + t = cls.vector_type() + + return cls( + min=t.componentwise_min([box.min for box in boxes]), + max=t.componentwise_max([box.max for box in boxes]), + ) + + def __contains__(self, point: T) -> bool: + if not all(i <= limit for i, limit in zip(point, self.max)): + return False + + if not all(i >= limit for i, limit in zip(point, self.min)): + return False + + return True + + def __bool__(self) -> bool: + return all(i < j for i, j in zip(self.min, self.max)) + + @property + def center(self) -> T: + return (self.min + self.max) / 2 + + @property + def size(self) -> T: + return self.max - self.min + + def translate(self, translation: T) -> Self: + return type(self)( + min=self.min + translation, + max=self.max + translation, + ) + + def scale(self, value: T) -> Self: + return type(self)( + min=value * self.min, + max=value * self.max, + ) + + +class Box2(Box[Vector2]): + @classmethod + def vector_type(cls) -> type[Vector2]: + return Vector2 + + @property + def width(self) -> float: + return self.size.x + + @property + def height(self) -> float: + return self.size.y + + @property + def corners(self) -> list[Vector2]: + return [ + self.min, + Vector2(self.min.x, self.max.y), + Vector2(self.max.x, self.min.y), + self.max, + ] + + +class Box3(Box[Vector3]): + @classmethod + def vector_type(cls) -> type[Vector3]: + return Vector3 + + @classmethod + def extrude(cls, xy: Box2, zmin: float = 0.0, zmax: float = 0.0) -> Self: + return cls( + min=Vector3(*xy.min, zmin), + max=Vector3(*xy.max, zmax), + ) + + @property + def width(self) -> float: + return self.size.x + + @property + def height(self) -> float: + return self.size.y + + @property + def depth(self) -> float: + return self.size.z + + @property + def xy(self) -> Box2: + return Box2(self.min.xy, self.max.xy) + + @property + def corners(self) -> list[Vector3]: + return [ + self.min, + Vector3(self.min.x, self.min.y, self.max.z), + Vector3(self.min.x, self.max.y, self.min.z), + Vector3(self.min.x, self.max.y, self.max.z), + Vector3(self.max.x, self.min.y, self.min.z), + Vector3(self.max.x, self.min.y, self.max.z), + Vector3(self.max.x, self.max.y, self.min.z), + self.max, + ] diff --git a/python/brayns/utils/color.py b/python/brayns/utils/color.py new file mode 100644 index 000000000..5d16c0e8e --- /dev/null +++ b/python/brayns/utils/color.py @@ -0,0 +1,67 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from typing import Self, TypeVarTuple + +from .vector import Vector + +Ts = TypeVarTuple("Ts") + + +class Color3(Vector[float, float, float]): + def __new__(cls, r: float = 0.0, g: float = 0.0, b: float = 0.0) -> Self: + return super().__new__(cls, r, g, b) + + @property + def r(self) -> float: + return self[0] + + @property + def g(self) -> float: + return self[1] + + @property + def b(self) -> float: + return self[2] + + +class Color4(Vector[float, float, float, float]): + def __new__(cls, r: float = 0.0, g: float = 0.0, b: float = 0.0, a: float = 1.0) -> Self: + return super().__new__(cls, r, g, b, a) + + @property + def r(self) -> float: + return self[0] + + @property + def g(self) -> float: + return self[1] + + @property + def b(self) -> float: + return self[2] + + @property + def a(self) -> float: + return self[3] + + @property + def rgb(self) -> Color3: + return Color3(self.r, self.g, self.b) diff --git a/python/brayns/utils/logger.py b/python/brayns/utils/logger.py index c65c14964..a0cf2a1f0 100644 --- a/python/brayns/utils/logger.py +++ b/python/brayns/utils/logger.py @@ -18,8 +18,8 @@ # along with this library; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -from logging import INFO, Formatter, Logger, StreamHandler import sys +from logging import INFO, Formatter, Logger, StreamHandler def create_logger(level: int | str = INFO) -> Logger: diff --git a/python/brayns/utils/parsing.py b/python/brayns/utils/parsing.py index 23ea7816c..edd658bb5 100644 --- a/python/brayns/utils/parsing.py +++ b/python/brayns/utils/parsing.py @@ -19,7 +19,11 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from types import UnionType -from typing import Any, cast, get_args, get_origin +from typing import Any, TypeVar, cast, get_args, get_origin + +from .box import Box, Box1, Box2, Box3 +from .quaternion import Quaternion +from .vector import Vector2, Vector3, Vector4 def has_type(value: Any, t: Any) -> bool: @@ -65,6 +69,9 @@ def check_type(value: Any, t: Any) -> None: def try_get(message: dict[str, Any], key: str, t: Any, default: Any = None) -> Any: value = message.get(key, default) + if value is default: + return value + check_type(value, t) return value @@ -72,9 +79,57 @@ def try_get(message: dict[str, Any], key: str, t: Any, default: Any = None) -> A def get(message: dict[str, Any], key: str, t: Any) -> Any: if key not in message: - raise KeyError(f"Missing mandatory key in JSON-RPC message {key}") + raise KeyError(f"Missing mandatory key in JSON-RPC message: '{key}'") value = message[key] check_type(value, t) return value + + +def get_tuple(message: dict[str, Any], key: str, t: Any, item_count: int) -> Any: + value = get(message, key, list[t]) + + if len(value) != item_count: + raise ValueError(f"Expected {item_count} items for '{key}'") + + return value + + +T = TypeVar("T", Vector2, Vector3, Vector4, Quaternion) + + +def deserialize_vector(message: dict[str, Any], key: str, t: type[T]) -> T: + value = get_tuple(message, key, float, t.component_count()) + return t(*value) + + +def serialize_box(value: Box2 | Box3) -> dict[str, Any]: + return { + "min": value.min, + "max": value.max, + } + + +U = TypeVar("U", bound=Box) + + +def deserialize_box(message: dict[str, Any], t: type[U]) -> U: + return t( + min=deserialize_vector(message, "min", t.vector_type()), + max=deserialize_vector(message, "max", t.vector_type()), + ) + + +def serialize_box1(value: Box1) -> dict[str, Any]: + return { + "min": value.min, + "max": value.max, + } + + +def deserialize_box1(message: dict[str, Any]) -> Box1: + return Box1( + min=get(message, "min", float), + max=get(message, "max", float), + ) diff --git a/python/brayns/utils/quaternion.py b/python/brayns/utils/quaternion.py new file mode 100644 index 000000000..36501e282 --- /dev/null +++ b/python/brayns/utils/quaternion.py @@ -0,0 +1,102 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import math +from typing import Self + +from .vector import Vector, Vector3 + + +class Quaternion(Vector[float, float, float, float]): + @classmethod + def component_count(cls) -> int: + return 4 + + def __new__(cls, x: float = 0.0, y: float = 0.0, z: float = 0.0, w: float = 1.0) -> Self: + return super().__new__(cls, x, y, z, w) + + def __mul__(self, value: int | float | Self) -> Self: + if isinstance(value, int | float): + return self.unpack(i * value for i in self) + + x0, y0, z0, w0 = self + x1, y1, z1, w1 = value + + return type(self)( + w0 * x1 + x0 * w1 + y0 * z1 - z0 * y1, + w0 * y1 - x0 * z1 + y0 * w1 + z0 * x1, + w0 * z1 + x0 * y1 - y0 * x1 + z0 * w1, + w0 * w1 - x0 * x1 - y0 * y1 - z0 * z1, + ) + + def __rmul__(self, value: int | float | Self) -> Self: + if isinstance(value, int | float): + return self * value + + return value * self + + def __truediv__(self, value: int | float | Self) -> Self: + if isinstance(value, int | float): + return self.unpack(i / value for i in self) + + return self * value.inverse + + def __rtruediv__(self, value: int | float | Self) -> Self: + if isinstance(value, int | float): + return self.unpack(value / i for i in self) + + return value * self.inverse + + @property + def x(self) -> float: + return self[0] + + @property + def y(self) -> float: + return self[1] + + @property + def z(self) -> float: + return self[2] + + @property + def w(self) -> float: + return self[3] + + @property + def xyz(self) -> Vector3: + return Vector3(self.x, self.y, self.z) + + @property + def axis(self) -> Vector3: + return self.xyz.normalized + + @property + def angle(self) -> float: + q = self.normalized + return 2 * math.acos(q.w) + + @property + def conjugate(self) -> Self: + return type(self)(-self.x, -self.y, -self.z, self.w) + + @property + def inverse(self) -> Self: + return self.conjugate / self.square_norm diff --git a/python/brayns/utils/rotation.py b/python/brayns/utils/rotation.py new file mode 100644 index 000000000..c1cbafefc --- /dev/null +++ b/python/brayns/utils/rotation.py @@ -0,0 +1,168 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import math +from dataclasses import dataclass +from typing import Self + +from .quaternion import Quaternion +from .vector import Vector3 + + +@dataclass(frozen=True) +class Rotation: + quaternion: Quaternion = Quaternion() + + @property + def euler(self) -> Vector3: + return _quaternion_to_euler(self.quaternion) + + @property + def axis(self) -> Vector3: + return self.quaternion.axis + + @property + def angle(self) -> float: + return self.quaternion.angle + + @property + def inverse(self) -> Self: + return type(self)(self.quaternion.conjugate) + + def then(self, other: Self) -> Self: + return type(self)(other.quaternion * self.quaternion) + + def apply(self, value: Vector3, center: Vector3 = Vector3()) -> Vector3: + value -= center + + quaternion = self.quaternion + vector = Quaternion(*value, 0) + + vector = quaternion * vector * quaternion.conjugate + + return center + vector.xyz + + +def euler(x: float, y: float, z: float) -> Rotation: + angles = Vector3(x, y, z) + quaternion = _euler_to_quaternion(angles) + + return Rotation(quaternion) + + +def axis_angle(x: float, y: float, z: float, angle: float) -> Rotation: + axis = Vector3(x, y, z) + quaternion = _axis_angle_to_quaternion(axis, angle) + + return Rotation(quaternion) + + +def get_rotation_between(source: Vector3, destination: Vector3) -> Rotation: + quaternion = _get_quaternion_between(source, destination) + + return Rotation(quaternion) + + +def _axis_angle_to_quaternion(axis: Vector3, angle: float) -> Quaternion: + half_angle = angle / 2 + + xyz = axis.normalized * math.sin(half_angle) + w = math.cos(half_angle) + + return Quaternion(*xyz, w) + + +def _euler_to_quaternion(euler: Vector3) -> Quaternion: + half_angles = euler / 2 + + cx, cy, cz = Vector3.unpack(math.cos(i) for i in half_angles) + sx, sy, sz = Vector3.unpack(math.sin(i) for i in half_angles) + + return Quaternion( + sx * cy * cz - cx * sy * sz, + cx * sy * cz + sx * cy * sz, + cx * cy * sz - sx * sy * cz, + cx * cy * cz + sx * sy * sz, + ) + + +def _quaternion_to_euler(q: Quaternion) -> Vector3: + return Vector3(_get_x(q), _get_y(q), _get_z(q)) + + +def _get_x(q: Quaternion) -> float: + sx_cy = 2 * (q.w * q.x + q.y * q.z) + cx_cy = 1 - 2 * (q.x * q.x + q.y * q.y) + + return math.atan2(sx_cy, cx_cy) + + +def _get_y(q: Quaternion) -> float: + sy = 2 * (q.w * q.y - q.z * q.x) + + if abs(sy) >= 1: + return math.copysign(math.pi / 2, sy) + + return math.asin(sy) + + +def _get_z(q: Quaternion) -> float: + sz_cy = 2 * (q.w * q.z + q.x * q.y) + cz_cy = 1 - 2 * (q.y * q.y + q.z * q.z) + + return math.atan2(sz_cy, cz_cy) + + +def _get_quaternion_between(u: Vector3, v: Vector3) -> Quaternion: + angle = _get_angle_between(u, v) + + if angle == 0: + return Quaternion() + + if angle == math.pi: + axis = _get_orthogonal(u).normalized + return Quaternion(axis.x, axis.y, axis.z, 0) + + axis = u.cross(v) + + return _axis_angle_to_quaternion(axis, angle) + + +def _get_angle_between(u: Vector3, v: Vector3) -> float: + u = u.normalized + v = v.normalized + + return math.acos(u.dot(v)) + + +def _get_orthogonal(v: Vector3) -> Vector3: + if v.x != 0: + x = (v.y + v.z) / v.x + return Vector3(x, -1, -1) + + if v.y != 0: + y = (v.x + v.z) / v.y + return Vector3(-1, y, -1) + + if v.z != 0: + z = (v.x + v.y) / v.z + return Vector3(-1, -1, z) + + raise ValueError(v) diff --git a/python/brayns/utils/vector.py b/python/brayns/utils/vector.py new file mode 100644 index 000000000..6ca92c845 --- /dev/null +++ b/python/brayns/utils/vector.py @@ -0,0 +1,223 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import math +from collections.abc import Sequence +from typing import Callable, Generic, Iterable, Self, TypeVarTuple + +Ts = TypeVarTuple("Ts") + + +class Vector(tuple[*Ts], Generic[*Ts]): + @classmethod + def component_count(cls) -> int: + raise NotImplementedError() + + @classmethod + def unpack(cls, components: Iterable[float]) -> Self: + return cls(*components) # type: ignore + + @classmethod + def full(cls, value: float) -> Self: + return cls.unpack(value for _ in range(cls.component_count())) + + @classmethod + def zeros(cls) -> Self: + return cls.full(0) + + @classmethod + def ones(cls) -> Self: + return cls.full(1) + + @classmethod + def componentwise(cls, values: Sequence[Self], operation: Callable[[Iterable[float]], float]) -> Self: + return cls.unpack(operation(value[i] for value in values) for i in range(cls.component_count())) # type: ignore + + @classmethod + def componentwise_min(cls, values: Sequence[Self]) -> Self: + return cls.componentwise(values, min) + + @classmethod + def componentwise_max(cls, values: Sequence[Self]) -> Self: + return cls.componentwise(values, max) + + def __new__(cls, *args: *Ts) -> Self: + return super().__new__(cls, args) + + def __neg__(self) -> Self: + return self.unpack(-i for i in self) # type: ignore + + def __pos__(self) -> Self: + return self.unpack(+i for i in self) # type: ignore + + def __abs__(self) -> Self: + return self.unpack(abs(i) for i in self) # type: ignore + + def __add__(self, value: int | float | Self) -> Self: # type: ignore + return self._apply(value, lambda x, y: x + y) + + def __radd__(self, value: int | float | Self) -> Self: # type: ignore + return self._apply(value, lambda x, y: y + x) + + def __sub__(self, value: int | float | Self) -> Self: + return self._apply(value, lambda x, y: x - y) + + def __rsub__(self, value: int | float | Self) -> Self: + return self._apply(value, lambda x, y: y - x) + + def __mul__(self, value: int | float | Self) -> Self: # type: ignore + return self._apply(value, lambda x, y: x * y) + + def __rmul__(self, value: int | float | Self) -> Self: # type: ignore + return self._apply(value, lambda x, y: y * x) + + def __truediv__(self, value: int | float | Self) -> Self: + return self._apply(value, lambda x, y: x / y) + + def __rtruediv__(self, value: int | float | Self) -> Self: + return self._apply(value, lambda x, y: y / x) + + def __floordiv__(self, value: int | float | Self) -> Self: + return self._apply(value, lambda x, y: x // y) + + def __rfloordiv__(self, value: int | float | Self) -> Self: + return self._apply(value, lambda x, y: y // x) + + def __mod__(self, value: int | float | Self) -> Self: + return self._apply(value, lambda x, y: x % y) + + def __rmod__(self, value: int | float | Self) -> Self: + return self._apply(value, lambda x, y: y % x) + + def __pow__(self, value: int | float | Self) -> Self: + return self._apply(value, lambda x, y: x**y) + + def __rpow__(self, value: int | float | Self) -> Self: + return self._apply(value, lambda x, y: y**x) + + @property + def square_norm(self) -> float: + return sum(i * i for i in self) # type: ignore + + @property + def norm(self) -> float: + return math.sqrt(self.square_norm) + + @property + def normalized(self) -> Self: + return self / self.norm + + def dot(self, other: Self) -> float: + return sum(i * j for i, j in zip(self, other)) # type: ignore + + def reduce(self, operation: Callable[[float, float], float]) -> float: + value = self[0] # type: ignore + + for i in range(1, self.component_count()): + value = operation(value, self[i]) # type: ignore + + return value # type: ignore + + def reduce_multiply(self) -> float: + return self.reduce(lambda x, y: x * y) + + def _apply(self, value, operation) -> Self: + if isinstance(value, (int, float)): + return self.unpack(operation(i, value) for i in self) + + return self.unpack(operation(i, j) for i, j in zip(self, value)) # type: ignore + + +class Vector2(Vector[float, float]): + @classmethod + def component_count(cls) -> int: + return 2 + + def __new__(cls, x: float = 0.0, y: float = 0.0) -> Self: + return super().__new__(cls, x, y) + + @property + def x(self) -> float: + return self[0] + + @property + def y(self) -> float: + return self[1] + + +class Vector3(Vector[float, float, float]): + @classmethod + def component_count(cls) -> int: + return 3 + + def __new__(cls, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> Self: + return super().__new__(cls, x, y, z) + + @property + def x(self) -> float: + return self[0] + + @property + def y(self) -> float: + return self[1] + + @property + def z(self) -> float: + return self[2] + + @property + def xy(self) -> Vector2: + return Vector2(self.x, self.y) + + def cross(self, other: Self) -> Self: + return type(self)( + self.y * other.z - self.z * other.y, + self.z * other.x - self.x * other.z, + self.x * other.y - self.y * other.x, + ) + + +class Vector4(Vector[float, float, float, float]): + @classmethod + def component_count(cls) -> int: + return 4 + + def __new__(cls, x: float = 0.0, y: float = 0.0, z: float = 0.0, w: float = 0.0) -> Self: + return super().__new__(cls, x, y, z, w) + + @property + def x(self) -> float: + return self[0] + + @property + def y(self) -> float: + return self[1] + + @property + def z(self) -> float: + return self[2] + + @property + def w(self) -> float: + return self[3] + + @property + def xyz(self) -> Vector3: + return Vector3(self.x, self.y, self.z) diff --git a/python/brayns/utils/view.py b/python/brayns/utils/view.py new file mode 100644 index 000000000..51804a918 --- /dev/null +++ b/python/brayns/utils/view.py @@ -0,0 +1,76 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from dataclasses import dataclass +from typing import Self + +from .rotation import Rotation, get_rotation_between +from .vector import Vector3 + +X = Vector3(1, 0, 0) +Y = Vector3(0, 1, 0) +Z = Vector3(0, 0, 1) + + +@dataclass +class View: + position: Vector3 = Vector3() + direction: Vector3 = -Z + up: Vector3 = Y + + @property + def right(self) -> Vector3: + return self.direction.cross(self.up).normalized + + @property + def left(self) -> Vector3: + return -self.right + + @property + def real_up(self) -> Vector3: + return self.right.cross(self.forward) + + @property + def down(self) -> Vector3: + return -self.real_up + + @property + def forward(self) -> Vector3: + return self.direction.normalized + + @property + def back(self) -> Vector3: + return -self.forward + + def rotate(self, rotation: Rotation, center: Vector3 = Vector3()) -> Self: + return type(self)( + position=rotation.apply(self.position, center), + direction=rotation.apply(self.direction), + up=rotation.apply(self.up), + ) + + def get_rotation_to(self, destination: Self) -> Rotation: + first = get_rotation_between(self.direction, destination.direction) + + up = first.apply(self.real_up) + + second = get_rotation_between(up, destination.real_up) + + return first.then(second) diff --git a/python/pyproject.toml b/python/pyproject.toml index eb4413062..ceb7cb4c7 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -31,6 +31,7 @@ version = { attr = "brayns.version.VERSION" } [tool.ruff] lint.ignore = ["E501"] line-length = 119 +lint.extend-select = ["I"] [tool.mypy] ignore_missing_imports = true diff --git a/python/tests/integration/conftest.py b/python/tests/integration/conftest.py index 1b78bdcae..12e4010bb 100644 --- a/python/tests/integration/conftest.py +++ b/python/tests/integration/conftest.py @@ -18,15 +18,15 @@ # along with this library; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -from logging import DEBUG import os from collections.abc import AsyncIterator +from logging import DEBUG from pathlib import Path -from ssl import SSLContext, create_default_context, Purpose +from ssl import Purpose, SSLContext, create_default_context import pytest_asyncio -from brayns import Connection, connect, create_logger, clear_objects +from brayns import Connection, clear_objects, connect, create_logger HOST = os.getenv("BRAYNS_HOST", "localhost") PORT = int(os.getenv("BRAYNS_PORT", "5000")) diff --git a/python/tests/integration/core/test_camera.py b/python/tests/integration/core/test_camera.py new file mode 100644 index 000000000..d8c2463c6 --- /dev/null +++ b/python/tests/integration/core/test_camera.py @@ -0,0 +1,158 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +import pytest + +from brayns import ( + CameraSettings, + Connection, + JsonRpcError, + OrthographicCamera, + OrthographicSettings, + PanoramicSettings, + PerspectiveSettings, + Stereo, + StereoMode, + clear_objects, + create_orthographic_camera, + create_panoramic_camera, + create_perspective_camera, + get_camera, + get_object, + get_orthographic_camera, + get_panoramic_camera, + get_perspective_camera, + remove_objects, + update_camera, + update_object, + update_orthographic_camera, + update_panoramic_camera, + update_perspective_camera, +) + + +def check_perspective(left: PerspectiveSettings, right: PerspectiveSettings) -> None: + assert left.architectural == right.architectural + assert left.depth_of_field == right.depth_of_field + assert left.stereo == right.stereo + assert left.fovy == pytest.approx(right.fovy) + + +@pytest.mark.integration_test +@pytest.mark.asyncio +async def test_perspective_camera(connection: Connection) -> None: + settings = CameraSettings() + perspective = PerspectiveSettings(fovy=1.0) + camera = await create_perspective_camera(connection, settings, perspective) + + assert settings == await get_camera(connection, camera) + + check_perspective(perspective, await get_perspective_camera(connection, camera)) + + settings.near_clip = 3 + await update_camera(connection, camera, settings) + + perspective.stereo = Stereo(mode=StereoMode.SIDE_BY_SIDE) + await update_perspective_camera(connection, camera, perspective) + + assert settings == await get_camera(connection, camera) + + check_perspective(perspective, await get_perspective_camera(connection, camera)) + + +@pytest.mark.integration_test +@pytest.mark.asyncio +async def test_orthographic_camera(connection: Connection) -> None: + settings = CameraSettings() + orthographic = OrthographicSettings(height=2.0) + camera = await create_orthographic_camera(connection, settings, orthographic) + + assert settings == await get_camera(connection, camera) + assert orthographic == await get_orthographic_camera(connection, camera) + + settings.near_clip = 3 + await update_camera(connection, camera, settings) + + orthographic.height = 3.0 + await update_orthographic_camera(connection, camera, orthographic) + + assert settings == await get_camera(connection, camera) + assert orthographic == await get_orthographic_camera(connection, camera) + + +@pytest.mark.integration_test +@pytest.mark.asyncio +async def test_panoramic_camera(connection: Connection) -> None: + settings = CameraSettings() + panoramic = PanoramicSettings() + camera = await create_panoramic_camera(connection, settings, panoramic) + + assert settings == await get_camera(connection, camera) + assert panoramic == await get_panoramic_camera(connection, camera) + + settings.near_clip = 3 + await update_camera(connection, camera, settings) + + panoramic.stereo = Stereo(mode=StereoMode.SIDE_BY_SIDE) + await update_panoramic_camera(connection, camera, panoramic) + + assert settings == await get_camera(connection, camera) + assert panoramic == await get_panoramic_camera(connection, camera) + + +@pytest.mark.integration_test +@pytest.mark.asyncio +async def test_object_management(connection: Connection) -> None: + settings = CameraSettings() + perspective = PerspectiveSettings() + orthographic = OrthographicSettings() + + camera1 = await create_perspective_camera(connection, settings, perspective) + camera2 = await create_orthographic_camera(connection, settings, orthographic) + + with pytest.raises(JsonRpcError): + await get_orthographic_camera(connection, OrthographicCamera(camera1.id)) + + await update_object(connection, camera1, "test") + + object1 = await get_object(connection, camera1) + + assert object1.id == camera1.id + assert object1.type == "PerspectiveCamera" + assert object1.user_data == "test" + + object2 = await get_object(connection, camera2) + + assert object2.id == camera2.id + assert object2.type == "OrthographicCamera" + assert object2.user_data is None + + await remove_objects(connection, [camera1]) + + with pytest.raises(JsonRpcError): + await get_object(connection, camera1) + + await get_object(connection, camera2) + + await clear_objects(connection) + + with pytest.raises(JsonRpcError): + await get_object(connection, camera2) diff --git a/python/tests/integration/core/test_framebuffer.py b/python/tests/integration/core/test_framebuffer.py new file mode 100644 index 000000000..c50597e4f --- /dev/null +++ b/python/tests/integration/core/test_framebuffer.py @@ -0,0 +1,81 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import pytest + +from brayns import ( + Accumulation, + Connection, + FramebufferChannel, + FramebufferFormat, + FramebufferSettings, + Size2, + ToneMapperSettings, + create_framebuffer, + create_tone_mapper, + get_framebuffer, + remove_objects, + update_framebuffer, +) + + +@pytest.mark.integration_test +@pytest.mark.asyncio +async def test_framebuffer(connection: Connection) -> None: + settings = FramebufferSettings( + resolution=Size2(1920, 1080), + format=FramebufferFormat.RGBA32F, + channels=set(FramebufferChannel), + accumulation=Accumulation(variance=True), + ) + + framebuffer = await create_framebuffer(connection, settings) + + info = await get_framebuffer(connection, framebuffer) + assert info.settings == settings + assert info.variance is None + + tone_mapper_settings = ToneMapperSettings() + tone_mapper = await create_tone_mapper(connection, tone_mapper_settings) + + settings.operations = [tone_mapper] + await update_framebuffer(connection, framebuffer, settings.operations) + + info = await get_framebuffer(connection, framebuffer) + assert info.settings == settings + assert info.variance is None + + +@pytest.mark.integration_test +@pytest.mark.asyncio +async def test_remove_operations(connection: Connection) -> None: + tone_mapper_settings = ToneMapperSettings() + tone_mapper = await create_tone_mapper(connection, tone_mapper_settings) + + settings = FramebufferSettings(operations=[tone_mapper]) + framebuffer = await create_framebuffer(connection, settings) + + before_remove = await get_framebuffer(connection, framebuffer) + assert before_remove.settings.operations == [tone_mapper] + + await remove_objects(connection, [tone_mapper]) + + after_remove = await get_framebuffer(connection, framebuffer) + assert after_remove.settings.operations[0].id == 0 diff --git a/python/tests/integration/core/test_image.py b/python/tests/integration/core/test_image.py new file mode 100644 index 000000000..c5c5b27aa --- /dev/null +++ b/python/tests/integration/core/test_image.py @@ -0,0 +1,98 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import pytest + +from brayns import ( + Connection, + FramebufferChannel, + FramebufferFormat, + FramebufferSettings, + JpegChannel, + JsonRpcError, + PngChannel, + create_framebuffer, + read_framebuffer, + read_framebuffer_as_exr, + read_framebuffer_as_jpeg, + read_framebuffer_as_png, +) + + +@pytest.mark.integration_test +@pytest.mark.asyncio +async def test_read_invalid_channel(connection: Connection) -> None: + settings = FramebufferSettings() + framebuffer = await create_framebuffer(connection, settings) + + channel = FramebufferChannel.PRIMITIVE_ID + + with pytest.raises(JsonRpcError): + await read_framebuffer(connection, framebuffer, channel) + + +@pytest.mark.integration_test +@pytest.mark.asyncio +async def test_read_framebuffer(connection: Connection) -> None: + settings = FramebufferSettings(channels={FramebufferChannel.PRIMITIVE_ID}) + framebuffer = await create_framebuffer(connection, settings) + + channel = FramebufferChannel.PRIMITIVE_ID + data = await read_framebuffer(connection, framebuffer, channel) + + primitive_id = int.from_bytes(data[:4], "little", signed=False) + + assert primitive_id == 0 + + +@pytest.mark.integration_test +@pytest.mark.asyncio +async def test_read_framebuffer_as_jpeg(connection: Connection) -> None: + settings = FramebufferSettings() + framebuffer = await create_framebuffer(connection, settings) + + channel = JpegChannel.COLOR + data = await read_framebuffer_as_jpeg(connection, framebuffer, channel) + + assert data + + +@pytest.mark.integration_test +@pytest.mark.asyncio +async def test_read_framebuffer_as_png(connection: Connection) -> None: + settings = FramebufferSettings() + framebuffer = await create_framebuffer(connection, settings) + + channel = PngChannel.COLOR + data = await read_framebuffer_as_png(connection, framebuffer, channel) + + assert data + + +@pytest.mark.integration_test +@pytest.mark.asyncio +async def test_read_framebuffer_as_exr(connection: Connection) -> None: + channels = set(FramebufferChannel) + settings = FramebufferSettings(format=FramebufferFormat.RGBA8, channels=channels) + framebuffer = await create_framebuffer(connection, settings) + + data = await read_framebuffer_as_exr(connection, framebuffer, channels) + + assert data diff --git a/python/tests/integration/core/test_image_operation.py b/python/tests/integration/core/test_image_operation.py new file mode 100644 index 000000000..ede5ebfd3 --- /dev/null +++ b/python/tests/integration/core/test_image_operation.py @@ -0,0 +1,43 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import pytest + +from brayns import ( + Connection, + ToneMapperSettings, + create_tone_mapper, + get_tone_mapper, + update_tone_mapper, +) + + +@pytest.mark.integration_test +@pytest.mark.asyncio +async def test_tone_mapper(connection: Connection) -> None: + settings = ToneMapperSettings() + tone_mapper = await create_tone_mapper(connection, settings) + + assert settings == await get_tone_mapper(connection, tone_mapper) + + settings.aces_color = False + await update_tone_mapper(connection, tone_mapper, settings) + + assert settings == await get_tone_mapper(connection, tone_mapper) diff --git a/python/tests/integration/core/test_objects.py b/python/tests/integration/core/test_objects.py index ac451bb6b..05314a14e 100644 --- a/python/tests/integration/core/test_objects.py +++ b/python/tests/integration/core/test_objects.py @@ -23,6 +23,7 @@ from brayns import ( Connection, JsonRpcError, + Object, clear_objects, create_empty_object, get_all_objects, @@ -35,7 +36,8 @@ @pytest.mark.integration_test @pytest.mark.asyncio async def test_create_empty_object(connection: Connection) -> None: - assert await create_empty_object(connection) == 1 + object = await create_empty_object(connection) + assert object.id == 1 @pytest.mark.integration_test @@ -44,7 +46,7 @@ async def test_get_all_objects(connection: Connection) -> None: created = [await create_empty_object(connection) for _ in range(10)] retreived = await get_all_objects(connection) - assert created == [object.id for object in retreived] + assert all(x.id == y.id for x, y in zip(created, retreived)) @pytest.mark.integration_test @@ -52,52 +54,52 @@ async def test_get_all_objects(connection: Connection) -> None: async def test_get_object(connection: Connection) -> None: created = [await create_empty_object(connection) for _ in range(10)] retreived = await get_all_objects(connection) - tests = [await get_object(connection, id) for id in created] + tests = [await get_object(connection, object) for object in created] - assert tests == retreived + assert all(x.id == y.id for x, y in zip(tests, retreived)) with pytest.raises(JsonRpcError): - await get_object(connection, 123) + await get_object(connection, Object(123)) @pytest.mark.integration_test @pytest.mark.asyncio async def test_update_object(connection: Connection) -> None: - ids = [await create_empty_object(connection) for _ in range(10)] + objects = [await create_empty_object(connection) for _ in range(10)] - await update_object(connection, ids[0], "test") + await update_object(connection, objects[0], "test") - test = await get_object(connection, ids[0]) + test = await get_object(connection, objects[0]) assert test.id == 1 - assert test.type == "empty-object" + assert test.type == "EmptyObject" assert test.user_data == "test" @pytest.mark.integration_test @pytest.mark.asyncio async def test_remove_objects(connection: Connection) -> None: - ids = [await create_empty_object(connection) for _ in range(10)] + objects = [await create_empty_object(connection) for _ in range(10)] - await remove_objects(connection, [1, 2, 3]) + await remove_objects(connection, objects[:3]) tests = await get_all_objects(connection) - assert [obj.id for obj in tests] == list(range(4, 11)) + assert [object.id for object in tests] == list(range(4, 11)) - for id in ids[:3]: + for object in objects[:3]: with pytest.raises(JsonRpcError): - await get_object(connection, id) + await get_object(connection, object) @pytest.mark.integration_test @pytest.mark.asyncio async def test_clear_objects(connection: Connection) -> None: - ids = [await create_empty_object(connection) for _ in range(10)] + objects = [await create_empty_object(connection) for _ in range(10)] await clear_objects(connection) assert not await get_all_objects(connection) - for id in ids: + for object in objects: with pytest.raises(JsonRpcError): - await get_object(connection, id) + await get_object(connection, object) diff --git a/python/tests/integration/core/test_regular_volume.py b/python/tests/integration/core/test_regular_volume.py new file mode 100644 index 000000000..2baea3f8b --- /dev/null +++ b/python/tests/integration/core/test_regular_volume.py @@ -0,0 +1,52 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import pytest + +from brayns import ( + Connection, + RegularVolumeSettings, + Size3, + VolumeFilter, + VoxelType, + create_regular_volume, + get_regular_volume, + update_regular_volume, +) + + +@pytest.mark.integration_test +@pytest.mark.asyncio +async def test_regular_volume(connection: Connection) -> None: + data = 8 * b"\1" + + settings = RegularVolumeSettings( + voxel_type=VoxelType.U8, + voxel_count=Size3(2, 2, 2), + ) + + function = await create_regular_volume(connection, settings, data) + + assert settings == await get_regular_volume(connection, function) + + settings.update.filter = VolumeFilter.CUBIC + await update_regular_volume(connection, function, settings.update) + + assert settings == await get_regular_volume(connection, function) diff --git a/python/tests/integration/core/test_service.py b/python/tests/integration/core/test_service.py index 70c89fe45..c8c85b93f 100644 --- a/python/tests/integration/core/test_service.py +++ b/python/tests/integration/core/test_service.py @@ -24,6 +24,7 @@ VERSION, Connection, JsonRpcError, + Task, cancel_task, get_endpoint, get_methods, @@ -51,8 +52,8 @@ async def test_methods(connection: Connection) -> None: @pytest.mark.integration_test @pytest.mark.asyncio async def test_schema(connection: Connection) -> None: - endpoint = await get_endpoint(connection, "get-version") - assert endpoint.method == "get-version" + endpoint = await get_endpoint(connection, "getVersion") + assert endpoint.method == "getVersion" assert isinstance(endpoint.description, str) assert isinstance(endpoint.params_schema, dict) assert isinstance(endpoint.result_schema, dict) @@ -65,10 +66,10 @@ async def test_tasks(connection: Connection) -> None: assert not tasks with pytest.raises(JsonRpcError): - await get_task(connection, 0) + await get_task(connection, Task(0)) with pytest.raises(JsonRpcError): - await cancel_task(connection, 0) + await cancel_task(connection, Task(0)) with pytest.raises(JsonRpcError): - await get_task_result(connection, 0) + await get_task_result(connection, Task(0)) diff --git a/python/tests/integration/core/test_transfer_function.py b/python/tests/integration/core/test_transfer_function.py new file mode 100644 index 000000000..2a936016a --- /dev/null +++ b/python/tests/integration/core/test_transfer_function.py @@ -0,0 +1,44 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import pytest + +from brayns import ( + Box1, + Connection, + LinearTransferFunctionSettings, + create_linear_transfer_function, + get_linear_transfer_function, + update_linear_transfer_function, +) + + +@pytest.mark.integration_test +@pytest.mark.asyncio +async def test_linear_transfer_function(connection: Connection) -> None: + settings = LinearTransferFunctionSettings() + function = await create_linear_transfer_function(connection, settings) + + assert settings == await get_linear_transfer_function(connection, function) + + settings.scalar_range = Box1(-1, 2) + await update_linear_transfer_function(connection, function, settings) + + assert settings == await get_linear_transfer_function(connection, function) diff --git a/python/tests/unit/network/mock_websocket.py b/python/tests/unit/network/mock_websocket.py index 34cabf852..1cc373c67 100644 --- a/python/tests/unit/network/mock_websocket.py +++ b/python/tests/unit/network/mock_websocket.py @@ -19,6 +19,8 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from dataclasses import dataclass, field + +from brayns.network.connection import Connection from brayns.network.json_rpc import ( JsonRpcErrorResponse, JsonRpcResponse, @@ -27,7 +29,6 @@ compose_text, ) from brayns.network.websocket import WebSocket -from brayns.network.connection import Connection @dataclass diff --git a/python/tests/unit/network/test_connection.py b/python/tests/unit/network/test_connection.py index defb454c4..e56390434 100644 --- a/python/tests/unit/network/test_connection.py +++ b/python/tests/unit/network/test_connection.py @@ -28,6 +28,7 @@ JsonRpcSuccessResponse, compose_request, ) + from .mock_websocket import mock_connection diff --git a/python/tests/unit/utils/__init__.py b/python/tests/unit/utils/__init__.py new file mode 100644 index 000000000..66765f01e --- /dev/null +++ b/python/tests/unit/utils/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. diff --git a/python/tests/unit/utils/test_box.py b/python/tests/unit/utils/test_box.py new file mode 100644 index 000000000..237f4e4e0 --- /dev/null +++ b/python/tests/unit/utils/test_box.py @@ -0,0 +1,160 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from brayns import Box1, Box2, Box3, Vector2, Vector3 + + +def test_box1() -> None: + test = Box1(0, 1) + assert 0 in test + assert 1 in test + assert 0.5 in test + assert -1 not in test + assert 2 not in test + + assert test.center == 0.5 + assert test.size == 1 + + assert test.translate(1) == Box1(1, 2) + assert test.scale(2) == Box1(0, 2) + + +def test_vector_type() -> None: + assert Box2.vector_type() is Vector2 + assert Box3.vector_type() is Vector3 + + +def test_full() -> None: + assert Box2.full(0) == Box2(Vector2.zeros(), Vector2.zeros()) + assert Box2.full(1) == Box2(-Vector2.ones(), Vector2.ones()) + + assert Box3.full(0) == Box3(Vector3.zeros(), Vector3.zeros()) + assert Box3.full(1) == Box3(-Vector3.ones(), Vector3.ones()) + + +def test_at() -> None: + center = Vector3(1, 1, 1) + size = Vector3.full(2) + extent = Vector3.ones() + assert Box3.at(center, size) == Box3(center - extent, center + extent) + + +def test_of() -> None: + size = Vector3.full(2) + extent = Vector3.ones() + assert Box3.of(size) == Box3(-extent, extent) + + +def test_merge() -> None: + boxes = [ + Box3(Vector3(-1, 2, 3), Vector3(6, 5, 4)), + Box3(Vector3(1, -2, 3), Vector3(5, 4, 6)), + Box3(Vector3(1, 2, -3), Vector3(4, 6, 5)), + ] + + lower = Vector3(-1, -2, -3) + upper = Vector3(6, 6, 6) + + assert Box3.merge(boxes) == Box3(lower, upper) + + +def test_iter() -> None: + test = Box2.ones() + lower, upper = test + + assert lower == Vector2(-1, -1) + assert upper == Vector2(1, 1) + + +def test_contains() -> None: + test2 = Box2(-Vector2(1, 2), Vector2(1, 2)) + + assert Vector2(1, 0) in test2 + assert Vector2(1, 1) in test2 + assert Vector2(2, 2) not in test2 + assert Vector2(-5, -5) not in test2 + + test3 = Box3(-Vector3(1, 2, 3), Vector3(1, 2, 3)) + + assert Vector3(1, 0, 2) in test3 + assert Vector3(1, 1, 1) in test3 + assert Vector3(2, 2, 3) not in test3 + assert Vector3(-5, -5, -5) not in test3 + + +def test_bool() -> None: + assert Box3.of(Vector3.full(2)) + assert not Box3.zeros() + assert not Box3(Vector3.ones(), Vector3.zeros()) + + +def test_corners_around() -> None: + test = Box2( + min=Vector2(1, 2), + max=Vector2(4, 5), + ) + assert Box2.around(test.corners) == test + + test = Box3( + min=Vector3(1, 2, 3), + max=Vector3(4, 5, 6), + ) + assert Box3.around(test.corners) == test + + +def test_translate() -> None: + test = Box3( + min=Vector3(1, 2, 3), + max=Vector3(4, 5, 6), + ) + + translation = Vector3(1, 2, 3) + + assert test.translate(translation) == Box3( + test.min + translation, + test.max + translation, + ) + + +def test_scale() -> None: + test = Box3( + min=Vector3(1, 2, 3), + max=Vector3(4, 5, 6), + ) + + scale = Vector3(1, 2, 3) + + assert test.scale(scale) == Box3( + test.min * scale, + test.max * scale, + ) + + +def test_center_and_size() -> None: + test = Box3( + min=Vector3(2, 4, 1), + max=Vector3(4, 5, 6), + ) + + assert test.center == Vector3(3, 4.5, 3.5) + assert test.size == Vector3(2, 1, 5) + assert test.width == test.size.x + assert test.height == test.size.y + assert test.depth == test.size.z diff --git a/python/tests/unit/utils/test_quaternion.py b/python/tests/unit/utils/test_quaternion.py new file mode 100644 index 000000000..35d94a65d --- /dev/null +++ b/python/tests/unit/utils/test_quaternion.py @@ -0,0 +1,73 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import math + +import pytest + +from brayns import Quaternion, Vector3 + + +def test_component_count() -> None: + assert Quaternion.component_count() == 4 + + +def test_identity() -> None: + assert Quaternion() == Quaternion(0, 0, 0, 1) + + +def test_properties() -> None: + test = Quaternion(1, 2, 3, 4) + + assert test.x == 1 + assert test.y == 2 + assert test.z == 3 + assert test.w == 4 + + +def test_mul() -> None: + assert 2 * Quaternion(1, 2, 3, 4) == Quaternion(2, 4, 6, 8) + assert Quaternion(1, 2, 3, 4) * 2 == Quaternion(2, 4, 6, 8) + + assert Quaternion(1, 2, 3, 4) * Quaternion(5, 6, 7, 8) == Quaternion(24, 48, 48, -6) + + +def test_div() -> None: + assert Quaternion(1, 2, 3, 4) / 2 == Quaternion(0.5, 1, 1.5, 2) + assert 2 / Quaternion(1, 2, 4, 5) == Quaternion(2, 1, 0.5, 0.4) + + test = Quaternion(1, 2, 3, 4) + assert test * test.inverse == Quaternion() + assert test / test == Quaternion() + + +def test_angle_axis() -> None: + test = Quaternion(1, 2, 3, 4) + + assert test.xyz == Vector3(1, 2, 3) + assert test.axis == test.xyz.normalized + + flip = Quaternion(1, 0, 0, 0) + + assert flip.angle == pytest.approx(math.radians(180)) + + +def test_conjugate() -> None: + assert Quaternion(1, 2, 3, 4).conjugate == Quaternion(-1, -2, -3, 4) diff --git a/python/tests/unit/utils/test_rotation.py b/python/tests/unit/utils/test_rotation.py new file mode 100644 index 000000000..27d43de21 --- /dev/null +++ b/python/tests/unit/utils/test_rotation.py @@ -0,0 +1,139 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import math + +import pytest + +from brayns import Quaternion, Rotation, Vector3, axis_angle, euler, get_rotation_between + + +def test_euler() -> None: + angles = Vector3(math.radians(34), math.radians(-22), math.radians(-80)) + + test = euler(*angles) + quaternion = test.quaternion + + assert quaternion.norm == pytest.approx(1) + assert quaternion.x == pytest.approx(0.10256431) + assert quaternion.y == pytest.approx(-0.32426137) + assert quaternion.z == pytest.approx(-0.56067163) + assert quaternion.w == pytest.approx(0.75497182) + + back_to_euler = test.euler + + assert back_to_euler.x == pytest.approx(angles.x) + assert back_to_euler.y == pytest.approx(angles.y) + assert back_to_euler.z == pytest.approx(angles.z) + + +def test_axis_angle() -> None: + axis = Vector3(1, 2, 3) + angle = math.radians(30) + + test = axis_angle(*axis, angle) + quaternion = test.quaternion + + assert quaternion.norm == pytest.approx(1) + + assert quaternion.x == pytest.approx(0.0691723) + assert quaternion.y == pytest.approx(0.1383446) + assert quaternion.z == pytest.approx(0.2075169) + assert quaternion.w == pytest.approx(0.96592583) + + assert test.axis.x == pytest.approx(axis.normalized.x) + assert test.axis.y == pytest.approx(axis.normalized.y) + assert test.axis.z == pytest.approx(axis.normalized.z) + + assert test.angle == pytest.approx(angle) + + +def test_rotation_between() -> None: + u = Vector3(1, 0, 0) + v = Vector3(1, 1, 0) + + rotation = get_rotation_between(u, v) + test = rotation.euler + + assert test.x == pytest.approx(0) + assert test.y == pytest.approx(0) + assert test.z == pytest.approx(math.radians(45)) + + u = Vector3(1, 0, 0) + v = Vector3(1, 0, 0) + + identity = get_rotation_between(u, v) + + assert identity == Rotation() + + u = Vector3(1, 0, 0) + v = Vector3(-1, 0, 0) + + opposite = get_rotation_between(u, v) + back = opposite.apply(u) + + assert back.x == pytest.approx(v.x) + assert back.y == pytest.approx(v.y) + assert back.z == pytest.approx(v.z) + + +def test_inverse() -> None: + quaternion = Quaternion(1, 2, 3, 4).normalized + rotation = Rotation(quaternion) + + identity = rotation.then(rotation.inverse).quaternion + + assert identity.x == pytest.approx(0) + assert identity.y == pytest.approx(0) + assert identity.z == pytest.approx(0) + assert identity.w == pytest.approx(1) + + +def test_then() -> None: + axis = Vector3(0, 1, 0) + angle = math.radians(30) + + r1 = axis_angle(*axis, angle) + r2 = axis_angle(*axis, angle) + + combined = r1.then(r2) + + assert combined.angle == pytest.approx(2 * angle) + assert combined.axis.x == pytest.approx(axis.x) + assert combined.axis.y == pytest.approx(axis.y) + assert combined.axis.z == pytest.approx(axis.z) + + +def test_apply() -> None: + rotation = euler(math.radians(22), math.radians(35), math.radians(68)) + value = Vector3(1, 2, 3) + + test = rotation.apply(value) + + assert test.x == pytest.approx(0.3881471) + assert test.y == pytest.approx(2.91087149) + assert test.z == pytest.approx(2.31865673) + + center = Vector3(4, 5, 6) + test = rotation.apply(value, center) + + assert test.x == pytest.approx(3.77731325) + assert test.y == pytest.approx(0.02357039) + assert test.z == pytest.approx(4.52163639) diff --git a/python/tests/unit/utils/test_vector.py b/python/tests/unit/utils/test_vector.py new file mode 100644 index 000000000..ebd255992 --- /dev/null +++ b/python/tests/unit/utils/test_vector.py @@ -0,0 +1,187 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import math + +import pytest + +from brayns import Vector2, Vector3, Vector4 + + +def test_component_count() -> None: + assert Vector2.component_count() == 2 + assert Vector3.component_count() == 3 + assert Vector4.component_count() == 4 + + +def test_full() -> None: + assert Vector2.zeros() == Vector2(0, 0) + assert Vector3.zeros() == Vector3(0, 0, 0) + assert Vector4.zeros() == Vector4(0, 0, 0, 0) + + assert Vector2.ones() == Vector2(1, 1) + assert Vector3.ones() == Vector3(1, 1, 1) + assert Vector4.ones() == Vector4(1, 1, 1, 1) + + value = 4.5 + assert Vector2.full(value) == Vector2(value, value) + assert Vector3.full(value) == Vector3(value, value, value) + assert Vector4.full(value) == Vector4(value, value, value, value) + + +def test_componentwise() -> None: + values = [Vector3(1, -2, 3), Vector3(-1, 2, -3)] + + assert Vector3.componentwise_min(values) == Vector3(-1, -2, -3) + assert Vector3.componentwise_max(values) == Vector3(1, 2, 3) + + +def test_conversion() -> None: + vector2 = Vector2(1, 2) + vector3 = Vector3(*vector2, 3) + vector4 = Vector4(*vector3, 4) + + assert vector3 == Vector3(1, 2, 3) + assert vector4 == Vector4(1, 2, 3, 4) + assert vector3.xy == vector2 + assert vector4.xyz == vector3 + + assert vector2.x == 1 + assert vector3.x == 1 + assert vector4.x == 1 + + assert vector2.y == 2 + assert vector3.y == 2 + assert vector4.y == 2 + + assert vector3.z == 3 + assert vector4.z == 3 + + assert vector4.w == 4 + + +def test_iter() -> None: + values = [1, 2, 3] + test = Vector3.unpack(values) + + assert test == Vector3(1, 2, 3) + assert list(test) == values + + x, y, z = test + assert x == 1 + assert y == 2 + assert z == 3 + + assert len(test) == 3 + assert test.component_count() == 3 + + +def test_getitem() -> None: + test = Vector3(1, 2, 3) + + assert test[0] == 1 + assert test[1] == 2 + assert test[2] == 3 + + +def test_compare() -> None: + test = Vector3(1, 2, 3) + + assert test == Vector3(1, 2, 3) + assert test != Vector3(0, 2, 3) + assert test < Vector3(1, 3, 3) + assert test < Vector3(3, 1, 2) + assert test <= Vector3(3, 1, 2) + assert test > Vector3(0, 1, 2) + assert test >= Vector3(0, 1, 2) + + +def test_pos_neg() -> None: + test = Vector3(1, 2, 3) + + assert +test == Vector3(1, 2, 3) + assert -test == Vector3(-1, -2, -3) + + +def test_abs() -> None: + assert abs(Vector3(1, -2, 0)) == Vector3(1, 2, 0) + + +def test_add() -> None: + assert Vector3(1, 2, 3) + Vector3(2, 3, 4) == Vector3(3, 5, 7) + assert Vector3(1, 2, 3) + 1 == Vector3(2, 3, 4) + assert 1 + Vector3(1, 2, 3) == Vector3(2, 3, 4) + + +def test_sub() -> None: + assert Vector3(2, 3, 4) - Vector3(1, 3, 2) == Vector3(1, 0, 2) + assert Vector3(1, 2, 3) - 1 == Vector3(0, 1, 2) + assert 1 - Vector3(1, 2, 3) == Vector3(0, -1, -2) + + +def test_mul() -> None: + assert Vector3(1, 2, 3) * Vector3(2, 3, 4) == Vector3(2, 6, 12) + assert Vector3(1, 2, 3) * 2 == Vector3(2, 4, 6) + assert 2 * Vector3(1, 2, 3) == Vector3(2, 4, 6) + + +def test_div() -> None: + assert Vector3(1, 2, 3) / Vector3(2, 5, 4) == Vector3(0.5, 0.4, 0.75) + assert Vector3(1, 2, 3) / 2 == Vector3(0.5, 1, 1.5) + assert 2 / Vector3(1, 2, 4) == Vector3(2, 1, 0.5) + + assert Vector3(1, 10, 3) // Vector3(2, 5, 3) == Vector3(0, 2, 1) + assert Vector3(1, 2, 3) // 3 == Vector3(0, 0, 1) + assert 4 // Vector3(1, 3, 5) == Vector3(4, 1, 0) + + +def test_mod() -> None: + assert Vector3(1, 5, 11) % Vector3(1, 2, 3) == Vector3(0, 1, 2) + assert Vector3(1, 2, 3) % 3 == Vector3(1, 2, 0) + assert 3 % Vector3(1, 2, 3) == Vector3(0, 1, 0) + + +def test_pow() -> None: + assert Vector3(1, 2, 3) ** Vector3(1, 2, 3) == Vector3(1, 4, 27) + assert Vector3(1, 2, 3) ** 3 == Vector3(1, 8, 27) + assert 3 ** Vector3(1, 2, 3) == Vector3(3, 9, 27) + + +def test_norm() -> None: + test = Vector3(1, 2, 3) + + assert test.square_norm == 14 + assert test.norm == math.sqrt(14) + assert test.normalized.norm == pytest.approx(1) + + +def test_dot() -> None: + assert Vector3(1, 2, 3).dot(Vector3(4, 5, 6)) == 32 + + +def test_cross() -> None: + assert Vector3(1, 2, 3).cross(Vector3(4, 5, 6)) == Vector3(-3, 6, -3) + + +def test_reduce() -> None: + assert Vector3(1, 2, 3).reduce_multiply() == 6 + assert Vector3(1, -2, 3).reduce_multiply() == -6 + assert Vector3(0, -2, 3).reduce_multiply() == 0 + assert Vector3(0, -2, 3).reduce_multiply() == 0 diff --git a/python/tests/unit/utils/test_view.py b/python/tests/unit/utils/test_view.py new file mode 100644 index 000000000..ec2d4dc22 --- /dev/null +++ b/python/tests/unit/utils/test_view.py @@ -0,0 +1,85 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import math + +import pytest + +from brayns import Vector3, View, X, Y, Z, axis_angle + + +def test_directions() -> None: + front = View() + + assert front.position == Vector3() + assert front.direction == -Z + assert front.up == Y + + assert front.right == X + assert front.left == -X + assert front.real_up == Y + assert front.down == -Y + assert front.forward == -Z + assert front.back == Z + + +def test_rotate() -> None: + front = View() + + axis = -X + angle = math.radians(90) + rotation = axis_angle(*axis, angle) + + top = front.rotate(rotation) + + assert top.position.x == pytest.approx(0) + assert top.position.y == pytest.approx(0) + assert top.position.z == pytest.approx(0) + + assert top.direction.x == pytest.approx(0) + assert top.direction.y == pytest.approx(-1) + assert top.direction.z == pytest.approx(0) + + assert top.up.x == pytest.approx(0) + assert top.up.y == pytest.approx(0) + assert top.up.z == pytest.approx(-1) + + +def test_rotation_to() -> None: + source = View() + destination = View(direction=-Y, up=-Z) + + rotation = source.get_rotation_to(destination) + + test = source.rotate(rotation) + + assert test.position.x == pytest.approx(destination.position.x) + assert test.position.y == pytest.approx(destination.position.y) + assert test.position.z == pytest.approx(destination.position.z) + + assert test.direction.x == pytest.approx(destination.direction.x) + assert test.direction.y == pytest.approx(destination.direction.y) + assert test.direction.z == pytest.approx(destination.direction.z) + + assert test.up.x == pytest.approx(destination.up.x) + assert test.up.y == pytest.approx(destination.up.y) + assert test.up.z == pytest.approx(destination.up.z) + assert test.up.z == pytest.approx(destination.up.z) + assert test.up.z == pytest.approx(destination.up.z) diff --git a/src/brayns/core/Launcher.cpp b/src/brayns/core/Launcher.cpp index cb52274d8..e700e2261 100644 --- a/src/brayns/core/Launcher.cpp +++ b/src/brayns/core/Launcher.cpp @@ -23,13 +23,17 @@ #include -#include +#include #include #include + #include + #include + #include #include + #include namespace @@ -60,6 +64,14 @@ WebSocketServerSettings extractServerSettings(const ServiceSettings &settings) }; } +DeviceSettings extractDeviceSettings(const ServiceSettings &settings) +{ + return { + .threadCount = settings.deviceThreadCount, + .affinity = settings.deviceAffinity, + }; +} + void startServerAndRunService(const ServiceSettings &settings, Logger &logger) { auto level = getEnumValue(settings.logLevel); @@ -67,6 +79,11 @@ void startServerAndRunService(const ServiceSettings &settings, Logger &logger) logger.info("{}", getCopyright()); + auto deviceSettings = extractDeviceSettings(settings); + + auto device = createDevice(logger, deviceSettings); + logger.info("OSPRay device version: {}", device.getVersion()); + logger.debug("Service options:{}", stringifyArgvSettings(settings)); auto token = StopToken(); @@ -78,14 +95,12 @@ void startServerAndRunService(const ServiceSettings &settings, Logger &logger) addServiceEndpoints(builder, api, token); - auto objects = ObjectManager(); - auto locked = LockedObjects(std::move(objects), logger); + auto objects = ObjectRegistry(); + auto locked = ObjectManager(std::move(objects), logger); addObjectEndpoints(builder, locked); - auto device = createDevice(logger); - - addCameraEndpoints(builder, locked, device); + addDeviceEndpoints(builder, locked, device); api = builder.build(); diff --git a/src/brayns/core/Launcher.h b/src/brayns/core/Launcher.h index 169494b56..aa8aa4c1c 100644 --- a/src/brayns/core/Launcher.h +++ b/src/brayns/core/Launcher.h @@ -41,6 +41,8 @@ struct ServiceSettings std::string certificateFile; std::string caLocation; std::string privateKeyPassphrase; + std::size_t deviceThreadCount; + bool deviceAffinity; }; template<> @@ -72,7 +74,7 @@ struct ArgvSettingsReflector .defaultValue(5000); builder.option("max-thread-count", [](auto &settings) { return &settings.maxThreadCount; }) .description("Maximum number of threads for the websocket server") - .defaultValue(2); + .defaultValue(10); builder.option("max-queue-size", [](auto &settings) { return &settings.maxQueueSize; }) .description("Maximum number of queued connections before they are rejected") .defaultValue(64); @@ -97,6 +99,13 @@ struct ArgvSettingsReflector .description("Passphrase for the private key if encrypted") .defaultValue(""); + builder.option("device-thread-count", [](auto &settings) { return &settings.deviceThreadCount; }) + .description("Number of thread the device is allowed to use to render, use 0 to use all hardware threads") + .defaultValue(0); + builder.option("device-affinity", [](auto &settings) { return &settings.deviceAffinity; }) + .description("Bind software threads to hardware threads if true") + .defaultValue(false); + return builder.build(); } }; diff --git a/src/brayns/core/api/Api.cpp b/src/brayns/core/api/Api.cpp index d3515a8af..9119b0b57 100644 --- a/src/brayns/core/api/Api.cpp +++ b/src/brayns/core/api/Api.cpp @@ -42,7 +42,7 @@ const TaskInterface &getRawTask(const std::map &tasks, Ta return i->second; } -TaskId addTask(TaskInterface task, std::map &tasks, IdGenerator ids) +TaskId addTask(TaskInterface task, std::map &tasks, IdGenerator &ids) { auto id = ids.next(); assert(!tasks.contains(id)); @@ -70,10 +70,7 @@ Api::Api(std::map endpoints): Api::~Api() { - for (const auto &[id, task] : _tasks) - { - task.cancel(); - } + cancelAllTasks(); } std::vector Api::getMethods() const @@ -182,4 +179,15 @@ void Api::cancelTask(TaskId id) _tasks.erase(id); _ids.recycle(id); } + +void Api::cancelAllTasks() +{ + for (const auto &[id, task] : _tasks) + { + task.cancel(); + } + + _tasks.clear(); + _ids.reset(); +} } diff --git a/src/brayns/core/api/Api.h b/src/brayns/core/api/Api.h index 23d470f10..b442ee481 100644 --- a/src/brayns/core/api/Api.h +++ b/src/brayns/core/api/Api.h @@ -48,6 +48,7 @@ class Api TaskInfo getTask(TaskId id) const; Payload waitForTaskResult(TaskId id); void cancelTask(TaskId id); + void cancelAllTasks(); private: std::map _endpoints; diff --git a/src/brayns/core/api/Task.h b/src/brayns/core/api/Task.h index 31f866052..903469b40 100644 --- a/src/brayns/core/api/Task.h +++ b/src/brayns/core/api/Task.h @@ -55,7 +55,7 @@ struct JsonObjectReflector static auto reflect() { auto builder = JsonBuilder(); - builder.field("task_id", [](auto &object) { return &object.taskId; }) + builder.field("taskId", [](auto &object) { return &object.taskId; }) .description("ID of the task started by the method"); return builder.build(); } @@ -75,9 +75,9 @@ struct JsonObjectReflector { auto builder = JsonBuilder(); builder.field("id", [](auto &object) { return &object.id; }).description("Task ID"); - builder.field("operation_count", [](auto &object) { return &object.operationCount; }) + builder.field("operationCount", [](auto &object) { return &object.operationCount; }) .description("Number of operations the task will perform"); - builder.field("current_operation", [](auto &object) { return &object.currentOperation; }) + builder.field("currentOperation", [](auto &object) { return &object.currentOperation; }) .description("Current task operation"); return builder.build(); } diff --git a/src/brayns/core/codecs/Common.h b/src/brayns/core/codecs/Common.h new file mode 100644 index 000000000..4ede592e3 --- /dev/null +++ b/src/brayns/core/codecs/Common.h @@ -0,0 +1,107 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace brayns +{ +enum class ImageFormat +{ + Rgb8, + Rgba8, +}; + +enum class RowOrder +{ + TopDown, + BottomUp, +}; + +struct ImageView +{ + const void *data; + Size2 size; + ImageFormat format; + RowOrder rowOrder = RowOrder::BottomUp; +}; + +inline char encodePixelTo8Bit(float value) +{ + return static_cast(std::round(value * 255.0F)); +} + +inline float encodePixelToFloat(char value) +{ + return static_cast(value) / 255.0F; +} + +template +std::string convertTo8Bit(std::span> items, std::size_t pixelSize = S) +{ + auto data = std::string(); + data.reserve(pixelSize * items.size()); + + for (const auto &item : items) + { + for (auto i = std::size_t(0); i < pixelSize; ++i) + { + data.push_back(encodePixelTo8Bit(item[i])); + } + } + + return data; +} + +template +std::vector> convertToFloat(std::string_view data) +{ + constexpr auto pixelSize = std::size_t(S); + + auto itemCount = data.size() / pixelSize; + + assert(data.size() % pixelSize == 0); + + auto items = std::vector>(); + items.reserve(itemCount); + + for (auto i = std::size_t(0); i < itemCount; ++i) + { + auto &item = items.emplace_back(); + + for (auto j = std::size_t(0); j < pixelSize; ++j) + { + auto index = i * pixelSize + j; + + item[j] = encodePixelToFloat(data[index]); + } + } + + return items; +} +} diff --git a/src/brayns/core/codecs/ExrCodec.h b/src/brayns/core/codecs/ExrCodec.h index 54efaba21..ab55c60a5 100644 --- a/src/brayns/core/codecs/ExrCodec.h +++ b/src/brayns/core/codecs/ExrCodec.h @@ -26,7 +26,7 @@ #include -#include "ImageView.h" +#include "Common.h" namespace brayns { @@ -40,7 +40,7 @@ struct ExrChannel { std::string name; const void *data; - ExrDataType dataType; + ExrDataType dataType = ExrDataType::F32; std::size_t stride = 0; }; diff --git a/src/brayns/core/codecs/JpegCodec.cpp b/src/brayns/core/codecs/JpegCodec.cpp index e77c279a4..86f16a374 100644 --- a/src/brayns/core/codecs/JpegCodec.cpp +++ b/src/brayns/core/codecs/JpegCodec.cpp @@ -40,7 +40,7 @@ struct Deleter } }; -using Holder = std::unique_ptr; +using JpegPtr = std::unique_ptr; TJPF getPixelFormat(ImageFormat format) { @@ -82,7 +82,7 @@ std::string encodeJpeg(const ImageView &image, const JpegSettings &settings) throw std::runtime_error("Failed to allocate JPEG encoder"); } - auto holder = Holder(compressor); + auto holder = JpegPtr(compressor); auto [data, size, format, rowOrder] = image; diff --git a/src/brayns/core/codecs/JpegCodec.h b/src/brayns/core/codecs/JpegCodec.h index 8d7916c05..b770100c6 100644 --- a/src/brayns/core/codecs/JpegCodec.h +++ b/src/brayns/core/codecs/JpegCodec.h @@ -23,7 +23,7 @@ #include -#include "ImageView.h" +#include "Common.h" namespace brayns { diff --git a/src/brayns/core/codecs/PngCodec.h b/src/brayns/core/codecs/PngCodec.h index 29088c3e1..7e4f02bb7 100644 --- a/src/brayns/core/codecs/PngCodec.h +++ b/src/brayns/core/codecs/PngCodec.h @@ -23,7 +23,7 @@ #include -#include "ImageView.h" +#include "Common.h" namespace brayns { diff --git a/src/brayns/core/endpoints/CameraEndpoints.cpp b/src/brayns/core/endpoints/CameraEndpoints.cpp index e63eb3cf2..0397bda42 100644 --- a/src/brayns/core/endpoints/CameraEndpoints.cpp +++ b/src/brayns/core/endpoints/CameraEndpoints.cpp @@ -21,304 +21,129 @@ #include "CameraEndpoints.h" -#include -#include +#include +#include -namespace brayns -{ -template<> -struct JsonObjectReflector +namespace { - static auto reflect() - { - auto builder = JsonBuilder(); - builder.field("position", [](auto &object) { return &object.position; }).description("Camera position XYZ"); - builder.field("direction", [](auto &object) { return &object.direction; }) - .description("Camera forward direction XYZ"); - builder.field("up", [](auto &object) { return &object.direction; }) - .description("Camera up direction XYZ") - .defaultValue(Vector3(0.0F, 1.0F, 0.0F)); - builder.field("near_clipping_distance", [](auto &object) { return &object.nearClippingDistance; }) - .description("Distance to clip objects that are too close to the camera") - .defaultValue(0.0F); - return builder.build(); - } -}; +using namespace brayns; -template -struct CameraParams +void validateCameraSettings(const CameraSettings &settings) { - CameraView view; - T projection; -}; + auto &view = settings.view; + auto right = cross(view.direction, view.up); -template -struct JsonObjectReflector> -{ - static auto reflect() + if (right.x == 0.0F && right.y == 0.0F && right.z == 0.0F) { - auto builder = JsonBuilder>(); - builder.field("view", [](auto &object) { return &object.view; }) - .description("Camera view (common to all camera types)"); - builder.field("projection", [](auto &object) { return &object.projection; }) - .description("Camera projection (specific to each camera type)"); - return builder.build(); + throw InvalidParams("Camera up and direction are colinear"); } -}; - -template -struct CameraReflector; - -template T> -using GetProjection = typename CameraReflector::Projection; - -template T> -using CameraParamsOf = CameraParams>; - -template T> -using ProjectionUpdate = UpdateParams>; - -using ViewUpdate = UpdateParams; - -template -concept ReflectedCamera = - ReflectedJson> && std::same_as::getType())> - && std::same_as::create(std::declval(), CameraParamsOf()))> - && std::is_void_v::setAspect(std::declval(), 0.0F))>; - -template -std::string getCameraType() -{ - return CameraReflector::getType(); } - -template -T createCamera(Device &device, const CameraParamsOf ¶ms) -{ - return CameraReflector::create(device, params); } -template -void setCameraAspect(T &camera, float aspect) +namespace brayns +{ +CameraInfo getCamera(ObjectManager &manager, const ObjectParams ¶ms) { - CameraReflector::setAspect(camera, aspect); + return manager.visit([&](ObjectRegistry &objects) { return getCamera(objects, params); }); } -template -struct UserCamera +void updateCamera(ObjectManager &manager, const CameraUpdate ¶ms) { - T deviceObject; - CameraParamsOf params; -}; + validateCameraSettings(params.settings); + manager.visit([&](ObjectRegistry &objects) { updateCamera(objects, params); }); +} -struct CameraInterface +ObjectResult createPerspectiveCamera(ObjectManager &manager, Device &device, const PerspectiveCameraParams ¶ms) { - std::any value; - std::function getType; - std::function getDeviceObject; - std::function getView; - std::function setView; - std::function setAspect; -}; + validateCameraSettings(params.base); + return manager.visit([&](ObjectRegistry &objects) { return createPerspectiveCamera(objects, device, params); }); +} -template<> -struct ObjectReflector +PerspectiveCameraInfo getPerspectiveCamera(ObjectManager &manager, const ObjectParams ¶ms) { - static std::string getType(const CameraInterface &camera) - { - return camera.getType(); - } -}; + return manager.visit([&](ObjectRegistry &objects) { return getPerspectiveCamera(objects, params); }); +} -template -CameraInterface createCameraInterface(const std::shared_ptr> &camera) +void updatePerspectiveCamera(ObjectManager &manager, const PerspectiveCameraUpdate ¶ms) { - return { - .value = camera, - .getType = [] { return getCameraType(); }, - .getDeviceObject = [=] { return camera->deviceObject; }, - .getView = [=] { return camera->params.view; }, - .setView = [=](const auto &view) { camera->params.view = view; }, - .setAspect = [=](auto aspect) { setCameraAspect(camera->deviceObject, aspect); }, - }; + manager.visit([&](ObjectRegistry &objects) { return updatePerspectiveCamera(objects, params); }); } -template -UserCamera &castCamera(CameraInterface &camera) +ObjectResult createOrthographicCamera(ObjectManager &manager, Device &device, const OrthographicCameraParams ¶ms) { - auto ptr = std::any_cast>>(&camera.value); - - if (ptr != nullptr) - { - return **ptr; - } - - auto expected = getCameraType(); - auto got = camera.getType(); - - throw InvalidParams(fmt::format("Invalid camera type: expected {}, got {}", expected, got)); + validateCameraSettings(params.base); + return manager.visit([&](ObjectRegistry &objects) { return createOrthographicCamera(objects, device, params); }); } -template -ObjectResult createUserCamera(LockedObjects &locked, Device &device, const CameraParamsOf ¶ms) +OrthographicCameraInfo getOrthographicCamera(ObjectManager &manager, const ObjectParams ¶ms) { - return locked.visit( - [&](ObjectManager &objects) - { - auto camera = createCamera(device, params); - auto object = UserCamera{camera, params}; - auto ptr = std::make_shared(std::move(object)); - - auto interface = createCameraInterface(ptr); - - auto stored = objects.add(std::move(interface)); - - return stored.getResult(); - }); + return manager.visit([&](ObjectRegistry &objects) { return getOrthographicCamera(objects, params); }); } -CameraView getCameraView(LockedObjects &locked, const ObjectParams ¶ms) +void updateOrthographicCamera(ObjectManager &manager, const OrthographicCameraUpdate ¶ms) { - return locked.visit( - [&](ObjectManager &objects) - { - auto &camera = objects.get(params.id); - - return camera.getView(); - }); + manager.visit([&](ObjectRegistry &objects) { return updateOrthographicCamera(objects, params); }); } -template -GetProjection getCameraProjection(LockedObjects &locked, const ObjectParams ¶ms) +ObjectResult createPanoramicCamera(ObjectManager &manager, Device &device, const PanoramicCameraParams ¶ms) { - return locked.visit( - [&](ObjectManager &objects) - { - auto &interface = objects.get(params.id); - auto &camera = castCamera(interface); - - return camera.params.projection; - }); + validateCameraSettings(params.base); + return manager.visit([&](ObjectRegistry &objects) { return createPanoramicCamera(objects, device, params); }); } -void updateCameraView(LockedObjects &locked, const ViewUpdate ¶ms) +PanoramicCameraInfo getPanoramicCamera(ObjectManager &manager, const ObjectParams ¶ms) { - locked.visit( - [&](ObjectManager &objects) - { - auto &camera = objects.get(params.id); - - camera.setView(params.properties); - }); + return manager.visit([&](ObjectRegistry &objects) { return getPanoramicCamera(objects, params); }); } -template -void updateCameraProjection(LockedObjects &locked, const ProjectionUpdate ¶ms) +void updatePanoramicCamera(ObjectManager &manager, const PanoramicCameraUpdate ¶ms) { - locked.visit( - [&](ObjectManager &objects) - { - auto &interface = objects.get(params.id); - auto &camera = castCamera(interface); - - camera.params.projection = params.properties; - }); + manager.visit([&](ObjectRegistry &objects) { return updatePanoramicCamera(objects, params); }); } -template -void addCameraType(ApiBuilder &builder, LockedObjects &objects, Device &device) +void addCameraEndpoints(ApiBuilder &builder, ObjectManager &manager, Device &device) { - auto type = getCameraType(); + builder.endpoint("getCamera", [&](ObjectParams params) { return getCamera(manager, params); }) + .description("Get a camera of any type"); + builder.endpoint("updateCamera", [&](CameraUpdate params) { updateCamera(manager, params); }) + .description("Update a camera of any type"); builder .endpoint( - "create-" + type, - [&](CameraParamsOf params) { return createUserCamera(objects, device, params); }) - .description("Create a camera of type " + type); - - builder.endpoint("get-" + type, [&](ObjectParams params) { return getCameraProjection(objects, params); }) - .description("Get projection part of a camera of type " + type); - - builder.endpoint("update-" + type, [&](ProjectionUpdate params) { updateCameraProjection(objects, params); }) - .description("Update projection part of a camera of type " + type); -} - -template<> -struct JsonObjectReflector -{ - static auto reflect() - { - auto builder = JsonBuilder(); - builder.field("fovy", [](auto &object) { return &object.fovy; }) - .description("Camera vertical field of view (horizontal is deduced from framebuffer aspect)") - .defaultValue(45.0F); - return builder.build(); - } -}; - -template<> -struct CameraReflector -{ - using Projection = Perspective; - - static std::string getType() - { - return "perspective-camera"; - } - - static PerspectiveCamera create(Device &device, const CameraParamsOf ¶ms) - { - return createPerspectiveCamera(device, params.view, params.projection); - } - - static void setAspect(PerspectiveCamera &camera, float aspect) - { - camera.setAspect(aspect); - } -}; - -template<> -struct JsonObjectReflector -{ - static auto reflect() - { - auto builder = JsonBuilder(); - builder.field("height", [](auto &object) { return &object.height; }) - .description("Camera viewport height in world coordinates (horizontal is deduced from framebuffer aspect)"); - return builder.build(); - } -}; - -template<> -struct CameraReflector -{ - using Projection = Orthographic; - - static std::string getType() - { - return "orthographic-camera"; - } - - static OrthographicCamera create(Device &device, const CameraParamsOf ¶ms) - { - return createOrthographicCamera(device, params.view, params.projection); - } - - static void setAspect(OrthographicCamera &camera, float aspect) - { - camera.setAspect(aspect); - } -}; - -void addCameraEndpoints(ApiBuilder &builder, LockedObjects &objects, Device &device) -{ - addCameraType(builder, objects, device); - addCameraType(builder, objects, device); + "createPerspectiveCamera", + [&](PerspectiveCameraParams params) { return createPerspectiveCamera(manager, device, params); }) + .description("Create a new perspective camera"); + builder.endpoint("getPerspectiveCamera", [&](ObjectParams params) { return getPerspectiveCamera(manager, params); }) + .description("Get perspective camera specific params"); + builder + .endpoint( + "updatePerspectiveCamera", + [&](PerspectiveCameraUpdate params) { updatePerspectiveCamera(manager, params); }) + .description("Update perspective camera specific params"); - builder.endpoint("get-camera", [&](ObjectParams params) { return getCameraView(objects, params); }) - .description("Get the view of a camera of any type"); + builder + .endpoint( + "createOrthographicCamera", + [&](OrthographicCameraParams params) { return createOrthographicCamera(manager, device, params); }) + .description("Create a new panoramic camera"); + builder + .endpoint("getOrthographicCamera", [&](ObjectParams params) { return getOrthographicCamera(manager, params); }) + .description("Get panoramic camera specific params"); + builder + .endpoint( + "updateOrthographicCamera", + [&](OrthographicCameraUpdate params) { updateOrthographicCamera(manager, params); }) + .description("Update panoramic camera specific params"); - builder.endpoint("update-camera", [&](ViewUpdate params) { return updateCameraView(objects, params); }) - .description("Update the view of a camera of any type"); + builder + .endpoint( + "createPanoramicCamera", + [&](PanoramicCameraParams params) { return createPanoramicCamera(manager, device, params); }) + .description("Create a new panoramic camera"); + builder.endpoint("getPanoramicCamera", [&](ObjectParams params) { return getPanoramicCamera(manager, params); }) + .description("Get panoramic camera specific params"); + builder + .endpoint("updatePanoramicCamera", [&](PanoramicCameraUpdate params) { updatePanoramicCamera(manager, params); }) + .description("Update panoramic camera specific params"); } } diff --git a/src/brayns/core/endpoints/CameraEndpoints.h b/src/brayns/core/endpoints/CameraEndpoints.h index dc53f2f2e..fafdaa65e 100644 --- a/src/brayns/core/endpoints/CameraEndpoints.h +++ b/src/brayns/core/endpoints/CameraEndpoints.h @@ -22,10 +22,10 @@ #pragma once #include -#include -#include +#include +#include namespace brayns { -void addCameraEndpoints(ApiBuilder &builder, LockedObjects &objects, Device &device); +void addCameraEndpoints(ApiBuilder &builder, ObjectManager &manager, Device &device); } diff --git a/src/brayns/core/endpoints/DeviceEndpoints.cpp b/src/brayns/core/endpoints/DeviceEndpoints.cpp new file mode 100644 index 000000000..10b4a0165 --- /dev/null +++ b/src/brayns/core/endpoints/DeviceEndpoints.cpp @@ -0,0 +1,42 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "DeviceEndpoints.h" + +#include "CameraEndpoints.h" +#include "FramebufferEndpoints.h" +#include "ImageEndpoints.h" +#include "ImageOperationEndpoints.h" +#include "TransferFunctionEndpoints.h" +#include "VolumeEndpoints.h" + +namespace brayns +{ +void addDeviceEndpoints(ApiBuilder &builder, ObjectManager &manager, Device &device) +{ + addCameraEndpoints(builder, manager, device); + addFramebufferEndpoints(builder, manager, device); + addImageOperationEndpoints(builder, manager, device); + addImageEndpoints(builder, manager); + addTransferFunctionEndpoints(builder, manager, device); + addVolumeEndpoints(builder, manager, device); +} +} diff --git a/src/brayns/core/codecs/ImageView.h b/src/brayns/core/endpoints/DeviceEndpoints.h similarity index 76% rename from src/brayns/core/codecs/ImageView.h rename to src/brayns/core/endpoints/DeviceEndpoints.h index dd3b633a1..93c809acf 100644 --- a/src/brayns/core/codecs/ImageView.h +++ b/src/brayns/core/endpoints/DeviceEndpoints.h @@ -21,29 +21,11 @@ #pragma once -#include - -#include +#include +#include +#include namespace brayns { -enum class ImageFormat -{ - Rgb8, - Rgba8, -}; - -enum class RowOrder -{ - TopDown, - BottomUp, -}; - -struct ImageView -{ - const void *data; - Size2 size; - ImageFormat format; - RowOrder rowOrder = RowOrder::BottomUp; -}; +void addDeviceEndpoints(ApiBuilder &builder, ObjectManager &manager, Device &device); } diff --git a/src/brayns/core/endpoints/FramebufferEndpoints.cpp b/src/brayns/core/endpoints/FramebufferEndpoints.cpp new file mode 100644 index 000000000..fcbac906f --- /dev/null +++ b/src/brayns/core/endpoints/FramebufferEndpoints.cpp @@ -0,0 +1,65 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "FramebufferEndpoints.h" + +#include + +namespace brayns +{ +ObjectResult createFramebuffer(ObjectManager &manager, Device &device, const FramebufferParams ¶ms) +{ + return manager.visit([&](ObjectRegistry &objects) { return createFramebuffer(objects, device, params); }); +} + +FramebufferInfo getFramebuffer(ObjectManager &manager, const ObjectParams ¶ms) +{ + return manager.visit([&](ObjectRegistry &objects) { return getFramebuffer(objects, params); }); +} + +void updateFramebuffer(ObjectManager &manager, const FramebufferUpdate ¶ms) +{ + return manager.visit([&](ObjectRegistry &objects) { updateFramebuffer(objects, params); }); +} + +void clearFramebuffer(ObjectManager &manager, const ObjectParams ¶ms) +{ + return manager.visit([&](ObjectRegistry &objects) { clearFramebuffer(objects, params); }); +} + +void addFramebufferEndpoints(ApiBuilder &builder, ObjectManager &manager, Device &device) +{ + builder + .endpoint( + "createFramebuffer", + [&](FramebufferParams params) { return createFramebuffer(manager, device, params); }) + .description("Create a new framebuffer"); + + builder.endpoint("getFramebuffer", [&](ObjectParams params) { return getFramebuffer(manager, params); }) + .description("Get framebuffer params"); + + builder.endpoint("updateFramebuffer", [&](FramebufferUpdate params) { updateFramebuffer(manager, params); }) + .description("Update framebuffer params"); + + builder.endpoint("clearFramebuffer", [&](ObjectParams params) { clearFramebuffer(manager, params); }) + .description("Reset accumulating channels of the framebuffer"); +} +} diff --git a/src/brayns/core/endpoints/FramebufferEndpoints.h b/src/brayns/core/endpoints/FramebufferEndpoints.h new file mode 100644 index 000000000..a140bec5b --- /dev/null +++ b/src/brayns/core/endpoints/FramebufferEndpoints.h @@ -0,0 +1,31 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include + +namespace brayns +{ +void addFramebufferEndpoints(ApiBuilder &builder, ObjectManager &manager, Device &device); +} diff --git a/src/brayns/core/endpoints/ImageEndpoints.cpp b/src/brayns/core/endpoints/ImageEndpoints.cpp new file mode 100644 index 000000000..652793865 --- /dev/null +++ b/src/brayns/core/endpoints/ImageEndpoints.cpp @@ -0,0 +1,229 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "ImageEndpoints.h" + +#include + +#include + +#include + +namespace +{ +using namespace brayns; + +void checkChannelInFramebuffer(const UserFramebuffer &framebuffer, FramebufferChannel channel) +{ + if (framebuffer.settings.channels.contains(channel)) + { + return; + } + + const auto &name = getEnumName(channel); + throw InvalidParams(fmt::format("The framebuffer has no channels '{}'", name)); +} +} + +namespace brayns +{ +struct RawImageParams +{ + FramebufferChannel channel; +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("channel", [](auto &object) { return &object.channel; }) + .description("Channel of the framebuffer to encode"); + return builder.build(); + } +}; + +struct JpegImageParams +{ + JpegChannel channel; + JpegSettings settings; +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("channel", [](auto &object) { return &object.channel; }) + .description("Channel of the framebuffer to encode"); + builder.field("settings", [](auto &object) { return &object.settings; }).description("JPEG encoder settings"); + return builder.build(); + } +}; + +struct PngImageParams +{ + PngChannel channel; +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("channel", [](auto &object) { return &object.channel; }) + .description("Channel of the framebuffer to encode"); + return builder.build(); + } +}; + +struct ExrImageParams +{ + std::set channels; +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("channels", [](auto &object) { return &object.channels; }) + .description("Channels of the framebuffer to encode"); + return builder.build(); + } +}; + +template +struct ImageParams +{ + ObjectId id; + T settings; +}; + +template +struct JsonObjectReflector> +{ + static auto reflect() + { + auto builder = JsonBuilder>(); + builder.field("id", [](auto &object) { return &object.id; }).description("ID of the framebuffer to read"); + builder.field("settings", [](auto &object) { return &object.settings; }) + .description("Settings to encode the framebuffer content"); + return builder.build(); + } +}; + +Result readFramebuffer(ObjectManager &manager, const ImageParams ¶ms) +{ + return manager.visit( + [&](ObjectRegistry &objects) + { + auto &framebuffer = objects.getAs(params.id); + + checkChannelInFramebuffer(framebuffer, params.settings.channel); + + auto data = readChannel(framebuffer, params.settings.channel); + + return Result{{}, std::move(data)}; + }); +} + +Result readFramebufferAsJpeg(ObjectManager &manager, const ImageParams ¶ms) +{ + return manager.visit( + [&](ObjectRegistry &objects) + { + auto &framebuffer = objects.getAs(params.id); + + auto channel = static_cast(params.settings.channel); + checkChannelInFramebuffer(framebuffer, channel); + + auto data = readChannelAsJpeg(framebuffer, params.settings.channel, params.settings.settings); + + return Result{{}, std::move(data)}; + }); +} + +Result readFramebufferAsPng(ObjectManager &manager, const ImageParams ¶ms) +{ + return manager.visit( + [&](ObjectRegistry &objects) + { + auto &framebuffer = objects.getAs(params.id); + + auto channel = static_cast(params.settings.channel); + checkChannelInFramebuffer(framebuffer, channel); + + auto data = readChannelAsPng(framebuffer, params.settings.channel); + + return Result{{}, std::move(data)}; + }); +} + +Result readFramebufferAsExr(ObjectManager &manager, const ImageParams ¶ms) +{ + return manager.visit( + [&](ObjectRegistry &objects) + { + auto &framebuffer = objects.getAs(params.id); + + for (auto channel : params.settings.channels) + { + checkChannelInFramebuffer(framebuffer, channel); + } + + auto data = readChannelsAsExr(framebuffer, params.settings.channels); + + return Result{{}, std::move(data)}; + }); +} + +void addImageEndpoints(ApiBuilder &builder, ObjectManager &manager) +{ + builder + .endpoint( + "readFramebuffer", + [&](ImageParams params) { return readFramebuffer(manager, params); }) + .description("Read a framebuffer channel and return it in the binary part of the message"); + + builder + .endpoint( + "readFramebufferAsJpeg", + [&](ImageParams params) { return readFramebufferAsJpeg(manager, params); }) + .description("Read a framebuffer channel and return it encoded as JPEG in the binary part of the message"); + + builder + .endpoint( + "readFramebufferAsPng", + [&](ImageParams params) { return readFramebufferAsPng(manager, params); }) + .description("Read a framebuffer channel and return it encoded as PNG in the binary part of the message"); + + builder + .endpoint( + "readFramebufferAsExr", + [&](ImageParams params) { return readFramebufferAsExr(manager, params); }) + .description("Read framebuffer channels and return them encoded as EXR in the binary part of the message"); +} +} diff --git a/src/brayns/core/endpoints/ImageEndpoints.h b/src/brayns/core/endpoints/ImageEndpoints.h new file mode 100644 index 000000000..75b0a567b --- /dev/null +++ b/src/brayns/core/endpoints/ImageEndpoints.h @@ -0,0 +1,30 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include + +namespace brayns +{ +void addImageEndpoints(ApiBuilder &builder, ObjectManager &manager); +} diff --git a/src/brayns/core/endpoints/ImageOperationEndpoints.cpp b/src/brayns/core/endpoints/ImageOperationEndpoints.cpp new file mode 100644 index 000000000..405463692 --- /dev/null +++ b/src/brayns/core/endpoints/ImageOperationEndpoints.cpp @@ -0,0 +1,55 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "ImageOperationEndpoints.h" + +#include + +namespace brayns +{ +ObjectResult createToneMapper(ObjectManager &manager, Device &device, const ToneMapperParams ¶ms) +{ + return manager.visit([&](ObjectRegistry &objects) { return createToneMapper(objects, device, params); }); +} + +ToneMapperInfo getToneMapper(ObjectManager &manager, const ObjectParams ¶ms) +{ + return manager.visit([&](ObjectRegistry &objects) { return getToneMapper(objects, params); }); +} + +void updateToneMapper(ObjectManager &manager, const ToneMapperUpdate ¶ms) +{ + return manager.visit([&](ObjectRegistry &objects) { updateToneMapper(objects, params); }); +} + +void addImageOperationEndpoints(ApiBuilder &builder, ObjectManager &manager, Device &device) +{ + builder + .endpoint("createToneMapper", [&](ToneMapperParams params) { return createToneMapper(manager, device, params); }) + .description("Create a tone mapper that can be attached to a framebuffer"); + + builder.endpoint("getToneMapper", [&](ObjectParams params) { return getToneMapper(manager, params); }) + .description("Get tone mapper specific params"); + + builder.endpoint("updateToneMapper", [&](ToneMapperUpdate params) { updateToneMapper(manager, params); }) + .description("Update tone mapper specific params"); +} +} diff --git a/src/brayns/core/endpoints/ImageOperationEndpoints.h b/src/brayns/core/endpoints/ImageOperationEndpoints.h new file mode 100644 index 000000000..4193375de --- /dev/null +++ b/src/brayns/core/endpoints/ImageOperationEndpoints.h @@ -0,0 +1,31 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include + +namespace brayns +{ +void addImageOperationEndpoints(ApiBuilder &builder, ObjectManager &manager, Device &device); +} diff --git a/src/brayns/core/endpoints/ObjectEndpoints.cpp b/src/brayns/core/endpoints/ObjectEndpoints.cpp index b5bcff86d..ec10c532a 100644 --- a/src/brayns/core/endpoints/ObjectEndpoints.cpp +++ b/src/brayns/core/endpoints/ObjectEndpoints.cpp @@ -34,43 +34,41 @@ struct JsonObjectReflector static auto reflect() { auto builder = JsonBuilder(); - builder.field("objects", [](auto &object) { return &object.objects; }) - .description("Generic properties of all objects in registry"); + builder.field("objects", [](auto &object) { return &object.objects; }).description("List of object info"); return builder.build(); } }; -AllObjectsResult getAllObjects(LockedObjects &locked) +AllObjectsResult getAllObjects(ObjectManager &manager) { - return {locked.visit([](auto &objects) { return objects.getAllObjects(); })}; + return {manager.visit([](auto &objects) { return objects.getAll(); })}; } -ObjectInfo getObject(LockedObjects &locked, const ObjectParams ¶ms) +ObjectInfo getObject(ObjectManager &manager, const ObjectParams ¶ms) { - return locked.visit([&](auto &objects) { return objects.getObject(params.id); }); + return manager.visit([&](auto &objects) { return objects.get(params.id); }); } -struct UserProperties +struct ObjectUpdate { JsonValue userData; }; template<> -struct JsonObjectReflector +struct JsonObjectReflector { static auto reflect() { - auto builder = JsonBuilder(); - builder.field("user_data", [](auto &object) { return &object.userData; }).description("User data"); + auto builder = JsonBuilder(); + builder.field("userData", [](auto &object) { return &object.userData; }) + .description("User data to store in the object (not used by Brayns)"); return builder.build(); } }; -using ObjectUpdate = UpdateParams; - -void updateObject(LockedObjects &locked, const ObjectUpdate ¶ms) +void updateObject(ObjectManager &manager, const UpdateParams ¶ms) { - locked.visit([&](auto &objects) { objects.setUserData(params.id, params.properties.userData); }); + manager.visit([&](auto &objects) { objects.update(params.id, params.settings.userData); }); } struct RemoveParams @@ -89,9 +87,9 @@ struct JsonObjectReflector } }; -void removeObjects(LockedObjects &locked, const RemoveParams ¶ms) +void removeObjects(ObjectManager &manager, const RemoveParams ¶ms) { - locked.visit( + manager.visit( [&](auto &objects) { for (auto id : params.ids) @@ -101,51 +99,42 @@ void removeObjects(LockedObjects &locked, const RemoveParams ¶ms) }); } -void clearObjects(LockedObjects &locked) +void clearObjects(ObjectManager &manager) { - locked.visit([](auto &objects) { objects.clear(); }); + manager.visit([](auto &objects) { objects.clear(); }); } struct EmptyObject { }; -template<> -struct ObjectReflector -{ - static std::string getType(const EmptyObject &) - { - return "empty-object"; - } -}; - -ObjectResult createEmptyObject(LockedObjects &locked) +ObjectResult createEmptyObject(ObjectManager &manager) { - return locked.visit( + return manager.visit( [&](auto &objects) { - auto object = objects.add(EmptyObject()); - return object.getResult(); + auto object = objects.add(EmptyObject(), "EmptyObject"); + return ObjectResult{object.getId()}; }); } -void addObjectEndpoints(ApiBuilder &builder, LockedObjects &objects) +void addObjectEndpoints(ApiBuilder &builder, ObjectManager &manager) { - builder.endpoint("get-all-objects", [&] { return getAllObjects(objects); }) + builder.endpoint("getAllObjects", [&] { return getAllObjects(manager); }) .description("Get generic properties of all objects, use get-{type} to get details of an object"); - builder.endpoint("get-object", [&](ObjectParams params) { return getObject(objects, params); }) + builder.endpoint("getObject", [&](ObjectParams params) { return getObject(manager, params); }) .description("Get generic object properties from given object IDs"); - builder.endpoint("update-object", [&](ObjectUpdate params) { return updateObject(objects, params); }); + builder.endpoint("updateObject", [&](UpdateParams params) { return updateObject(manager, params); }); - builder.endpoint("remove-objects", [&](RemoveParams params) { removeObjects(objects, params); }) + builder.endpoint("removeObjects", [&](RemoveParams params) { removeObjects(manager, params); }) .description("Remove selected objects from registry (but not from scene)"); - builder.endpoint("clear-objects", [&] { clearObjects(objects); }) + builder.endpoint("clearObjects", [&] { clearObjects(manager); }) .description("Remove all objects currently in registry"); - builder.endpoint("create-empty-object", [&] { return createEmptyObject(objects); }) + builder.endpoint("createEmptyObject", [&] { return createEmptyObject(manager); }) .description("Create an empty object (for testing or to store user data)"); } } diff --git a/src/brayns/core/endpoints/ObjectEndpoints.h b/src/brayns/core/endpoints/ObjectEndpoints.h index 01a8b944b..886737c77 100644 --- a/src/brayns/core/endpoints/ObjectEndpoints.h +++ b/src/brayns/core/endpoints/ObjectEndpoints.h @@ -22,9 +22,9 @@ #pragma once #include -#include +#include namespace brayns { -void addObjectEndpoints(ApiBuilder &builder, LockedObjects &objects); +void addObjectEndpoints(ApiBuilder &builder, ObjectManager &manager); } diff --git a/src/brayns/core/endpoints/ServiceEndpoints.cpp b/src/brayns/core/endpoints/ServiceEndpoints.cpp index 6cd471c93..b07ca5d9f 100644 --- a/src/brayns/core/endpoints/ServiceEndpoints.cpp +++ b/src/brayns/core/endpoints/ServiceEndpoints.cpp @@ -43,7 +43,7 @@ struct JsonObjectReflector builder.field("major", [](auto &object) { return &object.major; }).description("Major version"); builder.field("minor", [](auto &object) { return &object.minor; }).description("Minor version"); builder.field("patch", [](auto &object) { return &object.patch; }).description("Patch version"); - builder.field("pre_release", [](auto &object) { return &object.preRelease; }) + builder.field("preRelease", [](auto &object) { return &object.preRelease; }) .description("Pre-release version (0 if stable)"); builder.field("tag", [](auto &object) { return &object.tag; }) .description("Version tag major.minor.patch[-prerelease]"); @@ -113,32 +113,34 @@ struct JsonObjectReflector static auto reflect() { auto builder = JsonBuilder(); - builder.field("task_id", [](auto &object) { return &object.taskId; }).description("ID of the requested task"); + builder.field("taskId", [](auto &object) { return &object.taskId; }).description("ID of the requested task"); return builder.build(); } }; void addServiceEndpoints(ApiBuilder &builder, Api &api, StopToken &token) { - builder.endpoint("get-version", [] { return VersionResult(); }) + builder.endpoint("getVersion", [] { return VersionResult(); }) .description("Get the build version of the service currently running"); - builder.endpoint("get-methods", [&] { return MethodsResult{api.getMethods()}; }) + builder.endpoint("getMethods", [&] { return MethodsResult{api.getMethods()}; }) .description("Get available JSON-RPC methods"); - builder.endpoint("get-schema", [&](SchemaParams params) { return api.getSchema(params.method); }) + builder.endpoint("getSchema", [&](SchemaParams params) { return api.getSchema(params.method); }) .description("Get the schema of the given JSON-RPC method"); - builder.endpoint("get-tasks", [&] { return TasksResult{api.getTasks()}; }) + builder.endpoint("getTasks", [&] { return TasksResult{api.getTasks()}; }) .description("Get tasks which result has not been retreived with wait-for-task-result"); - builder.endpoint("get-task", [&](TaskParams params) { return api.getTask(params.taskId); }) + builder.endpoint("getTask", [&](TaskParams params) { return api.getTask(params.taskId); }) .description("Get current state of the given task"); - builder.endpoint("cancel-task", [&](TaskParams params) { api.cancelTask(params.taskId); }) + builder.endpoint("cancelTask", [&](TaskParams params) { api.cancelTask(params.taskId); }) .description("Cancel given task"); - builder.endpoint("get-task-result", [&](TaskParams params) { return api.waitForTaskResult(params.taskId); }) + builder.endpoint("cancelAllTasks", [&] { api.cancelAllTasks(); }).description("Cancel all tasks"); + + builder.endpoint("getTaskResult", [&](TaskParams params) { return api.waitForTaskResult(params.taskId); }) .description("Wait for given task to finish and return its result"); builder.endpoint("stop", [&] { token.stop(); }) diff --git a/src/brayns/core/endpoints/TransferFunctionEndpoints.cpp b/src/brayns/core/endpoints/TransferFunctionEndpoints.cpp new file mode 100644 index 000000000..c578e4a27 --- /dev/null +++ b/src/brayns/core/endpoints/TransferFunctionEndpoints.cpp @@ -0,0 +1,67 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "TransferFunctionEndpoints.h" + +#include + +namespace brayns +{ +ObjectResult createLinearTransferFunction( + ObjectManager &manager, + Device &device, + const LinearTransferFunctionParams ¶ms) +{ + return manager.visit( + [&](ObjectRegistry &objects) { return createLinearTransferFunction(objects, device, params); }); +} + +LinearTransferFunctionInfo getLinearTransferFunction(ObjectManager &manager, const ObjectParams ¶ms) +{ + return manager.visit([&](ObjectRegistry &objects) { return getLinearTransferFunction(objects, params); }); +} + +void updateLinearTransferFunction(ObjectManager &manager, const LinearTransferFunctionUpdate ¶ms) +{ + return manager.visit([&](ObjectRegistry &objects) { updateLinearTransferFunction(objects, params); }); +} + +void addTransferFunctionEndpoints(ApiBuilder &builder, ObjectManager &manager, Device &device) +{ + builder + .endpoint( + "createLinearTransferFunction", + [&](LinearTransferFunctionParams params) { return createLinearTransferFunction(manager, device, params); }) + .description("Create a linear transfer function that can be attached to a volume"); + + builder + .endpoint( + "getLinearTransferFunction", + [&](ObjectParams params) { return getLinearTransferFunction(manager, params); }) + .description("Get linear transfer function specific params"); + + builder + .endpoint( + "updateLinearTransferFunction", + [&](LinearTransferFunctionUpdate params) { updateLinearTransferFunction(manager, params); }) + .description("Update linear transfer function specific params"); +} +} diff --git a/src/brayns/core/endpoints/TransferFunctionEndpoints.h b/src/brayns/core/endpoints/TransferFunctionEndpoints.h new file mode 100644 index 000000000..55b3bf1b6 --- /dev/null +++ b/src/brayns/core/endpoints/TransferFunctionEndpoints.h @@ -0,0 +1,31 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include + +namespace brayns +{ +void addTransferFunctionEndpoints(ApiBuilder &builder, ObjectManager &manager, Device &device); +} diff --git a/src/brayns/core/endpoints/VolumeEndpoints.cpp b/src/brayns/core/endpoints/VolumeEndpoints.cpp new file mode 100644 index 000000000..400cb6dcd --- /dev/null +++ b/src/brayns/core/endpoints/VolumeEndpoints.cpp @@ -0,0 +1,57 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "VolumeEndpoints.h" + +#include + +namespace brayns +{ +ObjectResult createRegularVolume(ObjectManager &manager, Device &device, const RegularVolumeParams ¶ms) +{ + return manager.visit([&](ObjectRegistry &objects) { return createRegularVolume(objects, device, params); }); +} + +RegularVolumeInfo getRegularVolume(ObjectManager &manager, const ObjectParams ¶ms) +{ + return manager.visit([&](ObjectRegistry &objects) { return getRegularVolume(objects, params); }); +} + +void updateRegularVolume(ObjectManager &manager, const RegularVolumeUpdate ¶ms) +{ + return manager.visit([&](ObjectRegistry &objects) { updateRegularVolume(objects, params); }); +} + +void addVolumeEndpoints(ApiBuilder &builder, ObjectManager &manager, Device &device) +{ + builder + .endpoint( + "createRegularVolume", + [&](RegularVolumeParams params) { return createRegularVolume(manager, device, params); }) + .description("Create a regular volume"); + + builder.endpoint("getRegularVolume", [&](ObjectParams params) { return getRegularVolume(manager, params); }) + .description("Get regular volume specific params"); + + builder.endpoint("updateRegularVolume", [&](RegularVolumeUpdate params) { updateRegularVolume(manager, params); }) + .description("Update regular volume specific params"); +} +} diff --git a/src/brayns/core/endpoints/VolumeEndpoints.h b/src/brayns/core/endpoints/VolumeEndpoints.h new file mode 100644 index 000000000..b3346e0f2 --- /dev/null +++ b/src/brayns/core/endpoints/VolumeEndpoints.h @@ -0,0 +1,31 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include + +namespace brayns +{ +void addVolumeEndpoints(ApiBuilder &builder, ObjectManager &manager, Device &device); +} diff --git a/src/brayns/core/engine/Camera.cpp b/src/brayns/core/engine/Camera.cpp index d1496f6cf..f0a59cc32 100644 --- a/src/brayns/core/engine/Camera.cpp +++ b/src/brayns/core/engine/Camera.cpp @@ -25,28 +25,76 @@ namespace { using namespace brayns; -void setCameraViewParams(OSPCamera handle, const CameraView &view) +void setCameraParams(OSPCamera handle, const CameraSettings &settings) { - setObjectParam(handle, "position", view.position); - setObjectParam(handle, "direction", view.direction); - setObjectParam(handle, "up", view.up); - setObjectParam(handle, "nearClip", view.nearClippingDistance); + setObjectParam(handle, "position", settings.view.position); + setObjectParam(handle, "direction", settings.view.direction); + setObjectParam(handle, "up", settings.view.up); + setObjectParam(handle, "nearClip", settings.nearClip); + setObjectParam(handle, "imageStart", settings.imageRegion.lower); + setObjectParam(handle, "imageEnd", settings.imageRegion.upper); +} + +void setStereoParams(OSPCamera handle, const std::optional &stereo) +{ + if (stereo) + { + setObjectParam(handle, "stereoMode", static_cast(stereo->mode)); + setObjectParam(handle, "interpupillaryDistance", stereo->interpupillaryDistance); + } + else + { + removeObjectParam(handle, "stereoMode"); + removeObjectParam(handle, "interpupillaryDistance"); + } +} + +void setPerspectiveParams(OSPCamera handle, const PerspectiveCameraSettings &settings) +{ + setObjectParam(handle, "fovy", degrees(settings.fovy)); + setObjectParam(handle, "aspect", settings.aspect); + + if (settings.depthOfField) + { + setObjectParam(handle, "apertureRadius", settings.depthOfField->apertureRadius); + setObjectParam(handle, "focusDistance", settings.depthOfField->focusDistance); + } + else + { + removeObjectParam(handle, "apertureRadius"); + removeObjectParam(handle, "focusDistance"); + } + + setObjectParam(handle, "architectural", settings.architectural); + + setStereoParams(handle, settings.stereo); +} + +void setOrthographicParams(OSPCamera handle, const OrthographicCameraSettings &settings) +{ + setObjectParam(handle, "height", settings.height); + setObjectParam(handle, "aspect", settings.aspect); +} + +void setPanoramicParams(OSPCamera handle, const PanoramicCameraSettings &settings) +{ + setStereoParams(handle, settings.stereo); } } namespace brayns { -void Camera::setView(const CameraView &view) +void Camera::update(const CameraSettings &settings) { auto handle = getHandle(); - setCameraViewParams(handle, view); + setCameraParams(handle, settings); commitObject(handle); } -void PerspectiveCamera::setFovy(float fovy) +void PerspectiveCamera::update(const PerspectiveCameraSettings &settings) { auto handle = getHandle(); - setObjectParam(handle, "fovy", fovy); + setPerspectiveParams(handle, settings); commitObject(handle); } @@ -57,25 +105,26 @@ void PerspectiveCamera::setAspect(float aspect) commitObject(handle); } -PerspectiveCamera createPerspectiveCamera(Device &device, const CameraView &view, const Perspective &projection) +PerspectiveCamera createPerspectiveCamera( + Device &device, + const CameraSettings &settings, + const PerspectiveCameraSettings &perspective) { auto handle = ospNewCamera("perspective"); auto camera = wrapObjectHandleAs(device, handle); - setCameraViewParams(handle, view); + setCameraParams(handle, settings); + setPerspectiveParams(handle, perspective); - setObjectParam(handle, "fovy", projection.fovy); - setObjectParam(handle, "aspect", projection.aspect); - - commitObject(handle); + commitObject(device, handle); return camera; } -void OrthographicCamera::setHeight(float height) +void OrthographicCamera::update(const OrthographicCameraSettings &settings) { auto handle = getHandle(); - setObjectParam(handle, "height", height); + setOrthographicParams(handle, settings); commitObject(handle); } @@ -86,17 +135,41 @@ void OrthographicCamera::setAspect(float aspect) commitObject(handle); } -OrthographicCamera createOrthographicCamera(Device &device, const CameraView &view, const Orthographic &projection) +OrthographicCamera createOrthographicCamera( + Device &device, + const CameraSettings &settings, + const OrthographicCameraSettings &orthographic) { auto handle = ospNewCamera("orthographic"); auto camera = wrapObjectHandleAs(device, handle); - setCameraViewParams(handle, view); + setCameraParams(handle, settings); + setOrthographicParams(handle, orthographic); + + commitObject(device, handle); - setObjectParam(handle, "height", projection.height); - setObjectParam(handle, "aspect", projection.aspect); + return camera; +} +void PanoramicCamera::update(const PanoramicCameraSettings &settings) +{ + auto handle = getHandle(); + setPanoramicParams(handle, settings); commitObject(handle); +} + +PanoramicCamera createPanoramicCamera( + Device &device, + const CameraSettings &settings, + const PanoramicCameraSettings &panoramic) +{ + auto handle = ospNewCamera("panoramic"); + auto camera = wrapObjectHandleAs(device, handle); + + setCameraParams(handle, settings); + setPanoramicParams(handle, panoramic); + + commitObject(device, handle); return camera; } diff --git a/src/brayns/core/engine/Camera.h b/src/brayns/core/engine/Camera.h index a71b1901a..818091ede 100644 --- a/src/brayns/core/engine/Camera.h +++ b/src/brayns/core/engine/Camera.h @@ -26,12 +26,18 @@ namespace brayns { -struct CameraView +struct View { Vector3 position = {0.0F, 0.0F, 0.0F}; Vector3 direction = {0.0F, 0.0F, 1.0F}; Vector3 up = {0.0F, 1.0F, 0.0F}; - float nearClippingDistance = 1.0e-6F; +}; + +struct CameraSettings +{ + View view = {}; + float nearClip = 1.0e-6F; + Box2 imageRegion = {{0.0F, 0.0F}, {1.0F, 1.0F}}; }; class Camera : public Managed @@ -39,30 +45,54 @@ class Camera : public Managed public: using Managed::Managed; - void setView(const CameraView &view); + void update(const CameraSettings &settings); +}; + +struct DepthOfField +{ + float apertureRadius; + float focusDistance = 1.0F; +}; + +enum class StereoMode +{ + Left = OSP_STEREO_LEFT, + Right = OSP_STEREO_RIGHT, + SideBySide = OSP_STEREO_SIDE_BY_SIDE, + TopBottom = OSP_STEREO_TOP_BOTTOM, +}; + +struct Stereo +{ + StereoMode mode; + float interpupillaryDistance = 0.0635F; }; -struct Perspective +struct PerspectiveCameraSettings { - float fovy = 60.0F; + float fovy = radians(60.0F); float aspect = 1.0F; + std::optional depthOfField = std::nullopt; + bool architectural = false; + std::optional stereo = std::nullopt; }; class PerspectiveCamera : public Camera { public: using Camera::Camera; + using Camera::update; - void setFovy(float fovy); + void update(const PerspectiveCameraSettings &settings); void setAspect(float aspect); }; PerspectiveCamera createPerspectiveCamera( Device &device, - const CameraView &view = {}, - const Perspective &projection = {}); + const CameraSettings &settings = {}, + const PerspectiveCameraSettings &perspective = {}); -struct Orthographic +struct OrthographicCameraSettings { float height = 1.0F; float aspect = 1.0F; @@ -72,13 +102,33 @@ class OrthographicCamera : public Camera { public: using Camera::Camera; + using Camera::update; - void setHeight(float height); + void update(const OrthographicCameraSettings &settings); void setAspect(float aspect); }; OrthographicCamera createOrthographicCamera( Device &device, - const CameraView &view = {}, - const Orthographic &projection = {}); + const CameraSettings &settings = {}, + const OrthographicCameraSettings &orthographic = {}); + +struct PanoramicCameraSettings +{ + std::optional stereo; +}; + +class PanoramicCamera : public Camera +{ +public: + using Camera::Camera; + using Camera::update; + + void update(const PanoramicCameraSettings &settings); +}; + +PanoramicCamera createPanoramicCamera( + Device &device, + const CameraSettings &settings = {}, + const PanoramicCameraSettings &panoramic = {}); } diff --git a/src/brayns/core/engine/Device.cpp b/src/brayns/core/engine/Device.cpp index 60f775813..c3cc05e1b 100644 --- a/src/brayns/core/engine/Device.cpp +++ b/src/brayns/core/engine/Device.cpp @@ -21,6 +21,33 @@ #include "Device.h" +#include + +namespace +{ +using namespace brayns; + +OSPLogLevel getLogLevel(LogLevel level) +{ + switch (level) + { + case LogLevel::Trace: + case LogLevel::Debug: + return OSP_LOG_DEBUG; + case LogLevel::Info: + return OSP_LOG_INFO; + case LogLevel::Warn: + return OSP_LOG_WARNING; + case LogLevel::Error: + return OSP_LOG_ERROR; + case LogLevel::Off: + return OSP_LOG_NONE; + default: + throw std::invalid_argument("Invalid log level"); + } +} +} + namespace brayns { DeviceException::DeviceException(OSPError error, const char *message): @@ -70,6 +97,15 @@ OSPDevice Device::getHandle() const return _device.get(); } +std::string Device::getVersion() const +{ + auto major = ospDeviceGetProperty(_device.get(), OSP_DEVICE_VERSION_MAJOR); + auto minor = ospDeviceGetProperty(_device.get(), OSP_DEVICE_VERSION_MINOR); + auto patch = ospDeviceGetProperty(_device.get(), OSP_DEVICE_VERSION_PATCH); + + return fmt::format("{}.{}.{}", major, minor, patch); +} + void Device::throwIfError() { _handler->throwIfError(); @@ -81,7 +117,7 @@ void Device::Deleter::operator()(OSPDevice device) const ospShutdown(); } -Device createDevice(Logger &logger) +Device createDevice(Logger &logger, const DeviceSettings &settings) { auto error = ospLoadModule("cpu"); @@ -107,12 +143,20 @@ Device createDevice(Logger &logger) auto handler = std::make_unique(logger); - auto logLevel = OSP_LOG_DEBUG; + auto logLevel = getLogLevel(logger.getLevel()); ospDeviceSetParam(device, "logLevel", OSP_UINT, &logLevel); auto warnAsError = true; ospDeviceSetParam(device, "warnAsError", OSP_BOOL, &warnAsError); + if (settings.threadCount != 0) + { + auto threadCount = static_cast(settings.threadCount); + ospDeviceSetParam(device, "numThreads", OSP_INT, &threadCount); + } + + ospDeviceSetParam(device, "setAffinity", OSP_BOOL, &settings.affinity); + auto errorCallback = [](auto *userData, auto error, const auto *message) { auto &handler = *static_cast(userData); diff --git a/src/brayns/core/engine/Device.h b/src/brayns/core/engine/Device.h index da608c71f..da5b748b2 100644 --- a/src/brayns/core/engine/Device.h +++ b/src/brayns/core/engine/Device.h @@ -25,6 +25,7 @@ #include #include +#include #include @@ -60,6 +61,7 @@ class Device explicit Device(OSPDevice device, std::unique_ptr handler); OSPDevice getHandle() const; + std::string getVersion() const; void throwIfError(); private: @@ -72,5 +74,11 @@ class Device std::unique_ptr _handler; }; -Device createDevice(Logger &logger); +struct DeviceSettings +{ + std::size_t threadCount = 0; + bool affinity = false; +}; + +Device createDevice(Logger &logger, const DeviceSettings &settings = {}); } diff --git a/src/brayns/core/engine/Framebuffer.cpp b/src/brayns/core/engine/Framebuffer.cpp index 8a5972e94..ed123d8c4 100644 --- a/src/brayns/core/engine/Framebuffer.cpp +++ b/src/brayns/core/engine/Framebuffer.cpp @@ -21,30 +21,63 @@ #include "Framebuffer.h" +#include +#include + namespace brayns { -const void *Framebuffer::map(FramebufferChannel channel) +FramebufferData::FramebufferData(const void *data, OSPFrameBuffer handle): + _data(data, Deleter{handle}) { - auto handle = getHandle(); - return ospMapFrameBuffer(handle, static_cast(channel)); } -void Framebuffer::unmap(const void *data) +const void *FramebufferData::get() const +{ + return _data.get(); +} + +void FramebufferData::Deleter::operator()(const void *data) const { - auto handle = getHandle(); ospUnmapFrameBuffer(data, handle); } +FramebufferData Framebuffer::map(FramebufferChannel channel) +{ + auto handle = getHandle(); + const auto *data = ospMapFrameBuffer(handle, static_cast(channel)); + + if (data == nullptr) + { + throw std::invalid_argument("Cannot map channel (probably not in framebuffer)"); + } + + return FramebufferData(data, handle); +} + void Framebuffer::resetAccumulation() { auto handle = getHandle(); ospResetAccumulation(handle); } -float Framebuffer::getVariance() +std::optional Framebuffer::getVariance() +{ + auto handle = getHandle(); + auto variance = ospGetVariance(handle); + + if (std::isfinite(variance)) + { + return variance; + } + + return std::nullopt; +} + +void Framebuffer::update(const std::optional> &operations) { auto handle = getHandle(); - return ospGetVariance(handle); + setObjectParam(handle, "imageOperation", operations); + commitObject(handle); } Framebuffer createFramebuffer(Device &device, const FramebufferSettings &settings) @@ -76,7 +109,7 @@ Framebuffer createFramebuffer(Device &device, const FramebufferSettings &setting setObjectParam(handle, "imageOperation", settings.operations); - commitObject(handle); + commitObject(device, handle); return framebuffer; } diff --git a/src/brayns/core/engine/Framebuffer.h b/src/brayns/core/engine/Framebuffer.h index 5e17d0496..3bd0a896c 100644 --- a/src/brayns/core/engine/Framebuffer.h +++ b/src/brayns/core/engine/Framebuffer.h @@ -21,6 +21,7 @@ #pragma once +#include #include #include @@ -63,15 +64,39 @@ struct FramebufferSettings std::optional> operations = std::nullopt; }; +class FramebufferData +{ +public: + explicit FramebufferData(const void *data, OSPFrameBuffer handle); + + const void *get() const; + + template + const T *as() const + { + return static_cast(get()); + } + +private: + struct Deleter + { + OSPFrameBuffer handle; + + void operator()(const void *data) const; + }; + + std::unique_ptr _data; +}; + class Framebuffer : public Managed { public: using Managed::Managed; - const void *map(FramebufferChannel channel); - void unmap(const void *data); + FramebufferData map(FramebufferChannel channel); void resetAccumulation(); - float getVariance(); + std::optional getVariance(); + void update(const std::optional> &operations); }; Framebuffer createFramebuffer(Device &device, const FramebufferSettings &settings); diff --git a/src/brayns/core/engine/GeometricModel.cpp b/src/brayns/core/engine/GeometricModel.cpp index 818776bbe..c6fd2cc67 100644 --- a/src/brayns/core/engine/GeometricModel.cpp +++ b/src/brayns/core/engine/GeometricModel.cpp @@ -59,7 +59,7 @@ GeometricModel createGeometricModel(Device &device, const GeometricModelSettings setObjectParam(handle, "invertNormals", settings.invertedNormals); setObjectParam(handle, "id", settings.id); - commitObject(handle); + commitObject(device, handle); return model; } diff --git a/src/brayns/core/engine/Geometry.cpp b/src/brayns/core/engine/Geometry.cpp index 961ed4b60..a9290fcf8 100644 --- a/src/brayns/core/engine/Geometry.cpp +++ b/src/brayns/core/engine/Geometry.cpp @@ -117,7 +117,7 @@ TriangleMesh createTriangleMesh(Device &device, const TriangleMeshSettings &sett setMeshParams(handle, settings.base); setObjectParam(handle, "index", settings.indices); - commitObject(handle); + commitObject(device, handle); return mesh; } @@ -138,7 +138,7 @@ QuadMesh createQuadMesh(Device &device, const QuadMeshSettings &settings) setObjectParam(handle, "quadSoup", true); } - commitObject(handle); + commitObject(device, handle); return mesh; } @@ -152,7 +152,7 @@ Spheres createSpheres(Device &device, const SphereSettings &settings) setObjectParam(handle, "sphere.texcoord", settings.uvs); setObjectParam(handle, "type", OSP_SPHERE); - commitObject(handle); + commitObject(device, handle); return spheres; } @@ -175,7 +175,7 @@ Discs createDiscs(Device &device, const DiscSettings &settings) setObjectParam(handle, "type", OSP_DISC); } - commitObject(handle); + commitObject(device, handle); return discs; } @@ -193,7 +193,7 @@ Cylinders createCylinders(Device &device, const CylinderSettings &settings) setObjectParam(handle, "type", OSP_DISJOINT); setObjectParam(handle, "basis", OSP_LINEAR); - commitObject(handle); + commitObject(device, handle); return cylinders; } @@ -216,7 +216,7 @@ Curve createCurve(Device &device, const CurveSettings &settings) std::visit([=](const auto &value) { setCurveType(handle, value); }, settings.type); std::visit([=](const auto &value) { setCurveBasis(handle, value); }, settings.basis); - commitObject(handle); + commitObject(device, handle); return curve; } @@ -228,7 +228,7 @@ Boxes createBoxes(Device &device, const BoxSettings &settings) setObjectParam(handle, "box", settings.boxes); - commitObject(handle); + commitObject(device, handle); return boxes; } @@ -241,7 +241,7 @@ Planes createPlanes(Device &device, const PlaneSettings &settings) setObjectParam(handle, "plane.coefficients", settings.coefficients); setObjectParam(handle, "plane.bounds", settings.bounds); - commitObject(handle); + commitObject(device, handle); return planes; } @@ -254,7 +254,7 @@ Isosurfaces createIsosurfaces(Device &device, const IsosurfaceSettings &settings setObjectParam(handle, "volume", settings.volume); setObjectParam(handle, "isovalue", settings.isovalues); - commitObject(handle); + commitObject(device, handle); return isosurfaces; } diff --git a/src/brayns/core/engine/ImageOperation.cpp b/src/brayns/core/engine/ImageOperation.cpp index 9f50ab090..cfdba697e 100644 --- a/src/brayns/core/engine/ImageOperation.cpp +++ b/src/brayns/core/engine/ImageOperation.cpp @@ -21,6 +21,21 @@ #include "ImageOperation.h" +namespace +{ +using namespace brayns; + +void setToneMapperParams(OSPImageOperation handle, const ToneMapperSettings &settings) +{ + setObjectParam(handle, "exposure", settings.exposure); + setObjectParam(handle, "contrast", settings.contrast); + setObjectParam(handle, "shoulder", settings.shoulder); + setObjectParam(handle, "midIn", settings.midIn); + setObjectParam(handle, "midOut", settings.midOut); + setObjectParam(handle, "acesColor", settings.acesColor); +} +} + namespace brayns { ToneMapper createToneMapper(Device &device, const ToneMapperSettings &settings) @@ -28,15 +43,17 @@ ToneMapper createToneMapper(Device &device, const ToneMapperSettings &settings) auto handle = ospNewImageOperation("tonemapper"); auto toneMapper = wrapObjectHandleAs(device, handle); - setObjectParam(handle, "exposure", settings.exposure); - setObjectParam(handle, "contrast", settings.contrast); - setObjectParam(handle, "shoulder", settings.hightlightCompression); - setObjectParam(handle, "midIn", settings.midLevelAnchorInput); - setObjectParam(handle, "midOut", settings.midLevelAnchorOutput); - setObjectParam(handle, "acesColor", settings.aces); + setToneMapperParams(handle, settings); - commitObject(handle); + commitObject(device, handle); return toneMapper; } + +void ToneMapper::update(const ToneMapperSettings &settings) +{ + auto handle = getHandle(); + setToneMapperParams(handle, settings); + commitObject(handle); +} } diff --git a/src/brayns/core/engine/ImageOperation.h b/src/brayns/core/engine/ImageOperation.h index 2480c7597..bfc12dd4b 100644 --- a/src/brayns/core/engine/ImageOperation.h +++ b/src/brayns/core/engine/ImageOperation.h @@ -37,17 +37,19 @@ struct ToneMapperSettings { float exposure = 1.0F; float contrast = 1.6773F; - float hightlightCompression = 0.9714F; - float midLevelAnchorInput = 0.18F; - float midLevelAnchorOutput = 0.18F; - float maxHdr = 11.0785F; - bool aces = true; + float shoulder = 0.9714F; + float midIn = 0.18F; + float midOut = 0.18F; + float hdrMax = 11.0785F; + bool acesColor = true; }; class ToneMapper : public ImageOperation { public: using ImageOperation::ImageOperation; + + void update(const ToneMapperSettings &settings); }; ToneMapper createToneMapper(Device &device, const ToneMapperSettings &settings); diff --git a/src/brayns/core/engine/Light.cpp b/src/brayns/core/engine/Light.cpp index c47a725fd..081462563 100644 --- a/src/brayns/core/engine/Light.cpp +++ b/src/brayns/core/engine/Light.cpp @@ -45,7 +45,7 @@ DistantLight createDistantLight(Device &device, const DistantLightSettings &sett setObjectParam(handle, "direction", settings.direction); setObjectParam(handle, "angularDiameter", settings.angularDiameter); - commitObject(handle); + commitObject(device, handle); return light; } @@ -60,7 +60,7 @@ SphereLight createSphereLight(Device &device, const SphereLightSettings &setting setObjectParam(handle, "position", settings.position); setObjectParam(handle, "radius", settings.radius); - commitObject(handle); + commitObject(device, handle); return light; } @@ -79,7 +79,7 @@ SpotLight createSpotLight(Device &device, const SpotLightSettings &settings) setObjectParam(handle, "radius", settings.outerRadius); setObjectParam(handle, "innerRadius", settings.innerRadius); - commitObject(handle); + commitObject(device, handle); return light; } @@ -95,7 +95,7 @@ QuadLight createQuadLight(Device &device, const QuadLightSettings &settings) setObjectParam(handle, "edge1", settings.edge1); setObjectParam(handle, "edge2", settings.edge2); - commitObject(handle); + commitObject(device, handle); return light; } @@ -111,7 +111,7 @@ CylinderLight createCylinderLight(Device &device, const CylinderLightSettings &s setObjectParam(handle, "position1", settings.end); setObjectParam(handle, "radius", settings.radius); - commitObject(handle); + commitObject(device, handle); return light; } @@ -127,7 +127,7 @@ HdriLight createHdriLight(Device &device, const HdriLightSettings &settings) setObjectParam(handle, "direction", settings.direction); setObjectParam(handle, "map", settings.map); - commitObject(handle); + commitObject(device, handle); return light; } @@ -139,7 +139,7 @@ AmbientLight createAmbientLight(Device &device, const AmbientLightSettings &sett setLightParams(handle, settings.base); - commitObject(handle); + commitObject(device, handle); return light; } @@ -157,7 +157,7 @@ SunSkyLight createSunSkyLight(Device &device, const SunSkyLightSettings &setting setObjectParam(handle, "albedo", settings.albedo); setObjectParam(handle, "horizonExtension", settings.horizonExtension); - commitObject(handle); + commitObject(device, handle); return light; } diff --git a/src/brayns/core/engine/Material.cpp b/src/brayns/core/engine/Material.cpp index 31dd75c84..9666c0d4d 100644 --- a/src/brayns/core/engine/Material.cpp +++ b/src/brayns/core/engine/Material.cpp @@ -79,7 +79,7 @@ AoMaterial createAoMaterial(Device &device, const AoMaterialSettings &settings) setMaterialParam(handle, "kd", settings.diffuse); setMaterialParam(handle, "d", settings.opacity); - commitObject(handle); + commitObject(device, handle); return material; } @@ -95,7 +95,7 @@ ScivisMaterial createScivisMaterial(Device &device, const ScivisMaterialSettings setMaterialParam(handle, "ns", settings.shininess); setObjectParam(handle, "tf", settings.transparencyFilter); - commitObject(handle); + commitObject(device, handle); return material; } @@ -135,7 +135,7 @@ PrincipledMaterial createPrincipledMaterial(Device &device, const PrincipledMate setMaterialParam(handle, "opacity", settings.opacity); setMaterialParam(handle, "emissiveColor", settings.emissiveColor); - commitObject(handle); + commitObject(device, handle); return material; } diff --git a/src/brayns/core/engine/Object.h b/src/brayns/core/engine/Object.h index e2ce8a549..d2d869898 100644 --- a/src/brayns/core/engine/Object.h +++ b/src/brayns/core/engine/Object.h @@ -158,6 +158,12 @@ inline void commitObject(OSPObject handle) ospCommit(handle); } +inline void commitObject(Device &device, OSPObject handle) +{ + ospCommit(handle); + device.throwIfError(); +} + inline void removeObjectParam(OSPObject handle, const char *id) { ospRemoveParam(handle, id); diff --git a/src/brayns/core/engine/Renderer.cpp b/src/brayns/core/engine/Renderer.cpp index 02ced4345..2545907a4 100644 --- a/src/brayns/core/engine/Renderer.cpp +++ b/src/brayns/core/engine/Renderer.cpp @@ -25,7 +25,7 @@ namespace { using namespace brayns; -void setBackgroundParam(OSPRenderer handle, const Color4 &color) +void setBackgroundParam(OSPRenderer handle, const auto &color) { setObjectParam(handle, "backgroundColor", color); } @@ -44,9 +44,9 @@ void setRendererParams(OSPRenderer handle, const RendererSettings &settings) { setObjectParam(handle, "material", settings.materials); - setObjectParam(handle, "pixelSamples", static_cast(settings.pixelSamples)); - setObjectParam(handle, "maxPathLength", static_cast(settings.maxRayRecursionDepth)); - setObjectParam(handle, "minContribution", settings.minSampleContribution); + setObjectParam(handle, "pixelSamples", static_cast(settings.samples)); + setObjectParam(handle, "maxPathLength", static_cast(settings.maxRecursion)); + setObjectParam(handle, "minContribution", settings.minContribution); setObjectParam(handle, "varianceThreshold", settings.varianceThreshold); setBackground(handle, settings.background); @@ -73,7 +73,7 @@ AoRenderer createAoRenderer(Device &device, const AoRendererSettings &settings) setObjectParam(handle, "aoIntensity", settings.aoIntensity); setObjectParam(handle, "volumeSamplingRate", settings.volumeSamplingRate); - commitObject(handle); + commitObject(device, handle); return renderer; } @@ -91,7 +91,7 @@ ScivisRenderer createScivisRenderer(Device &device, const ScivisRendererSettings setObjectParam(handle, "volumeSamplingRate", settings.volumeSamplingRate); setObjectParam(handle, "visibleLights", settings.showVisibleLights); - commitObject(handle); + commitObject(device, handle); return renderer; } @@ -103,7 +103,7 @@ PathTracer createPathTracer(Device &device, const PathTracerSettings &settings) setRendererParams(handle, settings.base); - commitObject(handle); + commitObject(device, handle); return renderer; } diff --git a/src/brayns/core/engine/Renderer.h b/src/brayns/core/engine/Renderer.h index 100bb1a5e..6bce100d8 100644 --- a/src/brayns/core/engine/Renderer.h +++ b/src/brayns/core/engine/Renderer.h @@ -41,14 +41,14 @@ enum class PixelFilter BlackmanHarris = OSP_PIXELFILTER_BLACKMAN_HARRIS, }; -using Background = std::variant; +using Background = std::variant; struct RendererSettings { Data materials; - std::size_t pixelSamples = 1; - std::size_t maxRayRecursionDepth = 20; - float minSampleContribution = 0.001F; + std::size_t samples = 1; + std::size_t maxRecursion = 20; + float minContribution = 0.001F; float varianceThreshold = 0.0F; Background background = Color4(0.0F, 0.0F, 0.0F, 0.0F); std::optional maxDepth = std::nullopt; diff --git a/src/brayns/core/engine/Texture.cpp b/src/brayns/core/engine/Texture.cpp index ee6276233..241f80c20 100644 --- a/src/brayns/core/engine/Texture.cpp +++ b/src/brayns/core/engine/Texture.cpp @@ -33,7 +33,7 @@ Texture2D createTexture2D(Device &device, const Texture2DSettings &settings) setObjectParam(handle, "data", settings.data); setObjectParam(handle, "wrapMode", static_cast(settings.wrap)); - commitObject(handle); + commitObject(device, handle); return texture; } @@ -46,7 +46,7 @@ VolumeTexture createVolumeTexture(Device &device, const VolumeTextureSettings &s setObjectParam(handle, "volume", settings.volume); setObjectParam(handle, "transferFunction", settings.transferFunction); - commitObject(handle); + commitObject(device, handle); return texture; } diff --git a/src/brayns/core/engine/TransferFunction.cpp b/src/brayns/core/engine/TransferFunction.cpp index 8f6a9632c..c35a22ef5 100644 --- a/src/brayns/core/engine/TransferFunction.cpp +++ b/src/brayns/core/engine/TransferFunction.cpp @@ -21,13 +21,12 @@ #include "TransferFunction.h" -namespace brayns -{ -LinearTransferFunction createLinearTransferFunction(Device &device, const LinearTransferFunctionSettings &settings) +namespace { - auto handle = ospNewTransferFunction("piecewiseLinear"); - auto transferFunction = wrapObjectHandleAs(device, handle); +using namespace brayns; +void setLinearTransferFunctionParams(OSPTransferFunction handle, const LinearTransferFunctionSettings &settings) +{ setObjectParam(handle, "value", settings.scalarRange); auto stride = static_cast(sizeof(Color4)); @@ -39,9 +38,27 @@ LinearTransferFunction createLinearTransferFunction(Device &device, const Linear auto offset = sizeof(Color3); auto opacities = createDataView(settings.colors, {colorCount, stride, offset}); setObjectParam(handle, "opacity", opacities); +} +} - commitObject(handle); +namespace brayns +{ +LinearTransferFunction createLinearTransferFunction(Device &device, const LinearTransferFunctionSettings &settings) +{ + auto handle = ospNewTransferFunction("piecewiseLinear"); + auto transferFunction = wrapObjectHandleAs(device, handle); + + setLinearTransferFunctionParams(handle, settings); + + commitObject(device, handle); return transferFunction; } + +void LinearTransferFunction::update(const LinearTransferFunctionSettings &settings) +{ + auto handle = getHandle(); + setLinearTransferFunctionParams(handle, settings); + commitObject(handle); +} } diff --git a/src/brayns/core/engine/TransferFunction.h b/src/brayns/core/engine/TransferFunction.h index e5f381b9c..df02a19cd 100644 --- a/src/brayns/core/engine/TransferFunction.h +++ b/src/brayns/core/engine/TransferFunction.h @@ -43,6 +43,8 @@ class LinearTransferFunction : public TransferFunction { public: using TransferFunction::TransferFunction; + + void update(const LinearTransferFunctionSettings &settings); }; LinearTransferFunction createLinearTransferFunction(Device &device, const LinearTransferFunctionSettings &settings); diff --git a/src/brayns/core/engine/Volume.cpp b/src/brayns/core/engine/Volume.cpp index 2357e31f9..9a3c95030 100644 --- a/src/brayns/core/engine/Volume.cpp +++ b/src/brayns/core/engine/Volume.cpp @@ -21,19 +21,38 @@ #include "Volume.h" +namespace +{ +using namespace brayns; + +void setRegularVolumeParams(OSPVolume handle, const RegularVolumeSettings &settings) +{ + setObjectParam(handle, "gridOrigin", settings.origin); + setObjectParam(handle, "gridSpacing", settings.spacing); + setObjectParam(handle, "cellCentered", settings.type == VolumeType::CellCentered); + setObjectParam(handle, "filter", static_cast(settings.filter)); + setObjectParam(handle, "background", settings.background); +} +} + namespace brayns { -RegularVolume createRegularVolume(Device &device, const RegularVolumeSettings &settings) +void RegularVolume::update(const RegularVolumeSettings &settings) +{ + auto handle = getHandle(); + setRegularVolumeParams(handle, settings); + commitObject(handle); +} + +RegularVolume createRegularVolume(Device &device, const RegularVolumeData &data, const RegularVolumeSettings &settings) { auto handle = ospNewVolume("structuredRegular"); auto volume = wrapObjectHandleAs(device, handle); - setObjectParam(handle, "data", settings.data); - setObjectParam(handle, "cellCentered", settings.voxelType == VoxelType::CellCentered); - setObjectParam(handle, "filter", static_cast(settings.filter)); - setObjectParam(handle, "background", settings.background); + setObjectParam(handle, "data", data); + setRegularVolumeParams(handle, settings); - commitObject(handle); + commitObject(device, handle); return volume; } diff --git a/src/brayns/core/engine/Volume.h b/src/brayns/core/engine/Volume.h index 4dfe37dd1..f30c79a13 100644 --- a/src/brayns/core/engine/Volume.h +++ b/src/brayns/core/engine/Volume.h @@ -21,7 +21,7 @@ #pragma once -#include +#include #include "Data.h" #include "Device.h" @@ -42,9 +42,9 @@ enum class VolumeFilter Cubic = OSP_VOLUME_FILTER_CUBIC, }; -using VolumeData = std::variant, Data3D, Data3D, Data3D>; +using RegularVolumeData = std::variant, Data3D, Data3D, Data3D>; -enum class VoxelType +enum class VolumeType { CellCentered, VertexCentered, @@ -52,17 +52,23 @@ enum class VoxelType struct RegularVolumeSettings { - VolumeData data; - VoxelType voxelType = VoxelType::VertexCentered; + Vector3 origin = {0.0F, 0.0F, 0.0F}; + Vector3 spacing = {1.0F, 1.0F, 1.0F}; + VolumeType type = VolumeType::VertexCentered; VolumeFilter filter = VolumeFilter::Linear; - float background = std::numeric_limits::quiet_NaN(); + std::optional background = std::nullopt; }; class RegularVolume : public Volume { public: using Volume::Volume; + + void update(const RegularVolumeSettings &settings); }; -RegularVolume createRegularVolume(Device &device, const RegularVolumeSettings &settings); +RegularVolume createRegularVolume( + Device &device, + const RegularVolumeData &data, + const RegularVolumeSettings &settings = {}); } diff --git a/src/brayns/core/engine/VolumetricModel.cpp b/src/brayns/core/engine/VolumetricModel.cpp index 346bb5825..460bfc095 100644 --- a/src/brayns/core/engine/VolumetricModel.cpp +++ b/src/brayns/core/engine/VolumetricModel.cpp @@ -34,7 +34,7 @@ VolumetricModel createVolumetricModel(Device &device, const VolumetricModelSetti setObjectParam(handle, "anisotropy", settings.anisotropy); setObjectParam(handle, "id", settings.id); - commitObject(handle); + commitObject(device, handle); return model; } diff --git a/src/brayns/core/engine/World.cpp b/src/brayns/core/engine/World.cpp index c2108ffe6..1fe4e0c8a 100644 --- a/src/brayns/core/engine/World.cpp +++ b/src/brayns/core/engine/World.cpp @@ -39,7 +39,7 @@ Group createGroup(Device &device, const GroupSettings &settings) setObjectParam(handle, "volume", settings.volumes); setObjectParam(handle, "light", settings.lights); - commitObject(handle); + commitObject(device, handle); return group; } @@ -59,7 +59,7 @@ Instance createInstance(Device &device, const InstanceSettings &settings) setObjectParam(handle, "transform", toAffine(settings.transform)); setObjectParam(handle, "id", settings.id); - commitObject(handle); + commitObject(device, handle); return instance; } @@ -77,7 +77,7 @@ World createWorld(Device &device, const WorldSettings &settings) setObjectParam(handle, "instance", settings.instances); - commitObject(handle); + commitObject(device, handle); return world; } diff --git a/src/brayns/core/json/Json.h b/src/brayns/core/json/Json.h index cea00a76a..480ea929a 100644 --- a/src/brayns/core/json/Json.h +++ b/src/brayns/core/json/Json.h @@ -27,10 +27,13 @@ #include "JsonValue.h" #include "types/Arrays.h" +#include "types/Constants.h" #include "types/Enums.h" #include "types/Maps.h" #include "types/Math.h" #include "types/Objects.h" #include "types/Primitives.h" #include "types/Schema.h" +#include "types/Sets.h" #include "types/Variants.h" +#include "types/Vectors.h" diff --git a/src/brayns/core/json/JsonSchema.cpp b/src/brayns/core/json/JsonSchema.cpp index a36c15309..3149bd65c 100644 --- a/src/brayns/core/json/JsonSchema.cpp +++ b/src/brayns/core/json/JsonSchema.cpp @@ -25,20 +25,6 @@ namespace brayns { -EnumInfo EnumReflector::reflect() -{ - auto builder = EnumBuilder(); - builder.field("undefined", JsonType::Undefined); - builder.field("null", JsonType::Null); - builder.field("boolean", JsonType::Boolean); - builder.field("integer", JsonType::Integer); - builder.field("number", JsonType::Number); - builder.field("string", JsonType::String); - builder.field("array", JsonType::Array); - builder.field("object", JsonType::Object); - return builder.build(); -} - JsonType getJsonType(const JsonValue &json) { if (json.isEmpty()) diff --git a/src/brayns/core/json/JsonSchema.h b/src/brayns/core/json/JsonSchema.h index 898cb1023..f93a2ffca 100644 --- a/src/brayns/core/json/JsonSchema.h +++ b/src/brayns/core/json/JsonSchema.h @@ -49,7 +49,19 @@ enum class JsonType template<> struct EnumReflector { - static EnumInfo reflect(); + static auto reflect() + { + auto builder = EnumBuilder(); + builder.field("undefined", JsonType::Undefined); + builder.field("null", JsonType::Null); + builder.field("boolean", JsonType::Boolean); + builder.field("integer", JsonType::Integer); + builder.field("number", JsonType::Number); + builder.field("string", JsonType::String); + builder.field("array", JsonType::Array); + builder.field("object", JsonType::Object); + return builder.build(); + } }; constexpr bool isNumeric(JsonType type) @@ -105,7 +117,7 @@ struct JsonTypeReflector }; template -constexpr JsonType jsonTypeOf = JsonTypeReflector::type; +constexpr JsonType jsonTypeOf = JsonTypeReflector>::type; JsonType getJsonType(const JsonValue &json); @@ -152,10 +164,11 @@ struct JsonSchema JsonValue defaultValue = {}; std::vector oneOf = {}; JsonType type = JsonType::Undefined; - std::string constant = {}; + JsonValue constant = {}; std::optional minimum = {}; std::optional maximum = {}; std::vector items = {}; + bool uniqueItems = false; std::optional minItems = {}; std::optional maxItems = {}; std::map properties = {}; diff --git a/src/brayns/core/json/JsonValidator.cpp b/src/brayns/core/json/JsonValidator.cpp index 5dade5095..db8af8ed2 100644 --- a/src/brayns/core/json/JsonValidator.cpp +++ b/src/brayns/core/json/JsonValidator.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -31,7 +32,7 @@ namespace { using namespace brayns; -class ErrorContext +class ErrorBuilder { public: void push(JsonPathItem item) @@ -59,70 +60,119 @@ class ErrorContext std::vector _errors; }; -void check(const JsonValue &json, const JsonSchema &schema, ErrorContext &errors); +void check(const JsonValue &json, const JsonSchema &schema, ErrorBuilder &errors); -void checkOneOf(const JsonValue &json, const JsonSchema &schema, ErrorContext &errors) +void checkOneOf(const JsonValue &json, const JsonSchema &schema, ErrorBuilder &errors) { for (const auto &oneof : schema.oneOf) { auto suberrors = validate(json, oneof); + if (suberrors.empty()) { return; } } - errors.add(InvalidOneOf{}); + + errors.add(InvalidOneOf{json}); } -bool checkType(const JsonValue &json, const JsonSchema &schema, ErrorContext &errors) +bool checkType(const JsonValue &json, const JsonSchema &schema, ErrorBuilder &errors) { auto required = RequiredJsonType{schema.type}; auto type = getJsonType(json); + if (required.isCompatible(type)) { return true; } + errors.add(InvalidType{type, required.value}); + + return false; +} + +bool checkConst(const JsonValue &value, const JsonSchema &schema) +{ + if (value.isBoolean() && schema.constant.isBoolean()) + { + return value.extract() == schema.constant.extract(); + } + + if (value.isNumeric() && schema.constant.isNumeric()) + { + return value == schema.constant; + } + + if (value.isString() && schema.constant.isString()) + { + return value == schema.constant; + } + return false; } -void checkConst(const std::string &value, const JsonSchema &schema, ErrorContext &errors) +void checkConst(const JsonValue &value, const JsonSchema &schema, ErrorBuilder &errors) { - if (value != schema.constant) + if (!checkConst(value, schema)) { errors.add(InvalidConst{value, schema.constant}); } } -void checkRange(double value, const JsonSchema &schema, ErrorContext &errors) +void checkRange(double value, const JsonSchema &schema, ErrorBuilder &errors) { if (schema.minimum && value < *schema.minimum) { errors.add(BelowMinimum{value, *schema.minimum}); } + if (schema.maximum && value > *schema.maximum) { errors.add(AboveMaximum{value, *schema.maximum}); } } -void checkItemCount(std::size_t count, const JsonSchema &schema, ErrorContext &errors) +void checkUniqueItems(const JsonArray &array, const JsonSchema &schema, ErrorBuilder &errors) +{ + if (!schema.uniqueItems) + { + return; + } + + auto values = std::unordered_set(); + + for (const auto &item : array) + { + auto value = item.toString(); + auto [i, inserted] = values.insert(value); + + if (!inserted) + { + errors.add(DuplicatedItem{std::move(value)}); + } + } +} + +void checkItemCount(std::size_t count, const JsonSchema &schema, ErrorBuilder &errors) { if (schema.minItems && count < *schema.minItems) { errors.add(NotEnoughItems{count, *schema.minItems}); } + if (schema.maxItems && count > *schema.maxItems) { errors.add(TooManyItems{count, *schema.maxItems}); } } -void checkArrayItems(const JsonArray &array, const JsonSchema &schema, ErrorContext &errors) +void checkArrayItems(const JsonArray &array, const JsonSchema &schema, ErrorBuilder &errors) { const auto &itemSchema = schema.items.at(0); auto index = std::size_t(0); + for (const auto &value : array) { errors.push(index); @@ -133,7 +183,7 @@ void checkArrayItems(const JsonArray &array, const JsonSchema &schema, ErrorCont } } -void checkMapItems(const JsonObject &object, const JsonSchema &schema, ErrorContext &errors) +void checkMapItems(const JsonObject &object, const JsonSchema &schema, ErrorBuilder &errors) { const auto &itemSchema = schema.items.at(0); @@ -145,7 +195,7 @@ void checkMapItems(const JsonObject &object, const JsonSchema &schema, ErrorCont } } -void checkRequiredProperties(const JsonObject &object, const JsonSchema &schema, ErrorContext &errors) +void checkRequiredProperties(const JsonObject &object, const JsonSchema &schema, ErrorBuilder &errors) { for (const auto &[key, property] : schema.properties) { @@ -153,15 +203,17 @@ void checkRequiredProperties(const JsonObject &object, const JsonSchema &schema, { continue; } + if (object.has(key)) { continue; } + errors.add(MissingRequiredProperty{key}); } } -void checkUnknownProperties(const JsonObject &object, const JsonSchema &schema, ErrorContext &errors) +void checkUnknownProperties(const JsonObject &object, const JsonSchema &schema, ErrorBuilder &errors) { for (const auto &[key, value] : object) { @@ -169,11 +221,12 @@ void checkUnknownProperties(const JsonObject &object, const JsonSchema &schema, { continue; } + errors.add(UnknownProperty{key}); } } -void checkProperties(const JsonObject &object, const JsonSchema &schema, ErrorContext &errors) +void checkProperties(const JsonObject &object, const JsonSchema &schema, ErrorBuilder &errors) { for (const auto &[key, itemSchema] : schema.properties) { @@ -181,54 +234,61 @@ void checkProperties(const JsonObject &object, const JsonSchema &schema, ErrorCo { continue; } + errors.push(key); check(object.get(key), itemSchema, errors); errors.pop(); } } -void checkObject(const JsonObject &object, const JsonSchema &schema, ErrorContext &errors) +void checkObject(const JsonObject &object, const JsonSchema &schema, ErrorBuilder &errors) { if (!schema.items.empty()) { checkMapItems(object, schema, errors); return; } + checkUnknownProperties(object, schema, errors); checkRequiredProperties(object, schema, errors); checkProperties(object, schema, errors); } -void check(const JsonValue &json, const JsonSchema &schema, ErrorContext &errors) +void check(const JsonValue &json, const JsonSchema &schema, ErrorBuilder &errors) { if (!schema.oneOf.empty()) { checkOneOf(json, schema, errors); return; } + if (!checkType(json, schema, errors)) { return; } - if (!schema.constant.empty()) + + if (!schema.constant.isEmpty()) { - const auto &value = json.extract(); - checkConst(value, schema, errors); + checkConst(json, schema, errors); return; } + if (isNumeric(schema.type)) { auto value = json.convert(); checkRange(value, schema, errors); return; } + if (schema.type == JsonType::Array) { const auto &value = getArray(json); + checkUniqueItems(value, schema, errors); checkItemCount(value.size(), schema, errors); checkArrayItems(value, schema, errors); return; } + if (schema.type == JsonType::Object) { const auto &object = getObject(json); @@ -291,6 +351,11 @@ std::string toString(const AboveMaximum &error) return fmt::format("Value above maximum: {} > {}", error.value, error.maximum); } +std::string toString(const DuplicatedItem &error) +{ + return fmt::format("Duplicated item: '{}'", error.value); +} + std::string toString(const NotEnoughItems &error) { return fmt::format("Not enough items: {} < {}", error.count, error.minItems); @@ -311,9 +376,9 @@ std::string toString(const UnknownProperty &error) return fmt::format("Unknown property: '{}'", error.name); } -std::string toString(const InvalidOneOf &) +std::string toString(const InvalidOneOf &error) { - return "Invalid oneOf"; + return fmt::format("Invalid oneOf: {}", stringify(error.value)); } std::string toString(const JsonError &error) @@ -323,7 +388,7 @@ std::string toString(const JsonError &error) std::vector validate(const JsonValue &json, const JsonSchema &schema) { - auto errors = ErrorContext(); + auto errors = ErrorBuilder(); check(json, schema, errors); return errors.build(); } diff --git a/src/brayns/core/json/JsonValidator.h b/src/brayns/core/json/JsonValidator.h index fb9ac1258..b96ae6990 100644 --- a/src/brayns/core/json/JsonValidator.h +++ b/src/brayns/core/json/JsonValidator.h @@ -67,6 +67,13 @@ struct AboveMaximum std::string toString(const AboveMaximum &error); +struct DuplicatedItem +{ + std::string value; +}; + +std::string toString(const DuplicatedItem &error); + struct NotEnoughItems { std::size_t count; @@ -99,6 +106,7 @@ std::string toString(const UnknownProperty &error); struct InvalidOneOf { + JsonValue value; }; std::string toString(const InvalidOneOf &error); @@ -108,6 +116,7 @@ using JsonError = std::variant< InvalidConst, AboveMaximum, BelowMinimum, + DuplicatedItem, TooManyItems, NotEnoughItems, MissingRequiredProperty, diff --git a/src/brayns/core/json/types/Arrays.h b/src/brayns/core/json/types/Arrays.h index e2a6817f7..b81b48107 100644 --- a/src/brayns/core/json/types/Arrays.h +++ b/src/brayns/core/json/types/Arrays.h @@ -45,23 +45,28 @@ struct JsonArrayReflector static JsonValue serialize(const T &value) { auto array = createJsonArray(); + for (const auto &item : value) { auto jsonItem = serializeToJson(item); array->add(jsonItem); } + return array; } static T deserialize(const JsonValue &json) { const auto &array = getArray(json); + auto value = T(); + for (const auto &jsonItem : array) { auto item = deserializeAs(jsonItem); value.push_back(std::move(item)); } + return value; } }; diff --git a/src/brayns/core/json/types/Constants.h b/src/brayns/core/json/types/Constants.h new file mode 100644 index 000000000..8dce4992e --- /dev/null +++ b/src/brayns/core/json/types/Constants.h @@ -0,0 +1,68 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include + +#include "Primitives.h" + +namespace brayns +{ +struct JsonConstant +{ + auto operator<=>(const JsonConstant &) const = default; +}; + +template +concept ValidJsonConstant = std::derived_from && JsonPrimitive; + +template +struct JsonReflector +{ + using Type = decltype(T::value); + + static JsonSchema getSchema() + { + return {.type = jsonTypeOf, .constant = T::value}; + } + + static JsonValue serialize(const T &) + { + return serializeToJson(T::value); + } + + static T deserialize(const JsonValue &json) + { + if (json != T::value) + { + throw JsonException("Invalid const"); + } + + return {}; + } +}; + +struct JsonFalse : JsonConstant +{ + static constexpr auto value = false; +}; +} diff --git a/src/brayns/core/json/types/Math.h b/src/brayns/core/json/types/Math.h index 3dc168e8b..c8e9887e5 100644 --- a/src/brayns/core/json/types/Math.h +++ b/src/brayns/core/json/types/Math.h @@ -23,81 +23,49 @@ #include -#include "Primitives.h" +#include "Objects.h" +#include "Vectors.h" namespace brayns { template -struct JsonMathReflector +struct JsonBoxReflector { - using ValueType = typename T::Scalar; - - static inline constexpr auto itemCount = sizeof(T) / sizeof(ValueType); - - static JsonSchema getSchema() - { - return { - .type = JsonType::Array, - .items = {getJsonSchema()}, - .minItems = itemCount, - .maxItems = itemCount, - }; - } - - static JsonValue serialize(const T &value) - { - auto array = createJsonArray(); - for (auto i = std::size_t(0); i < itemCount; ++i) - { - const auto &item = getItem(value, i); - auto jsonItem = serializeToJson(item); - array->add(jsonItem); - } - return array; - } - - static T deserialize(const JsonValue &json) - { - const auto &array = getArray(json); - auto value = T(); - if (array.size() != itemCount) - { - throw JsonException("Invalid static array size"); - } - auto i = std::size_t(0); - for (const auto &jsonItem : array) - { - auto &item = getItem(value, i); - item = deserializeAs(jsonItem); - ++i; - } - return value; - } - -private: - static auto &getItem(auto &value, std::size_t index) + static auto reflect() { - return value[index]; - } - - static auto &getItem(const Quaternion &value, std::size_t index) - { - return (&value.i)[index]; + auto builder = JsonBuilder(); + builder.field("min", [](auto &object) { return &object.lower; }).description("Lower bounds"); + builder.field("max", [](auto &object) { return &object.upper; }).description("Upper bounds"); + return builder.build(); } +}; - static auto &getItem(Quaternion &value, std::size_t index) - { - return (&value.i)[index]; - } +template +struct JsonObjectReflector> : JsonBoxReflector> +{ }; template -struct JsonReflector> : JsonMathReflector> +struct JsonObjectReflector> : JsonBoxReflector> { }; template<> -struct JsonReflector : JsonMathReflector +struct JsonObjectReflector { + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("translation", [](auto &object) { return &object.translation; }) + .description("Translation XYZ") + .defaultValue(Vector3(0.0F, 0.0F, 0.0F)); + builder.field("rotation", [](auto &object) { return &object.rotation; }) + .description("Rotation quaternion XYZW") + .defaultValue(Quaternion(1.0F, 0.0F, 0.0F, 0.0F)); + builder.field("scale", [](auto &object) { return &object.scale; }) + .description("Scale XYZ") + .defaultValue(Vector3(1.0F, 1.0F, 1.0F)); + return builder.build(); + } }; } diff --git a/src/brayns/core/json/types/Objects.h b/src/brayns/core/json/types/Objects.h index 721c85406..2ac837b2c 100644 --- a/src/brayns/core/json/types/Objects.h +++ b/src/brayns/core/json/types/Objects.h @@ -29,6 +29,8 @@ #include +#include "Primitives.h" + namespace brayns { template @@ -147,56 +149,67 @@ struct JsonReflector } }; -template class JsonFieldBuilder { public: - explicit JsonFieldBuilder(JsonField &field): - _field(&field) + explicit JsonFieldBuilder(JsonSchema &schema): + _schema(&schema) { } JsonFieldBuilder description(std::string value) { - _field->schema.description = std::move(value); + _schema->description = std::move(value); return *this; } JsonFieldBuilder required(bool value) { - _field->schema.required = value; + _schema->required = value; return *this; } JsonFieldBuilder minimum(std::optional value) { - _field->schema.minimum = value; + _schema->minimum = value; return *this; } JsonFieldBuilder maximum(std::optional value) { - _field->schema.maximum = value; + _schema->maximum = value; + return *this; + } + + JsonFieldBuilder uniqueItems(bool value) + { + _schema->uniqueItems = value; return *this; } JsonFieldBuilder minItems(std::optional value) { - _field->schema.minItems = value; + _schema->minItems = value; return *this; } JsonFieldBuilder maxItems(std::optional value) { - _field->schema.maxItems = value; + _schema->maxItems = value; return *this; } - template + JsonFieldBuilder items() + { + assert(!_schema->items.empty()); + return JsonFieldBuilder(_schema->items[0]); + } + + template JsonFieldBuilder defaultValue(const T &value) { - _field->schema.defaultValue = serializeToJson(value); - _field->schema.required = false; + _schema->defaultValue = serializeToJson(value); + _schema->required = false; return *this; } @@ -206,7 +219,7 @@ class JsonFieldBuilder } private: - JsonField *_field; + JsonSchema *_schema; }; template @@ -229,7 +242,7 @@ class JsonBuilder } template U> - JsonFieldBuilder field(std::string name, U getFieldPtr) + JsonFieldBuilder field(std::string name, U getFieldPtr) { using FieldType = GetFieldType; @@ -251,31 +264,38 @@ class JsonBuilder value = deserializeAs(json); }; - return JsonFieldBuilder(field); + return JsonFieldBuilder(field.schema); } - JsonFieldBuilder constant(std::string name, const std::string &value) + template + JsonFieldBuilder constant(std::string name, const U &value) { auto &field = _fields.emplace_back(); field.name = std::move(name); - field.schema = getJsonSchema(); + field.schema = JsonSchema{.type = jsonTypeOf}; - field.schema.constant = value; + field.schema.constant = serializeToJson(value); field.serialize = [=](const auto &) { return value; }; field.deserialize = [=](const auto &json, auto &) { auto item = deserializeAs(json); + if (item != value) { throw JsonException("Invalid const"); } }; - return JsonFieldBuilder(field); + return JsonFieldBuilder(field.schema); + } + + JsonFieldBuilder constant(std::string name, const char *value) + { + return constant(std::move(name), std::string(value)); } JsonObjectInfo build() diff --git a/src/brayns/core/json/types/Primitives.h b/src/brayns/core/json/types/Primitives.h index 900a1e115..c2d187a45 100644 --- a/src/brayns/core/json/types/Primitives.h +++ b/src/brayns/core/json/types/Primitives.h @@ -37,12 +37,15 @@ struct JsonReflector static JsonSchema getSchema() { constexpr auto type = jsonTypeOf; + auto schema = JsonSchema{.type = type}; + if constexpr (isNumeric(type)) { schema.minimum = std::numeric_limits::lowest(); schema.maximum = std::numeric_limits::max(); } + return schema; } @@ -61,6 +64,7 @@ struct JsonReflector static T deserialize(const JsonValue &json) { throwIfNotCompatible(json); + if constexpr (std::is_same_v) { return json; diff --git a/src/brayns/core/json/types/Schema.cpp b/src/brayns/core/json/types/Schema.cpp index bc2e348e3..3af867764 100644 --- a/src/brayns/core/json/types/Schema.cpp +++ b/src/brayns/core/json/types/Schema.cpp @@ -156,7 +156,7 @@ JsonValue JsonReflector::serialize(const JsonSchema &schema) set(*object, "type", schema.type); } - if (!schema.constant.empty()) + if (!schema.constant.isEmpty()) { set(*object, "const", schema.constant); return object; diff --git a/src/brayns/core/json/types/Sets.h b/src/brayns/core/json/types/Sets.h new file mode 100644 index 000000000..b167c5d2d --- /dev/null +++ b/src/brayns/core/json/types/Sets.h @@ -0,0 +1,88 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include + +#include "Primitives.h" + +namespace brayns +{ +template +struct JsonSetReflector +{ + using ValueType = typename T::value_type; + + static JsonSchema getSchema() + { + return { + .type = JsonType::Array, + .items = {getJsonSchema()}, + .uniqueItems = true, + }; + } + + static JsonValue serialize(const T &value) + { + auto array = createJsonArray(); + + for (const auto &item : value) + { + auto jsonItem = serializeToJson(item); + array->add(jsonItem); + } + + return array; + } + + static T deserialize(const JsonValue &json) + { + const auto &array = getArray(json); + + auto value = T(); + + for (const auto &jsonItem : array) + { + auto item = deserializeAs(jsonItem); + auto [i, inserted] = value.insert(std::move(item)); + + if (!inserted) + { + throw JsonException("Duplicated item in set"); + } + } + + return value; + } +}; + +template +struct JsonReflector> : JsonSetReflector> +{ +}; + +template +struct JsonReflector> : JsonSetReflector> +{ +}; +} diff --git a/src/brayns/core/json/types/Vectors.h b/src/brayns/core/json/types/Vectors.h new file mode 100644 index 000000000..3bbcc6dd2 --- /dev/null +++ b/src/brayns/core/json/types/Vectors.h @@ -0,0 +1,110 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include + +#include "Primitives.h" + +namespace brayns +{ +template +struct JsonMathReflector +{ + using ValueType = typename T::Scalar; + + static inline constexpr auto itemCount = sizeof(T) / sizeof(ValueType); + + static JsonSchema getSchema() + { + return { + .type = JsonType::Array, + .items = {getJsonSchema()}, + .minItems = itemCount, + .maxItems = itemCount, + }; + } + + static JsonValue serialize(const T &value) + { + auto array = createJsonArray(); + + for (auto i = std::size_t(0); i < itemCount; ++i) + { + const auto &item = getItem(value, i); + auto jsonItem = serializeToJson(item); + + array->add(jsonItem); + } + return array; + } + + static T deserialize(const JsonValue &json) + { + const auto &array = getArray(json); + auto value = T(); + + if (array.size() != itemCount) + { + throw JsonException("Invalid static array size"); + } + + auto i = std::size_t(0); + + for (const auto &jsonItem : array) + { + auto &item = getItem(value, i); + + item = deserializeAs(jsonItem); + + ++i; + } + return value; + } + +private: + static auto &getItem(auto &value, std::size_t index) + { + return value[index]; + } + + static auto &getItem(const Quaternion &value, std::size_t index) + { + return (&value.i)[index]; + } + + static auto &getItem(Quaternion &value, std::size_t index) + { + return (&value.i)[index]; + } +}; + +template +struct JsonReflector> : JsonMathReflector> +{ +}; + +template<> +struct JsonReflector : JsonMathReflector +{ +}; +} diff --git a/src/brayns/core/objects/Messages.h b/src/brayns/core/manager/Messages.h similarity index 64% rename from src/brayns/core/objects/Messages.h rename to src/brayns/core/manager/Messages.h index 0335bea4f..45d5a8292 100644 --- a/src/brayns/core/objects/Messages.h +++ b/src/brayns/core/manager/Messages.h @@ -33,8 +33,8 @@ constexpr auto nullId = ObjectId(0); struct ObjectInfo { - ObjectId id; std::string type; + ObjectId id = nullId; JsonValue userData = {}; }; @@ -44,13 +44,12 @@ struct JsonObjectReflector static auto reflect() { auto builder = JsonBuilder(); + builder.field("type", [](auto &object) { return &object.type; }) + .description("Object type, use 'get{type}' to query detailed information about the object"); builder.field("id", [](auto &object) { return &object.id; }) .description("Object ID (starts at 1, uses 0 for objects that are not in registry)"); - builder.field("type", [](auto &object) { return &object.type; }) - .description("Object type, use 'get-{type}' to query detailed information about the object"); - builder.field("user_data", [](auto &object) { return &object.userData; }) - .description("Data set by user (not used by Brayns)") - .required(false); + builder.field("userData", [](auto &object) { return &object.userData; }) + .description("Data set by user (not used by Brayns)"); return builder.build(); } }; @@ -60,8 +59,6 @@ struct ObjectParams ObjectId id; }; -using ObjectResult = ObjectParams; - template<> struct JsonObjectReflector { @@ -73,22 +70,54 @@ struct JsonObjectReflector } }; +using ObjectResult = ObjectParams; + template struct UpdateParams { ObjectId id; - T properties; + T settings; }; -template +template struct JsonObjectReflector> { static auto reflect() { auto builder = JsonBuilder>(); builder.field("id", [](auto &object) { return &object.id; }).description("ID of the object to update"); - builder.field("properties", [](auto &object) { return &object.properties; }) - .description("New object properties to replace the old ones"); + builder.field("settings", [](auto &object) { return &object.settings; }) + .description("Settings to update the object"); + return builder.build(); + } +}; + +template +struct ComposedParams +{ + Base base; + Derived derived; +}; + +template +struct JsonObjectReflector> +{ + static auto reflect() + { + auto builder = JsonBuilder>(); + + if (!std::is_same_v) + { + builder.field("base", [](auto &object) { return &object.base; }) + .description("Base properties common to all derived objects (camera, renderer)"); + } + + if (!std::is_same_v) + { + builder.field("derived", [](auto &object) { return &object.derived; }) + .description("Derived properties that are specific to the object type (perspective, scivis)"); + } + return builder.build(); } }; diff --git a/src/brayns/core/objects/LockedObjects.h b/src/brayns/core/manager/ObjectManager.h similarity index 83% rename from src/brayns/core/objects/LockedObjects.h rename to src/brayns/core/manager/ObjectManager.h index 9efcc36bd..d56dfc9f0 100644 --- a/src/brayns/core/objects/LockedObjects.h +++ b/src/brayns/core/manager/ObjectManager.h @@ -26,20 +26,20 @@ #include -#include "ObjectManager.h" +#include "ObjectRegistry.h" namespace brayns { -class LockedObjects +class ObjectManager { public: - explicit LockedObjects(ObjectManager objects, Logger &logger): + explicit ObjectManager(ObjectRegistry objects, Logger &logger): _objects(std::move(objects)), _logger(&logger) { } - auto visit(std::invocable auto &&callable) -> decltype(callable(std::declval())) + auto visit(std::invocable auto &&callable) -> decltype(callable(std::declval())) { _logger->info("Waiting for object manager lock"); auto lock = std::lock_guard(_mutex); @@ -50,7 +50,7 @@ class LockedObjects private: std::mutex _mutex; - ObjectManager _objects; + ObjectRegistry _objects; Logger *_logger; }; } diff --git a/src/brayns/core/objects/ObjectManager.cpp b/src/brayns/core/manager/ObjectRegistry.cpp similarity index 59% rename from src/brayns/core/objects/ObjectManager.cpp rename to src/brayns/core/manager/ObjectRegistry.cpp index 942770220..532085b1d 100644 --- a/src/brayns/core/objects/ObjectManager.cpp +++ b/src/brayns/core/manager/ObjectRegistry.cpp @@ -19,20 +19,15 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "ObjectManager.h" +#include "ObjectRegistry.h" -#include +#include namespace { using namespace brayns; -void disableNullId(IdGenerator &ids) -{ - ids.next(); -} - -auto getStorageIterator(auto &objects, ObjectId id) +auto getIterator(auto &objects, ObjectId id) { auto i = objects.find(id); @@ -47,67 +42,71 @@ auto getStorageIterator(auto &objects, ObjectId id) namespace brayns { -ObjectManager::ObjectManager() +void ObjectRegistry::remove(ObjectId id) { - disableNullId(_ids); + auto i = getIterator(_objects, id); + + i->second->info.id = nullId; + + _objects.erase(i); + _ids.recycle(id); } -std::vector ObjectManager::getAllObjects() const +void ObjectRegistry::clear() { - auto objects = std::vector(); - objects.reserve(_objects.size()); - for (const auto &[id, object] : _objects) { - auto result = getObjectInfo(object); - objects.push_back(std::move(result)); + object->info.id = nullId; } - return objects; + _objects.clear(); + _ids.reset(); } -ObjectInfo ObjectManager::getObject(ObjectId id) const +ObjectInfo ObjectRegistry::get(ObjectId id) const { - const auto &interface = getInterface(id); - - return getObjectInfo(interface); + const auto &object = retreive(id); + return object->info; } -void ObjectManager::setUserData(ObjectId id, const JsonValue &userData) +std::vector ObjectRegistry::getAll() const { - auto i = getStorageIterator(_objects, id); + auto objects = std::vector(); + objects.reserve(_objects.size()); - i->second.setUserData(userData); + for (const auto &[id, object] : _objects) + { + objects.push_back(object->info); + } + + return objects; } -void ObjectManager::remove(ObjectId id) +void ObjectRegistry::update(ObjectId id, const JsonValue &userData) { - auto i = getStorageIterator(_objects, id); - - i->second.remove(); - - _objects.erase(i); - - _ids.recycle(id); + auto i = getIterator(_objects, id); + i->second->info.userData = userData; } -void ObjectManager::clear() +void ObjectRegistry::store(std::shared_ptr object) { - for (const auto &[id, object] : _objects) + auto id = _ids.next(); + + try { - object.remove(); + object->info.id = id; + _objects[id] = std::move(object); + } + catch (...) + { + _ids.recycle(id); + throw; } - - _objects.clear(); - - _ids = {}; - disableNullId(_ids); } -const ObjectInterface &ObjectManager::getInterface(ObjectId id) const +const std::shared_ptr &ObjectRegistry::retreive(ObjectId id) const { - auto i = getStorageIterator(_objects, id); - + auto i = getIterator(_objects, id); return i->second; } } diff --git a/src/brayns/core/manager/ObjectRegistry.h b/src/brayns/core/manager/ObjectRegistry.h new file mode 100644 index 000000000..ca226afda --- /dev/null +++ b/src/brayns/core/manager/ObjectRegistry.h @@ -0,0 +1,81 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +#include "Messages.h" +#include "UserObject.h" + +namespace brayns +{ +class ObjectRegistry +{ +public: + void remove(ObjectId id); + void clear(); + ObjectInfo get(ObjectId id) const; + std::vector getAll() const; + void update(ObjectId id, const JsonValue &userData); + + template + Stored add(T object, std::string type) + { + auto objectPtr = std::make_shared(std::move(object)); + auto info = ObjectInfo{std::move(type)}; + + auto wrapper = UserObject{std::move(info), objectPtr}; + auto wrapperPtr = std::make_shared(std::move(wrapper)); + + store(wrapperPtr); + + return Stored(std::move(wrapperPtr), std::move(objectPtr)); + } + + template + Stored getAsStored(ObjectId id) + { + const auto &wrapper = retreive(id); + const auto &object = castAsShared(wrapper->value, wrapper->info); + return Stored(wrapper, object); + } + + template + T &getAs(ObjectId id) + { + const auto &wrapper = retreive(id); + return *castAsShared(wrapper->value, wrapper->info); + } + +private: + std::map> _objects; + IdGenerator _ids{1}; + + void store(std::shared_ptr object); + const std::shared_ptr &retreive(ObjectId id) const; +}; +} diff --git a/src/brayns/core/manager/UserObject.h b/src/brayns/core/manager/UserObject.h new file mode 100644 index 000000000..69a6efa53 --- /dev/null +++ b/src/brayns/core/manager/UserObject.h @@ -0,0 +1,94 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +#include "Messages.h" + +namespace brayns +{ +struct UserObject +{ + ObjectInfo info; + std::any value; +}; + +template +class Stored +{ +public: + explicit Stored(std::shared_ptr wrapper, std::shared_ptr object): + _wrapper(std::move(wrapper)), + _object(std::move(object)) + { + } + + const ObjectInfo &getInfo() const + { + return _wrapper->info; + } + + ObjectId getId() const + { + return _wrapper->info.id; + } + + bool isRemoved() const + { + return getId() == nullId; + } + + T &get() const + { + return *_object; + } + +private: + std::shared_ptr _wrapper; + std::shared_ptr _object; +}; + +template +const std::shared_ptr &castAsShared(const std::any &value, const ObjectInfo &info) +{ + const auto *ptr = std::any_cast>(&value); + + if (ptr != nullptr) + { + return *ptr; + } + + throw InvalidParams(fmt::format("Invalid type for object with ID {}: {}", info.id, info.type)); +} + +template +T &castAs(const std::any &value, const ObjectInfo &info) +{ + return *castAsShared(value, info); +} +} diff --git a/src/brayns/core/objects/CameraObjects.cpp b/src/brayns/core/objects/CameraObjects.cpp new file mode 100644 index 000000000..4e64110dc --- /dev/null +++ b/src/brayns/core/objects/CameraObjects.cpp @@ -0,0 +1,160 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "CameraObjects.h" + +namespace +{ +using namespace brayns; + +template +auto getCameraAs(ObjectRegistry &objects, const ObjectParams ¶ms) +{ + auto stored = objects.getAsStored(params.id); + auto &derived = castAs(stored.get().value, stored.getInfo()); + return derived.settings; +} + +template +auto updateCameraAs(ObjectRegistry &objects, const UpdateParams ¶ms) +{ + auto stored = objects.getAsStored(params.id); + auto &base = stored.get(); + auto &derived = castAs(base.value, stored.getInfo()); + auto &device = base.device.get(); + + derived.value.update(params.settings); + device.throwIfError(); + + derived.settings = params.settings; +} +} + +namespace brayns +{ +CameraInfo getCamera(ObjectRegistry &objects, const ObjectParams ¶ms) +{ + auto &camera = objects.getAs(params.id); + return camera.settings; +} + +void updateCamera(ObjectRegistry &objects, const CameraUpdate ¶ms) +{ + auto &object = objects.getAs(params.id); + auto &device = object.device.get(); + auto camera = object.get(); + + camera.update(params.settings); + device.throwIfError(); + + object.settings = params.settings; +} + +ObjectResult createPerspectiveCamera(ObjectRegistry &objects, Device &device, const PerspectiveCameraParams ¶ms) +{ + auto camera = createPerspectiveCamera(device, params.base, params.derived); + + auto derived = UserPerspectiveCamera{params.derived, std::move(camera)}; + auto ptr = std::make_shared(std::move(derived)); + + auto object = UserCamera{ + .device = device, + .settings = params.base, + .value = ptr, + .get = [=] { return ptr->value; }, + .setAspect = [=](auto value) { ptr->value.setAspect(value); }, + }; + + auto stored = objects.add(std::move(object), "PerspectiveCamera"); + + return {stored.getId()}; +} + +PerspectiveCameraInfo getPerspectiveCamera(ObjectRegistry &objects, const ObjectParams ¶ms) +{ + return getCameraAs(objects, params); +} + +void updatePerspectiveCamera(ObjectRegistry &objects, const PerspectiveCameraUpdate ¶ms) +{ + updateCameraAs(objects, params); +} + +ObjectResult createOrthographicCamera(ObjectRegistry &objects, Device &device, const OrthographicCameraParams ¶ms) +{ + auto camera = createOrthographicCamera(device, params.base, params.derived); + + auto derived = UserOrthographicCamera{params.derived, std::move(camera)}; + auto ptr = std::make_shared(std::move(derived)); + + auto object = UserCamera{ + .device = device, + .settings = params.base, + .value = ptr, + .get = [=] { return ptr->value; }, + .setAspect = [=](auto value) { ptr->value.setAspect(value); }, + }; + + auto stored = objects.add(std::move(object), "OrthographicCamera"); + + return {stored.getId()}; +} + +OrthographicCameraInfo getOrthographicCamera(ObjectRegistry &objects, const ObjectParams ¶ms) +{ + return getCameraAs(objects, params); +} + +void updateOrthographicCamera(ObjectRegistry &objects, const OrthographicCameraUpdate ¶ms) +{ + updateCameraAs(objects, params); +} + +ObjectResult createPanoramicCamera(ObjectRegistry &objects, Device &device, const PanoramicCameraParams ¶ms) +{ + auto camera = createPanoramicCamera(device, params.base, params.derived); + + auto derived = UserPanoramicCamera{params.derived, std::move(camera)}; + auto ptr = std::make_shared(std::move(derived)); + + auto object = UserCamera{ + .device = device, + .settings = params.base, + .value = ptr, + .get = [=] { return ptr->value; }, + .setAspect = [](auto) {}, + }; + + auto stored = objects.add(std::move(object), "PanoramicCamera"); + + return {stored.getId()}; +} + +PanoramicCameraInfo getPanoramicCamera(ObjectRegistry &objects, const ObjectParams ¶ms) +{ + return getCameraAs(objects, params); +} + +void updatePanoramicCamera(ObjectRegistry &objects, const PanoramicCameraUpdate ¶ms) +{ + updateCameraAs(objects, params); +} +} diff --git a/src/brayns/core/objects/CameraObjects.h b/src/brayns/core/objects/CameraObjects.h new file mode 100644 index 000000000..b1db06a98 --- /dev/null +++ b/src/brayns/core/objects/CameraObjects.h @@ -0,0 +1,207 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include + +#include +#include + +namespace brayns +{ +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("position", [](auto &object) { return &object.position; }).description("Position XYZ"); + builder.field("direction", [](auto &object) { return &object.direction; }).description("Forward direction XYZ"); + builder.field("up", [](auto &object) { return &object.up; }) + .description("Up direction XYZ") + .defaultValue(Vector3(0.0F, 1.0F, 0.0F)); + return builder.build(); + } +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("view", [](auto &object) { return &object.view; }).description("Camera view in 3D space"); + builder.field("nearClip", [](auto &object) { return &object.nearClip; }) + .description("Distance to clip objects that are too close to the camera") + .defaultValue(0.0F) + .minimum(0.0F); + builder.field("imageRegion", [](auto &object) { return &object.imageRegion; }) + .description("Normalized region of the camera viewport to be rendered (does not affect resolution)") + .defaultValue(Box2{{0.0F, 0.0F}, {1.0F, 1.0F}}); + return builder.build(); + } +}; + +template +using CameraParams = ComposedParams; + +using CameraInfo = CameraSettings; +using CameraUpdate = UpdateParams; + +struct UserCamera +{ + std::reference_wrapper device; + CameraSettings settings; + std::any value; + std::function get; + std::function setAspect; +}; + +template T> +struct DerivedCamera +{ + Settings settings; + T value; +}; + +CameraInfo getCamera(ObjectRegistry &objects, const ObjectParams ¶ms); +void updateCamera(ObjectRegistry &objects, const CameraUpdate ¶ms); + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("apertureRadius", [](auto &object) { return &object.apertureRadius; }) + .description("Size of the aperture radius (0 is no depth of field)") + .defaultValue(0.1F); + builder.field("focusDistance", [](auto &object) { return &object.focusDistance; }) + .description("Distance at which the image is the sharpest") + .defaultValue(1.0F); + return builder.build(); + } +}; + +template<> +struct EnumReflector +{ + static auto reflect() + { + auto builder = EnumBuilder(); + builder.field("Left", StereoMode::Left).description("Render left eye"); + builder.field("Right", StereoMode::Right).description("Render right eye"); + builder.field("SideBySide", StereoMode::SideBySide).description("Render both eyes side by side"); + builder.field("TopBottom", StereoMode::TopBottom).description("Render left eye above right eye"); + return builder.build(); + } +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("mode", [](auto &object) { return &object.mode; }) + .description("How to render images for each eye"); + builder.field("interpupillaryDistance", [](auto &object) { return &object.interpupillaryDistance; }) + .description("Distance between observer eyes") + .defaultValue(0.0635F); + return builder.build(); + } +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("fovy", [](auto &object) { return &object.fovy; }) + .description("Camera vertical field of view in radians (horizontal is deduced from framebuffer aspect)") + .defaultValue(radians(45.0F)); + builder.field("depthOfField", [](auto &object) { return &object.depthOfField; }) + .description("Depth of field settings, set to null to disable it"); + builder.field("architectural", [](auto &object) { return &object.architectural; }) + .description("Vertical edges are projected to be parallel") + .defaultValue(false); + builder.field("stereo", [](auto &object) { return &object.stereo; }) + .description("Stereo settings, set to null to disable it"); + return builder.build(); + } +}; + +using PerspectiveCameraParams = CameraParams; +using PerspectiveCameraInfo = PerspectiveCameraSettings; +using PerspectiveCameraUpdate = UpdateParams; +using UserPerspectiveCamera = DerivedCamera; + +ObjectResult createPerspectiveCamera(ObjectRegistry &objects, Device &device, const PerspectiveCameraParams ¶ms); +PerspectiveCameraInfo getPerspectiveCamera(ObjectRegistry &objects, const ObjectParams ¶ms); +void updatePerspectiveCamera(ObjectRegistry &objects, const PerspectiveCameraUpdate ¶ms); + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("height", [](auto &object) { return &object.height; }) + .description("Camera viewport height in world coordinates (horizontal is deduced from framebuffer aspect)") + .defaultValue(1.0F); + return builder.build(); + } +}; + +using OrthographicCameraParams = CameraParams; +using OrthographicCameraInfo = OrthographicCameraSettings; +using OrthographicCameraUpdate = UpdateParams; +using UserOrthographicCamera = DerivedCamera; + +ObjectResult createOrthographicCamera(ObjectRegistry &objects, Device &device, const OrthographicCameraParams ¶ms); +OrthographicCameraInfo getOrthographicCamera(ObjectRegistry &objects, const ObjectParams ¶ms); +void updateOrthographicCamera(ObjectRegistry &objects, const OrthographicCameraUpdate ¶ms); + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("stereo", [](auto &object) { return &object.stereo; }) + .description("Stereo settings, set to null to disable it"); + return builder.build(); + } +}; + +using PanoramicCameraParams = CameraParams; +using PanoramicCameraInfo = PanoramicCameraSettings; +using PanoramicCameraUpdate = UpdateParams; +using UserPanoramicCamera = DerivedCamera; + +ObjectResult createPanoramicCamera(ObjectRegistry &objects, Device &device, const PanoramicCameraParams ¶ms); +PanoramicCameraInfo getPanoramicCamera(ObjectRegistry &objects, const ObjectParams ¶ms); +void updatePanoramicCamera(ObjectRegistry &objects, const PanoramicCameraUpdate ¶ms); +} diff --git a/src/brayns/core/objects/FramebufferObjects.cpp b/src/brayns/core/objects/FramebufferObjects.cpp new file mode 100644 index 000000000..db2348c72 --- /dev/null +++ b/src/brayns/core/objects/FramebufferObjects.cpp @@ -0,0 +1,133 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "FramebufferObjects.h" + +namespace +{ +using namespace brayns; + +std::vector> getImageOperations(ObjectRegistry &objects, const std::vector &ids) +{ + auto operations = std::vector>(); + operations.reserve(ids.size()); + + for (auto id : ids) + { + auto interface = objects.getAsStored(id); + operations.push_back(std::move(interface)); + } + + return operations; +} + +std::vector getImageOperationIds(const std::vector> &operations) +{ + auto ids = std::vector(); + ids.reserve(operations.size()); + + for (const auto &operation : operations) + { + ids.push_back(operation.getId()); + } + + return ids; +} + +std::optional> createImageOperationData( + Device &device, + const std::vector> &operations) +{ + auto itemCount = operations.size(); + + if (itemCount == 0) + { + return std::nullopt; + } + + auto data = allocateData(device, itemCount); + auto items = data.getItems(); + + for (auto i = std::size_t(0); i < itemCount; ++i) + { + const auto &interface = operations[i].get(); + items[i] = interface.get(); + } + + return data; +} +} + +namespace brayns +{ +ObjectResult createFramebuffer(ObjectRegistry &objects, Device &device, const FramebufferParams ¶ms) +{ + auto settings = params.settings; + + auto operations = getImageOperations(objects, params.operations); + settings.operations = createImageOperationData(device, operations); + + auto framebuffer = createFramebuffer(device, settings); + + auto object = UserFramebuffer{ + .device = device, + .settings = std::move(settings), + .operations = std::move(operations), + .value = std::move(framebuffer), + }; + + auto stored = objects.add(std::move(object), "Framebuffer"); + + return {stored.getId()}; +} + +FramebufferInfo getFramebuffer(ObjectRegistry &objects, const ObjectParams ¶ms) +{ + auto &framebuffer = objects.getAs(params.id); + + auto ids = getImageOperationIds(framebuffer.operations); + auto result = FramebufferParams{framebuffer.settings, std::move(ids)}; + auto variance = framebuffer.value.getVariance(); + + return {std::move(result), variance}; +} + +void updateFramebuffer(ObjectRegistry &objects, const FramebufferUpdate ¶ms) +{ + auto &framebuffer = objects.getAs(params.id); + auto &device = framebuffer.device.get(); + + auto operations = getImageOperations(objects, params.settings.operations); + auto data = createImageOperationData(device, operations); + + framebuffer.value.update(data); + device.throwIfError(); + + framebuffer.operations = std::move(operations); + framebuffer.settings.operations = std::move(data); +} + +void clearFramebuffer(ObjectRegistry &objects, const ObjectParams ¶ms) +{ + auto &framebuffer = objects.getAs(params.id); + framebuffer.value.resetAccumulation(); +} +} diff --git a/src/brayns/core/objects/FramebufferObjects.h b/src/brayns/core/objects/FramebufferObjects.h new file mode 100644 index 000000000..409a8ee10 --- /dev/null +++ b/src/brayns/core/objects/FramebufferObjects.h @@ -0,0 +1,164 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include + +#include "ImageOperationObjects.h" + +namespace brayns +{ +template<> +struct EnumReflector +{ + static auto reflect() + { + auto builder = EnumBuilder(); + builder.field("Color", FramebufferChannel::Color) + .description("Color RGBA, uint8 or float32 depending on framebuffer format"); + builder.field("Depth", FramebufferChannel::Depth) + .description("Euclidean distance from camera of the closest hit as float32"); + builder.field("Normal", FramebufferChannel::Normal).description("Accumulated normal XYZ as 3 x float32"); + builder.field("Albedo", FramebufferChannel::Albedo) + .description("Accumulated color without illumination RGB as 3 x float32"); + builder.field("PrimitiveId", FramebufferChannel::PrimitiveId) + .description("Index of first primitive hit as uint32"); + builder.field("ModelId", FramebufferChannel::ModelId) + .description("ID set by user of the first geometric/volumetric model hit as uint32"); + builder.field("InstanceId", FramebufferChannel::InstanceId) + .description("ID set by user of the first instance hit as uint32"); + return builder.build(); + } +}; + +template<> +struct EnumReflector +{ + static auto reflect() + { + auto builder = EnumBuilder(); + builder.field("Rgba8", FramebufferFormat::Rgba8).description("8 bit linear RGBA"); + builder.field("Srgba8", FramebufferFormat::Srgba8).description("8 bit gamma-encoded RGB and linear A"); + builder.field("Rgba32F", FramebufferFormat::Rgba32F).description("32 bit float RGBA"); + return builder.build(); + } +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("variance", [](auto &object) { return &object.variance; }) + .description("Wether to store per-pixel variance in a channel") + .defaultValue(false); + return builder.build(); + } +}; + +struct FramebufferParams +{ + FramebufferSettings settings; + std::vector operations; +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("resolution", [](auto &object) { return &object.settings.resolution; }) + .description("Framebuffer resolution in pixel") + .items() + .minimum(64) + .maximum(20'000); + builder.field("format", [](auto &object) { return &object.settings.format; }) + .description("Format of the framebuffer color channel") + .defaultValue(FramebufferFormat::Srgba8); + builder.field("channels", [](auto &object) { return &object.settings.channels; }) + .description("Framebuffer channels that can be accessed by user") + .defaultValue(std::set{FramebufferChannel::Color}); + builder.field("accumulation", [](auto &object) { return &object.settings.accumulation; }) + .description("If not null, the framebuffer will use accumulation with given settings"); + builder.field("operations", [](auto &object) { return &object.operations; }) + .description("List of image operation IDs that will be applied on the framebuffer") + .defaultValue(std::vector()) + .uniqueItems(true); + return builder.build(); + } +}; + +struct FramebufferInfo +{ + FramebufferParams params; + std::optional variance; +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("params", [](auto &object) { return &object.params; }) + .description("Params used to create the framebuffer"); + builder.field("variance", [](auto &object) { return &object.variance; }) + .description("Variance of the framebuffer (null if no estimate is available)"); + return builder.build(); + } +}; + +struct FramebufferOperations +{ + std::vector operations; +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("operations", [](auto &object) { return &object.operations; }) + .description("IDs of the image operations to attach to the framebuffer"); + return builder.build(); + } +}; + +using FramebufferUpdate = UpdateParams; + +struct UserFramebuffer +{ + std::reference_wrapper device; + FramebufferSettings settings; + std::vector> operations; + Framebuffer value; +}; + +ObjectResult createFramebuffer(ObjectRegistry &objects, Device &device, const FramebufferParams ¶ms); +FramebufferInfo getFramebuffer(ObjectRegistry &objects, const ObjectParams ¶ms); +void updateFramebuffer(ObjectRegistry &objects, const FramebufferUpdate ¶ms); +void clearFramebuffer(ObjectRegistry &objects, const ObjectParams ¶ms); +} diff --git a/src/brayns/core/objects/ImageOperationObjects.cpp b/src/brayns/core/objects/ImageOperationObjects.cpp new file mode 100644 index 000000000..9d1fe4040 --- /dev/null +++ b/src/brayns/core/objects/ImageOperationObjects.cpp @@ -0,0 +1,63 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "ImageOperationObjects.h" + +namespace brayns +{ +ObjectResult createToneMapper(ObjectRegistry &objects, Device &device, const ToneMapperParams ¶ms) +{ + auto operation = createToneMapper(device, params.derived); + + auto derived = UserToneMapper{params.derived, std::move(operation)}; + auto ptr = std::make_shared(std::move(derived)); + + auto object = UserImageOperation{ + .device = device, + .value = ptr, + .get = [=] { return ptr->value; }, + }; + + auto stored = objects.add(std::move(object), "ToneMapper"); + + return {stored.getId()}; +} + +ToneMapperInfo getToneMapper(ObjectRegistry &objects, const ObjectParams ¶ms) +{ + auto stored = objects.getAsStored(params.id); + auto &operation = castAs(stored.get().value, stored.getInfo()); + return operation.settings; +} + +void updateToneMapper(ObjectRegistry &objects, const ToneMapperUpdate ¶ms) +{ + auto stored = objects.getAsStored(params.id); + auto &base = stored.get(); + auto &derived = castAs(base.value, stored.getInfo()); + auto &device = base.device.get(); + + derived.value.update(params.settings); + device.throwIfError(); + + derived.settings = params.settings; +} +} diff --git a/src/brayns/core/objects/ImageOperationObjects.h b/src/brayns/core/objects/ImageOperationObjects.h new file mode 100644 index 000000000..7c7818966 --- /dev/null +++ b/src/brayns/core/objects/ImageOperationObjects.h @@ -0,0 +1,89 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include + +#include +#include + +namespace brayns +{ +template +using ImageOperationParams = ComposedParams; + +struct UserImageOperation +{ + std::reference_wrapper device; + std::any value; + std::function get; +}; + +template T> +struct DerivedImageOperation +{ + Settings settings; + T value; +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("exposure", [](auto &object) { return &object.exposure; }) + .description("Amount of light per unit area") + .defaultValue(1.0F); + builder.field("contrast", [](auto &object) { return &object.contrast; }) + .description("Constrast (toe of the curve)") + .defaultValue(1.6773F); + builder.field("shoulder", [](auto &object) { return &object.shoulder; }) + .description("Highlight compression (shoulder of the curve)") + .defaultValue(0.9714F); + builder.field("midIn", [](auto &object) { return &object.midIn; }) + .description("Mid-level anchor input") + .defaultValue(0.18F); + builder.field("midOut", [](auto &object) { return &object.midOut; }) + .description("Mid-level anchor output") + .defaultValue(0.18F); + builder.field("hdrMax", [](auto &object) { return &object.hdrMax; }) + .description("Maximum HDR input that is not clipped") + .defaultValue(11.0785F); + builder.field("acesColor", [](auto &object) { return &object.acesColor; }) + .description("Apply the ACES color transforms") + .defaultValue(true); + return builder.build(); + } +}; + +using ToneMapperParams = ImageOperationParams; +using ToneMapperInfo = ToneMapperSettings; +using ToneMapperUpdate = UpdateParams; +using UserToneMapper = DerivedImageOperation; + +ObjectResult createToneMapper(ObjectRegistry &objects, Device &device, const ToneMapperParams ¶ms); +ToneMapperInfo getToneMapper(ObjectRegistry &objects, const ObjectParams ¶ms); +void updateToneMapper(ObjectRegistry &objects, const ToneMapperUpdate ¶ms); +} diff --git a/src/brayns/core/objects/ObjectManager.h b/src/brayns/core/objects/ObjectManager.h deleted file mode 100644 index d80f0cd30..000000000 --- a/src/brayns/core/objects/ObjectManager.h +++ /dev/null @@ -1,110 +0,0 @@ -/* Copyright (c) 2015-2024 EPFL/Blue Brain Project - * All rights reserved. Do not distribute without permission. - * - * Responsible Author: adrien.fleury@epfl.ch - * - * This file is part of Brayns - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License version 3.0 as published - * by the Free Software Foundation. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#pragma once - -#include -#include -#include -#include - -#include - -#include -#include - -#include "Messages.h" -#include "UserObject.h" - -namespace brayns -{ -class ObjectManager -{ -public: - explicit ObjectManager(); - - std::vector getAllObjects() const; - ObjectInfo getObject(ObjectId id) const; - void setUserData(ObjectId id, const JsonValue &userData); - void remove(ObjectId id); - void clear(); - - template - T &get(ObjectId id) const - { - return getShared(id)->value; - } - - template - Stored getStored(ObjectId id) const - { - return Stored(getShared(id)); - } - - template - Stored add(T object) - { - auto id = _ids.next(); - - try - { - auto user = UserObject{id, std::move(object)}; - auto ptr = std::make_shared(std::move(user)); - - addObject(ptr->value, id); - - auto interface = createObjectInterface(ptr); - - _objects.emplace(id, std::move(interface)); - - return Stored(std::move(ptr)); - } - catch (...) - { - _ids.recycle(id); - throw; - } - } - -private: - std::map _objects; - IdGenerator _ids; - - const ObjectInterface &getInterface(ObjectId id) const; - - template - const std::shared_ptr> &getShared(ObjectId id) const - { - const auto &interface = getInterface(id); - - auto ptr = std::any_cast>>(&interface.value); - - if (ptr != nullptr) - { - return *ptr; - } - - auto type = interface.getType(); - - throw InvalidParams(fmt::format("Invalid type for object with ID {}: {}", id, type)); - } -}; -} diff --git a/src/brayns/core/objects/TransferFunctionObjects.cpp b/src/brayns/core/objects/TransferFunctionObjects.cpp new file mode 100644 index 000000000..904e2440d --- /dev/null +++ b/src/brayns/core/objects/TransferFunctionObjects.cpp @@ -0,0 +1,80 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "TransferFunctionObjects.h" + +namespace +{ +using namespace brayns; + +LinearTransferFunctionSettings extractSettings(Device &device, const LinearTransferFunctionInfo ¶ms) +{ + auto data = createData(device, params.colors); + return {params.scalarRange, std::move(data)}; +} +} + +namespace brayns +{ +ObjectResult createLinearTransferFunction( + ObjectRegistry &objects, + Device &device, + const LinearTransferFunctionParams ¶ms) +{ + auto settings = extractSettings(device, params.derived); + auto function = createLinearTransferFunction(device, settings); + + auto derived = UserLinearTransferFunction{params.derived, std::move(function)}; + auto ptr = std::make_shared(std::move(derived)); + + auto object = UserTransferFunction{ + .device = device, + .value = ptr, + .get = [=] { return ptr->value; }, + }; + + auto stored = objects.add(std::move(object), "LinearTransferFunction"); + + return {stored.getId()}; +} + +LinearTransferFunctionInfo getLinearTransferFunction(ObjectRegistry &objects, const ObjectParams ¶ms) +{ + auto stored = objects.getAsStored(params.id); + auto &operation = castAs(stored.get().value, stored.getInfo()); + return operation.settings; +} + +void updateLinearTransferFunction(ObjectRegistry &objects, const LinearTransferFunctionUpdate ¶ms) +{ + auto stored = objects.getAsStored(params.id); + auto &base = stored.get(); + auto &derived = castAs(base.value, stored.getInfo()); + auto &device = base.device.get(); + + auto settings = extractSettings(device, params.settings); + + derived.value.update(settings); + device.throwIfError(); + + derived.settings = params.settings; +} +} diff --git a/src/brayns/core/objects/TransferFunctionObjects.h b/src/brayns/core/objects/TransferFunctionObjects.h new file mode 100644 index 000000000..f296b6d71 --- /dev/null +++ b/src/brayns/core/objects/TransferFunctionObjects.h @@ -0,0 +1,82 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace brayns +{ +template +using TransferFunctionParams = ComposedParams; + +struct UserTransferFunction +{ + std::reference_wrapper device; + std::any value; + std::function get; +}; + +template T> +struct DerivedTransferFunction +{ + Settings settings; + T value; +}; + +struct LinearTransferFunctionInfo +{ + Box1 scalarRange; + std::vector colors; +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("scalarRange", [](auto &object) { return &object.scalarRange; }) + .description("Range of the scalar values sampled from the volume that will be mapped to colors"); + builder.field("colors", [](auto &object) { return &object.colors; }) + .description("Colors to map the values sampled from the volume") + .minItems(1); + return builder.build(); + } +}; + +using LinearTransferFunctionParams = TransferFunctionParams; +using LinearTransferFunctionUpdate = UpdateParams; +using UserLinearTransferFunction = DerivedTransferFunction; + +ObjectResult createLinearTransferFunction( + ObjectRegistry &objects, + Device &device, + const LinearTransferFunctionParams ¶ms); +LinearTransferFunctionInfo getLinearTransferFunction(ObjectRegistry &objects, const ObjectParams ¶ms); +void updateLinearTransferFunction(ObjectRegistry &objects, const LinearTransferFunctionUpdate ¶ms); +} diff --git a/src/brayns/core/objects/UserObject.h b/src/brayns/core/objects/UserObject.h deleted file mode 100644 index 9301ddd89..000000000 --- a/src/brayns/core/objects/UserObject.h +++ /dev/null @@ -1,173 +0,0 @@ -/* Copyright (c) 2015-2024 EPFL/Blue Brain Project - * All rights reserved. Do not distribute without permission. - * - * Responsible Author: adrien.fleury@epfl.ch - * - * This file is part of Brayns - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License version 3.0 as published - * by the Free Software Foundation. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#pragma once - -#include -#include -#include -#include - -#include "Messages.h" - -namespace brayns -{ -template -struct ObjectReflector; - -template -concept WithType = std::same_as::getType(std::declval()))>; - -template -concept WithAdd = std::is_void_v::add(std::declval(), ObjectId()))>; - -template -concept WithRemove = std::is_void_v::remove(std::declval()))>; - -template -concept ReflectedObject = WithType && std::default_initializable; - -template -std::string getObjectType(const T &object) -{ - return ObjectReflector::getType(object); -} - -template -void addObject(T &object, ObjectId id) -{ - (void)object; - (void)id; -} - -template -void addObject(T &object, ObjectId id) -{ - return ObjectReflector::add(object, id); -} - -template -void removeObject(T &object) -{ - (void)object; -} - -template -void removeObject(T &object) -{ - return ObjectReflector::remove(object); -} - -template -struct UserObject -{ - ObjectId id; - T value; - JsonValue userData = {}; -}; - -template -void removeUserObject(UserObject &object) -{ - object.id = nullId; - removeObject(object.value); -} - -template -class Stored -{ -public: - explicit Stored(std::shared_ptr> object): - _object(std::move(object)) - { - } - - ObjectId getId() const - { - return _object->id; - } - - bool isRemoved() const - { - return _object->id == nullId; - } - - std::string getType() const - { - return getObjectType(_object->value); - } - - JsonValue getUserData() const - { - return _object->userData; - } - - void setUserData(const JsonValue &userData) - { - _object->userData = userData; - } - - ObjectResult getResult() const - { - return {getId()}; - } - - T &get() const - { - return _object->value; - } - -private: - std::shared_ptr> _object; -}; - -struct ObjectInterface -{ - std::any value; - std::function getId; - std::function remove; - std::function getType; - std::function getUserData; - std::function setUserData; -}; - -template -ObjectInterface createObjectInterface(const std::shared_ptr> &object) -{ - return { - .value = object, - .getId = [=] { return object->id; }, - .remove = [=] { removeUserObject(*object); }, - .getType = [=] { return getObjectType(object->value); }, - .getUserData = [=] { return object->userData; }, - .setUserData = [=](const auto &userData) { object->userData = userData; }, - }; -} - -inline ObjectInfo getObjectInfo(const ObjectInterface &object) -{ - return { - .id = object.getId(), - .type = object.getType(), - .userData = object.getUserData(), - }; -} -} diff --git a/src/brayns/core/objects/VolumeObjects.cpp b/src/brayns/core/objects/VolumeObjects.cpp new file mode 100644 index 000000000..7c6045337 --- /dev/null +++ b/src/brayns/core/objects/VolumeObjects.cpp @@ -0,0 +1,90 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "VolumeObjects.h" + +#include "common/Binary.h" + +namespace +{ +using namespace brayns; + +RegularVolumeData createVolumeData(Device &device, const RegularVolumeInfo ¶ms, std::string_view binary) +{ + switch (params.voxelType) + { + case VoxelType::U8: + return createData3DFromBinaryOf(device, params.voxelCount, binary); + case VoxelType::U16: + return createData3DFromBinaryOf(device, params.voxelCount, binary); + case VoxelType::F32: + return createData3DFromBinaryOf(device, params.voxelCount, binary); + case VoxelType::F64: + return createData3DFromBinaryOf(device, params.voxelCount, binary); + default: + throw std::invalid_argument("Invalid voxel data type"); + } +} +} + +namespace brayns +{ +ObjectResult createRegularVolume(ObjectRegistry &objects, Device &device, const RegularVolumeParams ¶ms) +{ + const auto &[json, binary] = params; + + auto data = createVolumeData(device, json.derived, binary); + auto volume = createRegularVolume(device, data, json.derived.settings); + + auto derived = UserRegularVolume{json.derived, std::move(volume)}; + auto ptr = std::make_shared(std::move(derived)); + + auto object = UserVolume{ + .device = device, + .value = ptr, + .get = [=] { return ptr->value; }, + }; + + auto stored = objects.add(std::move(object), "RegularVolume"); + + return {stored.getId()}; +} + +RegularVolumeInfo getRegularVolume(ObjectRegistry &objects, const ObjectParams ¶ms) +{ + auto stored = objects.getAsStored(params.id); + auto &operation = castAs(stored.get().value, stored.getInfo()); + return operation.settings; +} + +void updateRegularVolume(ObjectRegistry &objects, const RegularVolumeUpdate ¶ms) +{ + auto stored = objects.getAsStored(params.id); + auto &base = stored.get(); + auto &derived = castAs(base.value, stored.getInfo()); + auto &device = base.device.get(); + + derived.value.update(params.settings); + device.throwIfError(); + + derived.settings.settings = params.settings; +} +} diff --git a/src/brayns/core/objects/VolumeObjects.h b/src/brayns/core/objects/VolumeObjects.h new file mode 100644 index 000000000..1e43b3dcf --- /dev/null +++ b/src/brayns/core/objects/VolumeObjects.h @@ -0,0 +1,152 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace brayns +{ +template +using VolumeParams = ComposedParams; + +struct UserVolume +{ + std::reference_wrapper device; + std::any value; + std::function get; +}; + +template T> +struct DerivedVolume +{ + Settings settings; + T value; +}; + +enum class VoxelType +{ + U8, + U16, + F32, + F64, +}; + +template<> +struct EnumReflector +{ + static auto reflect() + { + auto builder = EnumBuilder(); + builder.field("U8", VoxelType::U8).description("8 bit unsigned int"); + builder.field("U16", VoxelType::U16).description("16 bit unsigned int"); + builder.field("F32", VoxelType::F32).description("32 bit float"); + builder.field("F64", VoxelType::F64).description("32 bit float"); + return builder.build(); + } +}; + +template<> +struct EnumReflector +{ + static auto reflect() + { + auto builder = EnumBuilder(); + builder.field("Nearest", VolumeFilter::Nearest).description("Voxel sampling does no interpolations"); + builder.field("Linear", VolumeFilter::Linear).description("Voxel sampling does linear interpolation"); + builder.field("Cubic", VolumeFilter::Cubic).description("Voxel sampling does cubic interpolation"); + return builder.build(); + } +}; + +template<> +struct EnumReflector +{ + static auto reflect() + { + auto builder = EnumBuilder(); + builder.field("CellCentered", VolumeType::CellCentered).description("Volume data is provided per cell"); + builder.field("VertexCentered", VolumeType::VertexCentered).description("Volume data is provided per vertex"); + return builder.build(); + } +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("origin", [](auto &object) { return &object.origin; }) + .description("Position of the volume origin") + .defaultValue(Vector3(0, 0, 0)); + builder.field("spacing", [](auto &object) { return &object.spacing; }) + .description("Size of the volume cells") + .defaultValue(Vector3(0, 0, 0)); + builder.field("type", [](auto &object) { return &object.type; }) + .description("Wether the data is provided per vertex or per cell") + .defaultValue(VolumeType::VertexCentered); + builder.field("filter", [](auto &object) { return &object.filter; }) + .description("How to interpolate sampled voxels") + .defaultValue(VolumeFilter::Linear); + builder.field("background", [](auto &object) { return &object.background; }) + .description("Value used when sampling an undefined region outside the volume (null to use NaN)"); + return builder.build(); + } +}; + +struct RegularVolumeInfo +{ + VoxelType voxelType; + Size3 voxelCount; + RegularVolumeSettings settings; +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("voxelType", [](auto &object) { return &object.voxelType; }) + .description("Type of the provided voxels"); + builder.field("voxelCount", [](auto &object) { return &object.voxelCount; }) + .description("Volume dimensions XYZ"); + builder.field("settings", [](auto &object) { return &object.settings; }) + .description("Additional settings that can be updated"); + return builder.build(); + } +}; + +using RegularVolumeParams = Params>; +using RegularVolumeUpdate = UpdateParams; +using UserRegularVolume = DerivedVolume; + +ObjectResult createRegularVolume(ObjectRegistry &objects, Device &device, const RegularVolumeParams ¶ms); +RegularVolumeInfo getRegularVolume(ObjectRegistry &objects, const ObjectParams ¶ms); +void updateRegularVolume(ObjectRegistry &objects, const RegularVolumeUpdate ¶ms); +} diff --git a/src/brayns/core/objects/common/Binary.h b/src/brayns/core/objects/common/Binary.h new file mode 100644 index 000000000..83e3489e0 --- /dev/null +++ b/src/brayns/core/objects/common/Binary.h @@ -0,0 +1,117 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace brayns +{ +template +std::size_t getItemCountFromBinaryOf(std::string_view bytes) +{ + auto bytesSize = bytes.size(); + auto itemSize = getBinarySize(); + auto itemCount = bytesSize / itemSize; + + if (itemCount * itemSize != bytesSize) + { + throw InvalidParams(fmt::format("Binary size {} is not a multiple of item size {}", bytesSize, itemSize)); + } + + return itemCount; +} + +template +void copyBinaryTo(std::string_view bytes, std::span items) +{ + for (auto &item : items) + { + extractBytesTo(bytes, std::endian::little, item); + } +} + +template +Data3D createData3DFromBinaryOf(Device &device, const Size3 &itemCount, std::string_view bytes) +{ + auto totalItemCount = getItemCountFromBinaryOf(bytes); + + if (totalItemCount == 0) + { + throw InvalidParams("Empty data is not supported by OSPRay"); + } + + if (reduceMultiply(itemCount) != totalItemCount) + { + throw InvalidParams(fmt::format( + "Item count in binary {} does not match given item count {}x{}x{}", + totalItemCount, + itemCount.x, + itemCount.y, + itemCount.z)); + } + + auto data = allocateData3D(device, itemCount); + + copyBinaryTo(bytes, data.getItems()); + + return data; +} + +template +Data2D createData2DFromBinaryOf(Device &device, const Size2 &itemCount, std::string_view bytes) +{ + auto size3 = Size3(itemCount, 1); + auto data3 = createData3DFromBinaryOf(device, size3, bytes); + return Data2D(std::move(data3)); +} + +template +Data createDataFromBinaryOf(Device &device, std::size_t itemCount, std::string_view bytes) +{ + auto size3 = Size3(itemCount, 1, 1); + auto data3 = createData3DFromBinaryOf(device, size3, bytes); + return Data(std::move(data3)); +} + +template +std::string composeRangeToBinary(T items) +{ + auto output = std::string(); + output.reserve(items.size() + getBinarySize>()); + + for (const auto &item : items) + { + composeBytesTo(item, std::endian::little, output); + } + + return output; +} +} diff --git a/src/brayns/core/objects/common/Encoding.cpp b/src/brayns/core/objects/common/Encoding.cpp new file mode 100644 index 000000000..c867b4cb1 --- /dev/null +++ b/src/brayns/core/objects/common/Encoding.cpp @@ -0,0 +1,286 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "Encoding.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "Binary.h" + +namespace +{ +using namespace brayns; + +template +std::string readChannelAs(UserFramebuffer &framebuffer, FramebufferChannel channel) +{ + const auto &size = framebuffer.settings.resolution; + + auto data = framebuffer.value.map(channel); + auto itemCount = reduceMultiply(size); + + auto items = std::span(data.as(), itemCount); + + return composeRangeToBinary(items); +} + +template +concept Encoder = + std::invocable && std::same_as>; + +std::string encodeColorChannel(const FramebufferData &data, const Size2 &size, Encoder auto &&encoder) +{ + auto format = ImageFormat::Rgba8; + return encoder(ImageView{data.get(), size, format}); +} + +std::string convertAndEncodeColorChannel(const FramebufferData &data, const Size2 &size, Encoder auto &&encoder) +{ + const auto *items = data.as(); + auto itemCount = reduceMultiply(size); + auto converted = convertTo8Bit(std::span(items, itemCount)); + auto format = ImageFormat::Rgba8; + + return encoder(ImageView{converted.data(), size, format}); +} + +std::string convertAndEncodeAlbedoChannel(const FramebufferData &data, const Size2 &size, Encoder auto &&encoder) +{ + const auto *items = data.as(); + auto itemCount = reduceMultiply(size); + auto converted = convertTo8Bit(std::span(items, itemCount)); + auto format = ImageFormat::Rgb8; + + return encoder(ImageView{converted.data(), size, format}); +} + +std::string encodeChannelToJpegOrPng(UserFramebuffer &framebuffer, JpegChannel channel, Encoder auto &&encoder) +{ + const auto &size = framebuffer.settings.resolution; + auto format = framebuffer.settings.format; + + auto data = framebuffer.value.map(static_cast(channel)); + + if (channel == JpegChannel::Albedo) + { + return convertAndEncodeAlbedoChannel(data, size, encoder); + } + + assert(channel == JpegChannel::Color); + + switch (format) + { + case FramebufferFormat::Rgba8: + case FramebufferFormat::Srgba8: + return encodeColorChannel(data, size, encoder); + case FramebufferFormat::Rgba32F: + return convertAndEncodeColorChannel(data, size, encoder); + default: + throw std::invalid_argument("Invalid Framebuffer format"); + }; +} + +using ExrMappedData = std::variant>; + +const void *getExrChannelPtr(const FramebufferData &data) +{ + return data.get(); +} + +const void *getExrChannelPtr(const std::vector &data) +{ + return data.data(); +} + +const void *getExrChannelPtr(const ExrMappedData &data) +{ + return std::visit([](const auto &value) { return getExrChannelPtr(value); }, data); +} + +std::vector mapExrChannels( + Framebuffer &framebuffer, + std::size_t pixelCount, + FramebufferFormat format, + const std::set &channels) +{ + auto result = std::vector(); + result.reserve(channels.size()); + + for (auto channel : channels) + { + auto data = framebuffer.map(channel); + + if (format != FramebufferFormat::Rgba32F && channel == FramebufferChannel::Color) + { + auto pixels = std::string_view(data.as(), 4 * pixelCount); + auto items = convertToFloat<4>(pixels); + + result.push_back(std::move(items)); + + continue; + } + + result.push_back(std::move(data)); + } + + return result; +} + +std::string getExrChannelName(const char *layer, const char *channel) +{ + if (layer[0] == '\0') + { + return channel; + } + + return layer + std::string(".") + channel; +} + +template +std::vector splitExrChannels( + const Vector *items, + const char *layer, + const std::array &channels) +{ + constexpr auto dataType = ExrDataType::F32; + constexpr auto stride = sizeof(Vector); + + auto result = std::vector(); + result.reserve(channels.size()); + + for (auto i = std::size_t(0); i < channels.size(); ++i) + { + auto name = getExrChannelName(layer, channels[i]); + + result.push_back({std::move(name), &items[0][i], dataType, stride}); + } + + return result; +} + +std::vector createExrChannels(const void *data, FramebufferChannel channel) +{ + switch (channel) + { + case FramebufferChannel::Color: + return splitExrChannels(static_cast(data), "", {"R", "G", "B", "A"}); + case FramebufferChannel::Depth: + return {{"Z", data}}; + case FramebufferChannel::Normal: + return splitExrChannels(static_cast(data), "normal", {"X", "Y", "Z"}); + case FramebufferChannel::Albedo: + return splitExrChannels(static_cast(data), "albedo", {"R", "G", "B"}); + case FramebufferChannel::PrimitiveId: + return {{"primitive.ID", data, ExrDataType::U32}}; + case FramebufferChannel::ModelId: + return {{"model.ID", data, ExrDataType::U32}}; + case FramebufferChannel::InstanceId: + return {{"instance.ID", data, ExrDataType::U32}}; + default: + throw std::invalid_argument("Invalid framebuffer channel"); + } +} + +std::vector createExrChannels( + const std::vector &datas, + const std::set &channels) +{ + assert(datas.size() == channels.size()); + + auto result = std::vector(); + + auto i = std::size_t(0); + + for (auto channel : channels) + { + const auto &data = datas[i++]; + const auto *ptr = getExrChannelPtr(data); + + auto exrChannels = createExrChannels(ptr, channel); + + for (auto &exrChannel : exrChannels) + { + result.push_back(std::move(exrChannel)); + } + } + + return result; +} +} + +namespace brayns +{ +std::string readChannel(UserFramebuffer &framebuffer, FramebufferChannel channel) +{ + switch (channel) + { + case FramebufferChannel::Color: + return readChannelAs(framebuffer, channel); + case FramebufferChannel::Depth: + return readChannelAs(framebuffer, channel); + case FramebufferChannel::Normal: + return readChannelAs(framebuffer, channel); + case FramebufferChannel::Albedo: + return readChannelAs(framebuffer, channel); + case FramebufferChannel::PrimitiveId: + return readChannelAs(framebuffer, channel); + case FramebufferChannel::ModelId: + return readChannelAs(framebuffer, channel); + case FramebufferChannel::InstanceId: + return readChannelAs(framebuffer, channel); + default: + throw std::invalid_argument("Invalid raw channel"); + }; +} + +std::string readChannelAsJpeg(UserFramebuffer &framebuffer, JpegChannel channel, const JpegSettings &settings) +{ + auto encoder = [&](const auto &image) { return encodeJpeg(image, settings); }; + return encodeChannelToJpegOrPng(framebuffer, channel, encoder); +} + +std::string readChannelAsPng(UserFramebuffer &framebuffer, PngChannel channel) +{ + auto encoder = [&](const auto &image) { return encodePng(image); }; + return encodeChannelToJpegOrPng(framebuffer, channel, encoder); +} + +std::string readChannelsAsExr(UserFramebuffer &framebuffer, const std::set &channels) +{ + const auto &size = framebuffer.settings.resolution; + auto format = framebuffer.settings.format; + + auto pixelCount = reduceMultiply(size); + auto datas = mapExrChannels(framebuffer.value, pixelCount, format, channels); + + auto exrChannels = createExrChannels(datas, channels); + + return encodeExr({size, std::move(exrChannels)}); +} +} diff --git a/src/brayns/core/objects/common/Encoding.h b/src/brayns/core/objects/common/Encoding.h new file mode 100644 index 000000000..2c523f08a --- /dev/null +++ b/src/brayns/core/objects/common/Encoding.h @@ -0,0 +1,68 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include + +namespace brayns +{ +enum class JpegChannel +{ + Color = int(FramebufferChannel::Color), + Albedo = int(FramebufferChannel::Albedo), +}; + +using PngChannel = JpegChannel; + +template<> +struct EnumReflector +{ + static auto reflect() + { + auto builder = EnumBuilder(); + builder.field("Color", JpegChannel::Color).description("RGBA (PNG) or RGB (JPEG) color as uint8"); + builder.field("Albedo", JpegChannel::Albedo).description("RGB color as uint8"); + return builder.build(); + } +}; + +template<> +struct JsonObjectReflector +{ + static auto reflect() + { + auto builder = JsonBuilder(); + builder.field("quality", [](auto &object) { return &object.quality; }) + .description("JPEG quality, 1 = worst quality / best compression, 100 = best quality / worst compression") + .defaultValue(100) + .minimum(1) + .maximum(100); + return builder.build(); + } +}; + +std::string readChannel(UserFramebuffer &framebuffer, FramebufferChannel channel); +std::string readChannelAsJpeg(UserFramebuffer &framebuffer, JpegChannel channel, const JpegSettings &settings); +std::string readChannelAsPng(UserFramebuffer &framebuffer, PngChannel channel); +std::string readChannelsAsExr(UserFramebuffer &framebuffer, const std::set &channels); +} diff --git a/src/brayns/core/utils/Binary.h b/src/brayns/core/utils/Binary.h index d796c7562..b4ca035fb 100644 --- a/src/brayns/core/utils/Binary.h +++ b/src/brayns/core/utils/Binary.h @@ -29,6 +29,8 @@ #include #include +#include "Math.h" + namespace brayns { static_assert(std::endian::native == std::endian::little || std::endian::native == std::endian::big); @@ -48,11 +50,10 @@ std::span asBytes(T &value) } template -T swapBytes(T value) +void swapBytes(T &value) { auto bytes = asBytes(value); std::ranges::reverse(bytes); - return value; } inline std::string_view extractBytes(std::string_view &bytes, std::size_t count) @@ -63,7 +64,7 @@ inline std::string_view extractBytes(std::string_view &bytes, std::size_t count) } template -T extractBytesAs(std::string_view &bytes, std::endian bytesEndian) +void extractBytesAsPrimitive(std::string_view &bytes, std::endian endian, T &output) { static constexpr auto size = sizeof(T); @@ -71,41 +72,144 @@ T extractBytesAs(std::string_view &bytes, std::endian bytesEndian) auto extracted = extractBytes(bytes, size); - T value; - std::memcpy(&value, extracted.data(), size); + std::memcpy(&output, extracted.data(), size); - if (bytesEndian != std::endian::native) + if (endian != std::endian::native) { - return swapBytes(value); + swapBytes(output); } - - return value; } template -T parseBytesAs(std::string_view bytes, std::endian bytesEndian) +void composeBytesAsPrimtive(const T &value, std::endian endian, std::string &output) { - return extractBytesAs(bytes, bytesEndian); + auto bytes = asBytes(value); + auto size = bytes.size(); + + output.append(bytes.data(), size); + + if (endian != std::endian::native) + { + auto first = output.begin() + output.size() - size; + auto last = output.end(); + + auto appended = std::span(first, last); + std::ranges::reverse(appended); + } } template -void composeBytes(T value, std::endian outputEndian, std::string &output) +struct BinaryReflector { - auto bytes = asBytes(value); + static std::size_t getSize() + { + return sizeof(T); + } - if (outputEndian != std::endian::native) + static void extract(std::string_view &bytes, std::endian endian, T &output) { - std::ranges::reverse(bytes); + extractBytesAsPrimitive(bytes, endian, output); } - output.append(bytes.data(), bytes.size()); + static void compose(const T &value, std::endian endian, std::string &output) + { + composeBytesAsPrimtive(value, endian, output); + } +}; + +template +std::size_t getBinarySize() +{ + return BinaryReflector>::getSize(); } template -std::string composeBytes(T value, std::endian outputEndian) +void extractBytesTo(std::string_view &bytes, std::endian endian, T &output) { - auto bytes = std::string(); - composeBytes(value, outputEndian, bytes); - return bytes; + BinaryReflector::extract(bytes, endian, output); } + +template +T extractBytesAs(std::string_view &bytes, std::endian endian) +{ + T value; + extractBytesTo(bytes, endian, value); + return value; +} + +template +void parseBytesTo(std::string_view bytes, std::endian endian, T &output) +{ + return extractBytesTo(bytes, endian, output); +} + +template +T parseBytesAs(std::string_view bytes, std::endian endian) +{ + return extractBytesAs(bytes, endian); +} + +template +void composeBytesTo(const T &value, std::endian endian, std::string &output) +{ + BinaryReflector::compose(value, endian, output); +} + +template +std::string composeBytes(const T &value, std::endian endian) +{ + auto output = std::string(); + BinaryReflector::compose(value, endian, output); + return output; +} + +template +struct BinaryReflector> +{ + static std::size_t getSize() + { + return static_cast(S) * sizeof(T); + } + + static void extract(std::string_view &bytes, std::endian endian, Vector &output) + { + for (auto i = 0; i < S; ++i) + { + extractBytesTo(bytes, endian, output[i]); + } + } + + static void compose(const Vector &value, std::endian endian, std::string &output) + { + for (auto i = 0; i < S; ++i) + { + composeBytesTo(value[i], endian, output); + } + } +}; + +template<> +struct BinaryReflector +{ + static std::size_t getSize() + { + return 4 * sizeof(float); + } + + static void extract(std::string_view &bytes, std::endian endian, Quaternion &output) + { + extractBytesTo(bytes, endian, output.i); + extractBytesTo(bytes, endian, output.j); + extractBytesTo(bytes, endian, output.k); + extractBytesTo(bytes, endian, output.r); + } + + static void compose(const Quaternion &value, std::endian endian, std::string &output) + { + composeBytesTo(value.i, endian, output); + composeBytesTo(value.j, endian, output); + composeBytesTo(value.k, endian, output); + composeBytesTo(value.r, endian, output); + } +}; } diff --git a/src/brayns/core/utils/IdGenerator.h b/src/brayns/core/utils/IdGenerator.h index 618051cac..1e422cddb 100644 --- a/src/brayns/core/utils/IdGenerator.h +++ b/src/brayns/core/utils/IdGenerator.h @@ -21,6 +21,7 @@ #pragma once +#include #include #include #include @@ -31,6 +32,19 @@ template class IdGenerator { public: + explicit IdGenerator(T min = T(0), T max = std::numeric_limits::max()): + _min(min), + _max(max), + _current(min) + { + } + + void reset() + { + _current = _min; + _recycled.clear(); + } + T next() { if (!_recycled.empty()) @@ -42,21 +56,24 @@ class IdGenerator return id; } - if (_counter == std::numeric_limits::max()) + if (_current == _max) { throw std::out_of_range("No more available IDs"); } - return _counter++; + return _current++; } void recycle(T id) { + assert(id < _current && id >= _min); _recycled.push_back(id); } private: - T _counter = 0; + T _min; + T _max; + T _current; std::vector _recycled; }; } diff --git a/src/brayns/core/utils/Logger.h b/src/brayns/core/utils/Logger.h index b0d5adc79..586c0dbf0 100644 --- a/src/brayns/core/utils/Logger.h +++ b/src/brayns/core/utils/Logger.h @@ -45,7 +45,7 @@ enum class LogLevel template<> struct EnumReflector { - static EnumInfo reflect() + static auto reflect() { auto builder = EnumBuilder(); builder.field("trace", LogLevel::Trace); diff --git a/src/brayns/core/utils/Math.h b/src/brayns/core/utils/Math.h index 67bc298bd..ddb8d26e0 100644 --- a/src/brayns/core/utils/Math.h +++ b/src/brayns/core/utils/Math.h @@ -26,6 +26,8 @@ #include #include +#include + namespace brayns { template @@ -49,27 +51,65 @@ using Color4 = Vector4; using Quaternion = rkcommon::math::quaternionf; +template +using Range = rkcommon::math::range_t; + template using BoxT = rkcommon::math::box_t; -using Box1 = rkcommon::math::box1f; +using Box1 = Range; using Box2 = BoxT; using Box3 = BoxT; using Affine2 = rkcommon::math::AffineSpace2f; using Affine3 = rkcommon::math::AffineSpace3f; +template +constexpr T radians(T degrees) +{ + return degrees * T(M_PI) / T(360); +} + +template +constexpr T degrees(T radians) +{ + return radians * T(360) / T(M_PI); +} + template T reduceMultiply(const Vector &value) { return rkcommon::math::reduce_mul(value); } +template +Vector dot(const Vector &left, const Vector &right) +{ + return rkcommon::math::dot(left, right); +} + +template +Vector cross(const Vector &left, const Vector &right) +{ + return rkcommon::math::cross(left, right); +} + +template +Vector normalize(const Vector &value) +{ + return rkcommon::math::normalize(value); +} + +inline Quaternion normalize(const Quaternion &value) +{ + return rkcommon::math::normalize(value); +} + struct Transform { - Vector3 translation = {0, 0, 0}; - Quaternion rotation = {1, 0, 0, 0}; - Vector3 scale = {1, 1, 1}; + Vector3 translation = {0.0F, 0.0F, 0.0F}; + Quaternion rotation = {1.0F, 0.0F, 0.0F, 0.0F}; + Vector3 scale = {1.0F, 1.0F, 1.0F}; auto operator<=>(const Transform &other) const = default; }; diff --git a/tests/core/api/TestApi.cpp b/tests/core/api/TestApi.cpp index 52797a12b..966fc8c09 100644 --- a/tests/core/api/TestApi.cpp +++ b/tests/core/api/TestApi.cpp @@ -185,7 +185,7 @@ TEST_CASE("Task") CHECK(binary.empty()); auto object = getObject(json); - auto taskId = object.get("task_id").extract(); + auto taskId = object.get("taskId").extract(); CHECK_EQ(taskId, 0); diff --git a/tests/core/codecs/TestCodecs.cpp b/tests/core/codecs/TestCodecs.cpp index dae5902a8..097257503 100644 --- a/tests/core/codecs/TestCodecs.cpp +++ b/tests/core/codecs/TestCodecs.cpp @@ -74,12 +74,12 @@ TEST_CASE("JpegCodec") auto image = createTestImage(ImageFormat::Rgb8); auto data = encodeJpeg(view(image)); - // writeFile(data, "test1.jpg"); + writeFile(data, "test1.jpg"); image = createTestImage(ImageFormat::Rgba8); data = encodeJpeg(view(image)); - // writeFile(data, "test2.jpg"); + writeFile(data, "test2.jpg"); (void)data; } @@ -88,12 +88,12 @@ TEST_CASE("PngCodec") auto image = createTestImage(ImageFormat::Rgb8); auto data = encodePng(view(image)); - // writeFile(data, "test1.png"); + writeFile(data, "test1.png"); image = createTestImage(ImageFormat::Rgba8); data = encodePng(view(image)); - // writeFile(data, "test2.png"); + writeFile(data, "test2.png"); (void)data; } @@ -165,6 +165,6 @@ TEST_CASE("ExrCodec") }}; auto data = encodeExr(image); - // writeFile(data, "test.exr"); + writeFile(data, "test.exr"); (void)data; } diff --git a/tests/core/engine/TestRender.cpp b/tests/core/engine/TestRender.cpp index c10b9a696..2c56ff74e 100644 --- a/tests/core/engine/TestRender.cpp +++ b/tests/core/engine/TestRender.cpp @@ -54,6 +54,7 @@ TEST_CASE("Object creation") createPerspectiveCamera(device, {}); createOrthographicCamera(device, {}); + createPanoramicCamera(device, {}); createData(device, {1, 2, 3}); allocateData2D(device, {10, 10}); @@ -64,7 +65,7 @@ TEST_CASE("Object creation") auto volumeData = allocateData3D(device, {10, 10, 10}); std::ranges::fill(volumeData.getItems(), 1.0F); - auto volume = createRegularVolume(device, {volumeData}); + auto volume = createRegularVolume(device, volumeData, {}); auto colors = createData(device, {Color4{1, 0, 0, 1}, Color4{0, 0, 1, 1}}); auto transferFunction = createLinearTransferFunction(device, {{0, 1}, colors}); @@ -150,6 +151,7 @@ TEST_CASE("Render") auto renderer = createScivisRenderer(device, {{.materials = createData(device, {material})}}); auto camera = createPerspectiveCamera(device, {}, {.fovy = 45.0F, .aspect = float(width) / float(height)}); + // auto camera = createOrthographicCamera(device, {}, {.height = 3, .aspect = float(width) / float(height)}); auto sphereData = std::vector{{0, 0, 3, 0.25F}, {1, 0, 3, 0.25F}, {1, 1, 3, 0.25F}}; auto colors = std::vector{{1, 0, 0, 1}, {0, 0, 1, 1}, {0, 1, 0, 1}}; @@ -199,9 +201,7 @@ TEST_CASE("Render") auto data = framebuffer.map(FramebufferChannel::Color); - rkcommon::utility::writePPM("test.ppm", width, height, static_cast(data)); - - framebuffer.unmap(data); + rkcommon::utility::writePPM("test.ppm", width, height, data.as()); CHECK(error.empty()); } diff --git a/tests/core/json/TestJsonReflection.cpp b/tests/core/json/TestJsonReflection.cpp index a087133d4..d46b6aa84 100644 --- a/tests/core/json/TestJsonReflection.cpp +++ b/tests/core/json/TestJsonReflection.cpp @@ -35,11 +35,11 @@ enum class SomeEnum template<> struct EnumReflector { - static EnumInfo reflect() + static auto reflect() { auto builder = EnumBuilder(); - builder.field("value1", SomeEnum::Value1).description("Value 1"); - builder.field("value2", SomeEnum::Value2).description("Value 2"); + builder.field("Value1", SomeEnum::Value1).description("Value 1"); + builder.field("Value2", SomeEnum::Value2).description("Value 2"); return builder.build(); } }; @@ -94,228 +94,259 @@ struct JsonObjectReflector }; } -TEST_CASE("JsonReflection") +constexpr auto i32Min = std::numeric_limits::lowest(); +constexpr auto i32Max = std::numeric_limits::max(); +constexpr auto f32Min = std::numeric_limits::lowest(); +constexpr auto f32Max = std::numeric_limits::max(); + +TEST_CASE("Undefined") { - constexpr auto imin = std::numeric_limits::lowest(); - constexpr auto imax = std::numeric_limits::max(); - constexpr auto fmin = std::numeric_limits::lowest(); - constexpr auto fmax = std::numeric_limits::max(); + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Undefined}); + CHECK_EQ(deserializeAs(1), JsonValue(1)); + CHECK_EQ(serializeToJson(JsonValue("2")), JsonValue("2")); +} - SUBCASE("Undefined") - { - CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Undefined}); - CHECK_EQ(deserializeAs(1), JsonValue(1)); - CHECK_EQ(serializeToJson(JsonValue("2")), JsonValue("2")); - } - SUBCASE("Null") - { - CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Null}); - deserializeAs({}); - CHECK_EQ(serializeToJson(NullJson()), JsonValue()); - CHECK_THROWS_AS(deserializeAs("xyz"), JsonException); - } - SUBCASE("Boolean") - { - CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Boolean}); - CHECK_EQ(deserializeAs(true), true); - CHECK_EQ(serializeToJson(true), JsonValue(true)); - CHECK_THROWS_AS(deserializeAs("xyz"), JsonException); - } - SUBCASE("Integer") - { - CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Integer, .minimum = 0, .maximum = 255}); - CHECK_EQ(getJsonSchema().type, JsonType::Integer); - CHECK_EQ(getJsonSchema().type, JsonType::Integer); - CHECK_EQ(deserializeAs(1), 1); - CHECK_EQ(serializeToJson(1), JsonValue(1)); - CHECK_THROWS_AS(deserializeAs(1.5), JsonException); - } - SUBCASE("Number") - { - CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Number, .minimum = fmin, .maximum = fmax}); - CHECK_EQ(getJsonSchema().type, JsonType::Number); - CHECK_EQ(deserializeAs(1), 1.0f); - CHECK_EQ(serializeToJson(1.5f), JsonValue(1.5f)); - CHECK_THROWS_AS(deserializeAs("1.5"), JsonException); - } - SUBCASE("String") - { - CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::String}); - CHECK_EQ(deserializeAs("test"), JsonValue("test")); - CHECK_EQ(serializeToJson(std::string("test")), JsonValue("test")); - CHECK_THROWS_AS(deserializeAs(1), JsonException); - } - SUBCASE("Enum") - { - CHECK_EQ( - getJsonSchema(), - JsonSchema{ - .oneOf = { - JsonSchema{ - .description = "Value 1", - .type = JsonType::String, - .constant = "value1", - }, - JsonSchema{ - .description = "Value 2", - .type = JsonType::String, - .constant = "value2", - }, - }}); - CHECK_EQ(deserializeAs("value1"), SomeEnum::Value1); - CHECK_EQ(serializeToJson(SomeEnum::Value2), JsonValue("value2")); - CHECK_THROWS_AS(deserializeAs(1), JsonException); - CHECK_THROWS_AS(deserializeAs("value3"), JsonException); - } - SUBCASE("Array") - { - CHECK_EQ( - getJsonSchema>(), - JsonSchema{.type = JsonType::Array, .items = {JsonSchema{.type = JsonType::String}}}); - CHECK_EQ(parseJsonAs>("[1,2,3]"), std::vector{1, 2, 3}); - CHECK_EQ(stringifyToJson(std::vector{1, 2, 3}), "[1,2,3]"); - } - SUBCASE("Math") - { - CHECK_EQ( - getJsonSchema(), - JsonSchema{ - .type = JsonType::Array, - .items = {JsonSchema{.type = JsonType::Number, .minimum = fmin, .maximum = fmax}}, - .minItems = 3, - .maxItems = 3, - }); - - CHECK_EQ( - getJsonSchema(), - JsonSchema{ - .type = JsonType::Array, - .items = {JsonSchema{.type = JsonType::Number, .minimum = fmin, .maximum = fmax}}, - .minItems = 4, - .maxItems = 4, - }); - - CHECK_EQ(parseJsonAs("[1,2,3]"), Vector3(1, 2, 3)); - CHECK_EQ(parseJsonAs("[1,2,3,4]"), Quaternion(4, 1, 2, 3)); - - CHECK_EQ(stringifyToJson(Vector3(1, 2, 3)), "[1,2,3]"); - CHECK_EQ(stringifyToJson(Quaternion(4, 1, 2, 3)), "[1,2,3,4]"); - - CHECK_THROWS_AS(parseJsonAs("[1,2,3,4]"), JsonException); - CHECK_THROWS_AS(parseJsonAs("[1,2,3,4,5]"), JsonException); - - CHECK_THROWS_AS(parseJsonAs("[1,2]"), JsonException); - CHECK_THROWS_AS(parseJsonAs("[1,2]"), JsonException); - } - SUBCASE("Map") - { - using Map = std::map; +TEST_CASE("Null") +{ + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Null}); + deserializeAs({}); + CHECK_EQ(serializeToJson(NullJson()), JsonValue()); + CHECK_THROWS_AS(deserializeAs("xyz"), JsonException); +} - CHECK_EQ( - getJsonSchema>(), - JsonSchema{ - .type = JsonType::Object, - .items = {JsonSchema{.type = JsonType::String}}, - }); +TEST_CASE("Boolean") +{ + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Boolean}); + CHECK_EQ(deserializeAs(true), true); + CHECK_EQ(serializeToJson(true), JsonValue(true)); + CHECK_THROWS_AS(deserializeAs("xyz"), JsonException); +} - auto map = Map{{"test1", 1}, {"test2", 2}}; - auto json = R"({"test1":1,"test2":2})"; +TEST_CASE("Integer") +{ + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Integer, .minimum = 0, .maximum = 255}); + CHECK_EQ(getJsonSchema().type, JsonType::Integer); + CHECK_EQ(getJsonSchema().type, JsonType::Integer); + CHECK_EQ(deserializeAs(1), 1); + CHECK_EQ(serializeToJson(1), JsonValue(1)); + CHECK_THROWS_AS(deserializeAs(1.5), JsonException); +} - CHECK_EQ(parseJsonAs(json), map); - CHECK_EQ(stringifyToJson(map), json); +TEST_CASE("Number") +{ + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Number, .minimum = f32Min, .maximum = f32Max}); + CHECK_EQ(getJsonSchema().type, JsonType::Number); + CHECK_EQ(deserializeAs(1), 1.0f); + CHECK_EQ(serializeToJson(1.5f), JsonValue(1.5f)); + CHECK_THROWS_AS(deserializeAs("1.5"), JsonException); +} - CHECK_THROWS_AS(parseJsonAs(R"({"invalid":2.5})"), JsonException); - } - SUBCASE("Variant") - { - using Variant = std::variant; - - CHECK_EQ( - getJsonSchema(), - JsonSchema{ - .oneOf = { - JsonSchema{.type = JsonType::String}, - JsonSchema{.type = JsonType::Integer, .minimum = imin, .maximum = imax}, - }}); - CHECK_EQ(serializeToJson(Variant("test")), JsonValue("test")); - CHECK_EQ(serializeToJson(Variant(1)), JsonValue(1)); - CHECK_EQ(deserializeAs(1), Variant(1)); - CHECK_EQ(deserializeAs("test"), Variant("test")); - CHECK_THROWS_AS(deserializeAs(1.5), JsonException); - - CHECK_EQ( - getJsonSchema>(), - JsonSchema{ - .required = false, - .oneOf = {JsonSchema{.type = JsonType::String}, JsonSchema{.type = JsonType::Null}}, - }); - CHECK_EQ(serializeToJson(std::optional("test")), JsonValue("test")); - CHECK_EQ(serializeToJson(std::optional()), JsonValue()); - CHECK_EQ(deserializeAs>({}), std::nullopt); - CHECK_EQ(deserializeAs>("test"), std::string("test")); - CHECK_THROWS_AS(deserializeAs>(1.5), JsonException); - } - SUBCASE("Object") - { - const auto &schema = getJsonSchema(); - - CHECK_EQ(schema.description, "Test parent"); - CHECK_EQ(schema.type, JsonType::Object); - - const auto &properties = schema.properties; - - CHECK_EQ(properties.at("constant"), JsonSchema{.type = JsonType::String, .constant = "test"}); - CHECK_EQ(properties.at("required"), getJsonSchema()); - CHECK_EQ(properties.at("bounded"), JsonSchema{.type = JsonType::Integer, .minimum = 1, .maximum = 3}); - CHECK_EQ(properties.at("description"), JsonSchema{.description = "Test", .type = JsonType::Boolean}); - CHECK_EQ( - properties.at("default"), - JsonSchema{.required = false, .defaultValue = "test", .type = JsonType::String}); - CHECK_EQ(properties.at("optional"), getJsonSchema>()); - CHECK_EQ(properties.at("enum"), getJsonSchema()); - CHECK_EQ( - properties.at("array"), - JsonSchema{ - .type = JsonType::Array, - .items = {getJsonSchema()}, - .minItems = 1, - .maxItems = 3, - }); - CHECK_EQ( - properties.at("internal"), - JsonSchema{ - .description = "Test child", - .type = JsonType::Object, - .properties = {{"value", getJsonSchema()}}, - }); - - auto internal = createJsonObject(); - internal->set("value", 2); - - auto object = createJsonObject(); - object->set("constant", "test"); - object->set("required", true); - object->set("bounded", 2); - object->set("description", true); - object->set("enum", "value2"); - object->set("array", serializeToJson(std::vector{1, 2, 3})); - object->set("internal", internal); - - auto json = JsonValue(object); - - auto test = deserializeAs(json); - - CHECK(test.required); - CHECK_EQ(test.bounded, 2); - CHECK(test.description); - CHECK_EQ(test.withDefault, "test"); - CHECK_FALSE(test.optional); - CHECK_EQ(test.someEnum, SomeEnum::Value2); - CHECK_EQ(test.array, std::vector{1, 2, 3}); - CHECK_EQ(test.internal.value, 2); - - object->set("default", "test"); - - CHECK_EQ(stringifyToJson(test), stringifyToJson(json)); - } +TEST_CASE("String") +{ + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::String}); + CHECK_EQ(deserializeAs("test"), JsonValue("test")); + CHECK_EQ(serializeToJson(std::string("test")), JsonValue("test")); + CHECK_THROWS_AS(deserializeAs(1), JsonException); +} + +TEST_CASE("Const") +{ + CHECK_EQ(getJsonSchema(), JsonSchema{.type = JsonType::Boolean, .constant = false}); + CHECK_EQ(deserializeAs(false), JsonFalse()); + CHECK_EQ(serializeToJson(JsonFalse()), false); + CHECK_THROWS_AS(deserializeAs(1), JsonException); +} + +TEST_CASE("Enum") +{ + CHECK_EQ( + getJsonSchema(), + JsonSchema{ + .oneOf = { + JsonSchema{ + .description = "Value 1", + .type = JsonType::String, + .constant = "Value1", + }, + JsonSchema{ + .description = "Value 2", + .type = JsonType::String, + .constant = "Value2", + }, + }}); + CHECK_EQ(deserializeAs("Value1"), SomeEnum::Value1); + CHECK_EQ(serializeToJson(SomeEnum::Value2), JsonValue("Value2")); + CHECK_THROWS_AS(deserializeAs(1), JsonException); + CHECK_THROWS_AS(deserializeAs("Value3"), JsonException); +} + +TEST_CASE("Array") +{ + CHECK_EQ( + getJsonSchema>(), + JsonSchema{ + .type = JsonType::Array, + .items = {JsonSchema{.type = JsonType::String}}, + }); + CHECK_EQ(parseJsonAs>("[1,2,3]"), std::vector{1, 2, 3}); + CHECK_EQ(stringifyToJson(std::vector{1, 2, 3}), "[1,2,3]"); +} + +TEST_CASE("Set") +{ + CHECK_EQ( + getJsonSchema>(), + JsonSchema{ + .type = JsonType::Array, + .items = {JsonSchema{.type = JsonType::String}}, + .uniqueItems = true, + }); + CHECK_EQ(parseJsonAs>("[1,2,3]"), std::set{1, 2, 3}); + CHECK_EQ(stringifyToJson(std::set{1, 2, 3}), "[1,2,3]"); + CHECK_THROWS_AS(parseJsonAs>("[1,2,2]"), JsonException); +} + +TEST_CASE("Math") +{ + CHECK_EQ( + getJsonSchema(), + JsonSchema{ + .type = JsonType::Array, + .items = {JsonSchema{.type = JsonType::Number, .minimum = f32Min, .maximum = f32Max}}, + .minItems = 3, + .maxItems = 3, + }); + + CHECK_EQ( + getJsonSchema(), + JsonSchema{ + .type = JsonType::Array, + .items = {JsonSchema{.type = JsonType::Number, .minimum = f32Min, .maximum = f32Max}}, + .minItems = 4, + .maxItems = 4, + }); + + CHECK_EQ(parseJsonAs("[1,2,3]"), Vector3(1, 2, 3)); + CHECK_EQ(parseJsonAs("[1,2,3,4]"), Quaternion(4, 1, 2, 3)); + + CHECK_EQ(stringifyToJson(Vector3(1, 2, 3)), "[1,2,3]"); + CHECK_EQ(stringifyToJson(Quaternion(4, 1, 2, 3)), "[1,2,3,4]"); + + CHECK_THROWS_AS(parseJsonAs("[1,2,3,4]"), JsonException); + CHECK_THROWS_AS(parseJsonAs("[1,2,3,4,5]"), JsonException); + + CHECK_THROWS_AS(parseJsonAs("[1,2]"), JsonException); + CHECK_THROWS_AS(parseJsonAs("[1,2]"), JsonException); +} + +TEST_CASE("Map") +{ + using Map = std::map; + + CHECK_EQ( + getJsonSchema>(), + JsonSchema{ + .type = JsonType::Object, + .items = {JsonSchema{.type = JsonType::String}}, + }); + + auto map = Map{{"test1", 1}, {"test2", 2}}; + auto json = R"({"test1":1,"test2":2})"; + + CHECK_EQ(parseJsonAs(json), map); + CHECK_EQ(stringifyToJson(map), json); + + CHECK_THROWS_AS(parseJsonAs(R"({"invalid":2.5})"), JsonException); +} + +TEST_CASE("Variant") +{ + using Variant = std::variant; + + CHECK_EQ( + getJsonSchema(), + JsonSchema{ + .oneOf = { + JsonSchema{.type = JsonType::String}, + JsonSchema{.type = JsonType::Integer, .minimum = i32Min, .maximum = i32Max}, + }}); + CHECK_EQ(serializeToJson(Variant("test")), JsonValue("test")); + CHECK_EQ(serializeToJson(Variant(1)), JsonValue(1)); + CHECK_EQ(deserializeAs(1), Variant(1)); + CHECK_EQ(deserializeAs("test"), Variant("test")); + CHECK_THROWS_AS(deserializeAs(1.5), JsonException); + + CHECK_EQ( + getJsonSchema>(), + JsonSchema{ + .required = false, + .oneOf = {JsonSchema{.type = JsonType::String}, JsonSchema{.type = JsonType::Null}}, + }); + CHECK_EQ(serializeToJson(std::optional("test")), JsonValue("test")); + CHECK_EQ(serializeToJson(std::optional()), JsonValue()); + CHECK_EQ(deserializeAs>({}), std::nullopt); + CHECK_EQ(deserializeAs>("test"), std::string("test")); + CHECK_THROWS_AS(deserializeAs>(1.5), JsonException); +} + +TEST_CASE("Object") +{ + const auto &schema = getJsonSchema(); + + CHECK_EQ(schema.description, "Test parent"); + CHECK_EQ(schema.type, JsonType::Object); + + const auto &properties = schema.properties; + + CHECK_EQ(properties.at("constant"), JsonSchema{.type = JsonType::String, .constant = "test"}); + CHECK_EQ(properties.at("required"), getJsonSchema()); + CHECK_EQ(properties.at("bounded"), JsonSchema{.type = JsonType::Integer, .minimum = 1, .maximum = 3}); + CHECK_EQ(properties.at("description"), JsonSchema{.description = "Test", .type = JsonType::Boolean}); + CHECK_EQ(properties.at("default"), JsonSchema{.required = false, .defaultValue = "test", .type = JsonType::String}); + CHECK_EQ(properties.at("optional"), getJsonSchema>()); + CHECK_EQ(properties.at("enum"), getJsonSchema()); + CHECK_EQ( + properties.at("array"), + JsonSchema{ + .type = JsonType::Array, + .items = {getJsonSchema()}, + .minItems = 1, + .maxItems = 3, + }); + CHECK_EQ( + properties.at("internal"), + JsonSchema{ + .description = "Test child", + .type = JsonType::Object, + .properties = {{"value", getJsonSchema()}}, + }); + + auto internal = createJsonObject(); + internal->set("value", 2); + + auto object = createJsonObject(); + object->set("constant", "test"); + object->set("required", true); + object->set("bounded", 2); + object->set("description", true); + object->set("enum", "Value2"); + object->set("array", serializeToJson(std::vector{1, 2, 3})); + object->set("internal", internal); + + auto json = JsonValue(object); + + auto test = deserializeAs(json); + + CHECK(test.required); + CHECK_EQ(test.bounded, 2); + CHECK(test.description); + CHECK_EQ(test.withDefault, "test"); + CHECK_FALSE(test.optional); + CHECK_EQ(test.someEnum, SomeEnum::Value2); + CHECK_EQ(test.array, std::vector{1, 2, 3}); + CHECK_EQ(test.internal.value, 2); + + object->set("default", "test"); + + CHECK_EQ(stringifyToJson(test), stringifyToJson(json)); } diff --git a/tests/core/json/TestJsonSchema.cpp b/tests/core/json/TestJsonSchema.cpp index 1a6195ba2..efc313fa9 100644 --- a/tests/core/json/TestJsonSchema.cpp +++ b/tests/core/json/TestJsonSchema.cpp @@ -24,258 +24,294 @@ using namespace brayns; -TEST_CASE("JsonSchema") +TEST_CASE("Wildcard") { - SUBCASE("Wildcard") - { - auto schema = JsonSchema(); - auto json = parseJson(R"({"test": 10})"); - - auto errors = validate(json, schema); - CHECK(errors.empty()); - - json = 1; - errors = validate(json, schema); - CHECK(errors.empty()); - } - SUBCASE("One of") - { - auto schema = JsonSchema{.oneOf = {getJsonSchema(), getJsonSchema()}}; - - auto errors = validate(1.0f, schema); - CHECK(errors.empty()); - - errors = validate("Test", schema); - CHECK(errors.empty()); - - errors = validate(true, schema); - CHECK_EQ(errors.size(), 1); - CHECK_EQ(toString(errors[0].error), "Invalid oneOf"); - } - SUBCASE("Invalid type") - { - auto schema = JsonSchema{.type = JsonType::String}; - - auto errors = validate(1, schema); - CHECK_EQ(errors.size(), 1); - CHECK_EQ(toString(errors[0].error), "Invalid type: expected string got integer"); - - schema.type = JsonType::Number; - errors = validate(1, schema); - CHECK(errors.empty()); - } - SUBCASE("Limits") - { - auto schema = JsonSchema{ - .type = JsonType::Integer, - .minimum = -1, - .maximum = 3, - }; - - auto errors = validate(1, schema); - CHECK(errors.empty()); - - errors = validate(-1, schema); - CHECK(errors.empty()); - - errors = validate(3, schema); - CHECK(errors.empty()); - - errors = validate(-2, schema); - CHECK_EQ(errors.size(), 1); - CHECK_EQ(toString(errors[0].error), "Value below minimum: -2 < -1"); - - errors = validate(4, schema); - CHECK_EQ(errors.size(), 1); - CHECK_EQ(toString(errors[0].error), "Value above maximum: 4 > 3"); - - schema.minimum = std::nullopt; - schema.maximum = std::nullopt; - - errors = validate(-8, schema); - CHECK(errors.empty()); - - errors = validate(125, schema); - CHECK(errors.empty()); - } - SUBCASE("Constant") - { - auto schema = JsonSchema{ - .type = JsonType::String, - .constant = "test", - }; - - auto errors = validate("test", schema); - CHECK(errors.empty()); - - errors = validate("test1", schema); - CHECK_EQ(errors.size(), 1); - CHECK_EQ(toString(errors[0].error), "Invalid const: expected 'test' got 'test1'"); - - errors = validate(1, schema); - CHECK_EQ(errors.size(), 1); - CHECK_EQ(toString(errors[0].error), "Invalid type: expected string got integer"); - } - SUBCASE("Property type") - { - auto schema = JsonSchema{ - .type = JsonType::Object, - .properties = {{ - "internal", - JsonSchema{ - .type = JsonType::Object, - .properties = {{"integer", getJsonSchema()}}, - }, - }}}; - - auto json = parseJson(R"({"internal": 1})"); - auto errors = validate(json, schema); - CHECK_EQ(errors.size(), 1); - CHECK_EQ(toString(errors[0].error), "Invalid type: expected object got integer"); - - json = parseJson(R"({"internal": {"integer": true}})"); - errors = validate(json, schema); - CHECK_EQ(errors.size(), 1); - CHECK_EQ(toString(errors[0].path), "internal.integer"); - CHECK_EQ(toString(errors[0].error), "Invalid type: expected integer got boolean"); - - json = parseJson(R"({"internal": {"integer": 1}})"); - errors = validate(json, schema); - CHECK(errors.empty()); - } - SUBCASE("Missing property") - { - auto schema = JsonSchema{ - .type = JsonType::Object, - .properties = - { - {"integer", JsonSchema{.type = JsonType::Integer}}, - {"string", JsonSchema{.required = false, .type = JsonType::String}}, - }, - }; - - auto json = parseJson(R"({"integer": 1, "string": "test"})"); - auto errors = validate(json, schema); - CHECK(errors.empty()); - - json = parseJson(R"({"string": "test"})"); - errors = validate(json, schema); - CHECK_EQ(errors.size(), 1); - CHECK_EQ(toString(errors[0].error), "Missing required property: 'integer'"); - - json = parseJson(R"({"integer": 1})"); - errors = validate(json, schema); - CHECK(errors.empty()); - } - SUBCASE("Unknown properties") - { - auto schema = JsonSchema{.type = JsonType::Object}; - - auto json = parseJson(R"({"something": 1})"); - auto errors = validate(json, schema); - CHECK_EQ(errors.size(), 1); - CHECK_EQ(toString(errors[0].error), "Unknown property: 'something'"); - - json = parseJson(R"({})"); - errors = validate(json, schema); - CHECK(errors.empty()); - } - SUBCASE("Item type") - { - auto schema = JsonSchema{ - .type = JsonType::Array, - .items = {JsonSchema{.type = JsonType::Integer}}, - }; - - auto json = parseJson(R"([1, 2, 3])"); - auto errors = validate(json, schema); - CHECK(errors.empty()); - - json = parseJson(R"([])"); - errors = validate(json, schema); - CHECK(errors.empty()); - - json = parseJson(R"([1, "test", 2])"); - errors = validate(json, schema); - CHECK_EQ(errors.size(), 1); - CHECK_EQ(toString(errors[0].path), "[1]"); - CHECK_EQ(toString(errors[0].error), "Invalid type: expected integer got string"); - } - SUBCASE("Item count") - { - auto schema = JsonSchema{ - .type = JsonType::Array, - .items = {getJsonSchema()}, - .minItems = 1, - .maxItems = 3, - }; - - auto json = parseJson(R"([1])"); - auto errors = validate(json, schema); - CHECK(errors.empty()); - - json = parseJson(R"([1, 2, 3])"); - errors = validate(json, schema); - CHECK(errors.empty()); - - json = parseJson(R"([])"); - errors = validate(json, schema); - CHECK_EQ(errors.size(), 1); - CHECK_EQ(toString(errors[0].error), "Not enough items: 0 < 1"); - - json = parseJson(R"([1, 2, 3, 4])"); - errors = validate(json, schema); - CHECK_EQ(errors.size(), 1); - CHECK_EQ(toString(errors[0].error), "Too many items: 4 > 3"); - } - SUBCASE("Nested") - { - auto internal = JsonSchema{ - .type = JsonType::Object, - .properties = {{"test3", getJsonSchema>()}}, - }; - - auto schema = JsonSchema{ - .type = JsonType::Object, - .properties = { - {"test1", JsonSchema{.required = true, .type = JsonType::Integer}}, - {"test2", - JsonSchema{ - .type = JsonType::Object, - .properties = {{"test3", getJsonSchema>()}}, - }}, - }}; - - auto json = parseJson(R"({"test2": {"test3": [1.3]}})"); - auto errors = validate(json, schema); - CHECK_EQ(errors.size(), 2); - - CHECK_EQ(toString(errors[0].path), ""); - CHECK_EQ(toString(errors[0].error), "Missing required property: 'test1'"); - - CHECK_EQ(toString(errors[1].path), "test2.test3[0]"); - CHECK_EQ(toString(errors[1].error), "Invalid type: expected integer got number"); - } - SUBCASE("Schema as JSON") - { - auto schema = getJsonSchema(); - auto json = stringifyToJson(schema); - auto ref = R"({"type":"string"})"; - CHECK_EQ(json, ref); - - schema = getJsonSchema>(); - json = stringifyToJson(schema); - ref = R"({"items":{"type":"string"},"type":"array"})"; - CHECK_EQ(json, ref); - - schema = getJsonSchema>(); - json = stringifyToJson(schema); - ref = R"({"additionalProperties":{"type":"boolean"},"type":"object"})"; - CHECK_EQ(json, ref); - - schema = getJsonSchema>(); - json = stringifyToJson(schema); - ref = R"({"oneOf":[{"type":"string"},{"type":"boolean"}]})"; - CHECK_EQ(json, ref); - } + auto schema = JsonSchema(); + auto json = parseJson(R"({"test": 10})"); + + auto errors = validate(json, schema); + CHECK(errors.empty()); + + json = 1; + errors = validate(json, schema); + CHECK(errors.empty()); +} + +TEST_CASE("One of") +{ + auto schema = JsonSchema{.oneOf = {getJsonSchema(), getJsonSchema()}}; + + auto errors = validate(1.0f, schema); + CHECK(errors.empty()); + + errors = validate("Test", schema); + CHECK(errors.empty()); + + errors = validate(true, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Invalid oneOf: true"); +} + +TEST_CASE("Invalid type") +{ + auto schema = JsonSchema{.type = JsonType::String}; + + auto errors = validate(1, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Invalid type: expected string got integer"); + + schema.type = JsonType::Number; + errors = validate(1, schema); + CHECK(errors.empty()); +} + +TEST_CASE("Limits") +{ + auto schema = JsonSchema{ + .type = JsonType::Integer, + .minimum = -1, + .maximum = 3, + }; + + auto errors = validate(1, schema); + CHECK(errors.empty()); + + errors = validate(-1, schema); + CHECK(errors.empty()); + + errors = validate(3, schema); + CHECK(errors.empty()); + + errors = validate(-2, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Value below minimum: -2 < -1"); + + errors = validate(4, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Value above maximum: 4 > 3"); + + schema.minimum = std::nullopt; + schema.maximum = std::nullopt; + + errors = validate(-8, schema); + CHECK(errors.empty()); + + errors = validate(125, schema); + CHECK(errors.empty()); +} + +TEST_CASE("Constant") +{ + auto schema = JsonSchema{ + .type = JsonType::String, + .constant = "test", + }; + + auto errors = validate("test", schema); + CHECK(errors.empty()); + + errors = validate("test1", schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Invalid const: expected 'test' got 'test1'"); + + errors = validate(1, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Invalid type: expected string got integer"); +} + +TEST_CASE("Property type") +{ + auto schema = JsonSchema{ + .type = JsonType::Object, + .properties = {{ + "internal", + JsonSchema{ + .type = JsonType::Object, + .properties = {{"integer", getJsonSchema()}}, + }, + }}}; + + auto json = parseJson(R"({"internal": 1})"); + auto errors = validate(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Invalid type: expected object got integer"); + + json = parseJson(R"({"internal": {"integer": true}})"); + errors = validate(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].path), "internal.integer"); + CHECK_EQ(toString(errors[0].error), "Invalid type: expected integer got boolean"); + + json = parseJson(R"({"internal": {"integer": 1}})"); + errors = validate(json, schema); + CHECK(errors.empty()); +} + +TEST_CASE("Missing property") +{ + auto schema = JsonSchema{ + .type = JsonType::Object, + .properties = + { + {"integer", JsonSchema{.type = JsonType::Integer}}, + {"string", JsonSchema{.required = false, .type = JsonType::String}}, + }, + }; + + auto json = parseJson(R"({"integer": 1, "string": "test"})"); + auto errors = validate(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"({"string": "test"})"); + errors = validate(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Missing required property: 'integer'"); + + json = parseJson(R"({"integer": 1})"); + errors = validate(json, schema); + CHECK(errors.empty()); +} + +TEST_CASE("Unknown properties") +{ + auto schema = JsonSchema{.type = JsonType::Object}; + + auto json = parseJson(R"({"something": 1})"); + auto errors = validate(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Unknown property: 'something'"); + + json = parseJson(R"({})"); + errors = validate(json, schema); + CHECK(errors.empty()); +} + +TEST_CASE("Item type") +{ + auto schema = JsonSchema{ + .type = JsonType::Array, + .items = {JsonSchema{.type = JsonType::Integer}}, + }; + + auto json = parseJson(R"([1, 2, 3])"); + auto errors = validate(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"([])"); + errors = validate(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"([1, "test", 2])"); + errors = validate(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].path), "[1]"); + CHECK_EQ(toString(errors[0].error), "Invalid type: expected integer got string"); +} + +TEST_CASE("Unique items") +{ + auto schema = JsonSchema{ + .type = JsonType::Array, + .items = {getJsonSchema()}, + .uniqueItems = true, + }; + + auto json = parseJson(R"([1, 2, 3])"); + auto errors = validate(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"([])"); + errors = validate(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"([1, 2, 3, 2])"); + errors = validate(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Duplicated item: '2'"); + + json = parseJson(R"([1, 1, 2, 2])"); + errors = validate(json, schema); + CHECK_EQ(errors.size(), 2); + CHECK_EQ(toString(errors[0].error), "Duplicated item: '1'"); + CHECK_EQ(toString(errors[1].error), "Duplicated item: '2'"); +} + +TEST_CASE("Item count") +{ + auto schema = JsonSchema{ + .type = JsonType::Array, + .items = {getJsonSchema()}, + .minItems = 1, + .maxItems = 3, + }; + + auto json = parseJson(R"([1])"); + auto errors = validate(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"([1, 2, 3])"); + errors = validate(json, schema); + CHECK(errors.empty()); + + json = parseJson(R"([])"); + errors = validate(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Not enough items: 0 < 1"); + + json = parseJson(R"([1, 2, 3, 4])"); + errors = validate(json, schema); + CHECK_EQ(errors.size(), 1); + CHECK_EQ(toString(errors[0].error), "Too many items: 4 > 3"); +} + +TEST_CASE("Nested") +{ + auto internal = JsonSchema{ + .type = JsonType::Object, + .properties = {{"test3", getJsonSchema>()}}, + }; + + auto schema = JsonSchema{ + .type = JsonType::Object, + .properties = { + {"test1", JsonSchema{.required = true, .type = JsonType::Integer}}, + {"test2", + JsonSchema{ + .type = JsonType::Object, + .properties = {{"test3", getJsonSchema>()}}, + }}, + }}; + + auto json = parseJson(R"({"test2": {"test3": [1.3]}})"); + auto errors = validate(json, schema); + CHECK_EQ(errors.size(), 2); + + CHECK_EQ(toString(errors[0].path), ""); + CHECK_EQ(toString(errors[0].error), "Missing required property: 'test1'"); + + CHECK_EQ(toString(errors[1].path), "test2.test3[0]"); + CHECK_EQ(toString(errors[1].error), "Invalid type: expected integer got number"); +} + +TEST_CASE("Schema as JSON") +{ + auto schema = getJsonSchema(); + auto json = stringifyToJson(schema); + auto ref = R"({"type":"string"})"; + CHECK_EQ(json, ref); + + schema = getJsonSchema>(); + json = stringifyToJson(schema); + ref = R"({"items":{"type":"string"},"type":"array"})"; + CHECK_EQ(json, ref); + + schema = getJsonSchema>(); + json = stringifyToJson(schema); + ref = R"({"additionalProperties":{"type":"boolean"},"type":"object"})"; + CHECK_EQ(json, ref); + + schema = getJsonSchema>(); + json = stringifyToJson(schema); + ref = R"({"oneOf":[{"type":"string"},{"type":"boolean"}]})"; + CHECK_EQ(json, ref); } diff --git a/tests/core/manager/TestObjectRegistry.cpp b/tests/core/manager/TestObjectRegistry.cpp new file mode 100644 index 000000000..8cb95de31 --- /dev/null +++ b/tests/core/manager/TestObjectRegistry.cpp @@ -0,0 +1,87 @@ +/* Copyright (c) 2015-2024, EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include + +#include + +using namespace brayns; + +struct TestObject +{ + std::string value; +}; + +TEST_CASE("Create and remove objects") +{ + auto objects = ObjectRegistry(); + + auto object = objects.add(TestObject{"3"}, "TestObject"); + + auto id = object.getId(); + + CHECK_EQ(id, 1); + CHECK_EQ(object.get().value, "3"); + + auto info = objects.get(id); + + CHECK_EQ(info.id, id); + CHECK_EQ(info.type, "TestObject"); + CHECK(info.userData.isEmpty()); + + auto stored = objects.getAsStored(id); + + CHECK_EQ(stored.get().value, "3"); + + auto object2 = objects.add(TestObject{"2"}, "TestObject"); + auto id2 = object2.getId(); + + CHECK_EQ(objects.getAll().size(), 2); + + objects.remove(id); + + CHECK(stored.isRemoved()); + CHECK_EQ(stored.getId(), nullId); + + CHECK_THROWS_AS(objects.get(id), InvalidParams); + CHECK_THROWS_AS(objects.getAs(id), InvalidParams); + + auto object3 = objects.add(TestObject{"0"}, "TestObject"); + CHECK_EQ(object3.getId(), 1); + + auto stored2 = objects.getAsStored(id2); + + objects.clear(); + + CHECK(stored2.isRemoved()); + CHECK(object3.isRemoved()); + + CHECK(objects.getAll().empty()); +} + +TEST_CASE("Errors") +{ + auto objects = ObjectRegistry(); + + objects.add(TestObject{"0"}, "TestObject"); + + CHECK_THROWS_AS(objects.get(0), InvalidParams); + CHECK_THROWS_AS(objects.get(2), InvalidParams); + CHECK_THROWS_AS(objects.remove(2), InvalidParams); +} diff --git a/tests/core/objects/TestObjectManager.cpp b/tests/core/objects/TestObjectManager.cpp deleted file mode 100644 index e14361230..000000000 --- a/tests/core/objects/TestObjectManager.cpp +++ /dev/null @@ -1,115 +0,0 @@ -/* Copyright (c) 2015-2024, EPFL/Blue Brain Project - * All rights reserved. Do not distribute without permission. - * - * This file is part of Brayns - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License version 3.0 as published - * by the Free Software Foundation. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include - -#include - -using namespace brayns; - -namespace brayns -{ -struct TestObject -{ - std::string type; - ObjectId id = nullId; -}; - -template<> -struct ObjectReflector -{ - static std::string getType(const TestObject &object) - { - return object.type; - } - - static void add(TestObject &object, ObjectId id) - { - object.id = id; - } - - static void remove(TestObject &object) - { - object.id = nullId; - } -}; -} - -TEST_CASE("Create and remove objects") -{ - auto objects = ObjectManager(); - - auto object = objects.add(TestObject{"type"}); - - auto id = object.getId(); - - CHECK_EQ(id, 1); - - auto info = objects.getObject(id); - - CHECK_EQ(info.id, id); - CHECK_EQ(info.type, "type"); - CHECK(info.userData.isEmpty()); - - auto &retreived = objects.get(id); - - CHECK_EQ(retreived.type, "type"); - CHECK_EQ(retreived.id, id); - - auto stored = objects.getStored(id); - - CHECK_EQ(stored.getId(), id); - - auto object2 = objects.add(TestObject()); - auto id2 = object2.getId(); - - CHECK_EQ(objects.getAllObjects().size(), 2); - - objects.remove(id); - - CHECK(stored.isRemoved()); - CHECK_EQ(stored.get().id, nullId); - - CHECK_THROWS_AS(objects.getObject(id), InvalidParams); - CHECK_THROWS_AS(objects.get(id), InvalidParams); - CHECK_THROWS_AS(objects.getStored(id), InvalidParams); - - auto object3 = objects.add(TestObject()); - CHECK_EQ(object3.getId(), 1); - - auto stored2 = objects.getStored(id2); - - objects.clear(); - - CHECK(stored2.isRemoved()); - CHECK(object3.isRemoved()); - - CHECK(objects.getAllObjects().empty()); -} - -TEST_CASE("Errors") -{ - auto objects = ObjectManager(); - - objects.add({}); - - CHECK_THROWS_AS(objects.getObject(0), InvalidParams); - CHECK_THROWS_AS(objects.getObject(2), InvalidParams); - CHECK_THROWS_AS(objects.remove(2), InvalidParams); -} diff --git a/tests/core/utils/TestBinary.cpp b/tests/core/utils/TestBinary.cpp new file mode 100644 index 000000000..0e1d13e0f --- /dev/null +++ b/tests/core/utils/TestBinary.cpp @@ -0,0 +1,95 @@ +/* Copyright (c) 2015-2024, EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include + +#include + +using namespace brayns; + +TEST_CASE("As bytes") +{ + auto test = std::uint32_t(1); + + auto bytes = asBytes(test); + + CHECK_EQ(bytes.size(), 4); + + if (std::endian::native == std::endian::big) + { + CHECK_EQ(bytes[0], '\0'); + CHECK_EQ(bytes[3], '\1'); + } + else + { + CHECK_EQ(bytes[0], '\1'); + CHECK_EQ(bytes[3], '\0'); + } + + CHECK_EQ(bytes[1], '\0'); + CHECK_EQ(bytes[2], '\0'); + + swapBytes(test); + + CHECK_EQ(test, 16777216); + + swapBytes(test); + + CHECK_EQ(test, 1); +} + +TEST_CASE("Extract") +{ + const auto test = std::int64_t(1); + auto bytes = asBytes(test); + + CHECK_EQ(extractBytesAs(bytes, std::endian::native), test); + CHECK(bytes.empty()); + + const auto vector = Vector3(1, 2, 3); + bytes = asBytes(vector); + + CHECK_EQ(extractBytesAs(bytes, std::endian::native), vector); + CHECK(bytes.empty()); + + const auto quaternion = Quaternion(0, 1, 2, 3); + bytes = asBytes(quaternion); + + CHECK_EQ(extractBytesAs(bytes, std::endian::native), quaternion); + CHECK(bytes.empty()); +} + +TEST_CASE("Compose") +{ + for (auto endian : {std::endian::little, std::endian::big}) + { + auto buffer = std::string(); + composeBytesTo(std::uint32_t(1), endian, buffer); + composeBytesTo(2.0F, endian, buffer); + composeBytesTo(3.0, endian, buffer); + + auto data = std::string_view(buffer); + + CHECK_EQ(extractBytesAs(data, endian), 1); + CHECK_EQ(extractBytesAs(data, endian), 2.0F); + CHECK_EQ(extractBytesAs(data, endian), 3.0); + + CHECK(data.empty()); + } +} diff --git a/tests/core/utils/TestIdGenerator.cpp b/tests/core/utils/TestIdGenerator.cpp index 123f4284e..288cc40de 100644 --- a/tests/core/utils/TestIdGenerator.cpp +++ b/tests/core/utils/TestIdGenerator.cpp @@ -29,25 +29,25 @@ TEST_CASE("ID generation") CHECK_EQ(ids.next(), 0); CHECK_EQ(ids.next(), 1); - - ids.recycle(0); - CHECK_EQ(ids.next(), 0); + CHECK_EQ(ids.next(), 2); ids.recycle(1); CHECK_EQ(ids.next(), 1); + + ids.reset(); + + CHECK_EQ(ids.next(), 0); } TEST_CASE("Limits") { - auto ids = IdGenerator(); + auto ids = IdGenerator(2, 4); - for (auto i = 0; i < 255; ++i) - { - ids.next(); - } + CHECK_EQ(ids.next(), 2); + CHECK_EQ(ids.next(), 3); CHECK_THROWS_AS(ids.next(), std::out_of_range); - ids.recycle(0); - CHECK_EQ(ids.next(), 0); + ids.recycle(2); + CHECK_EQ(ids.next(), 2); }