예제 #1
0
 def closest_pt_params_to_ray(self, ray):
     ""
     p2, v2 = ray.params  # note: at first I wrote self.params() (using method not attr)
     p1, v1 = self.params
     # do some math, solve for k in p1 + k * v1 = that point (remember that the vecs can be of any length):
     # way 1: express p2-p1 as a weighted sum of v1, v2, cross(v1,v2), then take the v1 term in that sum and add it to p1.
     # way 2: There must be a NumPy function that would just do this in about one step...
     # way 3: or maybe we can mess around with dot(v1,v2) sort of like in corner_analyzer in demo_polygon...
     # way 4: or we could google for "closest points on two lines" or so...
     # way 5: or we could call it the intersection of self with the plane containing p2, and directions v2 and the cross prod,
     # and use a formula in VQT. Yes, that may not be self-contained but it's fastest to code!
     v1n = norm(v1)
     v2n = norm(v2)
     perp0 = cross(v1n, v2n)
     if vlen(perp0) < 0.01:
         ##k btw what if the lines are parallel, in what way should we fail?
         # and for that matter what if they are *almost* parallel so that we're too sensitive -- do we use an env param
         # to decide whether to fail in that case too? If we're an Instance we could do that from self.env... #e
         print "closest_pt_params_to_ray: too sensitive, returning None"  ###### teach caller to handle this; let 0.01 be option
         return None
     perpn = norm(perp0)
     perpperp = cross(perpn, v2n)
     inter = planeXline(
         p2, perpperp, p1, v1n
     )  # intersect plane (as plane point and normal) with line (as point and vector)
     if inter is None:
         print "inter is None (unexpected); data:", p1, v1, p2, v2, perp0
         return None
     # inter is the retval for a variant which just wants the closest point itself, i.e. closest_pt_to_ray
     return dot(inter - p1, v1n) / vlen(v1)
예제 #2
0
def baseframe_from_pam3_data(ss1, ax, ss2):
    """
    Given the positions of the Ss3-Ax3-Ss3 atoms in a PAM3 basepair,
    return the first Ss3's baseframe (and y_m) as a tuple of
    (origin, rel_to_abs_quat, y_m).
    """
    yprime_vector = ss2 - ss1
    yprime_length = vlen(yprime_vector)
    
    y_direction = yprime_vector / yprime_length # optimization of norm
    
    z_direction = norm(cross(ax - ss1, y_direction))
        # BUG: nothing checks for cross product being too small

    x_direction = norm(cross(y_direction, z_direction))
        # this norm is redundant, but might help with numerical stability

    rel_to_abs_quat = Q(x_direction, y_direction, z_direction)

    # still need origin, easy since we know SPRIME_D_SDFRAME -- but we do have to rotate that, using the quat

    # rel_to_abs_quat.rot( SPRIME_D_SDFRAME ) # this is Ss5 to Ss3 vector, abs coords

    Ss3_d_abspos = ss1
    Ss5_d_abspos = Ss3_d_abspos - rel_to_abs_quat.rot( SPRIME_D_SDFRAME )

    origin = Ss5_d_abspos

    # y_m = (|S'_u - S'_d| / 2) + y_s'
    y_m = yprime_length / 2.0 + Y_SPRIME

    return ( origin, rel_to_abs_quat, y_m )
예제 #3
0
 def on_press(self):
     point = self.current_event_mousepoint() # the touched point on the visible object (hitpoint)
         # (this method is defined in the Highlightable which is self.delegate)
     self.oldpoint = self.startpoint = point
     # decide type of drag now, so it's clearly constant during drag, and so decision code is only in one place.
     # (but note that some modkey meanings might require that changes to them during the same drag are detected [nim].)
     if self._delegate.altkey:
         self._this_drag = 'free x-y rotate'
             #e more options later, and/or more flags like this (maybe some should be booleans)
             ###e or better, set up a function or object which turns later points into their effects... hmm, a DragCommand instance!
             ##e or should that be renamed DragOperation??
         self._screenrect = (ll, lr, ur, ul) = self.screenrect( self.startpoint)
             # these points should be valid in our delegate's coords == self's coords
         self._dx = _dx = norm(lr - ll)
         self._dy = _dy = norm(ur - lr)
         self._dz = cross(_dx, _dy) # towards the eye (if view is ortho) (but alg is correct whether or not it is, i think)
             ###k check cross direction sign
         self._scale = min(vlen(lr - ll), vlen(ur - lr)) * 0.4
             # New motion UI suggests that 40% of that distance means 180 degrees of rotation.
             # We'll draw an axis whose length is chosen so that dragging on a sphere of that size
             # would have the same effect. (Maybe.)
         self._objcenter = self._delegate.center
         self.startrot = + self.rotation
     else:
         self._this_drag = 'free x-y translate'
     if debug070209:
         self.ndrags = 0
     return
예제 #4
0
def baseframe_from_pam3_data(ss1, ax, ss2):
    """
    Given the positions of the Ss3-Ax3-Ss3 atoms in a PAM3 basepair,
    return the first Ss3's baseframe (and y_m) as a tuple of
    (origin, rel_to_abs_quat, y_m).
    """
    yprime_vector = ss2 - ss1
    yprime_length = vlen(yprime_vector)

    y_direction = yprime_vector / yprime_length  # optimization of norm

    z_direction = norm(cross(ax - ss1, y_direction))
    # BUG: nothing checks for cross product being too small

    x_direction = norm(cross(y_direction, z_direction))
    # this norm is redundant, but might help with numerical stability

    rel_to_abs_quat = Q(x_direction, y_direction, z_direction)

    # still need origin, easy since we know SPRIME_D_SDFRAME -- but we do have to rotate that, using the quat

    # rel_to_abs_quat.rot( SPRIME_D_SDFRAME ) # this is Ss5 to Ss3 vector, abs coords

    Ss3_d_abspos = ss1
    Ss5_d_abspos = Ss3_d_abspos - rel_to_abs_quat.rot(SPRIME_D_SDFRAME)

    origin = Ss5_d_abspos

    # y_m = (|S'_u - S'_d| / 2) + y_s'
    y_m = yprime_length / 2.0 + Y_SPRIME

    return (origin, rel_to_abs_quat, y_m)
예제 #5
0
    def on_drag(self):
        # Note: we can assume this is a "real drag", since the caller (ultimately a selectMode method in testmode, as of 070209)
        # is tracking mouse motion and not calling this until it becomes large enough, as the debug070209 prints show.
        oldpoint = self.oldpoint  # was saved by prior on_drag or by on_press
        point = self.current_event_mousepoint(plane=self.startpoint)
        if debug070209:
            self.ndrags += 1


##            if (self.ndrags == 1) or 1:
##                print "drag event %d, model distance = %r, pixel dist not computed" % (self.ndrags, vlen(oldpoint - point),)
        if self._this_drag == 'free x-y rotate':
            # rotate using New motion UI
            #  [probably works for this specific kind of rotation, one of 4 that UI proposes;
            #   doesn't yet have fancy cursors or during-rotation graphics; add those only after it's a DragCommand]
            # two implem choices:
            # 1. know the eye direction and the screen dims in plane of startpoint, in model coords; compute in model coords
            # 2. get the mouse positions (startpoint and point) and screen dims in window x,y coords, compute rotation in eye coords,
            #   but remember to reorient it to correspond with model if model coords are rotated already.
            # Not sure which one is better.
            #   In general, placing user into model coords (or more precisely, into object local coords) seems more general --
            # for example, what if there were several interacting users, each visible to the others?
            # We'd want each user's eye & screen to be visible! (Maybe even an image of their face & screen, properly scaled and aligned?)
            # And we'd want their posns to be used in the computations here, all in model coords.
            # (Even if zoom had occurred, which means, even the user's *size* is quite variable!)
            #   I need "user in model coords" for other reasons too, so ok, I'll do it that way.
            #
            # [Hey, I might as well fix the bug in current_event_mousepoint which fakes the center of view, at the same time.
            # (I can't remember its details right now, but I think it assumed the local origin was the cov, which is obviously wrong.)
            # (But I didn't look at that code or fix that bug now.)]
            vec = point - self.startpoint
            uvec = norm(vec)  #k needed??
            axisvec = cross(
                self._dz, uvec
            )  # unit length (suitable for glRotate -- but we need to use it to make a quat!)
            axisvec = norm(
                axisvec)  # just to be sure (or to reduce numerical errors)
            scale = self._scale
            draw_axisvec = axisvec * scale  #e times some other length constant too?
            center = self._objcenter
            self.axisends = (center - axisvec, center + axisvec
                             )  # draw a rotation axis here ###e
            self.degrees = degrees = vlen(
                vec
            ) / scale * 180.0  # draw a textual indicator with degrees (and axisvec angle too) ###e
            ###e or print that info into sbar? or somewhere fixed in glpane? or in glpane near mouse?
            # now set self.rotation to a quat made from axisvec and degrees
            theta = degrees / 360.0 * 2 * pi
            # print "axisvec %r, degrees %r, theta %r" % (axisvec ,degrees,theta)
            rot = Q(axisvec, theta)
            self.rotation = self.startrot + rot  # note use of self.startrot rather than self.rotation on rhs
            # avoid += to make sure it gets changed-tracked -- and since it would be the wrong op!

        elif self._this_drag == 'free x-y translate':
            self._cmd_drag_from_to(
                oldpoint, point)  # use Draggable interface cmd on self
        else:
            assert 0
        self.oldpoint = point
        return
