diff --git a/CHANGELOG.md b/CHANGELOG.md index e914a86..d0d6a8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file, following t - Breaking: Renamed geometrical primitive `line` to `tube` - Breaking: Change multiple geometrical primitive parameters - Multi-state data model tweaks +- Support for representation-specific parameters (customize presentation visuals) ## [v1.1.0] - 2024-12-09 diff --git a/molviewspec/app/api/examples.py b/molviewspec/app/api/examples.py index 1a354a8..8401bb4 100644 --- a/molviewspec/app/api/examples.py +++ b/molviewspec/app/api/examples.py @@ -728,6 +728,21 @@ async def refs_example() -> MVSResponse: return PlainTextResponse(builder.get_state()) +@router.get("/repr-params") +async def repr_params_example() -> MVSResponse: + """ + Individual representations (cartoon, ball-and-stick, surface) can be further customized. The corresponding builder + function exposes additional method arguments depending on the chosen representation type. + """ + builder = create_builder() + component = ( + builder.download(url=_url_for_mmcif("1a23")).parse(format="mmcif").model_structure(ref="structure").component() + ) + component.representation(type="cartoon", size_factor=1.5, tubular_helices=True) + component.representation(type="surface", ignore_hydrogens=True).opacity(opacity=0.8) + return PlainTextResponse(builder.get_state()) + + @router.get("/primitives/cube") async def primitives_cube_example() -> MVSResponse: """ diff --git a/molviewspec/molviewspec/builder.py b/molviewspec/molviewspec/builder.py index c2190a5..3b7e5d8 100644 --- a/molviewspec/molviewspec/builder.py +++ b/molviewspec/molviewspec/builder.py @@ -8,7 +8,7 @@ import math import os from abc import ABC, abstractmethod -from typing import Literal, Self, Sequence +from typing import Any, Literal, Self, Sequence, overload from pydantic import BaseModel, PrivateAttr @@ -45,7 +45,7 @@ PrimitivesFromUriParams, PrimitivesParams, RefT, - RepresentationParams, + RepresentationTypeParams, RepresentationTypeT, SchemaFormatT, SchemaT, @@ -78,17 +78,14 @@ class _BuilderProtocol(ABC): @property @abstractmethod - def _root(self) -> Root: - ... + def _root(self) -> Root: ... @property @abstractmethod - def _node(self) -> Node: - ... + def _node(self) -> Node: ... @abstractmethod - def _add_child(self, node: Node) -> None: - ... + def _add_child(self, node: Node) -> None: ... class _Base(BaseModel, _BuilderProtocol): @@ -661,17 +658,82 @@ class Component(_Base, _FocusMixin): Builder step with operations relevant for a particular component. """ + @overload + def representation( + self, + *, + type: Literal["cartoon"], + size_factor: float = None, + tubular_helices: bool = None, + custom: CustomT = None, + ref: RefT = None, + ) -> Representation: + """ + Add a cartoon representation for this component. + :param type: the type of this representation ('cartoon') + :param size_factor: adjust the scale of the visuals (relative to 1.0) + :param tubular_helices: simplify helices to tubes + :param custom: optional, custom data to attach to this node + :param ref: optional, reference that can be used to access this node + :return: a builder that handles operations at representation level + """ + ... + + @overload + def representation( + self, + *, + type: Literal["ball_and_stick"], + ignore_hydrogens: bool = None, + size_factor: float = None, + custom: CustomT = None, + ref: RefT = None, + ) -> Representation: + """ + Add a ball-and-stick representation for this component. + :param type: the type of this representation ('ball_and_stick') + :param ignore_hydrogens: draw hydrogen atoms? + :param size_factor: adjust the scale of the visuals (relative to 1.0) + :param custom: optional, custom data to attach to this node + :param ref: optional, reference that can be used to access this node + :return: a builder that handles operations at representation level + """ + ... + + @overload + def representation( + self, + *, + type: Literal["surface"], + ignore_hydrogens: bool = None, + size_factor: float = None, + custom: CustomT = None, + ref: RefT = None, + ) -> Representation: + """ + Add a surface representation for this component. + :param type: the type of this representation ('surface') + :param ignore_hydrogens: draw hydrogen atoms? + :param size_factor: adjust the scale of the visuals (relative to 1.0) + :param custom: optional, custom data to attach to this node + :param ref: optional, reference that can be used to access this node + :return: a builder that handles operations at representation level + """ + ... + def representation( - self, *, type: RepresentationTypeT = "cartoon", custom: CustomT = None, ref: RefT = None + self, *, type: RepresentationTypeT = "cartoon", custom: CustomT = None, ref: RefT = None, **kwargs: Any ) -> Representation: """ Add a representation for this component. :param type: the type of representation, defaults to 'cartoon' :param custom: optional, custom data to attach to this node :param ref: optional, reference that can be used to access this node + :param kwargs: optional, representation-specific params :return: a builder that handles operations at representation level """ - params = make_params(RepresentationParams, locals()) + params_class = RepresentationTypeParams.get(type) + params = make_params(params_class, locals(), **kwargs) node = Node(kind="representation", params=params) self._add_child(node) return Representation(node=node, root=self._root) diff --git a/molviewspec/molviewspec/nodes.py b/molviewspec/molviewspec/nodes.py index b04f9f2..dc07f6c 100644 --- a/molviewspec/molviewspec/nodes.py +++ b/molviewspec/molviewspec/nodes.py @@ -438,6 +438,28 @@ class RepresentationParams(BaseModel): type: RepresentationTypeT = Field(description="Representation type, i.e. cartoon, ball-and-stick, etc.") +class CartoonParams(RepresentationParams): + type: Literal["cartoon"] = "cartoon" + size_factor: Optional[float] = Field(description="Scales the corresponding visuals.") + tubular_helices: Optional[bool] = Field(description="Simplify corkscrew helices to tubes.") + # TODO support for variable size, e.g. based on b-factors? + + +class BallAndStickParams(RepresentationParams): + type: Literal["ball_and_stick"] = "ball_and_stick" + ignore_hydrogens: Optional[bool] = Field(descripton="Controls whether hydrogen atoms are drawn.") + size_factor: Optional[float] = Field(description="Scales the corresponding visuals.") + + +class SurfaceParams(RepresentationParams): + type: Literal["surface"] = "surface" + ignore_hydrogens: Optional[bool] = Field(descripton="Controls whether hydrogen atoms are drawn.") + size_factor: Optional[float] = Field(description="Scales the corresponding visuals.") + + +RepresentationTypeParams = {t.__fields__["type"].default: t for t in (CartoonParams, BallAndStickParams, SurfaceParams)} + + SchemaT = Literal[ "whole_structure", "entity", diff --git a/molviewspec/molviewspec/utils.py b/molviewspec/molviewspec/utils.py index f36bd6d..df7cc25 100644 --- a/molviewspec/molviewspec/utils.py +++ b/molviewspec/molviewspec/utils.py @@ -11,6 +11,7 @@ def make_params(params_type: Type[TParams], values=None, /, **more_values: objec if values is None: values = {} result = {} + consumed_more_values = set() # propagate custom properties if values: @@ -27,11 +28,16 @@ def make_params(params_type: Type[TParams], values=None, /, **more_values: objec if more_values.get(key) is not None: result[key] = more_values[key] + consumed_more_values.add(key) elif values.get(key) is not None: result[key] = values[key] elif field.default is not None: # currently not used result[key] = field.default + non_model_keys = set(more_values.keys()) - consumed_more_values + if non_model_keys: + raise ValueError(f"Encountered unknown attribute on {params_type}: {non_model_keys}") + return result # type: ignore