Difference between revisions of "User:Remig/plico/tug"

From Jmol
Jump to navigation Jump to search
(Tug)
 
m (Add description)
Line 1: Line 1:
 +
'''Tug''' allows the user to pull or push by mouse actions to move or rotate one part of a polypeptide against the rest by rotation on its psi and phi bonds with collision detection and restriction.
 +
 
'''Tug''' is a member of the Plico suite of protein folding tools described in [[User:Remig/plico]] . It may be installed and accessed as a macro with the file:
 
'''Tug''' is a member of the Plico suite of protein folding tools described in [[User:Remig/plico]] . It may be installed and accessed as a macro with the file:
 
<pre>Title=PLICO Tug
 
<pre>Title=PLICO Tug
Script=script <path to your script folder>/tug.spt;plicotug</pre>
+
Script=script <path to your scripts folder>/tug.spt;plicotug</pre>
 
saved as plicotug.macro in your .jmol/macros folder as described in [[Macro]].
 
saved as plicotug.macro in your .jmol/macros folder as described in [[Macro]].
  
 +
Copy and paste the following into a text editor and save in your scripts folder as tug.spt.
 
<pre>#  tug - Jmol script by Ron Mignery
 
<pre>#  tug - Jmol script by Ron Mignery
 
#  v1.0 beta    2/4/2014 for Jmol 13.4
 
#  v1.0 beta    2/4/2014 for Jmol 13.4

Revision as of 18:14, 8 February 2014

Tug allows the user to pull or push by mouse actions to move or rotate one part of a polypeptide against the rest by rotation on its psi and phi bonds with collision detection and restriction.

Tug is a member of the Plico suite of protein folding tools described in User:Remig/plico . It may be installed and accessed as a macro with the file:

Title=PLICO Tug
Script=script <path to your scripts folder>/tug.spt;plicotug

saved as plicotug.macro in your .jmol/macros folder as described in Macro.

Copy and paste the following into a text editor and save in your scripts folder as tug.spt.

#   tug - Jmol script by Ron Mignery
#   v1.0 beta    2/4/2014 for Jmol 13.4
#
#   These functions support protein manipulation in Jmol
var kTug = 1
var kDtolerance = 0.2
var kAtolerance = 5.0
var kCtolerance = 1.85
var kMtolerance = 0.8
var kMinRama = 2
var kCollisionLimit = 200
var gCanchorIdx = -1
var gCanchorNo = -1
var gNanchorIdx = -1
var gNanchorNo = -1
var gNanchorPidx = -1
var gCanchorXyz = {0 0 0}
var gNanchorXyz = {0 0 0}
var gNanchorPxyz = {0 0 0}
var gCcargoIdx = -1
var gNcargoIdx = -1
var gCcargoXyz = {0 0 0}
var gNcargoXyz = {0 0 0}
var gCcargoNo = -1
var gNcargoNo = -1
var gDestAtomIdx = -1
var g1pivotIdx = -1
var g2pivotIdx = -1
var gSelSaves = ({}) 
var gCrotors = array()
var gNrotors = array() 
var gMouseX = 0
var gMouseY = 0
var gAc = ({})
var gChain = ""
var gMinNo = 1    
var gMaxNo = 9999    
var gScheme = "Jmol"
var gAltScheme = "Rasmol"
var gCargoAtoms = ({})
var gSeed = 23423.52
var gBusy = FALSE
var gSCidx = -1
var gSCcircle = -1
var gSCpt = {0 0 0}
var gOK = TRUE # global return value to work around jmol *feature*
var gOk2 = TRUE # "    "
var gTargetPt = {0 0 0}
var gNewDrag = FALSE
var gEcho = ""
var gCountdown = 0
var gZoom = ""
var gRotate = ""

# Return L tetrahedron point if i1<i2<i3, else R point
function getTet(i1, i2, i3, dist) {
    var v1 = {atomIndex=i3}.xyz - {atomIndex=i2}.xyz
    var v2 = {atomIndex=i1}.xyz - {atomIndex=i2}.xyz
    var axis = cross(v1, v2)
    var pma = ({atomIndex=i1}.xyz + {atomIndex=i3}.xyz)/2
    var pmo = {atomIndex=i2}.xyz + {atomIndex=i2}.xyz - pma
    var pt = pmo + (axis/axis)

    var v = pt - {atomIndex=i2}.xyz
    var cdist = distance(pt, {atomIndex=i2})
    var factor = (dist/cdist)
    var lpt = v * factor

    return lpt + {atomIndex=i2}.xyz
}

function getTrigonal(i1, i2, i3, dist) {
    var v1 = {atomIndex=i1}.xyz - {atomIndex=i2}.xyz
    var v2 = {atomIndex=i3}.xyz - {atomIndex=i2}.xyz
    var pt = {atomIndex=i2}.xyz - (v1 + v2)

    var v = pt - {atomIndex=i2}.xyz
    var cdist = distance(pt, {atomIndex=i2})
    var factor = (dist/cdist)
    var lpt = (v * factor)

    return lpt + {atomIndex=i2}.xyz
}

function abs( x) {
    if (x < 0) {
        x = -x
    }
    return x
}

function getCAmNo (iNo) {
    while ((iNo > 0) and ({(atomno=iNo) and (chain=gChain)}.atomName != "CA")) {
        iNo--
    }
    return iNo
}

function getCAmIdx (idx) {
    var no = {atomIndex=idx and (chain=gChain)}.atomno
    no = getCAmNo( no)
    return ({(atomno=no) and (chain=gChain)}.atomIndex)
}

function getCApNo (iNo) {
    while ((iNo < gMaxNo) and ({(atomno=iNo) and (chain=gChain)}.atomName != "CA")) {
        iNo++
    }
    return iNo
}

function getCpIdx (idx) {
    var no = {atomIndex=idx and (chain=gChain)}.atomno
    no = getCpNo( no)
    return ({(atomno=no) and (chain=gChain)}.atomIndex)
}

function getCpNo (iNo) {
    while ((iNo < gMaxNo) and ({(atomno=iNo) and (chain=gChain)}.atomName != "C")) {
        iNo++
    }
    return iNo
}

function getCApIdx (idx) {
    var no = {atomIndex=idx and (chain=gChain)}.atomno
    no = getCApNo( no)
    return ({(atomno=no) and (chain=gChain)}.atomIndex)
}

function getCmNo (iNo) {
    while ((iNo > 0) and ({(atomno=iNo) and (chain=gChain)}.atomName != "C")) {
        iNo--
    }
    return iNo
}

function getCmIdx (idx) {
    var no = {atomIndex=idx and (chain=gChain)}.atomno
    no = getCmNo( no)
    return ({(atomno=no) and (chain=gChain)}.atomIndex)
}

function getNmNo (iNo) {
    while ((iNo > 0) and ({(atomno=iNo) and (chain=gChain)}.atomName != "N")) {
        iNo--
    }
    return iNo
}

function getNmIdx (idx) {
    var no = {atomIndex=idx and (chain=gChain)}.atomno
    no = getNmNo( no)
    return ({(atomno=no) and (chain=gChain)}.atomIndex)
}

function getNpNo (iNo) {
    while ((iNo < gMaxNo) and ({(atomno=iNo) and (chain=gChain)}.atomName != "N")) {
        iNo++
    }
    return iNo
}

function getNpIdx (idx) {
    var no = {atomIndex=idx}.atomno
    no = getNpNo( no)
    return ({(atomno=no) and (chain=gChain)}.atomIndex)
}

function getCBidx (BBidx) {
    var no = {atomIndex=BBidx}.atomno
    var i = 1
    for (; i < 5; i++) {
        if ({(atomno=@{no+i}) and (chain=gChain)}.atomName == "CB") {
            break
        }
    }
    return {(atomno=@{no+i}) and (chain=gChain)}.atomIndex
}

function getOidx (BBidx) {
    var no = {atomIndex=BBidx}.atomno
    var i = 1
    for (; i < 4; i++) {
        if ({(atomno=@{no+i}) and (chain=gChain)}.atomName == "O") {
            break
        }
    }
    return {(atomno=@{no+i}) and (chain=gChain)}.atomIndex
}

function getNwardBBno (iNo) {
    while ((iNo >= 0) and (
        ({(atomno=iNo) and (chain=gChain)}.atomName != "N")
        and ({(atomno=iNo) and (chain=gChain)}.atomName != "C")
        and ({(atomno=iNo) and (chain=gChain)}.atomName != "CA"))) {
        iNo--
    }
    return iNo
}

function getNwardBBidx (idx) {
    var no = {atomIndex=idx}.atomno - 1
    no = getNwardBBno( no)
    return ((no >= 0) ? ({(atomno=no) and (chain=gChain)}.atomIndex) : -1)
}

