"""
Utility functions for the graphics3d package.
The common module contains functions commonly used by the graphics3d modules.
This includes the Group class, which is one clients use as a container for
actual drawing primitive instances (like Arrow, etc.). See the Group class'
docstring for more details.
This code will automatically keep track of and handle each instance that is
added to a Group instance. For information on how to create a Group and add
intances to the Group, see the class docstring for Group and the method
docstring for Group's add() method, respectively.
Copyright Schrodinger, LLC. All rights reserved.
"""
import numpy
import schrodinger
from schrodinger.structutils.color import Color
maestro = schrodinger.get_maestro()
# Constants that control level of transparency.
TRANSPARENCY_MIN = 0.0
TRANSPARENCY_MAX = 1.0
OPACITY_MIN = 0.0
OPACITY_MAX = 1.0
OPACITY_DEFAULT = 1.0
# Constants used to calculate bounding box:
BOUNDING_BOX_INIT_VALUE = 100000000.0
BOUNDING_BOX_ADJUST_VALUE = 0.1
# Treat as constants to specify drawing as lines or filled polygons.
LINE, FILL = list(range(2))
# Private list of all the currently instantiated groups.
# Client code should not reference or use this.
_all_groups = []
#############################################################################
# FUNCTIONS #
#############################################################################
[docs]def get_normalized(v):
"""
Calculate and return normalized vector.
Arguments:
v: List containing 3 floating values of x, y, z
Returns:
List of 3 floating values reprsenting the normalized vector.
"""
magnitude = numpy.linalg.norm(v)
if magnitude == 0.0:
return v
return v / magnitude
[docs]def get_cross(v, w):
"""
Get the cross product of v and w
:type v: [float, float, float]
:type w: [float, float, float]
:rtype: [float, floatg, float]
"""
result = []
result.append(v[1] * w[2] - v[2] * w[1])
result.append(v[2] * w[0] - v[0] * w[2])
result.append(v[0] * w[1] - v[1] * w[0])
return result
[docs]def create_normal(vertices):
"""
Method to create a normal for the polygon defined by specified
vertices.
Arguments:
vertices: List of floating value lists of [x, y, z] representing
a vertex.
Returns:
List of 3 floating values representing the normal
FIXME: We use only the first three vertices to calculate the normal
and currently ignore the rest. Other vertices won't matter if all
the vectors are in a plane, which is going to the be case most
of the time.
"""
v1, v2, v3 = vertices[0:3]
avec = []
avec.append(v1[0] - v2[0])
avec.append(v1[1] - v2[1])
avec.append(v1[2] - v2[2])
bvec = []
bvec.append(v2[0] - v3[0])
bvec.append(v2[1] - v3[1])
bvec.append(v2[2] - v3[2])
n = [] # Cross product to get normal
n.append(avec[1] * bvec[2] - avec[2] * bvec[1])
n.append(avec[2] * bvec[0] - avec[0] * bvec[2])
n.append(avec[0] * bvec[1] - avec[1] * bvec[0])
normal = get_normalized(n) # Normalize
return normal
[docs]def color_arg_to_rgb(colorarg):
"""
Returns a tuple of (r, g, b) [range 0.0-1.0 inclusive for each].
Arguments:
colorarg: Can be one of the following types -
Color: Color object instance
string: Color name
integer: Color index
"""
if hasattr(colorarg, 'rgb'): # Color object passed
colorobj = colorarg
elif isinstance(colorarg, int):
colorobj = Color(colorarg)
elif isinstance(colorarg, str):
colorobj = Color(colorarg)
elif isinstance(colorarg, (list, tuple)):
# Already R,G,B range 0.0-1.0
return colorarg[0:3]
else:
raise ValueError(f'Invalid color specified (type {type(colorarg)})')
r255, g255, b255 = colorobj.rgb
# convert to range 0.0 - 1.0:
return (r255 / 255.0, g255 / 255.0, b255 / 255.0)
#############################################################################
# CLASSES #
#############################################################################
[docs]class Primitive(object):
"""
All 3D objects derive from this class.
"""
[docs] def __init__(self, maestro_objects=[]): # noqa: M511
self._maestro_objects = maestro_objects
# Whether this object is shown:
self._shown = True
# Whether this object's group is shown:
if maestro_objects:
# *** EV 110631
self.groupHidden()
self._maestro_objects = maestro_objects
else:
self._group_shown = True
self._maestro_objects = []
def __del__(self, _hasattr=hasattr, _maestro=maestro):
if maestro and _hasattr(self, "_maestro_objects"):
for obj in self._maestro_objects:
maestro.remove_object(obj)
[docs] def hide(self):
"""
Hide the object. It will not be drawn when the group is drawn.
"""
self._shown = False
if maestro:
for obj in self._maestro_objects:
maestro.hide_object(obj)
[docs] def show(self):
"""
Display this object, if it was hidden
"""
self._shown = True
if self._group_shown and maestro:
for obj in self._maestro_objects:
maestro.show_object(obj)
[docs] def groupHidden(self):
"""
Called when the group of this object is hidden.
Hide the Maestro object(s).
"""
self._group_shown = False
if maestro:
for obj in self._maestro_objects:
maestro.hide_object(obj)
[docs] def groupShown(self):
"""
Called when the group of this object is shown.
Show the Maestro object, if we are being shown.
"""
self._group_shown = True
if self._shown and maestro:
for obj in self._maestro_objects:
maestro.show_object(obj)
[docs] def isShown(self):
"""Returns True if this object shown. False otherwise."""
return (self._shown)
[docs] def isGroupShown(self):
"""Returns True if this object's group is shown. False otherwise."""
return (self._group_shown)
[docs] def setEntryID(self, entry_id):
"""
Sets entry ID for Maestro object (necessary to render in tile-by-entry
mode.
:type entry_id: str
:param entry_id: Object's entry ID.
"""
for obj in self._maestro_objects:
maestro.set_entry_id(obj, entry_id)
[docs] def setRightClickOnGObject(self, pymodule, pyfunc):
"""
Sets the python callback which should be called whenever
given graphics object is right clicked.
:param pymodule: Python module
:type pymodule: str
:param pyfunc: Python function
:type pyfunc: str
"""
for obj in self._maestro_objects:
maestro.set_rightclick_on_gobject(obj, pymodule, pyfunc)
[docs] def setIsGlowing(self, is_glowing):
"""
Enables or disables glow effect for the object.
:param is_glowing: Whether the object is glowing.
:type is_glowing: bool
"""
for obj in self._maestro_objects:
maestro.set_is_glowing(obj, is_glowing)
[docs] def setGlowColor(self, r, g, b):
"""
Sets glow color for the object.
:param r: Red component of glow color [0.0 .. 1.0]
:type r: float
:param g: Green component of glow color [0.0 .. 1.0]
:type g: float
:param b: Blue component of glow color [0.0 .. 1.0]
:type b: float
"""
for obj in self._maestro_objects:
maestro.set_glow_color(obj, r, g, b)
class _MaestroPrimitiveMixin:
"""
Functions shared between Maestro primitives
"""
# Defaults so properties can run during init
_maestro_objects = ()
_r = 0.0
_g = 0.0
_b = 0.0
_radius = 0.0
_opacity = 0.0
_material = ""
_pick_id = 0
_persistent_name = ""
_pick_category = ""
def setRGBColors(self, r, g, b, a=None):
self._r = r
self._g = g
self._b = b
if a is not None:
self._opacity = a
self.setColors()
def setColors(self):
for obj in self._maestro_objects:
maestro.set_colors(obj, self._r, self._g, self._b, self._opacity)
@property
def r(self):
return self._r
@r.setter
def r(self, value):
self._r = value
self.setColors()
@property
def g(self):
return self._g
@g.setter
def g(self, value):
self._g = value
self.setColors()
@property
def b(self):
return self._b
@b.setter
def b(self, value):
self._b = value
self.setColors()
@property
def opacity(self):
return self._opacity
@opacity.setter
def opacity(self, value):
self._opacity = value
self.setColors()
@property
def material(self):
return self._material
@material.setter
def material(self, value):
self._material = value
for obj in self._maestro_objects:
maestro.set_material(obj, self._material)
@property
def pick_id(self):
return self._pick_id
@pick_id.setter
def pick_id(self, id):
self._pick_id = id
for obj in self._maestro_objects:
maestro.set_pick_id(obj, self._pick_id)
@property
def persistent_name(self):
return self._persistent_name
@persistent_name.setter
def persistent_name(self, name):
self._persistent_name = name
for obj in self._maestro_objects:
maestro.set_persistent_name(obj, self._persistent_name)
@property
def pick_category(self):
return self._pick_category
@pick_category.setter
def pick_category(self, pick_category):
self._pick_category = pick_category
for obj in self._maestro_objects:
maestro.set_pick_category(obj, self._pick_category)
[docs]class Group(object):
"""
Class to group a bunch of primitives and draw them out.
Example for non-Maestro objects:
my_box1 = box.Box(...)
my_box2 = box.Box(...)
my_sphere = sphere.Sphere(...)
my_group = box.Group() (or sphere.Group(), etc)
my_group.add(my_box1)
my_group.add(my_box2)
my_group.add(my_sphere)
"""
[docs] def __init__(self):
"""
Constructor takes no arguments.
"""
global _all_groups
self._primitives_dict = {}
self._shown = True
if maestro:
maestro.workspace_bounding_box_function_add(
self.boundingBoxCallback)
_all_groups.append(self)
return
def __del__(self):
if maestro:
maestro.workspace_bounding_box_function_remove(
self.boundingBoxCallback)
# Check existence of global, which is not guaranteed on shutdown
if _all_groups:
_all_groups.remove(self)
[docs] def __len__(self):
"""
Return the number of primitives in this group. Note
that each primitive type can have multiple instances registered
and each of these is counted towards the total.
"""
total = 0
for primitive_list in self._primitives_dict.values():
total += len(primitive_list)
return total
[docs] def getTotalTypes(self):
"""
Return the number of unique primitive types in this group.
This differs from the standard length operation. That returns
a count of all primitive instances.
"""
return len(self._primitives_dict)
[docs] def hide(self):
"""
Do not draw objects belonging to this group.
Will also modify Maestro's bounding rect to exclude these objects.
"""
self._shown = False
# Need to hide Maestro objects, if any
for primitive_list in self._primitives_dict.values():
if primitive_list == []:
continue
for primitive_instance in primitive_list:
primitive_instance.groupHidden()
[docs] def show(self):
"""
Cancel effect of hide() method, and draw the objects belonging to this
group. Will modify Maestro's bounding rect to exclude these objects.
"""
self._shown = True
# Need to show Maestro objects, if any
for primitive_list in self._primitives_dict.values():
if primitive_list == []:
continue
for primitive_instance in primitive_list:
primitive_instance.groupShown()
[docs] def isShown(self):
"""
Returns True if this group is shown; False otherwise.
"""
return (self._shown)
[docs] def add(self, item):
"""
Add a drawing primitive instance (Box, etc.) to this group.
"""
# So that we don't have to import box.py, etc.:
item_type = str(type(item))
# If no primitive of this type has been added, create a list
# for it now.
if item_type not in self._primitives_dict:
self._primitives_dict[item_type] = []
self._primitives_dict[item_type].append(item)
if self._shown:
self.show()
return
[docs] def remove(self, item):
"""
Remove a drawing primitive instance (Box, etc.) from this group.
"""
for primitive_list in self._primitives_dict.values():
if item in primitive_list:
primitive_list.remove(item)
return
raise ValueError("Group.remove(x): x not in group")
[docs] def clear(self, item_type=None):
"""
Clear up drawing instances.
:param item_type: Can be one of the following: None, Arrow, Box, etc.
Note:
- If item_type is None, all drawing instances will be removed; if
item_type is not None, all drawing instances of the `item_type` class
will be removed.
- No effects if there is no drawing instances of the `item_type` class.
"""
if item_type is None:
# 108192 Delete 3D objects when Group.clear() is called.
# Special case handling for Maestro* objects.
# These require us to explicitly delete them from Maestro
for primitive_list in self._primitives_dict.values():
for item in primitive_list:
if hasattr(item, "_maestro_objects"):
for obj in item._maestro_objects:
maestro.remove_object(obj)
self._primitives_dict = {}
else:
# 108192 Delete 3D objects when Group.clear() is called.
# Special case handling for Maestro* objects.
# These require us to explicitly delete them from Maestro
try:
self._primitives_dict[str(item_type)]
for item in self._primitives_dict[str(item_type)]:
if hasattr(item, "_maestro_objects"):
for obj in item._maestro_objects:
maestro.remove_object(obj)
except KeyError:
pass
# Empty list for Maestro and non-Maestro objects
try:
self._primitives_dict[str(item_type)] = []
except KeyError:
pass
[docs] def boundingBoxCallback(self, r00, r01, r02, r03, r10, r11, r12, r13, r20,
r21, r22, r23, r30, r31, r32, r33):
""" Called when the workspace is fit to screen """
if len(self) == 0: # No items added
return
if not self._shown:
return
if not maestro:
return
mat = [[float(r00), float(r01),
float(r02), float(r03)],
[float(r10), float(r11),
float(r12), float(r13)],
[float(r20), float(r21),
float(r22), float(r23)],
[float(r30), float(r31),
float(r32), float(r33)]]
xyzmin = []
xyzmax = []
for k in range(6):
xyzmin.append(BOUNDING_BOX_INIT_VALUE)
xyzmax.append(-BOUNDING_BOX_INIT_VALUE)
pxyzmin = []
pxyzmax = []
for primitive_list in self._primitives_dict.values():
if primitive_list == []:
continue
# Calculate bounding box for all the instances of this type
for primitive_instance in primitive_list:
if hasattr(primitive_instance, '_calculateBoundingBox'):
(pxyzmin,
pxyzmax) = primitive_instance._calculateBoundingBox(mat)
for k in range(6):
if xyzmin[k] > pxyzmin[k]:
xyzmin[k] = pxyzmin[k]
if xyzmax[k] < pxyzmax[k]:
xyzmax[k] = pxyzmax[k]
else:
print("Graphical object with no bounding box function")
break
# If resulting bounding box comes to 2D (a place) or 1D (a line),
# we need to adjust it so that it's a valid bounding box in 3D
for k in range(6):
if xyzmin[k] == xyzmax[k]:
xyzmin[k] = xyzmin[k] - BOUNDING_BOX_ADJUST_VALUE
xyzmax[k] = xyzmax[k] + BOUNDING_BOX_ADJUST_VALUE
# Set bounding box of Python graphics 3D objects to Maestro Workspace
maestro.set_workspace_bounding_box(xyzmin[0], xyzmin[1], xyzmin[2],
xyzmin[3], xyzmin[4], xyzmin[5],
xyzmax[0], xyzmax[1], xyzmax[2],
xyzmax[3], xyzmax[4], xyzmax[5])
return
[docs] def allPrimitives(self):
"""
Iterate through all the objects in this group and yield primitives.
"""
for primitive_list in self._primitives_dict.values():
if primitive_list == []:
continue
for primitive_instance in primitive_list:
yield primitive_instance