예제 #6
0
 def on_press(self):
     point = self.current_event_mousepoint(
     )  # the touched point on the visible object (hitpoint)
     # (this method is defined in the Highlightable which is self.delegate)
     self.oldpoint = self.startpoint = point
     # decide type of drag now, so it's clearly constant during drag, and so decision code is only in one place.
     # (but note that some modkey meanings might require that changes to them during the same drag are detected [nim].)
     if self._delegate.altkey:
         self._this_drag = 'free x-y rotate'
         #e more options later, and/or more flags like this (maybe some should be booleans)
         ###e or better, set up a function or object which turns later points into their effects... hmm, a DragCommand instance!
         ##e or should that be renamed DragOperation??
         self._screenrect = (ll, lr, ur,
                             ul) = self.screenrect(self.startpoint)
         # these points should be valid in our delegate's coords == self's coords
         self._dx = _dx = norm(lr - ll)
         self._dy = _dy = norm(ur - lr)
         self._dz = cross(
             _dx, _dy
         )  # towards the eye (if view is ortho) (but alg is correct whether or not it is, i think)
         ###k check cross direction sign
         self._scale = min(vlen(lr - ll), vlen(ur - lr)) * 0.4
         # New motion UI suggests that 40% of that distance means 180 degrees of rotation.
         # We'll draw an axis whose length is chosen so that dragging on a sphere of that size
         # would have the same effect. (Maybe.)
         self._objcenter = self._delegate.center
         self.startrot = +self.rotation
     else:
         self._this_drag = 'free x-y translate'
     if debug070209:
         self.ndrags = 0
     return
예제 #7
0
 def getAxisVector(self, atomAtVectorOrigin = None):
     """
     Returns the unit axis vector of the segment (vector between two axis 
     end points)
     """
     endPoint1, endPoint2 = self.getAxisEndPoints()
     
     if endPoint1 is None or endPoint2 is None:
         return V(0, 0, 0)
     
     
     if atomAtVectorOrigin is not None:
         #If atomAtVectorOrigin is specified, we will return a vector that
         #starts at this atom and ends at endPoint1 or endPoint2 . 
         #Which endPoint to choose will be dicided by the distance between
         #atomAtVectorOrigin and the respective endPoints. (will choose the 
         #frthest endPoint
         origin = atomAtVectorOrigin.posn()
         if vlen(endPoint2 - origin ) > vlen(endPoint1 - origin):
             return norm(endPoint2 - endPoint1)
         else:
             return norm(endPoint1 - endPoint2)
    
     
     return norm(endPoint2 - endPoint1)
예제 #8
0
 def closest_pt_params_to_ray(self, ray):
     ""
     p2, v2 = ray.params # note: at first I wrote self.params() (using method not attr)
     p1, v1 = self.params
     # do some math, solve for k in p1 + k * v1 = that point (remember that the vecs can be of any length):
     # way 1: express p2-p1 as a weighted sum of v1, v2, cross(v1,v2), then take the v1 term in that sum and add it to p1.
     # way 2: There must be a NumPy function that would just do this in about one step...
     # way 3: or maybe we can mess around with dot(v1,v2) sort of like in corner_analyzer in demo_polygon...
     # way 4: or we could google for "closest points on two lines" or so...
     # way 5: or we could call it the intersection of self with the plane containing p2, and directions v2 and the cross prod,
     # and use a formula in VQT. Yes, that may not be self-contained but it's fastest to code!
     v1n = norm(v1)
     v2n = norm(v2)
     perp0 = cross(v1n, v2n)
     if vlen(perp0) < 0.01:
         ##k btw what if the lines are parallel, in what way should we fail?
         # and for that matter what if they are *almost* parallel so that we're too sensitive -- do we use an env param
         # to decide whether to fail in that case too? If we're an Instance we could do that from self.env... #e
         print "closest_pt_params_to_ray: too sensitive, returning None" ###### teach caller to handle this; let 0.01 be option
         return None
     perpn = norm(perp0)
     perpperp = cross(perpn,v2n)
     inter = planeXline(p2, perpperp, p1, v1n) # intersect plane (as plane point and normal) with line (as point and vector)
     if inter is None:
         print "inter is None (unexpected); data:",p1,v1,p2,v2,perp0
         return None
     # inter is the retval for a variant which just wants the closest point itself, i.e. closest_pt_to_ray
     return dot(inter - p1, v1n) / vlen(v1)
예제 #9
0
def baseframe_from_pam5_data(ss1, gv, ss2):
    """
    Given the positions of the Ss5-Gv5-Ss5 atoms in a PAM5 basepair,
    return the first Ss5's baseframe (and y_m) as a tuple of
    (origin, rel_to_abs_quat, y_m).

    @note: this is correct even if gv is actually an Ax5 position.
    """
    # y axis is parallel to inter-sugar line

    # base plane orientation comes from the other atom, Gv

    # so get x and z axis around that line
    
    origin = ss1

    y_vector = ss2 - ss1
    y_length = vlen(y_vector)
    
    ## y_direction = norm(ss2 - ss1)
    y_direction = y_vector / y_length # optimization
    
    z_direction = norm(cross(gv - ss1, y_direction))
        # BUG: nothing checks for cross product being too small

    x_direction = norm(cross(y_direction, z_direction))
        # this norm is redundant, but might help with numerical stability

    rel_to_abs_quat = Q(x_direction, y_direction, z_direction)

    y_m = y_length / 2.0

    return ( origin, rel_to_abs_quat, y_m )
예제 #10
0
def baseframe_from_pam5_data(ss1, gv, ss2):
    """
    Given the positions of the Ss5-Gv5-Ss5 atoms in a PAM5 basepair,
    return the first Ss5's baseframe (and y_m) as a tuple of
    (origin, rel_to_abs_quat, y_m).

    @note: this is correct even if gv is actually an Ax5 position.
    """
    # y axis is parallel to inter-sugar line

    # base plane orientation comes from the other atom, Gv

    # so get x and z axis around that line

    origin = ss1

    y_vector = ss2 - ss1
    y_length = vlen(y_vector)

    ## y_direction = norm(ss2 - ss1)
    y_direction = y_vector / y_length  # optimization

    z_direction = norm(cross(gv - ss1, y_direction))
    # BUG: nothing checks for cross product being too small

    x_direction = norm(cross(y_direction, z_direction))
    # this norm is redundant, but might help with numerical stability

    rel_to_abs_quat = Q(x_direction, y_direction, z_direction)

    y_m = y_length / 2.0

    return (origin, rel_to_abs_quat, y_m)
예제 #11
0
    def rotateAboutPoint(self):
        """
        Rotates the selected entities along the specified vector, about the
        specified pivot point (pivot point it the starting point of the
        drawn vector.
        """
        
        if len(self.mouseClickPoints) != self.mouseClickLimit:
            print_compact_stack("Rotate about point bug: mouseclick points != mouseclicklimit: ")
            return
            
        
        pivotPoint = self.mouseClickPoints[0]
        ref_vec_endPoint = self.mouseClickPoints[1]
        rot_vec_endPoint = self.mouseClickPoints[2]
        
        reference_vec = norm(ref_vec_endPoint - pivotPoint)
        
        lineVector = norm(rot_vec_endPoint - pivotPoint)
                           
            
        #lineVector = endPoint - startPoint

        quat1 = Q(lineVector, reference_vec)
                
        #DEBUG Disabled temporarily . will not be used
        if dot(lineVector, reference_vec) < 0:
            theta = math.pi - quat1.angle
        else:
            theta = quat1.angle

        #TEST_DEBUG-- Works fine
        theta = quat1.angle

        rot_axis = cross(lineVector, reference_vec)
        
        
        if dot(lineVector, reference_vec) < 0:
            rot_axis = - rot_axis

        cross_prod_1 = norm(cross(reference_vec, rot_axis))
        cross_prod_2 = norm(cross(lineVector, rot_axis))

        if dot(cross_prod_1, cross_prod_2) < 0:
            quat2 = Q(rot_axis, theta)
        else:
            quat2 = Q(rot_axis, - theta)

        movables = self.graphicsMode.getMovablesForLeftDragging()
        self.assy.rotateSpecifiedMovables(
            quat2,
            movables = movables,
            commonCenter = pivotPoint)

        self.glpane.gl_update()
        return
    def rotateAboutPoint(self):
        """
        Rotates the selected entities along the specified vector, about the
        specified pivot point (pivot point it the starting point of the
        drawn vector.
        """
        startPoint = self.mouseClickPoints[0]
        endPoint = self.mouseClickPoints[1]
        pivotAtom = self.graphicsMode.pivotAtom
        #initial assignment of reference_vec. The selected movables will be
        #rotated by the angle between this vector and the lineVector
        reference_vec = self.glpane.right
        if isinstance(pivotAtom, Atom) and not pivotAtom.molecule.isNullChunk():
            mol = pivotAtom.molecule
            reference_vec, node_junk = mol.getAxis_of_self_or_eligible_parent_node(
                atomAtVectorOrigin = pivotAtom)
            del node_junk
        else:
            reference_vec = self.glpane.right

        lineVector = endPoint - startPoint

        quat1 = Q(lineVector, reference_vec)

        #DEBUG Disabled temporarily . will not be used
        ##if dot(lineVector, reference_vec) < 0:
            ##theta = math.pi - quat1.angle
        ##else:
            ##theta = quat1.angle

        #TEST_DEBUG-- Works fine
        theta = quat1.angle

        rot_axis = cross(lineVector, reference_vec)

        if dot(lineVector, reference_vec) < 0:
            rot_axis = - rot_axis

        cross_prod_1 = norm(cross(reference_vec, rot_axis))
        cross_prod_2 = norm(cross(lineVector, rot_axis))

        if dot(cross_prod_1, cross_prod_2) < 0:
            quat2 = Q(rot_axis,  theta)
        else:
            quat2 = Q(rot_axis,  - theta)


        movables = self.graphicsMode.getMovablesForLeftDragging()
        self.assy.rotateSpecifiedMovables(
            quat2,
            movables = movables,
            commonCenter = startPoint)

        self.glpane.gl_update()