function getCwardBBno (iNo) {
    while ((iNo < gMaxNo) and (
        ({(atomno=iNo) and (chain=gChain)}.atomName != "N")
        and ({(atomno=iNo) and (chain=gChain)}.atomName != "C")
        and ({(atomno=iNo) and (chain=gChain)}.atomName != "CA"))) {
        iNo++
    }
    return iNo
}

function getCwardBBidx (idx) {
    var no = {atomIndex=idx}.atomno + 1
    no = getCwardBBno( no)
    return ((no >= 0) ? ({(atomno=no) and (chain=gChain)}.atomIndex) : -1)
}

function getScBBidx (idx) {
    var no = {atomIndex=idx}.atomno
    for (; no > 0; no--) {
        if ({(atomno=no) and (chain=gChain)}.atomName == "CA") {
            break
        }
        else if ({(atomno=no) and (chain=gChain)}.atomName == "C") {
            break
        }
        else if ({(atomno=no) and (chain=gChain)}.atomName == "N") {
            break
        }
        else if ({(atomno=no) and (chain=gChain)}.atomName == "CB") {
            no -= 3
            break
        }
    }
    return {(atomno=no) and (chain=gChain)}.atomIndex
}

function isBBidx(aIdx) {
    var ret = FALSE
    switch({atomIndex=aIdx}.atomName) {
    case "N":
    case "CA":
    case "C":
        ret = TRUE
        break
    }
    return ret
}

function isSCidx(aIdx) {

    var ret = FALSE
    if (isBBidx(aIDx) == FALSE) {
        
        ret = TRUE
        switch({atomIndex=aIdx}.atomName) {
        case "O":
        case "CB":
            ret = FALSE
            break
        }
    }
    return ret
}

function addSideChainToSelection(CAno, isAdd, addOXT) {
    var iNo = CAno+3
    while ({(atomno=iNo) and (chain=gChain)}.resno == {(atomno=CAno) and (chain=gChain)}.resno) {
        {(atomno=iNo) and (chain=gChain)}.selected = isAdd
        if ({(atomno=iNo) and (chain=gChain)}.atomName == "OXT") {
            {(atomno=iNo) and (chain=gChain)}.selected = addOXT
        }
        iNo++    
    }
}

function selectAddSideChain(fromIdx) {
    var iNo = {atomIndex=fromIdx}.atomno
    select none
    while ({(atomno=iNo) and (chain=gChain)}.atomName != "N") {
        {(atomno=iNo) and (chain=gChain)}.selected = TRUE
        iNo++  
        if (iNo > gMaxNo) {
            break
        }  
    }
}

# First and last are BB atoms
# Any side atoms in the range are also selected
function selectNward (firstIdx, lastIdx) {
    var firstno = ((firstIdx < 0) ? {atomIndex=lastIdx}.atomno : {atomIndex=firstIdx}.atomno)
    var lastno = ((lastIdx < 0) ? firstno : {atomIndex=lastIdx}.atomno)
  
    select (atomno <= firstno) and (atomno >= lastno)
    
    if ({(atomno=firstno) and (chain=gChain)}.atomName == "C") { # if psi
        addSideChainToSelection(firstno-1, TRUE, TRUE)
        {(atomno=@{firstno+1}) and (chain=gChain)}.selected = TRUE # add O
    }
    if ({(atomno=firstno) and (chain=gChain)}.atomName == "CA") {
        addSideChainToSelection(firstno, TRUE, FALSE)
    }
    if ({(atomno=lastno) and (chain=gChain)}.atomName == "C") { # if psi
        addSideChainToSelection(lastno-1, FALSE, FALSE)
    }
}

# First and last are BB atoms
# Any side atoms in the range are also selected
function selectCward (firstIdx, lastIdx) {
    var firstno = ((firstIdx < 0) ? gMaxNo : {atomIndex=firstIdx}.atomno)
    var lastno = ((lastIdx < 0) ? 1 : {atomIndex=lastIdx}.atomno)
    
    # If nWard anchor in range, begin selection with it
    if (gNanchorIdx >= 0) {
        var aNo = {atomIndex=gNanchorIdx}.atomno
        if (aNo > firstNo) {
            firstno = aNo
        }
    }
    
    # If cWard anchor in range, end selection with it
    if (gCanchorIdx >= 0) {
        var aNo = {atomIndex=gCanchorIdx}.atomno
        if (aNo < lastNo) {
            lastno = aNo
        }
    }
    
    select (atomno >= firstno) and (atomno <= lastno)
    
    if ({(atomno=firstno) and (chain=gChain)}.atomName == "C") { # if psi
        addSideChainToSelection(firstno-1, FALSE, FALSE)
    }
    if ({(atomno=lastno) and (chain=gChain)}.atomName == "CA") {
        addSideChainToSelection(lastno, TRUE, FALSE)
    }
    if ({(atomno=lastno) and (chain=gChain)}.atomName == "C") { # if psi
        addSideChainToSelection(lastno-1, TRUE, TRUE)
        {(atomno=@{lastno+1}) and (chain=gChain)}.selected = TRUE # add O
    }
}

# Selected must include second parameter but not the first
function setDistanceIdx (staticIdx, mobileIdx, desired) {
    try {
        var v = {atomIndex=mobileIdx}.xyz - {atomIndex=staticIdx}.xyz
        var dist = distance({atomIndex=staticIdx}, {atomIndex=mobileIdx})
        translateSelected @{((v * (desired/dist)) - v)}
    }
    catch {
    }
}


# Selected must include third parameter but not the first
function setAngleIdx (statorIdx, pivotIdx, rotorIdx, toangle) {
    try {
        var v1={atomIndex=statorIdx}.xyz - {atomIndex=pivotIdx}.xyz
        var v2={atomIndex=rotorIdx}.xyz - {atomIndex=pivotIdx}.xyz
        var axis = cross(v1, v2) + {atomIndex=pivotIdx}.xyz
        var curangle =  angle({atomIndex=statorIdx},
            {atomIndex=pivotIdx}, {atomIndex=rotorIdx})
        rotateselected @axis {atomIndex = pivotIdx} @{curangle-toangle}
    }
    catch {
    }
}

# Selected must include fourth parameter but not the first
function setDihedralIdx (stator1idx, stator2idx, rotor1idx, rotor2idx, toangle) {
    try {
        var a1={atomIndex = stator1idx}.xyz
        var a2={atomIndex = stator2idx}.xyz
        var a3={atomIndex = rotor1idx}.xyz
        var a4={atomIndex = rotor2idx}.xyz
        var curangle =  angle(a1, a2, a3, a4)
        rotateselected {a3} {a2} @{curangle-toangle}
    }
    catch {
    }
}

function countCollisions(rc) {
    var lcAtoms = ({})
    for (var idx = {*}.min.atomIndex; idx <= {*}.max.atomIndex; idx++) {
        lcAtoms = lcAtoms or (within(kCtolerance, FALSE, {atomIndex=idx})
            and {atomIndex > idx}
            and not {rc}
            and not connected({atomIndex=idx}))
    }
    return lcAtoms.size
}

# Resolve collisions
function handleCollisions( nWard, targetIdx) {

    # For all selected atoms
    for (var iNo = {selected}.min.atomno; iNo <= {selected}.max.atomno; iNo++) {
        var idx = {(atomno=iNo) and (chain=gchain)}.atomIndex
        if ({atomindex=idx}.selected) {
       
            # Collect local colliders
            var lcAtoms = (within(kCtolerance, FALSE, {atomIndex=idx})
                and not {atomIndex=idx}
                and not {gOkCollide}
                and not connected({atomIndex=idx}))
            
            if (lcAtoms.size > 0) {
            
                # Ignore kinked BB
                if (isBBidx(idx) and angle({atomIndex=@{getCwardBBidx(idx)}},
                    {atomIndex=idx} , {atomIndex=@{getNwardBBidx(idx)}}) < 100) {
                    continue
                }
            
                #gCountdown--
                if (gCountdown < 0) {
                    timedOut("Too many collsions")
                }    
                
                # For all local water colliders, delete
                var recollect = FALSE
                for (var c = 1; c <= lcAtoms.size; c++ ) {
                    if (lcAtoms[c].group = "HOH") {
                        delete {atomIndex=@{lcAtoms[c].atomIndex}}
                        recollect = TRUE
                    }
                }
                
                # Recollect local colliders if needed
                if (recollect) {
                    lcAtoms = (within(kCtolerance, FALSE, {atomIndex=idx})
                        and not {atomIndex=idx}
                        and not {gOkCollide}
                        and not connected({atomIndex=idx}))
                    recollect = FALSE
                }
                    
                # For all local colliders
                for (var c = 1; c <= lcAtoms.size; c++ ) {
                
                    # If it is with side chain not proline, fix it
                    var cidx = lcAtoms[c].atomIndex
                    if (isSCidx(cidx) and ({atomIndex=cidx}.group != "PRO")) {
                        fixSCcollision2(cidx)
                        recollect = TRUE
                        
                        # If not fixed, exit fail
                        if (gOk2 == FALSE) {
                            return # early exit (break n jmol bug)
                        }
                    }
                }
                
                # Recollect local colliders if needed
                if (recollect) {
                    lcAtoms = (within(kCtolerance, FALSE, {atomIndex=idx})
                        and not {atomIndex=idx}
                        and not {gOkCollide}
                        and not connected({atomIndex=idx}))
                }
                    
                # For all local colliders
                for (var c = 1; c <= lcAtoms.size; c++ ) {
                
                    # If it is with O, counter-rotate
                    if ({atomIndex=@{lcAtoms[c].atomIndex}}.atomName = "O") {
                        counterRotate2(idx, lcAtoms[c].atomIndex, targetIdx)
                        
                        # If not fixed, exit fail
                        if (gOk2 == FALSE) {
                            return # early exit (break n jmol bug)
                        }
                    }
                }
            }
        }
    } # endfor iNo
}

