"""
Maestro 3D toruses.
The torus module allows creation and drawing of toruses.  Clients draw using
Group instances not through Torus instances.
Control over the center, radius, color, resolution and opacity of a torus are
provided. See the Torus class for more info.
To draw any number of toruses, create the Torus instance and add it to a Group
instance.  Then invoke the Group's draw() method.
Copyright Schrodinger, LLC. All rights reserved.
"""
# Contributors: Herc Silverstein, Scott MacHoffie, Matvey Adzhigirey, Lixing Fang
import copy
import math
import numpy
import schrodinger
from . import common
from .common import OPACITY_MAX
from .common import OPACITY_MIN
from .common import TRANSPARENCY_MAX
from .common import TRANSPARENCY_MIN
from .common import Group as TorusGroup  # noqa: F401
RESOLUTION_MIN = 2
RESOLUTION_MAX = 30
RESOLUTION_DEFAULT_U = 5
RESOLUTION_DEFAULT_V = 3
TRANSPARENCY_DEFAULT = 0.0  # for backwards-compatibility
OPACITY_DEFAULT = 1.0
# Constants used to calculate bounding box:
BOUNDING_BOX_INIT_VALUE = 100000000.0
EPSILON = 0.0001
maestro = schrodinger.get_maestro()
[docs]def vector_cross(X, Y):
    """
    Returns the cross product of the len 3 vectors X and Y
    """
    r = numpy.array([0.0, 0.0, 0.0])
    r[0] = X[1] * Y[2] - X[2] * Y[1]
    r[1] = X[2] * Y[0] - X[0] * Y[2]
    r[2] = X[0] * Y[1] - X[1] * Y[0]
    return r 
#############################################################################
#                              CLASSES                                      #
#############################################################################
[docs]class TorusCore(common.Primitive):
    """
    Base class for MaestroTorus.
    See doc string for __init__ for details on instantiation.
    Toruses should be added to a TorusGroup and drawing done via
    TorusGroup.  See TorusGroup documentation.
    """
    _blend = None
    _smooth_point = None
    _cull = None
    _lighting = None
    _lighting_model = None
    _shading_model = None