예제 #13
0
    def rotateAboutPoint(self):
        """
        Rotates the selected entities along the specified vector, about the
        specified pivot point (pivot point it the starting point of the
        drawn vector.
        """

        if len(self.mouseClickPoints) != self.mouseClickLimit:
            print_compact_stack(
                "Rotate about point bug: mouseclick points != mouseclicklimit: "
            )
            return

        pivotPoint = self.mouseClickPoints[0]
        ref_vec_endPoint = self.mouseClickPoints[1]
        rot_vec_endPoint = self.mouseClickPoints[2]

        reference_vec = norm(ref_vec_endPoint - pivotPoint)

        lineVector = norm(rot_vec_endPoint - pivotPoint)

        #lineVector = endPoint - startPoint

        quat1 = Q(lineVector, reference_vec)

        #DEBUG Disabled temporarily . will not be used
        if dot(lineVector, reference_vec) < 0:
            theta = math.pi - quat1.angle
        else:
            theta = quat1.angle

        #TEST_DEBUG-- Works fine
        theta = quat1.angle

        rot_axis = cross(lineVector, reference_vec)

        if dot(lineVector, reference_vec) < 0:
            rot_axis = -rot_axis

        cross_prod_1 = norm(cross(reference_vec, rot_axis))
        cross_prod_2 = norm(cross(lineVector, rot_axis))

        if dot(cross_prod_1, cross_prod_2) < 0:
            quat2 = Q(rot_axis, theta)
        else:
            quat2 = Q(rot_axis, -theta)

        movables = self.graphicsMode.getMovablesForLeftDragging()
        self.assy.rotateSpecifiedMovables(quat2,
                                          movables=movables,
                                          commonCenter=pivotPoint)

        self.glpane.gl_update()
        return
예제 #14
0
 def walk_great_circle(P, Q, D, R=radius):
     """Given two points P and Q on or near the surface of the
     sphere, use P and Q to define a great circle. Then walk along
     that great circle starting at P and going in the direction of
     Q, and traveling far enough the chord is of length D. P and Q
     are not required to lie exactly on the sphere's surface.
     """
     dP, dQ = P - sphere_center, Q - sphere_center
     dPs = cross(cross(dP, dQ), dP)
     cpart, spart = norm(dP), norm(dPs)
     theta = 2 * asin(0.5 * D / R)
     return sphere_center + R * (cpart * cos(theta) + spart * sin(theta))
예제 #15
0
 def walk_great_circle(P, Q, D, R=radius):
     """Given two points P and Q on or near the surface of the
     sphere, use P and Q to define a great circle. Then walk along
     that great circle starting at P and going in the direction of
     Q, and traveling far enough the chord is of length D. P and Q
     are not required to lie exactly on the sphere's surface.
     """
     dP, dQ = P - sphere_center, Q - sphere_center
     dPs = cross(cross(dP, dQ), dP)
     cpart, spart = norm(dP), norm(dPs)
     theta = 2 * asin(0.5 * D / R)
     return sphere_center + R * (cpart * cos(theta) + spart * sin(theta))
예제 #16
0
    def _orient_to_position_first_strandA_base_in_axis_plane(
            self, baseList, end1, end2):
        """
        The self._orient method orients the DNA duplex parallel to the screen
        (lengthwise) but it doesn't ensure align the vector
        through the strand end atom on StrandA and the corresponding axis end 
        atom  (at end1) , parallel to the screen. 

        This function does that ( it has some rare bugs which trigger where it
        doesn't do its job but overall works okay )

        What it does: After self._orient() is done orienting, it finds a Quat 
        that rotates between the 'desired vector' between strand and axis ends at
        end1(aligned to the screen)  and the actual vector based on the current
        positions of these atoms.  Using this quat we rotate all the chunks 
        (as a unit) around a common center.

        @BUG: The last part 'rotating as a unit' uses a readymade method in 
        ops_motion.py -- 'rotateSpecifiedMovables' . This method itself may have
        some bugs because the axis of the dna duplex is slightly offset to the
        original axis. 

        @see: self._determine_axis_and_strandA_endAtoms_at_end_1()
        @see: self.make()
        """

        #the vector between the two end points. these are more likely
        #points clicked by the user while creating dna duplex using endpoints
        #of a line. In genral, end1 and end2 are obtained from self.make()
        b = norm(end2 - end1)

        axis_strand_vector = (self.strandA_atom_end1.posn() - \
                              self.axis_atom_end1.posn())

        vectorAlongLadderStep = cross(-self.assy.o.lineOfSight, b)
        unitVectorAlongLadderStep = norm(vectorAlongLadderStep)

        self.final_pos_strand_end_atom = \
            self.axis_atom_end1.posn() + \
            vlen(axis_strand_vector)*unitVectorAlongLadderStep

        expected_vec = self.final_pos_strand_end_atom - self.axis_atom_end1.posn(
        )

        q_new = Q(axis_strand_vector, expected_vec)

        if dot(axis_strand_vector, self.assy.o.lineOfSight) < 0:
            q_new2 = Q(b, -q_new.angle)
        else:
            q_new2 = Q(b, q_new.angle)

        self.assy.rotateSpecifiedMovables(q_new2, baseList, end1)
예제 #17
0
    def _orient_to_position_first_strandA_base_in_axis_plane(self, baseList, end1, end2):
        """
        The self._orient method orients the DNA duplex parallel to the screen
        (lengthwise) but it doesn't ensure align the vector
        through the strand end atom on StrandA and the corresponding axis end 
        atom  (at end1) , parallel to the screen. 

        This function does that ( it has some rare bugs which trigger where it
        doesn't do its job but overall works okay )

        What it does: After self._orient() is done orienting, it finds a Quat 
        that rotates between the 'desired vector' between strand and axis ends at
        end1(aligned to the screen)  and the actual vector based on the current
        positions of these atoms.  Using this quat we rotate all the chunks 
        (as a unit) around a common center.

        @BUG: The last part 'rotating as a unit' uses a readymade method in 
        ops_motion.py -- 'rotateSpecifiedMovables' . This method itself may have
        some bugs because the axis of the dna duplex is slightly offset to the
        original axis. 

        @see: self._determine_axis_and_strandA_endAtoms_at_end_1()
        @see: self.make()
        """

        #the vector between the two end points. these are more likely
        #points clicked by the user while creating dna duplex using endpoints
        #of a line. In genral, end1 and end2 are obtained from self.make()
        b = norm(end2 - end1)        

        axis_strand_vector = (self.strandA_atom_end1.posn() - \
                              self.axis_atom_end1.posn())

        vectorAlongLadderStep =  cross(-self.assy.o.lineOfSight, b)
        unitVectorAlongLadderStep = norm(vectorAlongLadderStep)

        self.final_pos_strand_end_atom = \
            self.axis_atom_end1.posn() + \
            vlen(axis_strand_vector)*unitVectorAlongLadderStep

        expected_vec = self.final_pos_strand_end_atom - self.axis_atom_end1.posn()

        q_new = Q(axis_strand_vector, expected_vec)

        if dot(axis_strand_vector, self.assy.o.lineOfSight) < 0:            
            q_new2 = Q(b, -q_new.angle)
        else:     
            q_new2 = Q(b, q_new.angle) 


        self.assy.rotateSpecifiedMovables(q_new2, baseList, end1)
예제 #18
0
    def on_drag(self):
        # Note: we can assume this is a "real drag", since the caller (ultimately a selectMode method in testmode, as of 070209)
        # is tracking mouse motion and not calling this until it becomes large enough, as the debug070209 prints show.
        oldpoint = self.oldpoint # was saved by prior on_drag or by on_press
        point = self.current_event_mousepoint(plane = self.startpoint)
        if debug070209:
            self.ndrags += 1
##            if (self.ndrags == 1) or 1:
##                print "drag event %d, model distance = %r, pixel dist not computed" % (self.ndrags, vlen(oldpoint - point),)
        if self._this_drag == 'free x-y rotate':
            # rotate using New motion UI
            #  [probably works for this specific kind of rotation, one of 4 that UI proposes;
            #   doesn't yet have fancy cursors or during-rotation graphics; add those only after it's a DragCommand]
            # two implem choices:
            # 1. know the eye direction and the screen dims in plane of startpoint, in model coords; compute in model coords
            # 2. get the mouse positions (startpoint and point) and screen dims in window x,y coords, compute rotation in eye coords,
            #   but remember to reorient it to correspond with model if model coords are rotated already.
            # Not sure which one is better.
            #   In general, placing user into model coords (or more precisely, into object local coords) seems more general --
            # for example, what if there were several interacting users, each visible to the others?
            # We'd want each user's eye & screen to be visible! (Maybe even an image of their face & screen, properly scaled and aligned?)
            # And we'd want their posns to be used in the computations here, all in model coords.
            # (Even if zoom had occurred, which means, even the user's *size* is quite variable!)
            #   I need "user in model coords" for other reasons too, so ok, I'll do it that way.
            #
            # [Hey, I might as well fix the bug in current_event_mousepoint which fakes the center of view, at the same time.
            # (I can't remember its details right now, but I think it assumed the local origin was the cov, which is obviously wrong.)
            # (But I didn't look at that code or fix that bug now.)]
            vec = point - self.startpoint
            uvec = norm(vec) #k needed??
            axisvec = cross(self._dz, uvec) # unit length (suitable for glRotate -- but we need to use it to make a quat!)
            axisvec = norm(axisvec) # just to be sure (or to reduce numerical errors)
            scale = self._scale
            draw_axisvec = axisvec * scale #e times some other length constant too?
            center = self._objcenter
            self.axisends = (center - axisvec, center + axisvec) # draw a rotation axis here ###e
            self.degrees = degrees = vlen(vec) / scale * 180.0 # draw a textual indicator with degrees (and axisvec angle too) ###e
                ###e or print that info into sbar? or somewhere fixed in glpane? or in glpane near mouse?
            # now set self.rotation to a quat made from axisvec and degrees
            theta = degrees / 360.0 * 2 * pi
            # print "axisvec %r, degrees %r, theta %r" % (axisvec ,degrees,theta)
            rot = Q(axisvec, theta)
            self.rotation = self.startrot + rot # note use of self.startrot rather than self.rotation on rhs
                # avoid += to make sure it gets changed-tracked -- and since it would be the wrong op!

        elif self._this_drag == 'free x-y translate':
            self._cmd_drag_from_to( oldpoint, point) # use Draggable interface cmd on self
        else:
            assert 0
        self.oldpoint = point
        return