# Rotate rotor set to move target atom to its proper place
function tugTrackIdx(targetIdx, targetPt, nWard, cDetect) {
    gOK = FALSE
    var pt = targetPt
    var dist = distance(pt, {atomIndex=targetIdx}.xyz)

    var rotors = (nWard ? gNrotors : gCrotors)
    # For a number of passes
    for (var pass1 = 0; pass1 < 20; pass1++) {
        var blocked = ({})
        for (var pass2 = 0; pass2 < (rotors.size/4); pass2++) {
       
            var v1 = {atomIndex=targetIdx}.xyz - pt
            
            # Find the most orthgonal unused rotor
            var imax = 0
            var smax = 0.5
            for (var i = 1; i < rotors.size; i += 4) {
                var i2 = rotors[i+1]
                var i3 = rotors[i+2]
                var i4 = rotors[i+3]
                if ((i2 != targetIdx) and (i3 != targetIdx) and (i4 != targetIdx)) {
                    if ({blocked and {atomIndex=i2}}.count == 0) {
                        var v2 = {atomIndex=i3}.xyz - {atomIndex=i2}.xyz
                        
                        var s = sin(abs(angle(v1, {0 0 0}, v2)))
                        if (s > smax) {
                            smax = s
                            imax = i
                        }
                    }
                }
            }
            
            # If no more rotors, break to next full try
            if (imax == 0) {
               break
            }
            var i1 = rotors[imax+0]
            var i2 = rotors[imax+1]
            var i3 = rotors[imax+2]
            var i4 = rotors[imax+3]
            
            # Get dihedral of rotor with target point
            var dt = angle({atomIndex=targetIdx}, {atomIndex=i2}, {atomIndex=i3}, pt)
            var dh = angle({atomIndex=i1}, {atomIndex=i2}, {atomIndex=i3}, {atomIndex=i4})
            if (dh == "NaN") {
                dh = -50
            }
            var psi = dh + dt
            var phi = dh + dt
            
            # Compute resultant psi and phi
            #  and select from target atom to first half of rotor
            var movePt = FALSE
            if (nWard) {
                if ({atomIndex=i2}.atomName="CA") {
                    psi = angle({atomIndex=@{getCwardBBidx(i1)}}, {atomIndex=i1},
                        {atomIndex=i2}, {atomIndex=i3}) + dt
                }
                else {
                    phi = angle({atomIndex=i1}, {atomIndex=i2},
                        {atomIndex=i3}, {atomIndex=@{getNwardBBidx(i3)}}) + dt
                }
                
                if ({atomIndex=i2}.atomno > {atomIndex=targetIdx}.atomno) {
                    movePt = TRUE
                    selectNward(i3, getCwardBBidx(targetIdx))
                    {atomIndex=targetIdx}.selected = TRUE
                }
                else {
                    selectCward(i2, targetIdx)
                }
            }
            else {
                if (({atomIndex=i2}.atomName="CA")) {
                    phi = angle({atomIndex=@{getNwardBBidx(i1)}}, {atomIndex=i1},
                        {atomIndex=i2}, {atomIndex=i3}) + dt
                }
                else {
                    psi = angle({atomIndex=i2}, {atomIndex=i3},
                        {atomIndex=i4}, {atomIndex=@{getCwardBBidx(i4)}}) + dt
                }
                
                if ({atomIndex=i2}.atomno < {atomIndex=targetIdx}.atomno) {
                    movePt = TRUE
                    selectCward(i3, getNwardBBidx(targetIdx))
                    {atomIndex=targetIdx}.selected = TRUE
                }
                else {
                    selectNward(i2, targetIdx)
                }
            }
            
            # If rotation within ramachandran limits
            #var ridx = 1 + (36*(((-psi\10)%180)+18)+(((phi\10)%180)+18))
            if ((abs(dt) >= 0.1) and 
                (({atomIndex=i2}.group=="GLY") or (phi < 0))) {#kRama[ridx] >= kMinRama))) {

                # If moving target point, put the target atom there
                var cp = {atomIndex=targetIdx}.xyz
                if (movePt) {
                    dt = -dt
                    {atomIndex=targetIdx}.xyz = pt
                }
                
                # Rotate to minimize vector ====================
                rotateSelected {atomIndex=i2} {atomIndex=i3} @dt

                # If collision checking
                if (cDetect) {
                
                    # If collision, back off by eighths
                    var wasCollision = FALSE
                    for (var ci = 0; ci < 4; ci++) {
                        if (ci < 3) {
                            dt /= 2
                        }                        
                        handleCollisions( nWard, targetIdx)
                        if (gOk2==FALSE) {
                            wasCollision = TRUE
                            rotateSelected {atomIndex=i2} {atomIndex=i3} @{-dt}
                        }
                        else if (wasCollision) {
                            if (ci <3) {
                                rotateSelected {atomIndex=i2} {atomIndex=i3} @{dt}
                            }
                        }
                        else {
                            break
                        }
                        
                        if (dt < 0.01) {
                            break
                        }
                    } # endfor
                }
                
                # If moving target point, put the target atom back
                if (movePt) {
                    pt = {atomIndex=targetIdx}.xyz
                    {atomIndex=targetIdx}.xyz = cp
                }
                
            }
            
            # If close enough, stop
            if (distance(pt, {atomIndex=targetIdx}) < kDtolerance) {
                gOK = TRUE
                gTargetPt = pt
                break
            }
                
            # Block rotor
            blocked |= {atomIndex=i2}
            
        }   # endfor num rotors passes
        
        if (gOK) {
            break
        }
    }   # endfor 10 passes
}

# Counter rotate bonds on either side of a BB O
function docounterRotate2(caPhiIDx, nIdx, cIdx, oIdx, caPsiIdx, dir, nWard) {

    # Rotate phi
    {atomIndex=nIdx}.selected = nWard
    {atomIndex=cIdx}.selected = nWard
    {atomIndex=oIdx}.selected = nWard
    rotateSelected {atomIndex=caPsiIdx} {atomIndex=cIdx} @{dir}

    # Counter-rotate psi
    {atomIndex=nIdx}.selected = not nWard
    {atomIndex=cIdx}.selected = not nWard
    {atomIndex=oIdx}.selected = not nWard
    rotateSelected {atomIndex=nIdx} {atomIndex=caPhiIdx} @{-dir}
}
        
function counterRotate2(aIdx, bIdx, targetIdx) {
    var selsave = {selected}
    gOk2 = TRUE
    var oIdx = aIdx
    var xIdx = bIdx
    if ({atomIndex=bIdx}.atomName=="O") {
        oIdx = bIdx
        xIdx = aIdx
    }
    var cIdx = getScBBidx(oIdx)
    var nIdx = getCwardBBidx(cIdx)
    var caPhiIdx = getCwardBBidx(nIdx)
    var caPsiIdx = getNwardBBidx(cIdx)
    
    var nWard = ({atomIndex=oIdx}.atomno < {atomIndex=targetIdx}.atomno)
    if (nWard) {
        selectCward(cIdx, targetIdx)
    }
    else {
        selectNward(nIdx, targetIdx)
    }
     
    # Until all collisions cancelled
    var dir = 5
    var ang = angle({atomIndex=xIdx}, {atomIndex=oIdx}, {atomIndex=cIdx})
    var tcount = 0
    while (within(kCtolerance, FALSE, {atomIndex=oIdx})
            and not {atomIndex=oIdx} and not connected({atomIndex=oIdx})
            and not {gOkCollide} > 0) {
        
        # Counter-rotate
        docounterRotate2(caPhiIDx, nIdx, cIdx, oIdx, caPsiIdx, dir, nWard)
        var newang = angle({atomIndex=xIdx}, {atomIndex=oIdx}, {atomIndex=cIdx})
        
        # If wrong direction once, undo and reverse
        if (newang > ang) {
            docounterRotate2(caPhiIDx, nIdx, cIdx, oIdx, caPsiIdx, -dir, nWard)
            
            # If first time, continue in opposite direction
            dir *= -1
            if (dir < 0) {
                continue
            }
        }

        # If no go, undo and exit
        tcount++
        if (tcount > (360/abs(dir))) {
            gOk2 = FALSE
            break
        }
        
    } # endwhile
    select selsave    
}