[docs]    def __init__(
            self,
            x0=None,
            y0=None,
            z0=None,
            x1=None,
            y1=None,
            z1=None,
            color=None,
            r=None,
            g=None,
            b=None,  # for backwards-compatability
            radius=None,
            tube_radius=None,
            transparency=None,  # for backwards-compatability
            opacity=OPACITY_DEFAULT,
            resolution_u=RESOLUTION_DEFAULT_U,
            resolution_v=RESOLUTION_DEFAULT_V):
        """
        Constructor requires:
        x0, y0, z0:  coordinate specifying center of torus in Angstroms.
        x1, y1, z1:  coordinate specifying projected center of torus
                in Angstroms.
        color:    One of:
                    Color object
                    Color name (string)
                    Tuble of (R, G, B) (each 0.0-1.0)
        radius:   radius of the torus in Angstroms
        tube_radius:  radius of the tube of the torus in Angstroms
        Optional arguments:
        opacity:  0.0 (transparent) through 1.0 (opaque)
                       Defaults to 1.0
        resolution_u:    2 to 30, resolution in the U direction of the surface
                       Defaults to 5
        resolution_v:    2 to 30, resolution in the V direction of the surface
                       Defaults to 3
        """
        if x0 is None or y0 is None or z0 is None:
            raise ValueError(
                "Must specify x0, y0 and z0 values to define the torus center")
        elif x1 is None or y1 is None or z1 is None:
            raise ValueError(
                "Must specify x1, y1 and z1 values to define the torus projected center"
            )
        elif radius is None:
            raise ValueError("Must specify a value for radius")
        elif tube_radius is None:
            raise ValueError("Must specify a value for tube radius")
        else:
            self.x0 = x0
            self.y0 = y0
            self.z0 = z0
            self.x1 = x1
            self.y1 = y1
            self.z1 = z1
            self.radius = radius
            self.tube_radius = tube_radius
        if color is not None:
            self.r, self.g, self.b = common.color_arg_to_rgb(color)
        elif r is not None and g is not None and b is not None:
            # for backwards-compatability
            self.r = float(r)
            self.g = float(g)
            self.b = float(b)
        else:
            raise ValueError("Must specify a color")
        # Clamp to range of 0.0 and 1.0, inclusive
        if transparency is not None:  # for backwards-compatability
            if transparency < TRANSPARENCY_MIN:
                self.opacity = OPACITY_MAX
            elif transparency > TRANSPARENCY_MAX:
                self.opacity = OPACITY_MIN
            else:
                self.opacity = 1.0 - transparency
        else:  # Use opacity
            if opacity < OPACITY_MIN:
                self.opacity = OPACITY_MIN
            elif opacity > OPACITY_MAX:
                self.opacity = OPACITY_MAX
            else:
                self.opacity = opacity
        # Clamp the resolution in U direction
        if resolution_u < RESOLUTION_MIN:
            self.resolution_u = RESOLUTION_MIN
        elif resolution_u > RESOLUTION_MAX:
            self.resolution_u = RESOLUTION_MAX
        else:
            self.resolution_u = resolution_u
        # Clamp the resolution in U direction
        if resolution_v < RESOLUTION_MIN:
            self.resolution_v = RESOLUTION_MIN
        elif resolution_v > RESOLUTION_MAX:
            self.resolution_v = RESOLUTION_MAX
        else:
            self.resolution_v = resolution_v
        return 
    def _calculateBoundingBox(self, mat):
        xyzmin = []
        xyzmax = []
        for k in range(6):
            xyzmin.append(BOUNDING_BOX_INIT_VALUE)
            xyzmax.append(-BOUNDING_BOX_INIT_VALUE)
        # Two points for defining the torus axis
        C = numpy.array([self.x0, self.y0, self.z0])
        D = numpy.array([self.x1, self.y1, self.z1])
        # CD is the vector between points C and D
        CD = D - C
        # We need to find bottom point A and top point B of the torus
        A = C - self.tube_radius * CD
        B = C + self.tube_radius * CD
        # AB is the vector between points A and B
        AB = B - A
        # Perform the vector operation
        # We have coordinates for two positions (A, B) which
        # define the membrane planes. We need to figure out a vector
        # perpendiclar to the vector AB and a series of points
        # in the planes in order to draw a polygon
        # Assign the size of the planes
        l = self.radius + self.tube_radius
        # C is the midpoint between the two points A and B
        # N is an arbitrary point we will project onto C to get
        # a perpendicular vector
        N = copy.copy(C)
        if math.fabs(AB[0]) < EPSILON and math.fabs(AB[1]) < EPSILON:
            N[0] += 10.0
        elif math.fabs(AB[1]) < EPSILON and math.fabs(AB[2]) < EPSILON:
            N[1] += 10.0
        elif math.fabs(AB[2]) < EPSILON and math.fabs(AB[0]) < EPSILON:
            N[2] += 10.0
        else:
            N[0] += 10.0
        #  CN is a vector crosses AB at C
        CN = N - C
        # O is the projection of N onto AB. The vector O-N perpendicular
        # to A-B
        dot1 = numpy.dot(AB, AB)
        dot2 = numpy.dot(AB, CN)
        K = dot2 / dot1
        O = C + numpy.dot(K, AB)
        ON = common.get_normalized(N - O)
        # P is perpendicular to both AB and ON
        P = common.get_normalized(vector_cross(AB, ON))
        # Now we have everything we need to generate the planes
        plane1 = []
        plane2 = []
        # Op is used to generate a vertex at each corner of what will
        # be drawn as a square plane. Basically it's atom1+S, atom1+U,
        # atom1-S, atom1-U
        op = [[l, 0.0], [0.0, l], [-l, 0.0], [0.0, -l]]
        for i in range(0, 4):
            plane1.append(A + op[i][0] * ON + op[i][1] * P)
            plane2.append(B + op[i][0] * ON + op[i][1] * P)
        tmp = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
        for i in range(4):
            for k in range(3):
                tmp[k + 3] = plane1[i][k]
            tmp[0] = mat[0][0] * tmp[3] + mat[0][1] * tmp[4] + mat[0][2] * tmp[
                5] + mat[0][3]
            tmp[1] = mat[1][0] * tmp[3] + mat[1][1] * tmp[4] + mat[1][2] * tmp[
                5] + mat[1][3]
            tmp[2] = mat[2][0] * tmp[3] + mat[2][1] * tmp[4] + mat[2][2] * tmp[
                5] + mat[2][3]
            for k in range(6):
                if xyzmin[k] > tmp[k]:
                    xyzmin[k] = tmp[k]
                if xyzmax[k] < tmp[k]:
                    xyzmax[k] = tmp[k]
        for i in range(4):
            for k in range(3):
                tmp[k + 3] = plane2[i][k]
            tmp[0] = mat[0][0] * tmp[3] + mat[0][1] * tmp[4] + mat[0][2] * tmp[
                5] + mat[0][3]
            tmp[1] = mat[1][0] * tmp[3] + mat[1][1] * tmp[4] + mat[1][2] * tmp[
                5] + mat[1][3]
            tmp[2] = mat[2][0] * tmp[3] + mat[2][1] * tmp[4] + mat[2][2] * tmp[
                5] + mat[2][3]
            for k in range(6):
                if xyzmin[k] > tmp[k]:
                    xyzmin[k] = tmp[k]
                if xyzmax[k] < tmp[k]:
                    xyzmax[k] = tmp[k]
        return (xyzmin, xyzmax) 