예제 #19
0
    def placePlaneOffsetToAnother(self):
        """
        Orient the plane such that it is parallel to a selected plane , with an
        offset.
        """

        cmd = self.editCommand.cmd

        jigList = self.win.assy.getSelectedJigs()
        if jigList:
            planeList = []
            for j in jigList:
                if isinstance(j, Plane) and (j is not self):
                    planeList.append(j)

            #First, clear all the direction arrow drawings if any in
            #the existing Plane objectes in the part
            if not self.assy.part.topnode.members:
                msg = redmsg("Select a different plane first to place the"
                             " current plane offset to it")
                env.history.message(cmd + msg)
                return

            for p in self.assy.part.topnode.members:
                if isinstance(p, Plane):
                    if p.directionArrow:
                        p.directionArrow.setDrawRequested(False)

            if len(planeList) == 1:
                self.offsetParentGeometry = planeList[0]
                self.offsetParentGeometry.directionArrow.setDrawRequested(True)

                if self.offsetParentGeometry.directionArrow.flipDirection:
                    offset = 2 * norm(self.offsetParentGeometry.getaxis())
                else:
                    offset = -2 * norm(self.offsetParentGeometry.getaxis())

                self.center = self.offsetParentGeometry.center + offset
                self.quat = Q(self.offsetParentGeometry.quat)
            else:
                msg = redmsg("Select exactly one plane to\
                             create a plane offset to it.")
                env.history.message(cmd + msg)
                return
        else:
            msg = redmsg("Select an existing plane first to\
                         create a plane offset to it.")
            env.history.message(cmd + msg)
            return
        self.glpane.gl_update()
예제 #20
0
    def placePlaneOffsetToAnother(self):
        """
        Orient the plane such that it is parallel to a selected plane , with an
        offset.
        """

        cmd = self.editCommand.cmd 

        jigList = self.win.assy.getSelectedJigs()
        if jigList:
            planeList = []
            for j in jigList:
                if isinstance(j, Plane) and (j is not self):
                    planeList.append(j)  

            #First, clear all the direction arrow drawings if any in 
            #the existing Plane objectes in the part 
            if not self.assy.part.topnode.members:
                msg = redmsg("Select a different plane first to place the"
                             " current plane offset to it")
                env.history.message(cmd + msg)
                return                            

            for p in self.assy.part.topnode.members:
                if isinstance(p, Plane):
                    if p.directionArrow:
                        p.directionArrow.setDrawRequested(False)

            if len(planeList) == 1:                
                self.offsetParentGeometry = planeList[0]
                self.offsetParentGeometry.directionArrow.setDrawRequested(True)

                if self.offsetParentGeometry.directionArrow.flipDirection:
                    offset =  2 * norm(self.offsetParentGeometry.getaxis())
                else:
                    offset = -2 * norm(self.offsetParentGeometry.getaxis())

                self.center = self.offsetParentGeometry.center + offset   
                self.quat   = Q(self.offsetParentGeometry.quat)
            else:
                msg = redmsg("Select exactly one plane to\
                             create a plane offset to it.")
                env.history.message(cmd + msg)
                return            
        else:            
            msg = redmsg("Select an existing plane first to\
                         create a plane offset to it.")
            env.history.message(cmd + msg)
            return    
        self.glpane.gl_update()            
예제 #21
0
    def updateHandlePositions(self):
        """
        Update handle positions and also update the resize handle radii and
        their 'stopper' lengths.
        @see: self._update_resizeHandle_radius()
        @see: self._update_resizeHandle_stopper_length()
        @see: EditNanotube_GraphicsMode._drawHandles()
        """
        self.handlePoint1 = None  # Needed!
        self.handlePoint2 = None

        #TODO: Call this method less often by implementing model_changed
        #see bug 2729 for a planned optimization
        self.cylinderWidth = CYLINDER_WIDTH_DEFAULT_VALUE
        self.cylinderWidth2 = CYLINDER_WIDTH_DEFAULT_VALUE

        self._update_resizeHandle_radius()

        handlePoint1, handlePoint2 = self.struct.nanotube.getEndPoints()

        if 0:  # Debug prints
            print "updateHandlePositions(): handlePoint1=", handlePoint1
            print "updateHandlePositions(): handlePoint2=", handlePoint2

        if handlePoint1 is not None and handlePoint2 is not None:
            # (that condition is bugfix for deleted axis segment, bruce 080213)

            self.handlePoint1, self.handlePoint2 = handlePoint1, handlePoint2

            #Update the 'stopper'  length where the resize handle being dragged
            #should stop. See self._update_resizeHandle_stopper_length()
            #for more details
            self._update_resizeHandle_stopper_length()

            if DEBUG_ROTATION_HANDLES:
                self.rotation_distance1 = CYLINDER_WIDTH_DEFAULT_VALUE
                self.rotation_distance2 = CYLINDER_WIDTH_DEFAULT_VALUE
                #Following computes the base points for rotation handles.
                #to be revised -- Ninad 2008-02-13
                unitVectorAlongAxis = norm(self.handlePoint1 -
                                           self.handlePoint2)

                v = cross(self.glpane.lineOfSight, unitVectorAlongAxis)

                self.rotationHandleBasePoint1 = self.handlePoint1 + norm(
                    v) * 4.0
                self.rotationHandleBasePoint2 = self.handlePoint2 + norm(
                    v) * 4.0
        return
예제 #22
0
    def _orient_for_modify(self, end1, end2):
        """
        Do the final orientation of the newly generated dna, around its own
        axis so that the axis and strand atoms on this new dna are
        fusable with the resize end on the original dna.
        @param end1: end1 of new dna (segment) generated
        @param end2: end2 of new dna (segment) generated
        """

        b = norm(end2 - end1)
        new_ladder = self.axis_atom_end1.molecule.ladder

        chunkListForRotation = new_ladder.all_chunks()

        endBaseAtomList = new_ladder.get_endBaseAtoms_containing_atom(
            self.axis_atom_end1)

        endStrandbaseAtoms = (endBaseAtomList[0], endBaseAtomList[2])

        self.strandA_atom_end1 = None
        #TODO: REVIEW / REFACTOR THIS and DOC
        for atm in endStrandbaseAtoms:
            if atm is not None:
                self.strandA_atom_end1 = atm
                self._set_bond_direction_on_new_strand(atm)


        axis_strand_vector = (self.strandA_atom_end1.posn() - \
                              self.axis_atom_end1.posn())


        vectorAlongLadderStep =  self._resizeEndStrand1Atom.posn() - \
                              self._resizeEndAxisAtom.posn()

        unitVectorAlongLadderStep = norm(vectorAlongLadderStep)

        self.final_pos_strand_end_atom = \
            self.axis_atom_end1.posn() + \
            vlen(axis_strand_vector)*unitVectorAlongLadderStep

        q_new = Q(axis_strand_vector, vectorAlongLadderStep)

        if dot(axis_strand_vector, cross(vectorAlongLadderStep, b)) < 0:
            q_new2 = Q(b, -q_new.angle)
        else:
            q_new2 = Q(b, q_new.angle)

        self.assy.rotateSpecifiedMovables(q_new2, chunkListForRotation, end1)
예제 #23
0
def drawRotateSign(color, pos1, pos2, radius, rotation=0.0):
    """Rotate sign on top of the caps of the cylinder """
    glPushMatrix()
    glColor3fv(color)
    vec = pos2 - pos1
    axis = norm(vec)
    glTranslatef(pos1[0], pos1[1], pos1[2])

    ##Huaicai 1/17/05: To avoid rotate around (0, 0, 0), which causes
    ## display problem on some platforms
    angle = -acos(axis[2]) * 180.0 / pi
    if (axis[2] * axis[2] >= 1.0):
        glRotate(angle, 0.0, 1.0, 0.0)
    else:
        glRotate(angle, axis[1], -axis[0], 0.0)
    glRotate(rotation, 0.0, 0.0, 1.0)  #bruce 050518
    glScale(radius, radius, Numeric.dot(vec, vec)**.5)

    glLineWidth(2.0)
    glDisable(GL_LIGHTING)
    glCallList(drawing_globals.rotSignList)
    glEnable(GL_LIGHTING)
    glLineWidth(1.0)

    glPopMatrix()
    return