# Repair proline
function repairProline(idx) {
    var cbidx = getCBidx(idx)
    var cbno = {atomIndex=cbidx}.atomno
    var cgidx = {(atomno=@{cbno+1}) and (chain=gChain)}.atomIndex
    var cdidx = {(atomno=@{cbno+2}) and (chain=gChain)}.atomIndex
    var caidx = {(atomno=@{cbno-3}) and (chain=gChain)}.atomIndex
    var nidx = {(atomno=@{cbno-4}) and (chain=gChain)}.atomIndex
    
    select {atomIndex=cbidx}
    setAngleIdx(nidx, caidx, cbidx, 109.5)
    
    select {atomIndex=cdidx}
    setDistanceIdx(nidx, cdidx, 1.47)
    setAngleIdx(caidx, nidx, cdidx, 102.7)
    setDihedralIdx(cbidx, caidx, nidx, cdidx, 16.2)
    
    select {atomIndex=cgidx}
    setDistanceIdx(cdidx, cgidx, 1.51)
    setAngleIdx(nidx, cdidx, cgidx, 106.4)
    setDihedralIdx(caidx, nidx, cdidx, cgidx, 16.2)
}

# Repair side chain
function plicoRepairSideChain(targetIdx, nWard) {

    var idx = (nWard ? getCwardBBidx(targetIdx) : getNwardBBidx(targetIdx))

    if (({atomIndex=targetIdx}.atomName == "CA")
        and ({atomIndex=targetIdx}.group != "GLY")) {
        var cbidx = getCBidx(targetIdx)
        select none
        selectAddSideChain(cbidx)
        setAngleIdx(idx, targetIdx, cbidx, 110.0)
        setDistanceIdx(targetIdx, cbidx, 1.5)
        if ({atomIndex=targetIdx}.group != "PRO") {
            var colliders = (within(kCtolerance, FALSE, {selected})
                and not {atomIndex=targetIdx} and not {selected})
            if (colliders.size > 0) {
                if ({atomIndex=targetIdx}.group != "ALA") {
                    fixSCcollision2(cbidx)
                }
            }
        }
        else {
            if (nWard) {
            }
            else {
                setDihedralIdx(getNwardBBidx(idx), idx, targetIdx, cbidx, 174.2)
            }
        }
    }
    
    else if ({atomIndex=targetIdx}.atomName == "C") {
        var oidx = getOidx(targetIdx)
        select {atomIndex=oidx}
        setAngleIdx(idx, targetIdx, oidx, 120.0)
        setDistanceIdx(targetIdx, oidx, 1.21)
        if (nWard) {
            setDihedralIdx(getCwardBBidx(idx), idx, targetIdx, oidx, 0.0)
        }
        if ({atomIndex=idx}.group == "PRO") {
            repairProline(idx)
            var dNo = {atomIndex=targetIdx}.atomno + 4
            var dIdx = {(atomno=dNO) and (chain=gChain)}.atomIndex
            var colliders = (within(kCtolerance, FALSE, {atomIndex=dIdx})
                and not connected({atomIndex=dIdx})
                and not {atomIndex=dIdx})
            for (var i = 1; i <= colliders.size; i++) {
                if (colliders[i].atomName == "O") {
                    counterRotate2(dIdx, colliders[i].atomIndex, targetIdx)
                }
            }
        }
    }
}

# Rebuild Cward rotors set
function tugTrackC() {

    # For all bb atoms cWard of cargo
    var targetIdx = gCcargoIdx
    var okCount = 0
        
    # Allow collisions with cargo
    gOkCollide = gCargoAtoms
    var tcount = 0                
    while (targetIdx != gCanchorIdx) {
    
        # Step to next atom
        targetIdx = getCwardBBidx(targetIdx)
        
        # No collision with cargo allowed after two atoms placed
        if (tcount == 2) {
           gOkCollide = ({})
        }
        tcount++
        
        # Compute targets desired coords
        var c1idx = getCwardBBidx(targetIdx)
        var n1idx = getNwardBBidx(targetIdx)
        var n2idx = getNwardBBidx(n1Idx)
        var n3idx = getNwardBBidx(n2Idx)
        var pt = {0 0 0}
        if ({atomIndex=targetIdx}.atomName == "N") {
            var oidx = getOidx(n1idx)
            select {atomIndex=oidx}
            
            # Desired target location is trigonal O       
            setDistanceIdx(n1idx, oidx, 1.5)
            pt = getTrigonal(n2idx, n1idx, oidx, 1.37)
            setDistanceIdx(n1idx, oidx, 1.21)
        }
        else if (({atomIndex=targetIdx}.atomName == "C")
            and ({atomIndex=targetIdx}.group != "GLY")) {
            
            # Desired target location is tetragonal CB
            var cbidx = getCBidx(n1idx)
            pt = getTet(n2idx, n1idx, cbidx, 1.5)
        }
        else { # CA (or GLY C)
            
            # Save current target coords
            var cp = {atomIndex=targetIdx}.xyz
    
            # Set target atom at desired distance and angle
            select {atomIndex=targetIdx}
            setDistanceIdx(n1idx, targetIdx, 1.5)
            setAngleIdx(n2idx, n1idx, targetIdx, 110.0)
            if ({atomIndex=targetIdx}.atomName == "CA") {
                setDihedralIdx(n3idx, n2idx, n1idx, targetIdx, 180)
            }
            
            # Record and restore target
            pt = {atomIndex=targetIdx}.xyz
            {atomIndex=targetIdx}.xyz = cp
        }

        # If target not at desired location        
        if (distance(pt, {atomIndex=targetIdx}) > kDtolerance) {
            okCount = 0
            gTargetPt = pt
            var xcount = 0
            gOK = FALSE
            while ((xcount < 20) and (gOK == FALSE)) {
            
                # Rotate on cWard rotor set to move it there
                tugTrackIdx(targetIdx, pt, FALSE, FALSE)
                    #(distance(pt, {atomIndex=targetIdx}) > kMtolerance))
                xcount++
            }
        }
        else {
            gOK = TRUE
            okCount++
        }    
        
        # If successful
        if (gOK == TRUE) {
        
            # Adust any side atoms
            plicoRepairSideChain(targetIdx, FALSE)
        }
        
        # Else fail
        else {
            break
        }
        
        # If no movement in 4 tries, we are done
        if (okCount > 3) {
            break
        }
    } # endwhile (targetIdx != gCanchorIdx) {
}

# Rebuild Nward rotors set
function tugTrackN() {

    gOK = TRUE
    
    # For all bb atoms nWard of cargo
    var targetIdx = gNcargoIdx
    var okCount = 0
    
    # Allow collisions with cargo
    gOkCollide = gCargoAtoms
    var tcount = 0                
    while (targetIdx != gNanchorIdx) {

        # Step to next atom
        targetIdx = getNwardBBidx(targetIdx)
        
        # No collision with cargo allowed after two atoms placed
        if (tcount == 2) {
           gOkCollide = ({})
        }
        tcount++
        
        # Compute targets desired coords
        var n1idx = getNwardBBidx(targetIdx)
        var c1idx = getCwardBBidx(targetIdx)
        var c2idx = getCwardBBidx(c1idx)
        var c3idx = getCwardBBidx(c2idx)
        var pt = {0 0 0}
        if ({atomIndex=targetIdx}.atomName == "CA") {
        
            # Desired target location is trigonal O       
            var oidx = getOidx(c1idx)
            select {atomIndex=oidx}
            setDistanceIdx(c1idx, oidx, 1.39)
            pt = getTrigonal(c2idx, c1idx, oidx, 1.41)
            setDistanceIdx(c1idx, oidx, 1.21)
        }
        else if (({atomIndex=targetIdx}.atomName == "N")
            and ({atomIndex=targetIdx}.group != "GLY")) {
            
            # Desired target location is r-tetragonal CB       
            var cbidx = getCBidx(c1idx)
            pt = getTet(cbidx, c1idx, c2idx, 1.5)
        }
        else { # C
        
            # Save current target coords
            var cp = {atomIndex=targetIdx}.xyz
            
            # Set target atom at desired distance and angle
            select {atomIndex=targetIdx}
            setDistanceIdx(c1idx, targetIdx, 1.37)
            setAngleIdx(c2idx, c1idx, targetIdx, 110.0)
                
            if ({atomIndex=targetIdx}.group == "PRO") {
                setDihedralIdx(c3idx, c2idx, c1idx, targetIdx, -57.0)
            }
        
            # Record and restore target
            pt = {atomIndex=targetIdx}.xyz
            {atomIndex=targetIdx}.xyz = cp
        }
        
        # If target not at desired location        
        if (distance(pt, {atomIndex=targetIdx}) > kDtolerance) {
            var okCount = 0
            gTargetPt = pt
            var xcount = 0
            gOK = FALSE
            while ((xcount < 20) and (gOK == FALSE)) {
            
                # Rotate on cWard rotor set to move it there
                tugTrackIdx(targetIdx, pt, TRUE, FALSE)
                    #(distance(pt, {atomIndex=targetIdx}) > kMtolerance))
                xcount++
            }
        }
        else {
            gOK = TRUE
            okCount++
        }    

        # If sucessful
        if (gOK == TRUE) {
        
            # Adust any side atoms
            plicoRepairSideChain(targetIdx, TRUE)
        }
        
        # Else fail
        else {
            break
        }
        
        # If no movement in 4 tries, we are done
        if (okCount > 3) {
            break
        }
        
    }   # endwhile (targetIdx != gNanchorIdx) {

}

