# Copyright 2004-2008 Nanorex, Inc. See LICENSE file for details. """ jigs_measurements.py -- Classes for measurement jigs. @version: $Id$ @copyright: 2004-2008 Nanorex, Inc. See LICENSE file for details. History: 060324 wware - with a debug pref, we can now get a drawing style for the distance measuring jig that looks like a dimension on a mechanical drawing. Future plans: similar drawing styles for angle and dihedral measurements, and switch drawing to OpenGL display lists for performance improvement. 05(later) wware - Simulator support was added, as well as MMP file records. 051104 wware - three measurement jigs (distance, angle, dihedral) written over the last few days. No simulator support yet, but they work fine in the CAD system, and the MMP file records should be acceptable when the time comes for sim support. """ import sys import Numeric from Numeric import dot import foundation.env as env from geometry.VQT import V, A, norm, cross, vlen, angleBetween from foundation.Utility import Node from utilities.Log import redmsg, greenmsg, orangemsg from utilities.debug import print_compact_stack, print_compact_traceback from model.jigs import Jig from graphics.drawing.dimensions import drawLinearDimension, drawAngleDimension, drawDihedralDimension from graphics.drawing.drawers import drawtext from utilities.constants import black from utilities.prefs_constants import dynamicToolTipAtomDistancePrecision_prefs_key from utilities.prefs_constants import dynamicToolTipBendAnglePrecision_prefs_key from OpenGL.GLU import gluUnProject def _constrainHandleToAngle(pos, p0, p1, p2): """ This works in two steps. (1) Project pos onto the plane defined by (p0, p1, p2). (2) Confine the projected point to lie within the angular arc. """ u = pos - p1 z0 = norm(p0 - p1) z2 = norm(p2 - p1) oop = norm(cross(z0, z2)) u = u - dot(oop, u) * oop # clip the point so it lies within the angle if dot(cross(z0, u), oop) < 0: # Clip on the z0 side of the angle. u = vlen(u) * z0 elif dot(cross(u, z2), oop) < 0: # Clip on the z2 side of the angle. u = vlen(u) * z2 return p1 + u # == Measurement Jigs # rename class for clarity, remove spurious methods, wware 051103 class MeasurementJig(Jig): """ superclass for Measurement jigs """ # constructor moved to base class, wware 051103 def __init__(self, assy, atomlist): Jig.__init__(self, assy, atomlist) self.font_name = "Helvetica" self.font_size = 10.0 # pt size self.color = black # This is the "draw" color. When selected, this will become highlighted red. self.normcolor = black # This is the normal (unselected) color. self.cancelled = True # We will assume the user will cancel self.handle_offset = V(0.0, 0.0, 0.0) # move some things to base class, wware 051103 copyable_attrs = Jig.copyable_attrs + ('font_name', 'font_size') def constrainedPosition(self): """ The jig maintains an unconstrained position. Constraining the position can mean projecting it onto a particular surface, and/or confining it to a particular region satisfying some linear inequalities in position. """ raise Exception('expected to be overloaded') def clickedOn(self, pos): self.handle_offset = pos - self.center() self.constrain() def constrain(self): self.handle_offset = self.constrainedPosition() - self.center() def move(self, offset): ###k NEEDS REVIEW: does this conform to the new Node API method 'move', or should it be renamed? [bruce 070501 question] self.handle_offset += offset self.constrain() def posn(self): return self.center() + self.handle_offset # move to base class, wware 051103 # Email from Bruce, 060327 <<<< # It's either a bug or a bad style error to directly call Node.kill, # skipping the superclass kill methods if any (and there is one, # at least Jig.kill). # In this case it might not be safe to call Jig.kill, since that might # recurse into this method, but (1) this needs a comment, (2) this is a # possible bug since removing the atoms is not happening when this jig # is killed, but perhaps needs to (otherwise the atoms will be listing # this jig as one of their jigs even after it's killed -- not sure what # harm this causes but it might be bad, e.g. a minor memory leak or # maybe some problem when copying them). # If you're not sure what to do about any of it, just commenting it # or filing a reminder bug is ok for now. It interacts with some code # I'm working on now in other files, so it might be just as well for me # to be the one to change the code. >>>> # For now, I think I'll go with Jig.kill, and let Bruce modify as needed. # [... but this is still Node.kill. Maybe Jig.kill caused a bug and someone # else changed it back and didn't comment it? Who knows. REVIEW sometime. # bruce 080311 comment.] def remove_atom(self, atom, **opts): # bruce 080311 added **opts to match superclass method signature """ Delete self if *any* of its atoms are deleted [overrides superclass method] """ Node.kill(self) # superclass is Jig, not Node; see long comment above # Set the properties for a Measure Distance jig read from a (MMP) file # include atomlist, wware 051103 def setProps(self, name, color, font_name, font_size, atomlist): self.name = name self.color = color self.font_name = font_name self.font_size = font_size self.setAtoms(atomlist) # simplified, wware 051103 # Following Postscript: font names NEVER have parentheses in them. # So we can use parentheses to delimit them. def mmp_record_jigspecific_midpart(self): return " (%s) %d" % (self.font_name, self.font_size) # unify text-drawing to base class, wware 051103 def _drawtext(self, text, color): # use atom positions to compute center, where text should go if self.picked: # move the text to the lower left corner, and make it big pos = A(gluUnProject(5, 5, 0)) drawtext(text, color, pos, 3 * self.font_size, self.assy.o) else: pos1 = self.atoms[0].posn() pos2 = self.atoms[-1].posn() pos = (pos1 + pos2) / 2 drawtext(text, color, pos, self.font_size, self.assy.o) # move into base class, wware 051103 def set_cntl(self): from command_support.JigProp import JigProp self.cntl = JigProp(self, self.assy.o) # move into base class, wware 051103 def writepov(self, file, dispdef): sys.stderr.write(self.__class__.__name__ + ".writepov() not implemented yet") def will_copy_if_selected(self, sel, realCopy): """ copy only if all my atoms are selected [overrides Jig.will_copy_if_selected] """ # for measurement jigs, copy only if all atoms selected, wware 051107 # [bruce 060329 adds: this doesn't prevent the copy if the jig is inside a Group, and that causes a bug] for atom in self.atoms: if not sel.picks_atom(atom): if realCopy: msg = "Can't copy a measurement jig [%s] unless all its atoms are selected." % (self.name,) env.history.message(orangemsg(msg)) return False return True def center(self): c = Numeric.array((0.0, 0.0, 0.0)) n = len(self.atoms) for a in self.atoms: c += a.posn() / n return c def writemmp_info_leaf(self, mapping): Node.writemmp_info_leaf(self, mapping) x, y, z = self.constrainedPosition() mapping.write("info leaf handle = %g %g %g\n" % (x, y, z)) def readmmp_info_leaf_setitem(self, key, val, interp): import string, Numeric if key == ['handle']: self.handle_offset = Numeric.array(map(string.atof, val.split())) - self.center() else: Jig.readmmp_info_leaf_setitem(self, key, val, interp) pass # end of class MeasurementJig # == Measure Distance class MeasureDistance(MeasurementJig): """ A Measure Distance jig has two atoms and draws a line with a distance label between them. """ sym = "Distance" icon_names = ["modeltree/Measure_Distance.png", "modeltree/Measure_Distance-hide.png"] featurename = "Measure Distance Jig" # added, wware 20051202 def constrainedPosition(self): a = self.atoms pos, p0, p1 = self.center() + self.handle_offset, a[0].posn(), a[1].posn() z = p1 - p0 nz = norm(z) dotprod = dot(pos - p0, nz) if dotprod < 0.0: return pos - dotprod * nz elif dotprod > vlen(z): return pos - (dotprod - vlen(z)) * nz else: return pos def _getinfo(self): return "[Object: Measure Distance] [Name: " + str(self.name) + "] " + \ "[Nuclei Distance = " + str(self.get_nuclei_distance()) + " ]" + \ "[VdW Distance = " + str(self.get_vdw_distance()) + " ]" def _getToolTipInfo(self): #ninad060825 """ Return a string for display in Dynamic Tool tip """ #honor user preferences for digit after decimal distPrecision = env.prefs[dynamicToolTipAtomDistancePrecision_prefs_key] nucleiDist = round(self.get_nuclei_distance(),distPrecision) vdwDist = round(self.get_vdw_distance(),distPrecision) attachedAtoms = str(self.atoms[0]) + "-" + str(self.atoms[1]) return str(self.name) + "
" + " Jig Type:Measure Distance"\ + "
" + "Atoms: " + attachedAtoms+"
"\ +"Nuclei Distance: " + str(nucleiDist) + " A" \ + "
" + "VdW Distance: " + str(vdwDist) + " A" def getstatistics(self, stats): # Should be _getstatistics(). Mark stats.num_mdistance += 1 # Helper functions for the measurement jigs. Should these be general Atom functions? Mark 051030. def get_nuclei_distance(self): """ Returns the distance between two atoms (nuclei) """ return vlen (self.atoms[0].posn()-self.atoms[1].posn()) def get_vdw_distance(self): """ Returns the VdW distance between two atoms """ return self.get_nuclei_distance() - self.atoms[0].element.rvdw - self.atoms[1].element.rvdw # Measure Distance jig is drawn as a line between two atoms with a text label between them. # A wire cube is also drawn around each atom. def _draw_jig(self, glpane, color, highlighted=False): """ Draws a wire frame cube around two atoms and a line between them. A label displaying the VdW and nuclei distances (e.g. 1.4/3.5) is included. """ MeasurementJig._draw_jig(self, glpane, color, highlighted) text = "%.2f/%.2f" % (self.get_vdw_distance(), self.get_nuclei_distance()) # mechanical engineering style dimensions drawLinearDimension(color, self.assy.o.right, self.assy.o.up, self.constrainedPosition(), self.atoms[0].posn(), self.atoms[1].posn(), text, highlighted=highlighted) mmp_record_name = "mdistance" pass # end of class MeasureDistance # == Measure Angle class MeasureAngle(MeasurementJig): # new class. wware 051031 """ A Measure Angle jig has three atoms. """ sym = "Angle" icon_names = ["modeltree/Measure_Angle.png", "modeltree/Measure_Angle-hide.png"] featurename = "Measure Angle Jig" # added, wware 20051202 def constrainedPosition(self): import types a = self.atoms return _constrainHandleToAngle(self.center() + self.handle_offset, a[0].posn(), a[1].posn(), a[2].posn()) def _getinfo(self): # add atom list, wware 051101 return "[Object: Measure Angle] [Name: " + str(self.name) + "] " + \ ("[Atoms = %s %s %s]" % (self.atoms[0], self.atoms[1], self.atoms[2])) + \ "[Angle = " + str(self.get_angle()) + " ]" def _getToolTipInfo(self): #ninad060825 """ Return a string for display in Dynamic Tool tip """ #honor user preferences for digit after decimal anglePrecision = env.prefs[dynamicToolTipBendAnglePrecision_prefs_key] bendAngle = round(self.get_angle(),anglePrecision) attachedAtoms = str(self.atoms[0]) + "-" + str(self.atoms[1]) + "-" + str(self.atoms[2]) return str(self.name) + "
" + " Jig Type: Measure Angle"\ + "
" + "Atoms: " + attachedAtoms + "
"\ + "Angle " + str(bendAngle) + " Degrees" def getstatistics(self, stats): # Should be _getstatistics(). Mark stats.num_mangle += 1 # Helper functions for the measurement jigs. Should these be general Atom functions? Mark 051030. def get_angle(self): """ Returns the angle between two atoms (nuclei) """ v01 = self.atoms[0].posn()-self.atoms[1].posn() v21 = self.atoms[2].posn()-self.atoms[1].posn() return angleBetween(v01, v21) # Measure Angle jig is drawn as a line between two atoms with a text label between them. # A wire cube is also drawn around each atom. def _draw_jig(self, glpane, color, highlighted=False): """ Draws a wire frame cube around two atoms and a line between them. A label displaying the angle is included. """ MeasurementJig._draw_jig(self, glpane, color, highlighted) # draw boxes around each of the jig's atoms. text = "%.2f" % self.get_angle() # mechanical engineering style dimensions drawAngleDimension(color, self.assy.o.right, self.assy.o.up, self.constrainedPosition(), self.atoms[0].posn(), self.atoms[1].posn(), self.atoms[2].posn(), text, highlighted=highlighted) mmp_record_name = "mangle" pass # end of class MeasureAngle # == Measure Dihedral class MeasureDihedral(MeasurementJig): # new class. wware 051031 """ A Measure Dihedral jig has four atoms. """ sym = "Dihedral" icon_names = ["modeltree/Measure_Dihedral.png", "modeltree/Measure_Dihedral-hide.png"] featurename = "Measure Dihedral Jig" # added, wware 20051202 def constrainedPosition(self): a = self.atoms p0, p1, p2, p3 = a[0].posn(), a[1].posn(), a[2].posn(), a[3].posn() axis = norm(p2 - p1) midpoint = 0.5 * (p1 + p2) return _constrainHandleToAngle(self.center() + self.handle_offset, p0 - dot(p0 - midpoint, axis) * axis, midpoint, p3 - dot(p3 - midpoint, axis) * axis) def _getinfo(self): # add atom list, wware 051101 return "[Object: Measure Dihedral] [Name: " + str(self.name) + "] " + \ ("[Atoms = %s %s %s %s]" % (self.atoms[0], self.atoms[1], self.atoms[2], self.atoms[3])) + \ "[Dihedral = " + str(self.get_dihedral()) + " ]" def _getToolTipInfo(self): #ninad060825 """ Return a string for display in Dynamic Tool tip """ #honor user preferences for digit after decimal anglePrecision = env.prefs[dynamicToolTipBendAnglePrecision_prefs_key] dihedral = round(self.get_dihedral(),anglePrecision) attachedAtoms = str(self.atoms[0]) + "-" + str(self.atoms[1]) + "-" + str(self.atoms[2]) + "-" + str(self.atoms[3]) return str(self.name) + "
" + " Jig Type: Measure Dihedral"\ + "
" + "Atoms: " + attachedAtoms + "
"\ + "Angle " + str(dihedral) + " Degrees" def getstatistics(self, stats): # Should be _getstatistics(). Mark stats.num_mdihedral += 1 # Helper functions for the measurement jigs. Should these be general Atom functions? Mark 051030. def get_dihedral(self): """ Returns the dihedral between two atoms (nuclei) """ wx = self.atoms[0].posn()-self.atoms[1].posn() yx = self.atoms[2].posn()-self.atoms[1].posn() xy = -yx zy = self.atoms[3].posn()-self.atoms[2].posn() u = cross(wx, yx) v = cross(xy, zy) if dot(zy, u) < 0: # angles go from -180 to 180, wware 051101 return -angleBetween(u, v) # use new acos(dot) func, wware 051103 else: return angleBetween(u, v) # Measure Dihedral jig is drawn as a line between two atoms with a text label between them. # A wire cube is also drawn around each atom. def _draw_jig(self, glpane, color, highlighted=False): """ Draws a wire frame cube around two atoms and a line between them. A label displaying the dihedral is included. """ MeasurementJig._draw_jig(self, glpane, color, highlighted) # draw boxes around each of the jig's atoms. text = "%.2f" % self.get_dihedral() # mechanical engineering style dimensions drawDihedralDimension(color, self.assy.o.right, self.assy.o.up, self.constrainedPosition(), self.atoms[0].posn(), self.atoms[1].posn(), self.atoms[2].posn(), self.atoms[3].posn(), text, highlighted=highlighted) mmp_record_name = "mdihedral" pass # end of class MeasureDihedral # end of module jigs_measurements.py