예제 #24
0
def add_basepair_handles_to_atoms(atoms):  #bruce 080515
    """
    """
    goodcount, badcount = 0, 0
    for atom in atoms:
        atom.unpick()
        if atom.element is Gv5 and len(atom.strand_neighbors()) == 2:

            goodcount += 1

            # Figure out the position from the Gv5 and its presumed-to-be Ss5
            # neighbors. [Fixed per Eric D spec, bruce 080516]
            sn = atom.strand_neighbors()
            ss_midpoint = average_value([a.posn() for a in sn])
            towards_Gv = norm(atom.posn() - ss_midpoint)
            newpos = ss_midpoint + \
                     BASEPAIR_HANDLE_DISTANCE_FROM_SS_MIDPOINT * towards_Gv
            ##            if 0: # stub for computing new position
            ##                oldposns = [a.posn() for a in ([atom] + sn)]
            ##                newpos = average_value(oldposns)

            Atom = atom.molecule.assy.Atom  # (avoid model.chem import cycle)

            newatom = Atom('Ah5', newpos, atom.molecule)  # PAM5-Axis-handle
            bond_atoms_faster(newatom, atom, V_SINGLE)
            # note: no bondpoints need creation or removal

            newatom.pick()
            pass
        else:
            badcount += 1
        continue

    return goodcount, badcount
예제 #25
0
    def viewParallelTo(self):
        """
        Set view parallel to the vector defined by 2 selected atoms.
        """
        cmd = greenmsg("Set View Parallel To: ")

        atoms = self.assy.selatoms_list()

        if len(atoms) != 2:
            msg = redmsg("You must select 2 atoms.")
            env.history.message(cmd + msg)
            return

        v = norm(atoms[0].posn()-atoms[1].posn())

        if vlen(v) < 0.0001: # Atoms are on top of each other.
            info = 'The selected atoms are on top of each other.  No change in view.'
            env.history.message(cmd + info)
            return

        # If vec is pointing into the screen, negate (reverse) vec.
        if dot(v, self.glpane.lineOfSight) > 0:
            v = -v

        # Compute the destination quat (q2).
        q2 = Q(V(0,0,1), v)
        q2 = q2.conj()

        self.glpane.rotateView(q2)

        info = 'View set parallel to the vector defined by the 2 selected atoms.'
        env.history.message(cmd + info)
예제 #26
0
def drawRotateSign(color, pos1, pos2, radius, rotation = 0.0):
    """Rotate sign on top of the caps of the cylinder """
    glPushMatrix()
    glColor3fv(color)
    vec = pos2-pos1
    axis = norm(vec)
    glTranslatef(pos1[0], pos1[1], pos1[2])

    ##Huaicai 1/17/05: To avoid rotate around (0, 0, 0), which causes 
    ## display problem on some platforms
    angle = -acos(axis[2])*180.0/pi
    if (axis[2]*axis[2] >= 1.0):
        glRotate(angle, 0.0, 1.0, 0.0)
    else:
        glRotate(angle, axis[1], -axis[0], 0.0)
    glRotate(rotation, 0.0, 0.0, 1.0) #bruce 050518
    glScale(radius,radius,Numeric.dot(vec,vec)**.5)

    glLineWidth(2.0)
    glDisable(GL_LIGHTING)
    glCallList(drawing_globals.rotSignList)
    glEnable(GL_LIGHTING)
    glLineWidth(1.0)

    glPopMatrix()
    return
예제 #27
0
def arbitrary_perpendicular( vec, nicevecs = [] ): #bruce 060608, probably duplicates other code somewhere (check in VQT?)
    """
    Return an arbitrary unit vector perpendicular to vec (a Numeric array, 3 floats),
    making it from vec and as-early-as-possible nicevecs (if any are passed).
    """
    nicevecs = map(norm, nicevecs) + [X_AXIS, Y_AXIS]
    # we'll look at vec and each nicevec until they span a subspace which includes a perpendicular.
    # if we're not done, it means our subspace so far is spanned by just vec itself.
    vec = norm(vec)
    if not vec:
        return nicevecs[0] # the best we can do
    for nice in nicevecs:
        res = norm( nice - vec * dot(nice, vec) )
        if res:
            return res
    return nicevecs[0] # should never happen
예제 #28
0
 def getAxisVector(self, atomAtVectorOrigin = None):
     """
     Returns the unit axis vector of the segment (vector between two axis 
     end points)
     """
     # REVIEW: use common code for this method? [bruce 081217 comment]
     endPoint1, endPoint2 = self.Peptide.getEndPoints()
             
     if endPoint1 is None or endPoint2 is None:
         return V(0, 0, 0)
     
     #@see: RotateAboutAPoint command. The following code is disabled 
     #as it has bugs (not debugged but could be in 
     #self.Peptide.getEndPoints). So, rotate about a point won't work for 
     #rotating a Peptide. -- Ninad 2008-05-13
   
     ##if atomAtVectorOrigin is not None:
         ###If atomAtVectorOrigin is specified, we will return a vector that
         ###starts at this atom and ends at endPoint1 or endPoint2 . 
         ###Which endPoint to choose will be dicided by the distance between
         ###atomAtVectorOrigin and the respective endPoints. (will choose the 
         ###frthest endPoint
         ##origin = atomAtVectorOrigin.posn()
         ##if vlen(endPoint2 - origin ) > vlen(endPoint1 - origin):
             ##return norm(endPoint2 - endPoint1)
         ##else:
             ##return norm(endPoint1 - endPoint2)
     
     return norm(endPoint2 - endPoint1)
예제 #29
0
    def _neighborSegment_ok_for_crossover_search(self, neighborSegment,
                                                 reference_segment_end1,
                                                 reference_segment_axisVector):

        ok_for_search = False
        orthogonal_vector = None

        neighbor_end1, neighbor_end2 = neighborSegment.getAxisEndPoints()
        #Use end1 of neighbor segment and find out the perpendicular
        #distance (and the vector) between this atom and the
        #axis vector of dnaSegment (whose neighbors are being
        #searched). If this distance is less than a specified amount
        #then 'neighbor' is an approved neighbor of 'dnaSegment'
        if neighbor_end1 is not None:
            p1 = reference_segment_end1
            v1 = reference_segment_axisVector
            p2 = neighbor_end1
            dist, orthogonal_dist = orthodist(p1, v1, p2)

            #Check if the orthogonal distance is withing the
            #specified limit.
            if orthogonal_dist <= MAX_PERPENDICULAR_DISTANCE_BET_SEGMENT_AXES:
                ok_for_search = True
                vec = p1 + dist * v1 - p2
                orthogonal_vector = orthogonal_dist * norm(vec)

                if self.graphicsMode.DEBUG_DRAW_PLANE_NORMALS:
                    self._DEBUG_plane_normals_ends_for_drawing.append(
                        (p2, (p2 + orthogonal_vector)))

        return ok_for_search, orthogonal_vector
예제 #30
0
def arb_perp_unit_vector(vec):
    """
    Given a nonzero vector, return an arbitrary unit vector
    perpendicular to it.
    """
    vec2 = arb_non_parallel_vector(vec)
    return norm(cross(vec, vec2))
예제 #31
0
    def bareMotion(self, event):
        """
        Event handler for simple drag event. (i.e. the free drag without holding
        down any mouse button)
        @see: self.isSpecifyPlaneToolActive()
        @see: self.getDrawingPlane()
        @see: DnaDuplex_Graphicsmode.isSpecifyPlaneToolActive()
        """
        if not self.isSpecifyPlaneToolActive():
            if len(self.command.mouseClickPoints) > 0:
                plane = self.getDrawingPlane()
                if plane:
                    self.endPoint2 = self.dragto(self.endPoint1,
                                                 event,
                                                 perp=norm(plane.getaxis()))
                else:
                    self.endPoint2 = self.dragto(self.endPoint1, event)

                self.endPoint2 = self.snapLineEndPoint()
                self.update_cursor_for_no_MB()
                self.glpane.gl_update()

        value = _superclass_for_GM.bareMotion(self, event)

        #Needed to make sure that the cursor is updated properly when
        #the mouse is moved after the 'specify reference plane tool is
        #activated/deactivated
        self.update_cursor()

        return value  # russ 080527