# gPlicoRecord is maintained by the macro pilcoRecord
function plicoRecord(s) {
    var g = format("show file \"%s\"", gPlicoRecord)
    var ls = script(g)
    if (ls.find("FileNotFoundException")) {
        ls = ""
    }
    ls += s
    write var ls @gPlicoRecord
}

# gPlicoRecord is maintained by the macro pilcoRecord
function translateSelectedRecord(pt) {
    if (gPlicoRecord != "") {
        plicoRecord(format("select %s;translateSelected %s;", {selected}, pt))
    }
    translateSelected @pt
}

# gPlicoRecord is maintained by the macro pilcoRecord
function rotateSelectedRecord(g1pivotIdx, axis, a) {
    if (gPlicoRecord != "") {
        plicoRecord(format("select %s;", {selected}))
        plicoRecord(format("rotateSelected {atomIndex=%d} @%s @%s;",
            g1pivotIdx, axis, a))
    }
    rotateSelected {atomIndex=g1pivotIdx} @axis @a
}

function collectSCrotors(no) {
    var scBondIdxs = array()
    for (var iNo = no; iNo >= 0; iNo--) {
        var ile = 0
        switch ({(atomno=iNo) and (chain=gChain)}.atomName) {
        case "CA" :
            return scBondIdxs # Early exit since break 1 appears broken
            #break 1
        case "CZ" :
            if ({(atomno=iNo) and (chain=gChain)}.group == "TYR") {
                break
            }
        case "CE" :
            if ({(atomno=iNo) and (chain=gChain)}.group == "MET") {
                break
            }
        case "CG1" :
            if ({(atomno=iNo) and (chain=gChain)}.group == "VAL") {
                break
            }
            if ({(atomno=iNo) and (chain=gChain)}.group == "ILE") {
                ile = 1
            }
        case "NE" :
        case "CD" :
        case "SD" :
        case "CG" :
        case "CB" :
            scBondIdxs += {(atomno=@{iNo+1+ile}) and (chain=gChain)}.atomIndex
            scBondIdxs += {(atomno=@{iNo+0}) and (chain=gChain)}.atomIndex
            if ({(atomno=iNo) and (chain=gChain)}.atomName%2 == "CG") {
                scBondIdxs += {(atomno=@{iNo-1}) and (chain=gChain)}.atomIndex
                scBondIdxs += {(atomno=@{iNo-4}) and (chain=gChain)}.atomIndex
            }
            else if ({(atomno=iNo) and (chain=gChain)}.atomName == "CB") {
                scBondIdxs += {(atomno=@{iNo-3}) and (chain=gChain)}.atomIndex
                scBondIdxs += {(atomno=@{iNo-4}) and (chain=gChain)}.atomIndex
            }
            else {
                scBondIdxs += {(atomno=@{iNo-1}) and (chain=gChain)}.atomIndex
                scBondIdxs += {(atomno=@{iNo-2}) and (chain=gChain)}.atomIndex
            }
            break
        }
    
    }
    
    return scBondIdxs 
}

# Drag Side Chain
function dragSC() {

    var iNo = {atomIndex=gSCidx}.atomno
    
    if ({atomIndex=gSCidx}.group != "PRO") {
    
        var scBondIdxs = collectSCrotors( iNo)
        var numChi = scBondIdxs.size / 4
        var dist = distance({atomIndex=gSCidx}.xyz, gSCpt)
        # For all rotor combinations
        var dh = array()
        for (var i = 0; i < numChi; i++) {
            dh += angle({atomIndex=@{scBondIdxs[4+(4*i)]}},
                {atomIndex=@{scBondIdxs[3+(4*i)]}},
                {atomIndex=@{scBondIdxs[2+(4*i)]}},
                {atomIndex=@{scBondIdxs[1+(4*i)]}})
        }
        for (var i = 0; i < numChi; i++) {
            var rot = -120
            for (var j = 0; j < 6; j++) {
                rot += 60*j
                selectAddSideChain(scBondIdxs[1+(4*i)])
                setDihedralIdx(scBondIdxs[4+(4*i)], scBondIdxs[3+(4*i)],
                    scBondIdxs[2+(4*i)], scBondIdxs[1+(4*i)], rot)
                var newDist = distance({atomIndex=gSCidx}.xyz, gSCpt)
                
                # Find the best
                if (newDist < dist) {
                    dist = newDist
                    for (var k = 0; k < numChi; k++) {
                        dh[k+1] = angle({atomIndex=@{scBondIdxs[4+(4*k)]}},
                        {atomIndex=@{scBondIdxs[3+(4*k)]}},
                        {atomIndex=@{scBondIdxs[2+(4*k)]}},
                        {atomIndex=@{scBondIdxs[1+(4*k)]}})
                    }
                }
            }
        }
        
        # Now set the best
        for (var i = 0; i < numChi; i++) {
            selectAddSideChain(scBondIdxs[1+(4*i)])
            setDihedralIdx(scBondIdxs[4+(4*i)], scBondIdxs[3+(4*i)],
                scBondIdxs[2+(4*i)], scBondIdxs[1+(4*i)], dh[i+1])
        }
    }
    else { # PRO - toggle between puckers up and down
        var icd = {(atomno=@{iNo+1}) and (chain=gChain)}.atomIndex
        var icb = {(atomno=@{iNo-1}) and (chain=gChain)}.atomIndex
        var ica = {(atomno=@{iNo-4}) and (chain=gChain)}.atomIndex
        var in = {(atomno=@{iNo-5}) and (chain=gChain)}.atomIndex
        select {atomIndex=gSCidx}
       
        if (angle({atomIndex=ica}, {atomIndex=in},
            {atomIndex=icd}, {atomIndex=gSCidx}) < -10.0) {
            setDihedralIdx(ica, in, icd, gSCidx, 8.7)
            setAngleIdx(in, icd, gSCidx, 110.0)
            setDistanceIdx(icd, gSCidx, 1.5)
        }
        else {
            setDihedralIdx(ica, in, icd, gSCidx, -29.5)
            setAngleIdx(in, icd, gSCidx, 108.8)
            setDistanceIdx(icd, gSCidx, 1.5)
        }
    }
    
    draw gSCcircle CIRCLE {atomIndex=gSCidx} MESH NOFILL
    gSCpt = {atomIndex=gSCidx}.xyz
}
 
# Fix side chain collisions
function fixSCcollision2(idx) {
    gOk2 = FALSE
    var iNo = {atomIndex=idx}.atomno
    var resno = {(atomno=iNo) and (chain=gChain)}.resno
    
    # Get SC terminus
    while (resno == {(atomno=iNo) and (chain=gChain)}.resno) {
        iNo++
    }
    iNo--
    
    var sc = array()
    var iBno = iNo
    while ({(atomno=iBno) and (chain=gChain)}.atomName != "CB") {
        sc += {(atomno=iBno) and (chain=gChain)}
        iBno--
    }
    var cbidx = {(atomno=iBno) and (chain=gChain)}.atomIndex
    
    var scBondIdxs = collectSCrotors( iNo)
    var numChi = scBondIdxs.size / 4
    # For all rotor combinations
    for (var i = 0; i < numChi; i++) {
        var rot = -120
        for (var j = 0; j < 6; j++) {
            rot += 60
            selectAddSideChain(scBondIdxs[1+(4*i)])
            #setDihedralIdx(scBondIdxs[4+(4*i)], scBondIdxs[3+(4*i)],
            #    scBondIdxs[2+(4*i)], scBondIdxs[1+(4*i)], rot)
            setDihedralIdx(scBondIdxs[1+(4*i)], scBondIdxs[2+(4*i)],
                scBondIdxs[3+(4*i)], scBondIdxs[4+(4*i)], rot)
            # If no collision, exit
            colliders = (within(kCtolerance, FALSE, {sc})
                and not {atomIndex=cbidx} and not {sc})
                        
            # If it is with water, delete the water
            for (var c = 1; c < colliders.size; c++ ) {
                if (colliders[c].group = "HOH") {
                    delete {atomIndex=@{colliders[c].atomIndex}}
                    colliders = {colliders and not @{colliders[c]}}
                }
            }
                
            if (colliders.size == 0) {
                gOk2 = TRUE
                return # Early exit since break 1 appears broken
                #break 1
            }
            
        }
    }
}

