# molsysviewer/shapes/spheres.py
from typing import Sequence
from ..layers import Layer
[docs]
class SphereShapes:
"""Sphere helpers for the scene."""
def __init__(self, view) -> None:
self._view = view
[docs]
def add_sphere(
self,
center=(0.0, 0.0, 0.0),
radius: float = 10.0,
color: int = 0x00FF00,
alpha: float = 0.4,
tag: str | None = None,
) -> Layer:
"""Add a (possibly transparent) sphere to the scene."""
tag = tag or self._view._next_layer_tag() # noqa: SLF001
self._view._send(
{
"op": "add_sphere",
"options": {
"center": list(center),
"radius": float(radius),
"color": int(color),
"alpha": float(alpha),
"tag": tag,
},
}
)
# Ensure the layer is immediately available in the Python registry.
if tag not in self._view._layers: # noqa: SLF001
self._view._layers[tag] = Layer(self._view, tag, kind="shape", meta={}) # noqa: SLF001
return self._view._layers[tag] # noqa: SLF001
[docs]
def add_spheres(
self,
centers: Sequence[Sequence[float]],
radii: float | Sequence[float] = 1.0,
colors: int | Sequence[int] = 0x00FF00,
alphas: float | Sequence[float] = 0.4,
tags: str | Sequence[str] | None = None,
):
"""Add multiple spheres to the scene.
Parameters
----------
centers
Sequence of centers, each `(x, y, z)`.
radii
Radius (scalar for all) or a list of radii (one per sphere).
colors
Color in `0xRRGGBB` (scalar or list).
alphas
Alpha (0.0-1.0), scalar or list.
tags
Optional tag (scalar or list, one per sphere).
"""
centers_list = [list(c) for c in centers]
n = len(centers_list)
if n == 0:
return
def _as_list(value, n, cast):
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
if len(value) != n:
raise ValueError(
f"Expected {n} values but got {len(value)}."
)
return [cast(v) for v in value]
else:
return [cast(value)] * n
radii = _as_list(radii, n, float)
colors = _as_list(colors, n, int)
alphas = _as_list(alphas, n, float)
tags = _as_list(tags, n, str) if tags is not None else [None] * n
layers = []
for c, r, col, a, t in zip(centers_list, radii, colors, alphas, tags):
layers.append(self.add_sphere(center=c, radius=r, color=col, alpha=a, tag=t))
return layers
[docs]
def add_set_alpha_spheres(
self,
*,
centers: Sequence[Sequence[float]],
radii: Sequence[float],
atom_centers: Sequence[Sequence[float]] | None = None,
atom_radius: float = 1.0,
color_alpha_spheres: int = 0x00FF00,
color_atoms: int = 0x0000FF,
alpha_alpha_spheres: float = 0.3,
alpha_atoms: float = 0.5,
tag: str | None = None,
) -> Layer:
"""Render a set of alpha-spheres (and optionally contact atoms) in a single message.
- `centers`, `radii`: alpha-sphere positions and radii.
- `atom_centers`: contact atom centers (optional).
- Separate colors and alpha for alpha-spheres and atoms.
- Uses a single message to minimize per-shape overhead.
"""
centers_list = [list(c) for c in centers]
radii_list = [float(r) for r in radii]
if len(centers_list) != len(radii_list):
raise ValueError("centers and radii must have the same length")
options: dict = {
"alpha_spheres": {
"centers": centers_list,
"radii": radii_list,
"color": int(color_alpha_spheres),
"alpha": float(alpha_alpha_spheres),
}
}
if atom_centers:
options["atom_spheres"] = {
"centers": [list(c) for c in atom_centers],
"radius": float(atom_radius),
"color": int(color_atoms),
"alpha": float(alpha_atoms),
}
tag = tag or self._view._next_layer_tag() # noqa: SLF001
options["tag"] = tag
self._view._send({"op": "add_alpha_sphere_set", "options": options})
if tag not in self._view._layers: # noqa: SLF001
self._view._layers[tag] = Layer(self._view, tag, kind="shape", meta={}) # noqa: SLF001
return self._view._layers[tag] # noqa: SLF001
[docs]
def clear(self, tag: str | None = None):
"""Delete shapes in the frontend (all or by tag)."""
self._view._send({"op": "clear_shapes_by_tag", "tag": tag})