예제 #32
0
def arb_perp_unit_vector(vec):
    """
    Given a nonzero vector, return an arbitrary unit vector
    perpendicular to it.
    """
    vec2 = arb_non_parallel_vector(vec)
    return norm(cross(vec, vec2))
    def _leftDown_preparation_for_dragging(self, objectUnderMouse, event):
        """
        Handle left down event. Preparation for rotation and/or selection
        This method is called inside of self.leftDown.
        @param event: The mouse left down event.
        @type  event: QMouseEvent instance
        @see: self.leftDown
        @see: self.leftDragRotation
        Overrides _superclass._leftDown_preparation_for_dragging
        """

        _superclass._leftDown_preparation_for_dragging(self, objectUnderMouse, event)
        self.o.SaveMouse(event)
        self.picking = True
        self.dragdist = 0.0
        # delta for constrained rotations.
        self.rotDelta = 0

        if self.rotateOption == "ROTATEDEFAULT":
            self.o.trackball.start(self.o.MousePos[0], self.o.MousePos[1])
        else:
            if self.rotateOption == "ROTATEX":
                ma = V(1, 0, 0)  # X Axis
                self.axis = "X"
            elif self.rotateOption == "ROTATEY":
                ma = V(0, 1, 0)  # Y Axis
                self.axis = "Y"
            elif self.rotateOption == "ROTATEZ":
                ma = V(0, 0, 1)  # Z Axis
                self.axis = "Z"
            elif self.rotateOption == "ROT_TRANS_ALONG_AXIS":
                # The method 'self._leftDown_preparation_for_dragging should
                # never be reached if self.rotateOption is 'ROT_TRANS_ALONG_AXIS'
                # If this code is reached, it indicates a bug. So fail gracefully
                # by calling self.leftADown()
                if debug_flags.atom_debug:
                    print_compact_stack(
                        "bug: _leftDown_preparation_for_dragging" " called for rotate option" "'ROT_TRANS_ALONG_AXIS'"
                    )

                self.leftADown(objectUnderMouse, event)
                return

            else:
                print "Move_Command: Error - unknown rotateOption value =", self.rotateOption
                return

            ma = norm(V(dot(ma, self.o.right), dot(ma, self.o.up)))
            # When in the front view, right = 1,0,0 and up = 0,1,0, so ma will
            # be computed as 0,0.This creates a special case problem when the
            # user wants to constrain rotation around the Z axis because Zmat
            # will be zero.  So we have to test for this case (ma = 0,0) and
            # fix ma to -1,0.  This was needed to fix bug 537.  Mark 050420
            if ma[0] == 0.0 and ma[1] == 0.0:
                ma = [-1.0, 0.0]
            self.Zmat = A([ma, [-ma[1], ma[0]]])

        self.leftDownType = "ROTATE"

        return
    def _neighborSegment_ok_for_crossover_search(
        self,
        neighborSegment,
        reference_segment_end1,
        reference_segment_axisVector):

        ok_for_search = False
        orthogonal_vector = None

        neighbor_end1 , neighbor_end2 = neighborSegment.getAxisEndPoints()
        #Use end1 of neighbor segment and find out the perpendicular
        #distance (and the vector) between this atom and the
        #axis vector of dnaSegment (whose neighbors are being
        #searched). If this distance is less than a specified amount
        #then 'neighbor' is an approved neighbor of 'dnaSegment'
        if neighbor_end1 is not None:
            p1 = reference_segment_end1
            v1 = reference_segment_axisVector
            p2 = neighbor_end1
            dist, orthogonal_dist = orthodist(p1, v1, p2)

            #Check if the orthogonal distance is withing the
            #specified limit.
            if orthogonal_dist <= MAX_PERPENDICULAR_DISTANCE_BET_SEGMENT_AXES:
                ok_for_search = True
                vec = p1 + dist*v1 - p2
                orthogonal_vector = orthogonal_dist*norm(vec)

                if self.graphicsMode.DEBUG_DRAW_PLANE_NORMALS:
                        self._DEBUG_plane_normals_ends_for_drawing.append(
                            (p2, (p2 + orthogonal_vector)) )

        return ok_for_search, orthogonal_vector
예제 #35
0
    def bareMotion(self, event):
        """
        Event handler for simple drag event. (i.e. the free drag without holding
        down any mouse button)
        @see: self.isSpecifyPlaneToolActive()
        @see: self.getDrawingPlane()
        @see: DnaDuplex_Graphicsmode.isSpecifyPlaneToolActive()
        """       
        if not self.isSpecifyPlaneToolActive():
            if len(self.command.mouseClickPoints) > 0:
                plane = self.getDrawingPlane()
                if plane:
                    self.endPoint2 = self.dragto( self.endPoint1, 
                                                  event, 
                                                  perp = norm(plane.getaxis()))
                else:
                    self.endPoint2 = self.dragto( self.endPoint1, event)
    
                self.endPoint2 = self.snapLineEndPoint()  
                self.update_cursor_for_no_MB()
                self.glpane.gl_update()    

        value = _superclass_for_GM.bareMotion(self,event)
        
        #Needed to make sure that the cursor is updated properly when 
        #the mouse is moved after the 'specify reference plane tool is 
        #activated/deactivated
        self.update_cursor()  
        
        return value # russ 080527   
예제 #36
0
 def findHandles_exact(self, p1, p2, cutoff = 0.0, backs_ok = 1, offset = V(0,0,0)):
     """
     return a list of (dist, handle) pairs, in arbitrary order,
     which includes, for each handle (spherical surface) hit by the ray from p1 thru p2,
     its front-surface intersection with the ray,
     unless that has dist < cutoff and backs_ok,
     in which case include its back-surface intersection
     (unless *that* has dist < cutoff).
     """
     #e For now, just be simple, don't worry about speed.
     # Someday we can preprocess self.handlpos using Numeric functions,
     # like in nearSinglets and/or findSinglets
     # (I have untested prototype code for this in extrude-outs.py).
     hh = self.handles
     res = []
     v = norm(p2-p1)
     ## is this modifying the vector in-place, causing a bug?? offset += self.origin # treat our handles' pos as relative to this
     ## I don't know, but one of the three instances of += was doing this!!! probably i was resetting the atom or mol pos....
     offset = offset + self.origin # treat our handles' pos as relative to this
     radius_multiplier = self.radius_multiplier
     for (pos,radius,info) in hh:
         ## bug in this? pos += offset
         pos = pos + offset
         radius *= radius_multiplier
         dist, wid = orthodist(p1, v, pos)
         if radius >= wid: # the ray hits the sphere
             delta = sqrt(radius*radius - wid*wid)
             front = dist - delta # depth from p1 of front surface of sphere, where it's hit
             if front >= cutoff:
                 res.append((front,(pos,radius,info)))
             elif backs_ok:
                 back = dist + delta
                 if back >= cutoff:
                     res.append((back,(pos,radius,info)))
     return res
예제 #37
0
    def update_numberOfBases(self):
        """
        Updates the numberOfBases in the PM while a resize handle is being 
        dragged. 
        @see: self.getCursorText() where it is called. 
        """
        #@Note: originally (before 2008-04-05, it was called in 
        #DnaStrand_ResizeHandle.on_drag() but that 'may' have some bugs 
        #(not verified) also see self.getCursorText() to know why it is
        #called there (basically a workaround for bug 2729
        if self.grabbedHandle is None:
            return

        currentPosition = self.grabbedHandle.currentPosition
        resize_end = self.grabbedHandle.origin

        new_duplexLength = vlen( currentPosition - resize_end )

        numberOfBasePairs_to_change = getNumberOfBasePairsFromDuplexLength('B-DNA', 
                                                                           new_duplexLength)

        original_numberOfBases = self.struct.getNumberOfBases()
        #If the dot product of handle direction and the direction in which it 
        #is dragged is negative, this means we need to subtract bases
        direction_of_drag = norm(currentPosition - resize_end)
        if dot(self.grabbedHandle.direction, direction_of_drag ) < 0:
            total_number_of_bases = original_numberOfBases - numberOfBasePairs_to_change
            self.propMgr.numberOfBasesSpinBox.setValue(total_number_of_bases)
        else:
            total_number_of_bases = original_numberOfBases + numberOfBasePairs_to_change
            self.propMgr.numberOfBasesSpinBox.setValue(total_number_of_bases - 1)
예제 #38
0
def add_basepair_handles_to_atoms(atoms): #bruce 080515
    """
    """
    goodcount, badcount = 0, 0
    for atom in atoms:
        atom.unpick()
        if atom.element is Gv5 and len(atom.strand_neighbors()) == 2:

            goodcount += 1

            # Figure out the position from the Gv5 and its presumed-to-be Ss5
            # neighbors. [Fixed per Eric D spec, bruce 080516]
            sn = atom.strand_neighbors()
            ss_midpoint = average_value([a.posn() for a in sn])
            towards_Gv = norm(atom.posn() - ss_midpoint)
            newpos = ss_midpoint + \
                     BASEPAIR_HANDLE_DISTANCE_FROM_SS_MIDPOINT * towards_Gv
##            if 0: # stub for computing new position
##                oldposns = [a.posn() for a in ([atom] + sn)]
##                newpos = average_value(oldposns)

            Atom = atom.molecule.assy.Atom # (avoid model.chem import cycle)

            newatom = Atom( 'Ah5', newpos, atom.molecule ) # PAM5-Axis-handle
            bond_atoms_faster( newatom, atom, V_SINGLE)
                # note: no bondpoints need creation or removal

            newatom.pick()
            pass
        else:
            badcount += 1
        continue

    return goodcount, badcount
예제 #39
0
    def getAxisVector(self, atomAtVectorOrigin=None):
        """
        Returns the unit axis vector of the segment (vector between two axis 
        end points)
        """
        # REVIEW: use common code for this method? [bruce 081217 comment]
        endPoint1, endPoint2 = self.nanotube.getEndPoints()

        if endPoint1 is None or endPoint2 is None:
            return V(0, 0, 0)

        #@see: RotateAboutAPoint command. The following code is disabled
        #as it has bugs (not debugged but could be in
        #self.nanotube.getEndPoints). So, rotate about a point won't work for
        #rotating a nanotube. -- Ninad 2008-05-13

        ##if atomAtVectorOrigin is not None:
        ###If atomAtVectorOrigin is specified, we will return a vector that
        ###starts at this atom and ends at endPoint1 or endPoint2 .
        ###Which endPoint to choose will be dicided by the distance between
        ###atomAtVectorOrigin and the respective endPoints. (will choose the
        ###frthest endPoint
        ##origin = atomAtVectorOrigin.posn()
        ##if vlen(endPoint2 - origin ) > vlen(endPoint1 - origin):
        ##return norm(endPoint2 - endPoint1)
        ##else:
        ##return norm(endPoint1 - endPoint2)

        return norm(endPoint2 - endPoint1)
    def _orient_for_modify(self, end1, end2):
        """
        Do the final orientation of the newly generated dna, around its own
        axis so that the axis and strand atoms on this new dna are
        fusable with the resize end on the original dna.
        @param end1: end1 of new dna (segment) generated
        @param end2: end2 of new dna (segment) generated
        """

        b = norm(end2 - end1)
        new_ladder =   self.axis_atom_end1.molecule.ladder

        chunkListForRotation = new_ladder.all_chunks()

        endBaseAtomList  = new_ladder.get_endBaseAtoms_containing_atom(self.axis_atom_end1)

        endStrandbaseAtoms = (endBaseAtomList[0], endBaseAtomList[2])

        self.strandA_atom_end1 = None
        #TODO: REVIEW / REFACTOR THIS and DOC
        for atm in endStrandbaseAtoms:
            if atm is not None:
                self.strandA_atom_end1 = atm
                self._set_bond_direction_on_new_strand(atm)


        axis_strand_vector = (self.strandA_atom_end1.posn() - \
                              self.axis_atom_end1.posn())


        vectorAlongLadderStep =  self._resizeEndStrand1Atom.posn() - \
                              self._resizeEndAxisAtom.posn()

        unitVectorAlongLadderStep = norm(vectorAlongLadderStep)

        self.final_pos_strand_end_atom = \
            self.axis_atom_end1.posn() + \
            vlen(axis_strand_vector)*unitVectorAlongLadderStep

        q_new = Q(axis_strand_vector, vectorAlongLadderStep)

        if dot(axis_strand_vector, cross(vectorAlongLadderStep, b)) < 0:
            q_new2 = Q(b, -q_new.angle)
        else:
            q_new2 = Q(b, q_new.angle)

        self.assy.rotateSpecifiedMovables(q_new2, chunkListForRotation, end1)