function isMovableSideChain(aIdx) {

    var ret = (({atomIndex=aIdx}.group != "PRO")
        or ({atomIndex=aIdx}.atomName == "CG"))
    switch({atomIndex=aIdx}.atomName) {
    case "N":
    case "CA":
    case "C":
    case "CB":
    case "O":
    case "O4\'":
        ret = FALSE
        break
    }
    return ret
}

# gFreeze is maintained by the script plicoFreeze that allows the user
# to inhibit rotation on selected rotors
function isRotorAvailable(idx) {
    return (gFreeze.find(idx) == 0)
}

function collectBBrotors(nWard) {
    var anchorNo = (nWard
        ? ((gNanchorIdx >= 0) ? {atomIndex=gNanchorIdx}.atomno : gMinNo)
        : ((gCanchorIdx >= 0) ? {atomIndex=gCanchorIdx}.atomno : gMaxNo))
    var cargoNo = (nWard
        ? ((gNcargoIdx >= 0) ? {atomIndex=gNcargoIdx}.atomno
        : {atomIndex=gCcargoIdx}.atomno)
        : {atomIndex=gCcargoIdx}.atomno)
    var rotors = array()
    if (cargoNo < anchorNo) {
    
        # If cWard cargo is C, include its psi
        if ({(atomno=cargoNo) and (chain=gChain)}.atomName == "C") {
            #cargoNo--
        }
        
        for (var iNo = cargoNo; iNo <= anchorNo; iNo++) {
            if ({(atomno=iNo) and (chain=gChain)}.atomName == "CA") {
                if (isRotorAvailable(iNo)) {
                    if (({(atomno=iNo) and (chain=gChain)}.group != "PRO") and (iNo > cargoNo)) { # phi
                        rotors += [{(atomno=@{getCmNo(iNo-1)}) and (chain=gChain)}.atomIndex,
                            {(atomno=@{iNo-1}) and (chain=gChain)}.atomIndex]
                        rotors += [{(atomno=@{iNo}) and (chain=gChain)}.atomIndex,
                            {(atomno=@{iNo+1}) and (chain=gChain)}.atomIndex]
                    }
                    if (iNo != (anchorNo-1)) { # psi
                        rotors += [{(atomno=@{iNo-1}) and (chain=gChain)}.atomIndex,
                            {(atomno=@{iNo}) and (chain=gChain)}.atomIndex]
                        rotors += [{(atomno=@{iNo+1}) and (chain=gChain)}.atomIndex,
                            {(atomno=@{getNpNo(iNo+2)}) and (chain=gChain)}.atomIndex]
                    }
                }
            }
        }
    }
    else {
    
        # If nWard cargo is C, include its phi
        if ({(atomno=cargoNo) and (chain=gChain)}.atomName == "N") {
            #cargoNo++
        }
        
        for (var iNo = cargoNo; iNo >= anchorNo; iNo--) {
            if ({(atomno=iNo) and (chain=gChain)}.atomName == "CA") {
                if (isRotorAvailable(iNo)) {
                    if ((iNo != (anchorNo-1)) and (iNo < cargoNo)) { # psi
                        rotors += [{(atomno=@{getNpNo(iNo+2)}) and (chain=gChain)}.atomIndex,
                            {(atomno=@{iNo+1}) and (chain=gChain)}.atomIndex]
                        rotors += [{(atomno=@{iNo}) and (chain=gChain)}.atomIndex,
                            {(atomno=@{iNo-1}) and (chain=gChain)}.atomIndex]
                    }
                    if ({(atomno=iNo) and (chain=gChain)}.group != "PRO") { # phi
                        rotors += [{(atomno=@{iNo+1}) and (chain=gChain)}.atomIndex,
                            {(atomno=@{iNo}) and (chain=gChain)}.atomIndex]
                        rotors += [{(atomno=@{iNo-1}) and (chain=gChain)}.atomIndex,
                            {(atomno=@{getCmNo(iNo-1)}) and (chain=gChain)}.atomIndex]
                    }
                }
            }
        }
    }
    
    if (nWard) {
        gNrotors = rotors
        if (FALSE) {#DEBUG
            print ("gNrotors atomnos")#DEBUG
            for (var i = 1; i < gNrotors.size; i += 4) {#DEBUG
                print format("%d %d %s", {atomIndex=@{gNrotors[i]}}.atomno,
                    {atomIndex=@{gNrotors[i+1]}}.atomno, format("%d %d",
                    {atomIndex=@{gNrotors[i+2]}}.atomno,
                    {atomIndex=@{gNrotors[i+3]}}.atomno))
            }
        }#DEBUG
    }
    else {
        gCrotors = rotors
        if (FALSE) {#DEBUG
            print ("gCrotors atomnos")#DEBUG
            for (var i = 1; i < gCrotors.size; i += 4) {#DEBUG
                print format("%d %d %s", {atomIndex=@{gCrotors[i]}}.atomno,
                    {atomIndex=@{gCrotors[i+1]}}.atomno, format("%d %d",
                    {atomIndex=@{gCrotors[i+2]}}.atomno,
                    {atomIndex=@{gCrotors[i+3]}}.atomno))
            }
        }#DEBUG
    }
}

function collectRotors() {
    collectBBrotors(FALSE)
    collectBBrotors(TRUE)
}

function tugSideChain(pt) {
                
    # If destination atom defined
    if (gDestAtomIdx >= 0) {
        v = {atomIndex=gDestAtomIdx}.xyz - {atomIndex=gSCidx}.xyz
        if (abs(angle({atomIndex=gDestAtomIdx}.xyz, {0 0 0}, pt)) < 90) {
            pt = -v/20.0
        }
        else {
            pt = v/20.0
        }
    }
    gSCpt += pt
    draw arrow {atomIndex=gSCidx} @gSCpt 
}

function setColors() {
    select all
    color {selected} @gScheme
    color {atomIndex=gCanchorIdx} @gAltScheme
    color {atomIndex=gNanchorIdx} @gAltScheme
    color {atomIndex=g1pivotIdx} green
    color {atomIndex=g2pivotIdx} green
    color @gCargoAtoms @gAltScheme
    select {(atomIndex=gCcargoIdx) or (atomIndex=gNcargoIdx)}
    halo on
    select {atomIndex=gDestAtomIdx}
    star on
    select none
}

function clearAtomIdxs() {
    gCcargoIdx = -1
    gNcargoIdx = -1
    gCanchorIdx = -1
    gNanchorIdx = -1
    g1pivotIdx = -1
    g2pivotIdx = -1
    gDestAtomIdx = -1
    gSCidx = -1
}

function timedOut (s) {
    timeout ID"tug" OFF
    prompt(s)
    gBusy = FALSE
    background ECHO yellow
    restore state gState
    select gCargoAtoms
    refresh
    quit
}

function recordDrag() {
    var ls = format("select %s;", {selected})
    ls += format("gCanchorIdx = %d;", gCanchorIdx)
    ls += format("gCanchorNo = %d;", gCanchorNo)
    ls += format("gNanchorIdx = %d;", gNanchorIdx)
    ls += format("gNanchorNo = %d;", gNanchorNo)
    ls += format("gNanchorPidx = %d;", gNanchorPidx)
    ls += format("gCanchorXyz = %s;", gCanchorXyz)
    ls += format("gNanchorXyz = %s;", gNanchorXyz)
    ls += format("gNanchorPxyz = %s;", gNanchorPxyz)
    ls += format("gCcargoIdx = %d;", gCcargoIdx)
    ls += format("gNcargoIdx = %d;", gNcargoIdx)
    ls += format("gCcargoXyz = %s;", gCcargoXyz)
    ls += format("gNcargoXyz = %s;", gNcargoXyz)
    ls += format("gCcargoNo = %d;", gCcargoNo)
    ls += format("gNcargoNo = %d;", gNcargoNo)
    ls += format("gDestAtomIdx = %d;", gDestAtomIdx)
    ls += format("g1pivotIdx = %d;", g1pivotIdx)
    ls += format("g2pivotIdx = %d;", g2pivotIdx)
    ls += format("gAc = %s;", gAc)
    ls += format("gChain = \"%s\";", gChain)
    ls += format("gMinNo = %d;", gMinNo)
    ls += format("gMaxNo = %d;", gMaxNo)    
    ls += format("gCargoAtoms = %s;", gCargoAtoms)
    ls += format("gSCidx = %d;", gSCidx)
    ls += format("gSCcircle = %d;", gSCcircle)
    ls += format("gSCpt = %s;", gSCpt)
    ls += "collectRotors();"
    ls += "tugDragDoneMB();"
    plicoRecord(ls)
}