[docs]class MaestroTorus(common._MaestroPrimitiveMixin, TorusCore):
    """
    Class to create a 3D torus in Maestro.  This torus will be drawn at the
    appropriate time in Maestro to interact properly with transparent objects.
    Toruses should be added to a Group and drawing done via the Group. See
    Group documentation.
    API Example::
        import schrodinger.maestro.maestro as maestro
        import schrodinger.graphics3d.common as common
        import schrodinger.graphics3d.torus as torus
        torus_grp = common.Group()
        st = maestro.workspace_get()
        for bond in st.bond:
            trs = torus.MaestroTorus(
                x0=bond.atom1.x,
                y0=bond.atom1.y,
                z0=bond.atom1.z,
                x1=bond.atom2.x,
                y1=bond.atom2.y,
                z1=bond.atom2.z,
                radius=0.1,
                tube_radius=0.5,
                resolution_u=7,
                resolution_v=5,
                opacity=0.8,
                color='red'
            )
            # Add the primative to the container.
            torus_grp.add(trs)
        # MaestroTorus needs to be drawn.
        # No special callback like there is for Toruses is needed.
        torus_grp.show()
        # Hide the markers.
        torus_grp.hide()
        # Remove the markers and the callback.
        torus_grp.clear()
        maestro.workspace_draw_function_remove(torus_grp.draw)
    """
[docs]    def __init__(
            self,
            x0=None,
            y0=None,
            z0=None,
            x1=None,
            y1=None,
            z1=None,
            color=None,
            r=None,
            g=None,
            b=None,  # for backwards-compatability
            radius=None,
            tube_radius=None,
            transparency=None,  # for backwards-compatability
            opacity=OPACITY_DEFAULT,
            resolution_u=RESOLUTION_DEFAULT_U,
            resolution_v=RESOLUTION_DEFAULT_V):
        """
        Constructor requires:
        x0, y0, z0:  coordinate specifying center of torus in Angstroms.
        x1, y1, z1:  coordinate specifying projected center of torus
                in Angstroms.
        color:    One of:
                    Color object
                    Color name (string)
                    Tuble of (R, G, B) (each 0.0-1.0)
        radius:   radius of the torus in Angstroms
        tube_radius:  radius of the tube of the torus in Angstroms
        Optional arguments:
        opacity:  0.0 (transparent) through 1.0 (opaque)
                       Defaults to 1.0
        resolution_u:    2 to 30, resolution in the U direction of the surface
                       Defaults to 5
        resolution_v:    2 to 30, resolution in the V direction of the surface
                       Defaults to 3
        """
        self.torus = None
        self._x0 = 0
        self._y0 = 0
        self._z0 = 0
        self._x1 = 0
        self._y1 = 0
        self._z1 = 0
        self._radius = 0
        self._tube_radius = 0
        TorusCore.__init__(self, x0, y0, z0, x1, y1, z1, color, r, g, b, radius,
                           tube_radius, transparency, opacity, resolution_u,
                           resolution_v)
        self.torus = maestro.create_torus(self.x0, self.y0, self.z0, self.x1,
                                          self.y1, self.z1, self.r, self.g,
                                          self.b, self.radius, self.tube_radius,
                                          self.opacity, self.resolution_u,
                                          self.resolution_v)
        maestro_objects = [self.torus]
        common.Primitive.__init__(self, maestro_objects) 
    # Helper functions
[docs]    def setCoords(self):
        if self.torus:
            maestro.set_coords(self.torus, self._x0, self._y0, self._z0) 
    # Accessors
    def _getX0(self):
        return self._x0
    def _setX0(self, value):
        self._x0 = value
        self.setCoords()
    def _getY0(self):
        return self._y0
    def _setY0(self, value):
        self._y0 = value
        self.setCoords()
    def _getZ0(self):
        return self._z0
    def _setZ0(self, value):
        self._z0 = value
        self.setCoords()
    def _getX1(self):
        return self._x1
    def _setX1(self, value):
        self._x1 = value
    def _getY1(self):
        return self._y1
    def _setY1(self, value):
        self._y1 = value
    def _getZ1(self):
        return self._z1
    def _setZ1(self, value):
        self._z1 = value
    def _getRadius(self):
        return self._radius
    def _setRadius(self, value):
        self._radius = value
        if self.torus:
            maestro.set_radius(self.torus, self._radius)
    def _getTubeRadius(self):
        return self._tube_radius
    def _setTubeRadius(self, value):
        self._tube_radius = value
    x0 = property(_getX0, _setX0, doc="X0 coordinate")
    y0 = property(_getY0, _setY0, doc="Y0 coordinate")
    z0 = property(_getZ0, _setZ0, doc="Z0 coordinate")
    x1 = property(_getX1, _setX1, doc="X1 coordinate")
    y1 = property(_getY1, _setY1, doc="Y1 coordinate")
    z1 = property(_getZ1, _setZ1, doc="Z1 coordinate")
    radius = property(_getRadius, _setRadius, doc="Torus's radius")
    tube_radius = property(_getTubeRadius,
                           _setTubeRadius,
                           doc="Torus's tube_radius")