예제 #41
0
    def _snapEndPointHorizontalOrVertical(self):
        """
        Snap the second endpoint of the line (and thus the whole line) to the
        screen horizontal or vertical vectors. 
        @return: The new endPoint2 i.e. the moving endpoint of the rubberband 
                 line . This value may be same as previous or snapped so that 
                 line is horizontal or vertical depending upon the angle it 
                 makes with the horizontal and vertical. 
        @rtype: B{A}
        """
        up = self.glpane.up
        down = self.glpane.down
        left = self.glpane.left
        right = self.glpane.right  

        endPoint2 = self.endPoint2

        snapVector = V(0, 0, 0)

        currentLineVector = norm(self.endPoint2 - self.endPoint1)

        theta_horizontal = angleBetween(right, currentLineVector) 
        theta_vertical = angleBetween(up, currentLineVector) 

        theta_horizontal_old = theta_horizontal
        theta_vertical_old = theta_vertical

        if theta_horizontal != 90.0:            
            theta_horizontal = min(theta_horizontal, 
                                   (180.0 - theta_horizontal))

        if theta_vertical != 90.0:            
            theta_vertical = min(theta_vertical, 
                                 180.0 - theta_vertical)

        theta = min(theta_horizontal, theta_vertical)

        if theta <= 2.0 and theta != 0.0:
            self._snapOn = True
            if theta == theta_horizontal:
                self._snapType = 'HORIZONTAL'
                if theta_horizontal == theta_horizontal_old:
                    snapVector = right                   
                else:
                    snapVector = left
            elif theta == theta_vertical:
                self._snapType = 'VERTICAL'
                if theta_vertical == theta_vertical_old:
                    snapVector = up
                else:
                    snapVector = down

            endPoint2 = self.endPoint1 + \
                      vlen(self.endPoint1 - self.endPoint2)*snapVector

        else:                
            self._snapOn = False

        return endPoint2
예제 #42
0
    def _snapEndPointHorizontalOrVertical(self):
        """
        Snap the second endpoint of the line (and thus the whole line) to the
        screen horizontal or vertical vectors. 
        @return: The new endPoint2 i.e. the moving endpoint of the rubberband 
                 line . This value may be same as previous or snapped so that 
                 line is horizontal or vertical depending upon the angle it 
                 makes with the horizontal and vertical. 
        @rtype: B{A}
        """
        up = self.glpane.up
        down = self.glpane.down
        left = self.glpane.left
        right = self.glpane.right  

        endPoint2 = self.endPoint2

        snapVector = V(0, 0, 0)

        currentLineVector = norm(self.endPoint2 - self.endPoint1)

        theta_horizontal = angleBetween(right, currentLineVector) 
        theta_vertical = angleBetween(up, currentLineVector) 

        theta_horizontal_old = theta_horizontal
        theta_vertical_old = theta_vertical

        if theta_horizontal != 90.0:            
            theta_horizontal = min(theta_horizontal, 
                                   (180.0 - theta_horizontal))

        if theta_vertical != 90.0:            
            theta_vertical = min(theta_vertical, 
                                 180.0 - theta_vertical)

        theta = min(theta_horizontal, theta_vertical)

        if theta <= 2.0 and theta != 0.0:
            self._snapOn = True
            if theta == theta_horizontal:
                self._snapType = 'HORIZONTAL'
                if theta_horizontal == theta_horizontal_old:
                    snapVector = right                   
                else:
                    snapVector = left
            elif theta == theta_vertical:
                self._snapType = 'VERTICAL'
                if theta_vertical == theta_vertical_old:
                    snapVector = up
                else:
                    snapVector = down

            endPoint2 = self.endPoint1 + \
                      vlen(self.endPoint1 - self.endPoint2)*snapVector

        else:                
            self._snapOn = False

        return endPoint2
예제 #43
0
    def getDnaRibbonParams(self):
        """
        Returns parameters for drawing the dna ribbon. 

        If the dna rubberband line should NOT be drawn (example when you are 
        removing bases from the strand or  if its unable to get dnaSegment) , 
        it retuns None. So the caller should check if the method return value
        is not None. 
        @see: DnaStrand_GraphicsMode._draw_handles()
        """

        if self.grabbedHandle is None:
            return None

        if self.grabbedHandle.origin is None:
            return None

        direction_of_drag = norm(self.grabbedHandle.currentPosition - \
                                 self.grabbedHandle.origin)

        #If the strand bases are being removed (determined by checking the 
        #direction of drag) , no need to draw the rubberband line. 
        if dot(self.grabbedHandle.direction, direction_of_drag) < 0:
            return None

        strandEndAtom, axisEndAtom = self.get_strand_and_axis_endAtoms_at_resize_end()

        #DnaStrand.get_DnaSegment_with_content_atom saely handles the case where
        #strandEndAtom is None. 
        dnaSegment = self.struct.get_DnaSegment_with_content_atom(strandEndAtom)

        ribbon1_direction = None

        if dnaSegment:     
            basesPerTurn = dnaSegment.getBasesPerTurn()
            duplexRise = dnaSegment.getDuplexRise()        
            ribbon1_start_point = strandEndAtom.posn()

            if strandEndAtom:
                ribbon1_start_point = strandEndAtom.posn()
                for bond_direction, neighbor in strandEndAtom.bond_directions_to_neighbors():
                    if neighbor and neighbor.is_singlet():
                        ribbon1_direction = bond_direction
                        break

            ribbon1Color = strandEndAtom.molecule.color
            if not ribbon1Color:
                ribbon1Color = strandEndAtom.element.color

            return (self.grabbedHandle.origin,
                    self.grabbedHandle.currentPosition,
                    basesPerTurn, 
                    duplexRise, 
                    ribbon1_start_point,
                    ribbon1_direction,
                    ribbon1Color
                )

        return None
예제 #44
0
 def __init__(self, point0, z, uhint, uhint2):
     # u and v and zn are unit vectors
     # z is NOT a unit vector
     self.p0 = point0
     self.p1 = point1 = point0 + z
     self.z = z
     zlen = vlen(z)
     if zlen < 1.0e-6:
         raise ZeroLengthCylinder()
     self.zinv = 1.0 / zlen
     self.zn = zn = norm(z)
     u = norm(uhint - (dot(uhint, z) / zlen**2) * z)
     if vlen(u) < 1.0e-4:
         u = norm(uhint2 - (dot(uhint2, z) / zlen**2) * z)
     v = cross(zn, u)
     self.u = u
     self.v = v
예제 #45
0
 def __init__(self, point0, z, uhint, uhint2):
     # u and v and zn are unit vectors
     # z is NOT a unit vector
     self.p0 = point0
     self.p1 = point1 = point0 + z
     self.z = z
     zlen = vlen(z)
     if zlen < 1.0e-6:
         raise ZeroLengthCylinder()
     self.zinv = 1.0 / zlen
     self.zn = zn = norm(z)
     u = norm(uhint - (dot(uhint, z) / zlen**2) * z)
     if vlen(u) < 1.0e-4:
         u = norm(uhint2 - (dot(uhint2, z) / zlen**2) * z)
     v = cross(zn, u)
     self.u = u
     self.v = v
예제 #46
0
def arb_ortho_pair(vec): #k not presently used # see also some related code in pi_vectors()
    """
    Given a nonzero vector, return an arbitrary pair of unit vectors
    perpendicular to it and to each other.
    """
    res1 = arb_perp_unit_vector(vec)
    res2 = norm(cross(vec, res1))
    return res1, res2