# Bound to LEFT-UP by tugEnableDrag
function tugDragDoneMB() {
    if (gPlicoRecord != "") {
        recordDrag()
    }
 
    # Move by rotation on rotor sets, smallest first
    gBusy = TRUE
    background ECHO pink
    refresh

    # If side chain mode
    if (gSCidx >= 0) {
        dragSC()
    }
    
    # Else
    else {
        gOK = TRUE
        timeout ID"tug" 20.0 "timedOut(\"Tug timed out\")"
        gCountdown = kCollisionLimit
        if ((gCrotors.size < gNrotors.size) or (gNanchorIdx < 0)) {
            if (gCrotors.size > 4) {
                tugTrackC()  # PULLSTRING MODEL
            }
            if (gOK and (gNrotors.size > 4)) {
                tugTrackN()  # PULLSTRING MODEL
            }
        }
        else {
            if (gNrotors.size > 4) {
                tugTrackN()  # PULLSTRING MODEL
            }
            if (gOK and (gCrotors.size > 4)) {
                tugTrackC()  # PULLSTRING MODEL
            }
        }
        timeout ID"tug" OFF
        
        # If anchor angles acute, fail
        if (gOK == TRUE) {
            if (gCanchorIdx >= 0) {
                var ic = getCwardBBidx(gCanchorIdx)
                var in = getNwardBBidx(gCanchorIdx)
                if ((ic >= 0) and
                    angle({atomIndex=ic}, {atomIndex=gCanchorIdx}, {atomIndex=in})
                    < 100.0) {
                    gOK = FALSE
                }
            }
            if (gNanchorIdx >= 0) {
                var ic = getCwardBBidx(gNanchorIdx)
                var in = getNwardBBidx(gNanchorIdx)
                if ((in >= 0) and
                    angle({atomIndex=ic}, {atomIndex=gNanchorIdx}, {atomIndex=in})
                    < 100.0) {
                    gOK = FALSE
                }
            }
        }
        
        # If too far
        if (gOK == FALSE) {
            timedOut("TUG TOO FAR!")
        }
        
        # Else OK
        else {

            select ((atomno >= gNanchorNo) and (atomno <= gCanchorNo)
                and (chain = gChain))
            var idx = {atomno=@{{chain=gChain}.min.atomno}}.atomIndex

            
            var i = 0
            for (; i < 3; i++) {
                handleCollisions(FALSE, idx)
                if (countCollisions() == 0) {
                    break
                }
            }
            if (i == 3) {
                var p = prompt("Unable to handle all collisions!")
                restore state gState
            }                    
        }
    }
    select {gCargoAtoms}
    gBusy = FALSE
    background ECHO yellow
    refresh
}

# Bound to ALT-LEFT-DRAG by tugEnableDrag
function tugDragMB() {
    if (gBusy == FALSE) {
        var dx = (20.0 * (_mouseX - gMouseX))/_width
        var dy = (20.0 * (_mouseY - gMouseY))/_height
        var q = quaternion(script("show rotation"))
        var ptd = {@dx @dy 0}
        var pt = (!q)%ptd
    
        if (distance(pt,  {0 0 0}) > 0.004) {

            # If sidechain mode
            if (gSCidx >= 0) {
                tugSideChain(pt)
            }

            # Else            
            else {
            
                # If new drag
                if (gNewDrag) {
                    gNewDrag = FALSE
                    save state gState
                }
                
                #mp = ({atomIndex=gNcargoIdx}.xyz + {atomIndex=gCcargoIdx}.xyz )/2.0
                
                # If destination atom defined
                if (gDestAtomIdx >= 0) {
                    var v = {atomIndex=gDestAtomIdx}.xyz - {selected}.xyz
                    if (abs(angle({atomIndex=gDestAtomIdx}.xyz, {0 0 0}, pt)) < 90) {
                        pt = -v/20.0
                    }
                    else {
                        pt = v/20.0
                    }
                }
                
                # Move the cargo
                select {gCargoAtoms}
                
                # If pivots defined, rotate it
                if (g1pivotIdx >= 0) {
                
                    # If two pivots
                    var axis = {0 0 0}
                    if (g2pivotIdx >= 0) {
                        axis = {atomIndex=g2pivotIdx}
                    }
                    
                    # Else
                    else {
                        axis = cross(pt, {0 0 0}) + {atomIndex=g1pivotIdx}.xyz
                    }
                    
                    var a = ((abs(angle(pt + {atomIndex=g1pivotIdx}.xyz,
                        {atomIndex=g1pivotIdx}.xyz, axis)) < 90) ? -2 : 2)
                    #rotateSelected {atomIndex=g1pivotIdx} @axis @a
                    rotateSelectedRecord(g1pivotIdx, axis, a)
        
                }
                
                # Else translate it
                else {
                    #translateSelected @pt
                    translateSelectedRecord(pt)
                }
                
                # If collisions
                var aset = {((atomno < gCanchorNo) and (atomno > gNanchorNo)
                    and (chain=gChain))}   
                var ac = (within(kCtolerance, FALSE, {aset}) and not {aset}
                    and not connected(aset))
                if (ac.size > 0) {
                    # Resolve them
                    for (var i = 1; i <= ac.size;  i++) {
                        select ac[i]
                        handleCollisions()
                    }
                    
                    # If unable
                    if (gOk2 == FALSE) {
                    
                        # Back off
                        background ECHO pink
                        delay 1
                        if (g1pivotIdx >= 0) {
                            rotateSelectedRecord(g1pivotIdx, axis, -a)
                        }
                        else {
                            translateSelectedRecord(-pt)
                        }
                        background ECHO yellow
                    }
                }
                #tugDragDoneMB()
            }
            
            gMouseX = _mouseX
            gMouseY = _mouseY
        }
        select {gCargoAtoms}
    }
}

# Bound to ALT-LEFT-DOWN by tugEnableDrag
function tugMarkMB() {
    gMouseX = _mouseX
    gMouseY = _mouseY
    gNcargoXyz = {atomIndex=gNcargoIdx}.xyz
    gCcargoXyz = {atomIndex=gCcargoIdx}.xyz
    gNewDrag = TRUE
}

# Called by tugCargoMB and by tugAnchorMB or tugPivotMB when cargo exists
function tugEnableDrag() {
    gEcho = "ALT-CLICK=set cargo range|ALT-DRAG=move|SHIFT=set anchors|ALT-CTRL=set pivots|ALT-SHIFT=set dest atom |DOUBLE-CLICK=exit"
    echo @gEcho
    
    # Allow atoms to be dragged
    bind "ALT-LEFT-DOWN" "tugMarkMB";
    bind "ALT-LEFT-UP" "tugDragDoneMB";#ONCEMODE
    bind "ALT-LEFT-DRAG" "tugDragMB";
}

# Bound to SHIFT-LEFT-CLICK by tugCargoMB
function tugAnchorMB() {
    if ({atomIndex=_atomPicked}.chain == gChain) {
        var aPidx = getScBBidx( _atomPicked)
        
        var pno = {atomIndex=aPidx}.atomno
        if (pno > {atomIndex=gCcargoIdx}.atomno) {
            color {atomIndex=gCanchorIdx} @gScheme
            if (gCanchorIdx == aPidx) {
                gCanchorIdx = -1
                gCanchorNo = gMaxNo + 1
            }
            else {
                gCanchorIdx = aPidx
                gCanchorNo = {atomIndex=gCanchorIdx}.atomno
                gCanchorXyz = {atomIndex=gCanchorIdx}.xyz
                color {atomIndex=gCanchorIdx} @gAltScheme
            }
            collectBBrotors(FALSE)
        }
        if (pno < {atomIndex=gNcargoIdx}.atomno) {
            color {atomIndex=gNanchorIdx} @gScheme
            if (gNanchorIdx == aPidx) {
                gNanchorIdx = -1
                gNanchorNo = gMinNo - 1
            }
            else {
                gNanchorIdx = aPidx
                gNanchorNo = {atomIndex=gNanchorIdx}.atomno
                gNanchorPidx = getCwardBBidx( aPidx)
                gNanchorXyz = {atomIndex=gNanchorIdx}.xyz
                gNanchorPxyz = {atomIndex=gNanchorPidx}.xyz
                color {atomIndex=gNanchorIdx} @gAltScheme
            }
            collectBBrotors(TRUE)
        }
    }
    
    # Get connectors between fixed and moving part
    var aset = {((atomno < gCanchorNo) and (atomno > gNanchorNo)
        and (chain=gChain))}   
    gAc = (within(kCtolerance, FALSE, {aset}) and not {aset})
    select {gCargoAtoms}
}

