"""
Maestro 3D boxes.
The box module allows creation and drawing of boxes and parallelepipeds.
A box is a special case of a parallelepiped where angles between edges are 90°
and edges are parallel to the XYZ axes.
Control over the positioning, color, and opacity of a box are provided. See the
MaestroBos and MaestroParallelepiped classes for more info.
To draw any number of boxes, create the class instances, add them to a Group
instance then invoke the Group's draw() method.
Copyright Schrodinger, LLC. All rights reserved.
"""
import schrodinger
from . import common
from .common import FILL
from .common import LINE
from .common import OPACITY_MAX
from .common import OPACITY_MIN
from .common import Group # noqa: F401
DASHED_LINE = 2
maestro = schrodinger.get_maestro()
try:
from schrodinger.ui import maestro_ui
except ImportError:
maestro_ui = None
OPACITY_DEFAULT = 1.0
# Constants used to calculate bounding box:
BOUNDING_BOX_INIT_VALUE = 100000000.0
#############################################################################
# CLASSES #
#############################################################################
[docs]class MaestroParallelepiped(common.Primitive):
"""
Class for placing a parallelepiped into the Workspace. Can be used for
drawing boxes that are not positioned with edges parallel to 3 axes.
3D objects should be added to a graphics3d.common.Group. See the
graphics3d.common.Group documentation.
See note on transparency in the module level docstring.
API Example::
import schrodinger.maestro.maestro as maestro
import schrodinger.graphics3d.common as common
import schrodinger.graphics3d.box as box
box_grp = common.Group()
st = maestro.workspace_get()
bx = box.Parallelepiped(
points=[origin, xyz2, xyz3, xyz4]
color='red',
opacity=1.0,
style=box.LINE
)
# Add the primitive to the container.
box_grp.add(bx)
# Hide the markers.
box_grp.hide()
# Remove the markers and the callback.
box_grp.clear()
"""
[docs] def __init__(self,
points,
color,
opacity=OPACITY_DEFAULT,
line_width=1.0,
style=LINE):
"""
Constructor requires:
points: List of 4 (x, y, z) coordinates, representing half of the
corners of the box. First coordinate must be adjacent to
each of the other 3 coordinates.
color: One of:
Color object
Color name (string)
Tuple of (R, G, B) (each 0.0-1.0)
Optional arguments:
opacity: 0.0 (invisible) through 1.0 (opaque)
Defaults to 1.0
line_width: Line width. Default is 1.0.
style: FILL, LINE or DASHED_LINE. Default is LINE.
"""
r, g, b = common.color_arg_to_rgb(color)
# Clamp to range of 0.0 and 1.0, inclusive
if opacity < OPACITY_MIN or opacity > OPACITY_MAX:
raise ValueError(
"Invalid opacity: %s (must be betwen 0.0 and 1.0)" % opacity)
if style != FILL and style != LINE and style != DASHED_LINE:
raise ValueError("Style must be one of: FILL, LINE, DASHED_LINE")
if len(points) != 4:
raise ValueError("Must specify 4 points")
assert len(points) == 4
assert len(points[0]) == 3
points = [maestro_ui.MM_GraphicsVec3d(*xyz) for xyz in points]
parallelepiped = maestro_ui.MM_GraphicsParallelepiped(points)
# Create 8 vertices, for boundary calculation. Also used for
# create_polyhedron() call.
self.vertices = [
(p.x(), p.y(), p.z()) for p in parallelepiped.getPoints()
]
if style == FILL:
# Will be creating a polygon 3D object, with filled sides
# Parallelepiped objects don't support side filling.
faces = [
[0, 2, 4, 1],
[0, 1, 5, 3],
[0, 3, 6, 2],
[2, 6, 7, 4],
[1, 4, 7, 5],
[3, 5, 7, 6],
]
normals = [common.create_normal([self.vertices[i] for i in indices])
for indices in faces] # yapf: disable
assert len(normals) == 6
obj = maestro.create_polyhedron(self.vertices, faces, normals, r, g,
b, opacity, style)
else:
# Will be creating a parallelepiped 3D object, with lines.
# Polygon objects don't support variable line widths.
# style: 0=solid lines, 1=short dashed lines, 2=dashed cylinders
if style == DASHED_LINE:
style = 1
elif style == LINE:
style = 0
obj = maestro.create_parallelepiped(parallelepiped, r, g, b,
line_width, style)
maestro_objects = [obj]
common.Primitive.__init__(self, maestro_objects)
def _calculateBoundingBox(self, mat):
xyzmin = []
xyzmax = []
for k in range(6):
xyzmin.append(BOUNDING_BOX_INIT_VALUE)
xyzmax.append(-BOUNDING_BOX_INIT_VALUE)
tmp = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
for xyz in self.vertices:
for k in range(3):
tmp[k + 3] = xyz[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 MaestroBox(MaestroParallelepiped):
"""
Class for placing a 3D box into the Workspace. Edges of the box will
be aligned with X/Y/Z axes. For custom rotation, or to create "skewed"
boxes, use MaestroParallelepiped class.
3D objects should be added to a graphics3d.common.Group. See the
graphics3d.common.Group documentation.
See note on transparency in the module level docstring.
API Example::
import schrodinger.maestro.maestro as maestro
import schrodinger.graphics3d.common as common
import schrodinger.graphics3d.box as box
box_grp = common.Group()
st = maestro.workspace_get()
bx = box.Box(
center=(x, y, z),
extents=(10, 10, 10),
color='red',
opacity=1.0,
style=box.LINE
)
# Add the primitive to the container.
box_grp.add(bx)
# Hide the markers.
box_grp.hide()
# Remove the markers and the callback.
box_grp.clear()
"""
[docs] def __init__(self,
center,
extents,
color,
opacity=OPACITY_DEFAULT,
line_width=1.0,
style=LINE):
"""
Constructor requires:
center: List of 3 Angstrom values indicating the center coordinate
of the box.
extents: List of 3 float values in Angstroms -
x length, y length, z length.
color: One of:
Color object
Color name (string)
Tuple of (R, G, B) (each 0.0-1.0)
Optional arguments:
opacity: 0.0 (invisible) through 1.0 (opaque)
Defaults to 1.0
line_width: Line width. Default is 1.0.
style: FILL, LINE or DASHED_LINE. Default is LINE.
"""
xoff = 0.5 * extents[0]
yoff = 0.5 * extents[1]
zoff = 0.5 * extents[2]
points = [
[center[0] - xoff, center[1] - yoff, center[2] - zoff],
[center[0] + xoff, center[1] - yoff, center[2] - zoff],
[center[0] - xoff, center[1] + yoff, center[2] - zoff],
[center[0] - xoff, center[1] - yoff, center[2] + zoff],
] # yapf: disable
super().__init__(points, color, opacity, line_width, style)