예제 #47
0
def arb_ortho_pair(vec): #k not presently used # see also some related code in pi_vectors()
    """
    Given a nonzero vector, return an arbitrary pair of unit vectors
    perpendicular to it and to each other.
    """
    res1 = arb_perp_unit_vector(vec)
    res2 = norm(cross(vec, res1))
    return res1, res2
    def updateHandlePositions(self):
        """
        Update handle positions and also update the resize handle radii and
        their 'stopper' lengths. 
        @see: self._update_resizeHandle_radius()
        @see: self._update_resizeHandle_stopper_length()
        @see: NanotubeSegment_GraphicsMode._drawHandles()
        """
        self.handlePoint1 = None # Needed!
        self.handlePoint2 = None

        #TODO: Call this method less often by implementing model_changed
        #see bug 2729 for a planned optimization
        self.cylinderWidth = CYLINDER_WIDTH_DEFAULT_VALUE
        self.cylinderWidth2 = CYLINDER_WIDTH_DEFAULT_VALUE      

        self._update_resizeHandle_radius()

        handlePoint1, handlePoint2 = self.struct.nanotube.getEndPoints()

        if 0: # Debug prints
            print "updateHandlePositions(): handlePoint1=", handlePoint1
            print "updateHandlePositions(): handlePoint2=", handlePoint2

        if handlePoint1 is not None and handlePoint2 is not None:
            # (that condition is bugfix for deleted axis segment, bruce 080213)

            self.handlePoint1, self.handlePoint2 = handlePoint1, handlePoint2            

            #Update the 'stopper'  length where the resize handle being dragged 
            #should stop. See self._update_resizeHandle_stopper_length()
            #for more details
            self._update_resizeHandle_stopper_length()            

            if DEBUG_ROTATION_HANDLES:
                self.rotation_distance1 = CYLINDER_WIDTH_DEFAULT_VALUE
                self.rotation_distance2 = CYLINDER_WIDTH_DEFAULT_VALUE
                #Following computes the base points for rotation handles. 
                #to be revised -- Ninad 2008-02-13
                unitVectorAlongAxis = norm(self.handlePoint1 - self.handlePoint2)

                v  = cross(self.glpane.lineOfSight, unitVectorAlongAxis)

                self.rotationHandleBasePoint1 = self.handlePoint1 + norm(v) * 4.0  
                self.rotationHandleBasePoint2 = self.handlePoint2 + norm(v) * 4.0
        return
예제 #49
0
    def computeEndPointsFromChunk(self, chunk, update = True):
        """
        Derives and returns the endpoints and radius of a Peptide chunk.
        @param chunk: a Peptide chunk
        @type  chunk: Chunk
        @return: endPoint1, endPoint2 and radius
        @rtype: Point, Point and float
        
        @note: computing the endpoints works fine when n=m or m=0. Otherwise,
               the endpoints can be slightly off the central axis, especially
               if the Peptide is short.
        @attention: endPoint1 and endPoint2 may not be the original endpoints,
                    and they may be flipped (opposites of) the original 
                    endpoints.
        """
        # Since chunk.axis is not always one of the vectors chunk.evecs 
        # (actually chunk.poly_evals_evecs_axis[2]), it's best to just use
        # the axis and center, then recompute a bounding cylinder.
        if not chunk.atoms:
            return None
        
        axis = chunk.axis
        axis = norm(axis) # needed
        center = chunk._get_center()
        points = chunk.atpos - center # not sure if basepos points are already centered
        # compare following Numeric Python code to findAtomUnderMouse and its caller
        matrix = matrix_putting_axis_at_z(axis)
        v = dot( points, matrix)
        # compute xy distances-squared between axis line and atom centers
        r_xy_2 = v[:,0]**2 + v[:,1]**2

        # to get radius, take maximum -- not sure if max(r_xy_2) would use Numeric code, but this will for sure:
        i = argmax(r_xy_2)
        max_xy_2 = r_xy_2[i]
        radius = sqrt(max_xy_2)
        # to get limits along axis (since we won't assume center is centered between them), use min/max z:
        z = v[:,2]
        min_z = z[argmin(z)]
        max_z = z[argmax(z)]
        
        # Adjust the endpoints such that the ladder rungs (rings) will fall
        # on the ring segments. 
        # TO DO: Fix drawPeptideLadder() to offset the first ring, then I can
        # remove this adjustment. --Mark 2008-04-12
        z_adjust = self.getEndPointZOffset()
        min_z += z_adjust
        max_z -= z_adjust
        
        endpoint1 = center + min_z * axis
        endpoint2 = center + max_z * axis
        
        if update:
            #print "Original endpoints:", self.getEndPoints()
            self.setEndPoints(endpoint1, endpoint2)
            #print "New endpoints:", self.getEndPoints()
            
        return (endpoint1, endpoint2, radius)
예제 #50
0
def pi_info_from_abs_pvecs(bond, bond_axis, pvec1, pvec2, abs_coords=False):
    """
    #doc;
    bond_axis (passed only as an optim) and pvecs are in abs coords; retval is in bond coords unless abs_coords is true
    """
    a1py = pvec1
    a2py = pvec2
    a1pz = norm(cross(bond_axis, pvec1))
    a2pz = norm(cross(bond_axis, pvec2))
    ord_pi_y, ord_pi_z = pi_orders(bond)
    if not abs_coords:
        # put into bond coords (which might be the same as abs coords)
        quat = bond.bond_to_abs_coords_quat()
        a1py = quat.unrot(a1py)
        a2py = quat.unrot(a2py)
        a1pz = quat.unrot(a1pz)
        a2pz = quat.unrot(a2pz)
    return ((a1py, a1pz), (a2py, a2pz), ord_pi_y, ord_pi_z)
예제 #51
0
 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)
예제 #52
0
def pi_info_from_abs_pvecs( bond, bond_axis, pvec1, pvec2, abs_coords = False):
    """
    #doc;
    bond_axis (passed only as an optim) and pvecs are in abs coords; retval is in bond coords unless abs_coords is true
    """
    a1py = pvec1
    a2py = pvec2
    a1pz = norm(cross(bond_axis, pvec1))
    a2pz = norm(cross(bond_axis, pvec2))
    ord_pi_y, ord_pi_z = pi_orders(bond)
    if not abs_coords:
        # put into bond coords (which might be the same as abs coords)
        quat = bond.bond_to_abs_coords_quat()
        a1py = quat.unrot(a1py)
        a2py = quat.unrot(a2py)
        a1pz = quat.unrot(a1pz)
        a2pz = quat.unrot(a2pz)
    return ((a1py, a1pz), (a2py, a2pz), ord_pi_y, ord_pi_z)
예제 #53
0
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
def arbitrary_perpendicular(
    vec,
    nicevecs=[]
):  #bruce 060608, probably duplicates other code somewhere (check in VQT?)
    """
    Return an arbitrary unit vector perpendicular to vec (a Numeric array, 3 floats),
    making it from vec and as-early-as-possible nicevecs (if any are passed).
    """
    nicevecs = map(norm, nicevecs) + [X_AXIS, Y_AXIS]
    # we'll look at vec and each nicevec until they span a subspace which includes a perpendicular.
    # if we're not done, it means our subspace so far is spanned by just vec itself.
    vec = norm(vec)
    if not vec:
        return nicevecs[0]  # the best we can do
    for nice in nicevecs:
        res = norm(nice - vec * dot(nice, vec))
        if res:
            return res
    return nicevecs[0]  # should never happen
예제 #55
0
    def compute_memo(self, chunk):
        """
        If drawing chunk in this display mode can be optimized by precomputing some info from chunk's appearance,
        compute that info and return it.
           If this computation requires preference values, access them as env.prefs[key],
        and that will cause the memo to be removed (invalidated) when that preference value is changed by the user.
           This computation is assumed to also depend on, and only on, chunk's appearance in ordinary display modes
        (i.e. it's invalidated whenever havelist is). There is not yet any way to change that,
        so bugs will occur if any ordinarily invisible chunk info affects this rendering,
        and potential optimizations will not be done if any ordinarily visible info is not visible in this rendering.
        These can be fixed if necessary by having the real work done within class Chunk's _recompute_ rules,
        with this function or drawchunk just accessing the result of that (and sometimes causing its recomputation),
        and with whatever invalidation is needed being added to appropriate setter methods of class Chunk.
        If the real work can depend on more than chunk's ordinary appearance can, the access would need to be in drawchunk;
        otherwise it could be in drawchunk or in this method compute_memo.
        """
        # for this example, we'll turn the chunk axes into a cylinder.
        # Since chunk.axis is not always one of the vectors chunk.evecs (actually chunk.poly_evals_evecs_axis[2]),
        # it's best to just use the axis and center, then recompute a bounding cylinder.
        if not chunk.atoms:
            return None
        axis = chunk.axis
        axis = norm(
            axis
        )  # needed (unless we're sure it's already unit length, which is likely)
        center = chunk.center
        points = chunk.atpos - center  # not sure if basepos points are already centered
        # compare following Numeric Python code to findAtomUnderMouse and its caller
        matrix = matrix_putting_axis_at_z(axis)
        v = dot(points, matrix)
        # compute xy distances-squared between axis line and atom centers
        r_xy_2 = v[:, 0]**2 + v[:, 1]**2
        ## r_xy = sqrt(r_xy_2) # not needed

        # to get radius, take maximum -- not sure if max(r_xy_2) would use Numeric code, but this will for sure:
        i = argmax(r_xy_2)
        max_xy_2 = r_xy_2[i]
        radius = sqrt(max_xy_2)
        # to get limits along axis (since we won't assume center is centered between them), use min/max z:
        z = v[:, 2]
        min_z = z[argmin(z)]
        max_z = z[argmax(z)]
        bcenter = chunk.abs_to_base(center)
        # return, in chunk-relative coords, end1, end2, and radius of the cylinder, and color.
        color = chunk.color
        if color is None:
            color = V(0.5, 0.5, 0.5)
        # make sure it's longer than zero (in case of a single-atom chunk); in fact, add a small margin all around
        # (note: this is not sufficient to enclose all atoms entirely; that's intentional)
        margin = 0.2
        min_z -= margin
        max_z += margin
        radius += margin
        return (bcenter + min_z * axis, bcenter + max_z * axis, radius, color)