# Bound to ALT-SHIFT-LEFT-CLICK by tugCargoMB
function tugDestAtomMB() {
    var aOk = TRUE
    if ({atomIndex=_atomPicked}.chain == gChain) {

        var pno = {atomIndex=_atomPicked}.atomno
        if ((pno <= {atomIndex=gCcargoIdx}.atomno) and (pno >= {atomIndex=gNcargoIdx}.atomno)) {
            aOk = FALSE
        }
    }
    if (aOk) {
        select {atomIndex=gDestAtomIdx}
        star off
        if (gDestAtomIdx == _atomPicked) {
            gDestAtomIdx = -1
        }
        else {
            gDestAtomIdx = _atomPicked
            select {atomIndex=gDestAtomIdx}
            star on
        }
        select {gCargoAtoms}
    }
}

# Bound to CTRL-LEFT-CLICK by tugCargoMB
function tugPivotMB() {
    if (g1pivotIdx == _atomPicked) {
        color {atomIndex=g1pivotIdx} @gScheme
        if (g2pivotIdx >= 0) {
            g1pivotIdx = g2pivotIdx
            g2pivotIdx = -1
        }
        else {
            g1pivotIdx = -1
        }
    }
    else if (g2pivotIdx == _atomPicked) {
        color {atomIndex=g2pivotIdx} @gScheme
        g2pivotIdx = -1
    }
    else if (g1pivotIdx >= 0) {
        if (g2pivotIdx >= 0) {
            color {atomIndex=g2pivotIdx} @gScheme
        }
            
        g2pivotIdx = _atomPicked
        color {atomIndex=g2pivotIdx} green
    }
    else {
        g1pivotIdx = _atomPicked
        color {atomIndex=g1pivotIdx} green
    }
    select {gCargoAtoms}
}

# Bound to ALT-LEFT-CLICK by plicoTug
function tugCargoMB() {
    
    if (gChain != {atomIndex=_atomPicked}.chain) {
        clearAtomIdxs()
        setColors()
        gChain = {atomIndex=_atomPicked}.chain
    }
    
    # If movable side chain atom picked
    if (isMovableSideChain( _atomPicked)) {
        if (gSCidx >= 0) {
            draw gSCcircle DELETE
        }
        if (gSCidx == _atomPicked) {
            gSCidx = -1
        }        
        else {
            gSCidx = _atomPicked
            gSCpt = {atomIndex=gSCidx}.xyz
            draw gSCcircle CIRCLE {atomIndex=gSCidx} MESH NOFILL
        }
    }
    else if ((gChain == "") or ({atomIndex=_atomPicked}.chain == gChain)) {
        gMinNo = {chain=gChain}.atomno.min    
        gMaxNo = {chain=gChain}.atomno.max
        if (gNanchorIdx < 0) {
            gNanchorNo = gMinNo - 1
        }
        if (gCanchorIdx < 0) {
            gCanchorNo = gMaxNo + 1    
        }
        var aPidx = getScBBidx( _atomPicked)
        
        gSCidx = -1
        draw gSCcircle DELETE

        # If existing cWard cargo picked
        if (gCcargoIdx == aPidx) {
            
            # Clear the highlight
            select {atomIndex=gCcargoIdx}
            halo off
            
            # If nWard cargo exists, mark it as the cWard cargo
            if (gNcargoIdx != gCcargoIdx) {
                gCcargoIdx = getCpIdx(gNcargoIdx)
                gCcargoNo = {atomIndex=gCcargoIdx}.atomno
            }
            else {
                gCcargoIdx = -1
                gNcargoIdx = -1
            }
        }
        else if (gNcargoIdx == aPidx) {
            select {atomIndex=gNcargoIdx}
            halo off
            gNcargoIdx = getNmIdx(gCcargoIdx)
            gNcargoNo = {atomIndex=gNcargoIdx}.atomno
        }
        else if (gCcargoIdx >= 0) {
            var no = {atomIndex=aPidx}.atomno
        
            # If pick is nWard of it
            if (no < {atomIndex=gCcargoIdx}.atomno) {
            
                # If exists, clear its highlight
                if (gNcargoIdx != gCcargoIdx) {
                    select {atomIndex=gNcargoIdx}
                    halo off
                }
                
                # Set new nWard cargo and highlight it
                gNcargoIdx = getNmIdx(aPidx)
                gNcargoNo = {atomIndex=gNcargoIdx}.atomno
            }
            
            # Else cWard
            else {
            
                # Clear its old highlight
                select {atomIndex=gCcargoIdx}
                if (gNcargoIdx != gCcargoIdx) {
                    halo off
                }
             
                # Set new cWard cargo and highlight
                gCcargoIdx = getCpIdx(aPidx)
                gCcargoNo = {atomIndex=gCcargoIdx}.atomno
            }
        }
        
        # Else no cWard cargo
        else {
        
            # Set new cWard cargo and highlight
            gCcargoIdx = getCpIdx(aPidx)
            gCcargoNo = {atomIndex=gCcargoIdx}.atomno
            gNcargoIdx = getNmIdx(gCcargoIdx)
            gNcargoNo = {atomIndex=gNcargoIdx}.atomno
            
            # Set default cWard anchor at cWard N
            var iNo = gMaxNo
            for (; iNo > 0; iNo--) {
                if ({(atomno=iNo) and (chain=gChain)}.atomName == "N") {
                    break;
                }
            }
            gCanchorIdx = {(atomno=iNo) and (chain=gChain)}.atomIndex
            gCanchorNo = {atomIndex=gCanchorIdx}.atomno
            gCanchorXyz = {atomIndex=gCanchorIdx}.xyz
        }
        
        # If any anchor now inside cargo cluster, kill it
        if ({atomIndex=gCanchorIdx}.atomno <= {atomIndex=gCcargoIdx}.atomno) {
            gCanchorIdx = -1
            gCanchorNo = gMaxNo + 1
        }
        if ({atomIndex=gNanchorIdx}.atomno >= {atomIndex=gNcargoIdx}.atomno) {
            gNanchorIdx = -1
            gNanchorNo = gMinNo - 1
        }
        
        # Highlight cargo cluster
        selectNward(gCcargoIdx, gNcargoIdx)
        gCargoAtoms = {selected}
        setColors()
                             
        # Collect the rotor sets
        collectRotors()
    
        # Get connectors between fixed and moving part
        var aset = {((atomno < gCanchorNo) and (atomno > gNanchorNo)
            and (chain=gChain))}   
        gAc = (within(kCtolerance, FALSE, {aset}) and not {aset})
    }
    
    # Enable dragging
    tugEnableDrag()

    # Bind other keys
    bind "SHIFT-LEFT-CLICK" "_pickAtom";
    bind "SHIFT-LEFT-CLICK" "+:tugAnchorMB";
    
    bind "ALT-CTRL-LEFT-CLICK" "_pickAtom";
    bind "ALT-CTRL-LEFT-CLICK" "+:tugPivotMB";
    
    bind "ALT-SHIFT-LEFT-CLICK" "_pickAtom";
    bind "ALT-SHIFT-LEFT-CLICK" "+:tugDestAtomMB";
    
    select {gCargoAtoms}
}

# Top level of Tug
function plicoTug() {
    set allowModelKit TRUE
    set allowRotateSelected TRUE
    set allowMoveAtoms TRUE
    
    # Push selected
    gSelSaves = {selected}
    select all
    gZoom = script("show zoom")
    gRotate = script("show rotation")
    write tugsave.pdb
    select none
    
    gScheme = defaultColorScheme
    gAltScheme = ((gScheme == "Jmol") ? "Rasmol" : "Jmol")
    set echo TOP LEFT
    background ECHO yellow
    gEcho = "ALT-CLICK=set cargo range"
    echo @gEcho
    gCrotors = array()
    gNrotors = array()
    clearAtomIdxs()
    gChain = ""
    unbind

    set picking ON
    bind "ALT-LEFT-CLICK" "_pickAtom";
    bind "ALT-LEFT-CLICK" "+:tugCargoMB";
    bind "DOUBLE" "tugExit";
}

# Bound to DOUBLE by plicoTug
function tugExit() {
    var p = prompt("Exit tug?", "Yes|No|Undo", TRUE)
    if (p == "Undo") {
        load tugsave.pdb
        script inline gZoom
        rotate @gRotate
        echo Tug session undone
        if (gPlicoRecord != "") {
            plicoRecord("load tugsave.pdb;")
        }
    }
    if (p != "No") {
        unbind
        halo off 
        set allowMoveAtoms FALSE
        echo
        select all
        halo off
        star off
        color {selected} @gScheme
        draw gSCcircle DELETE
        gBusy = FALSE
        background ECHO yellow
        
        # Pop selected
        select gSelSaves
    }
}
# End of TUG.SPT

Contributors

Remig