Skip to main content
Chuck Uebele
Adobe Expert
October 23, 2020
Question

Changing Arrow Parameter on Line Tool

  • October 23, 2020
  • 1 reply
  • 2438 views

Photoshop 2021 Line Tool is different than previous versions. In previous versions the width of the line/arrow could be set. Now it's set with a stroke. In both the old and new versions, it was difficult to change the arrowhead shapes. You basically had to redraw them. This script will allow you to change an existing arrow and save the preferences so that you can change multiple arrows.

Use at your own risk.

 

//copyright 2020 Chuck Uebele, all rights reserved
//Use at your own risk.

var xmlFile = new File(Folder.desktop+'/ArrowPrefs2.xml');
var xmlPref

if(xmlFile.exists){
    xmlPref = readXMLFile (xmlFile)
    }
else{xmlPref = new XML('<root><prefs><arrows/></prefs>')};

var doc = activeDocument
var oldPrefs = app.preferences.rulerUnits
var oldRes = doc.resolution
changeRes (72)
app.preferences.rulerUnits = Units.POINTS;

var dlg
var lineAngTan1;//from first point to oppsite side of line
var lineAngTan2;//From oppsite side of line to first point
var lineLengthAng;
var lineLength;
var arrowWidth;
var arrowHeadMid;
var arrowHeadBase;
var arrowLength;
var concaveLength;
var arrowConcave;
var arrowConcaveFactor;
var newPointArray = [];
var dropLst = new Array()
var prefsDrop
var run = true;

try{
    var sLay = doc.activeLayer
    var sLayName = sLay.name + ' Shape Path'
    var cPath = doc.pathItems.getByName(sLayName)
    var ptArray = new Array();
    var ptNewArray = new Array();
    var idPath;

recordAnchors (cPath);
var numAnchors = ptArray.length;

    }
catch(e){
    run = false;
    var numAnchors=0
    }

if(run){main ()}

function main(){    
    
    switch (numAnchors){
        case 0:
            alert('An arrow Layer is not selected');
            break;
        case 6:
            if(Math.round (ptArray[0][0])==Math.round (ptArray[4][0])&&Math.round (ptArray[0][1])==Math.round (ptArray[4][1])){
                lineLength = lineL (ptArray[2][0], ptArray[2][1], ptArray[5][0], ptArray[5][1]);
                arrowWidth = lineL (ptArray[1][0], ptArray[1][1], ptArray[3][0], ptArray[3][1]);
                lineLengthAng = getAng (ptArray[2][0], ptArray[2][1], ptArray[5][0], ptArray[5][1]);
                lineAngTan1 = getAng (ptArray[1][0], ptArray[1][1], ptArray[3][0], ptArray[3][1]);
                lineAngTan2 = getAng (ptArray[3][0], ptArray[3][1], ptArray[1][0], ptArray[1][1]);
                arrowHeadMid = bisect (ptArray[1][0], ptArray[1][1], ptArray[3][0], ptArray[3][1]);
                arrowHeadBase = (ptArray[0][0], ptArray[0][1]);
                arrowLength = lineL (ptArray[2][0], ptArray[2][1], arrowHeadMid[0], arrowHeadMid[1]);
                concaveLength = lineL (ptArray[2][0], ptArray[2][1], ptArray[0][0], ptArray[0][1]);
                arrowConcaveFactor = concaveLength/arrowLength;
                
                ui ();
                
                //Get new points
                 newPointArray[2] = [ptArray[2][0],ptArray[2][1]];  //tip
                 newPointArray[4] = newPointArray[0] = newPt (ptArray[2][0], ptArray[2][1], -(arrowLength*arrowConcaveFactor), lineLengthAng); // arrow base
                 var tempPt = newPt (ptArray[2][0], ptArray[2][1], -(arrowLength), lineLengthAng);//temp midpoint for arrow ends
                 newPointArray[1] = newPt (tempPt[0],tempPt[1], -(arrowWidth)/2, lineAngTan2); //first arrow end
                 newPointArray[3] = newPt (tempPt[0],tempPt[1], -(arrowWidth)/2, lineAngTan1); //second arrow end
                 newPointArray[5] = newPt (ptArray[2][0], ptArray[2][1], -(lineLength), lineLengthAng);      //arrow end         
                }
            else{
                lineLength = lineL (ptArray[0][0], ptArray[0][1], ptArray[3][0], ptArray[3][1]);
                arrowWidth = lineL (ptArray[2][0], ptArray[2][1], ptArray[4][0], ptArray[4][1]);
                lineLengthAng = getAng (ptArray[0][0], ptArray[0][1], ptArray[3][0], ptArray[3][1]);
                lineAngTan1 = getAng (ptArray[2][0], ptArray[2][1], ptArray[4][0], ptArray[4][1]);
                lineAngTan2 = getAng (ptArray[4][0], ptArray[4][1], ptArray[2][0], ptArray[2][1]);
                arrowHeadMid = bisect (ptArray[2][0], ptArray[2][1], ptArray[4][0], ptArray[4][1]);
                arrowHeadBase = (ptArray[3][0], ptArray[3][1]);               
                arrowLength = lineL (ptArray[3][0], ptArray[3][1], arrowHeadMid[0], arrowHeadMid[1]);
                concaveLength = lineL (ptArray[3][0], ptArray[3][1], ptArray[1][0], ptArray[1][1]);
                arrowConcaveFactor = concaveLength/arrowLength;  
                
                ui ();
                
                //Get new points
                 newPointArray[3] = [ptArray[3][0],ptArray[3][1]];  //tip
                 newPointArray[5] = newPointArray[1] = newPt (ptArray[3][0], ptArray[3][1], (arrowLength*arrowConcaveFactor), lineLengthAng); // arrow base
                 var tempPt = newPt (ptArray[3][0], ptArray[3][1], (arrowLength), lineLengthAng);//temp midpoint for arrow ends
                 newPointArray[2] = newPt (tempPt[0],tempPt[1], -(arrowWidth)/2, lineAngTan2); //first arrow end
                 newPointArray[4] = newPt (tempPt[0],tempPt[1], -(arrowWidth)/2, lineAngTan1); //second arrow end
                 newPointArray[0] = newPt (ptArray[3][0], ptArray[3][1], (lineLength), lineLengthAng);      //arrow end              
                }
            break;
        case 10:
                lineLength = lineL (ptArray[2][0], ptArray[2][1], ptArray[7][0], ptArray[7][1]);
                arrowWidth = lineL (ptArray[1][0], ptArray[1][1], ptArray[3][0], ptArray[3][1]);
                lineLengthAng = getAng (ptArray[2][0], ptArray[2][1], ptArray[7][0], ptArray[7][1]);
                lineAngTan1 = getAng (ptArray[1][0], ptArray[1][1], ptArray[3][0], ptArray[3][1]);
                lineAngTan2 = getAng (ptArray[3][0], ptArray[3][1], ptArray[1][0], ptArray[1][1]);
                arrowHeadMid = bisect (ptArray[1][0], ptArray[1][1], ptArray[3][0], ptArray[3][1]);
                arrowHeadBase = (ptArray[0][0], ptArray[0][1]);
                arrowLength = lineL (ptArray[2][0], ptArray[2][1], arrowHeadMid[0], arrowHeadMid[1]);
                concaveLength = lineL (ptArray[2][0], ptArray[2][1], ptArray[0][0], ptArray[0][1]);
                arrowConcaveFactor = concaveLength/arrowLength;     
                
                ui ();
                
                 //Get new points
                 newPointArray[2] = [ptArray[2][0],ptArray[2][1]];  //tip1
                 newPointArray[7] = [ptArray[7][0],ptArray[7][1]];  //tip2
                 newPointArray[4] = newPointArray[0] = newPt (ptArray[2][0], ptArray[2][1], -(arrowLength*arrowConcaveFactor), lineLengthAng); // arrow base 1
                 newPointArray[5] = newPointArray[9] = newPt (ptArray[7][0], ptArray[7][1], (arrowLength*arrowConcaveFactor), lineLengthAng); // arrow base 2
                 var tempPt1 = newPt (ptArray[2][0], ptArray[2][1], -(arrowLength), lineLengthAng);//temp midpoint for arrow ends 1
                 var tempPt2 = newPt (ptArray[7][0], ptArray[7][1], (arrowLength), lineLengthAng);//temp midpoint for arrow ends 2                 
                 newPointArray[1] = newPt (tempPt1[0],tempPt1[1], -(arrowWidth)/2, lineAngTan2); //first arrow end 1
                 newPointArray[6] = newPt (tempPt2[0],tempPt2[1], -(arrowWidth)/2, lineAngTan2); //first arrow end 2
                 newPointArray[3] = newPt (tempPt1[0],tempPt1[1], -(arrowWidth)/2, lineAngTan1); //second arrow end 1
                 newPointArray[8] = newPt (tempPt2[0],tempPt2[1], -(arrowWidth)/2, lineAngTan1); //second arrow end 2
                 //newPointArray[5] = newPt (ptArray[2][0], ptArray[2][1], -(lineLength), lineLengthAng);      //arrow end  
                           
                
            break;        
        };

    var myPathInfo = mkNewPathInfo ();
    deltPath ();
    var tempPath = app.activeDocument.pathItems.add( 'temp', myPathInfo);
    selPath ();
    doc.activeLayer = sLay;
    mkPath ()
    tempPath.remove();
 
app.preferences.rulerUnits = oldPrefs;       
changeRes (oldRes)

}//end function run

function ui(){
         dlg = new Window('dialog','Edit Arrow');
            dlg.alignChildren = ['left','top'];   
                dlg.aLgp = dlg.add('group');
                    dlg.aLgp.alignChildren = ['left','top'];
                    dlg.aLgp.sTxts = dlg.aLgp.add('statictext',undefined,'Arrow Length');
                    dlg.aLgp.ALen = dlg.aLgp.add('edittext',undefined,arrowLength.toFixed (2));
                        dlg.aLgp.ALen.size = [50,12];
                    dlg.aLgp.sTxte = dlg.aLgp.add('statictext',undefined,'px');
                    
                for(var i=0;i<xmlPref.prefs.arrows.children().length();i++){
                    dropLst[i]= xmlPref.prefs.arrows.children()[i].@preset
                    }                    
                    
                dlg.aWgp = dlg.add('group');
                    dlg.aWgp.alignChildren = ['left','top'];
                    dlg.aWgp.sTxts = dlg.aWgp.add('statictext',undefined,'Arrow Width');
                    dlg.aWgp.AWid = dlg.aWgp.add('edittext',undefined,arrowWidth.toFixed (2));
                        dlg.aWgp.AWid.size = [50,12];
                    dlg.aWgp.sTxte = dlg.aWgp.add('statictext',undefined,'px');     
                    
                dlg.aConGp = dlg.add('group');
                    dlg.aConGp.alignChildren = ['left','top'];
                    dlg.aConGp.sTxts = dlg.aConGp.add('statictext',undefined,'Arrow Concave');
                    dlg.aConGp.ACon = dlg.aConGp.add('edittext',undefined,((1-arrowConcaveFactor)*100).toFixed (2));
                        dlg.aConGp.ACon.size = [50,12];
                    dlg.aConGp.sTxte = dlg.aConGp.add('statictext',undefined,'%');      
                    
        dlg.btnGp = dlg.add('group');
        dlg.btnGp.okay = dlg.btnGp.add('button',undefined,'Okay');
        dlg.btnGp.cancel = dlg.btnGp.add('button',undefined,'Cancel');
        
        dlg.prefsPn = dlg.add('panel',undefined,'Preferences');
            dlg.prefsPn.alignChildren = ['left','top'];
            dlg.prefsPn.save = dlg.prefsPn.add('button',undefined,'Save Preference');
            dlg.prefsPn.del = dlg.prefsPn.add('button',undefined,'Delete Preference');
            
            prefsDrop = dlg.prefsPn.add('dropdownlist',undefined,dropLst);
            prefsDrop.title = 'Load Preferences';            
            prefsDrop.size = [300,25];           
            
        prefsDrop.onChange = function(){
                dlg.aLgp.ALen.text = xmlPref.prefs.arrows.children()[parseInt(prefsDrop.selection)].leng
                dlg.aWgp.AWid.text = xmlPref.prefs.arrows.children()[parseInt(prefsDrop.selection)].aWidth
                dlg.aConGp.ACon.text = xmlPref.prefs.arrows.children()[parseInt(prefsDrop.selection)].concave
            };
        
        dlg.aLgp.ALen.onChange = function(){
            textToNum (1, 10000, dlg.aLgp.ALen, arrowLength);
            }       
        
        dlg.aWgp.AWid.onChange = function(){
            textToNum (1, 50000, dlg.aWgp.AWid, arrowWidth);
            } 
        
        dlg.aConGp.ACon.onChange = function(){
            textToNum (-50, 50, dlg.aConGp.ACon, arrowConcaveFactor);
            }    

            
        dlg.prefsPn.save.onClick = function(){
            savePrefs ();

                }
            
            dlg.prefsPn.del.onClick = function(){
                if(prefsDrop.selection != null){
                    var selc = parseInt(prefsDrop.selection);                    
                    prefsDrop.remove(selc);
                    delete xmlPref.prefs.arrows.children()[selc];
                    writeXMLFile (xmlFile, xmlPref)
                    }
                }                        
                                
                    
        dlg.btnGp.okay.onClick = function(){
            dlg.close();
                arrowLength = parseInt (dlg.aLgp.ALen.text);
                arrowWidth = parseInt (dlg.aWgp.AWid.text);
                arrowConcaveFactor = (100 - parseInt (dlg.aConGp.ACon.text))*.01;
                }

        dlg.btnGp.cancel.onClick = function(){            
            run = false;
            dlg.close()
            }                      
            
    dlg.show();
    }

function textToNum(sMin,sMax,e,def){
    def=Math.round (def)
	var sHold = def;
			if(isNaN(parseInt(e.text))){
			alert('"' + e.text + '" is not a number\nEnter a value between ' + sMin + '-' + sMax );
			e.text = def};
            else{
                sHold = parseInt(e.text)
			if(sHold < sMin){
				rangeAlert();
				sHold = sMin;				
				};
			if(sHold > sMax){
				rangeAlert();
				sHold = sMax;
				};
			e.text = sHold;
            }
	function rangeAlert(){alert('Number range must be between ' + sMin + '-' + sMax)}; 
};//end function textToNum
    

function recordAnchors(pathObj){
    ptArray = new Array();
    for(var i=0;i<pathObj.subPathItems[0].pathPoints.length;i++){
        ptNewArray [i] = ptArray[i] = pathObj.subPathItems[0].pathPoints[i].anchor;
        }//end loop
    }//end function

function lineL (ax,ay,bx,by){
    var x = Math.abs (ax-bx);
    var y = Math.abs (ay-by);
    var len = Math.sqrt (Math.pow (x, 2) + Math.pow (y, 2));
    return len
    }

function getAng(ax,ay,bx,by){
    var dx = ax-bx;
    var dy = ay-by;
    var theta = Math.atan2 (dy, dx);
    //theta *= 180/Math.PI
    return theta;
    }

function newPt (x1,y1,rad,ang){
    var newEnd = new Array();
    newEnd[0] = x1 + rad * Math.cos (ang);
    newEnd[1] = y1 + rad * Math.sin (ang);
    return newEnd;
    }

function bisect(x1,y1,x2,y2){
    var midPoint = new Array();
    midPoint[0] = (x1+x2)/2;
    midPoint[1] = (y1+y2)/2;
    return midPoint;
    }

/////////////////////
    function extractSubPathInfo(pathObj,oldPoint,newPoint){
        var pathArray = new Array();
        var pl = pathObj.subPathItems.length;
        for(var s=0;s<pl;s++){
            var pArray = new Array();
              for(var i=0;i<pathObj.subPathItems[s].pathPoints.length;i++){
                 pArray[i] = new PathPointInfo;
                 pArray[i].kind = pathObj.subPathItems[s].pathPoints[i].kind;
             if(pathObj.subPathItems[s].pathPoints[i].anchor[0]==oldPoint[0] &&
                   pathObj.subPathItems[s].pathPoints[i].anchor[1]==oldPoint[1]){
                pArray[i].anchor = newPoint;
                pArray[i].leftDirection = newPoint;
                pArray[i].rightDirection = newPoint;
             }else{
                pArray[i].anchor = pathObj.subPathItems[s].pathPoints[i].anchor;
                pArray[i].leftDirection = pathObj.subPathItems[s].pathPoints[i].leftDirection;
                pArray[i].rightDirection = pathObj.subPathItems[s].pathPoints[i].rightDirection;
             }
              };
            pathArray[pathArray.length] = new Array();
            pathArray[pathArray.length - 1] = new SubPathInfo();
            pathArray[pathArray.length - 1].operation = pathObj.subPathItems[s].operation;
            pathArray[pathArray.length - 1].closed = pathObj.subPathItems[s].closed;
            pathArray[pathArray.length - 1].entireSubPath = pArray;
        };
        return pathArray;
    };
        
    function deltPath(){
        var idDlt = charIDToTypeID( "Dlt " );
        var desc2 = new ActionDescriptor();
        var idnull = charIDToTypeID( "null" );
            var ref1 = new ActionReference();
            idPath = charIDToTypeID( "Path" );
            var idvectorMask = stringIDToTypeID( "vectorMask" );
            ref1.putEnumerated( idPath, idPath, idvectorMask );
            var idLyr = charIDToTypeID( "Lyr " );
            var idOrdn = charIDToTypeID( "Ordn" );
            var idTrgt = charIDToTypeID( "Trgt" );
            ref1.putEnumerated( idLyr, idOrdn, idTrgt );
        desc2.putReference( idnull, ref1 );
        executeAction( idDlt, desc2, DialogModes.NO );
        }
    
    function selPath(){
         var idslct = charIDToTypeID( "slct" );
            var desc2 = new ActionDescriptor();
            var idnull = charIDToTypeID( "null" );
                var ref1 = new ActionReference();
                var idPath = charIDToTypeID( "Path" );
                ref1.putName( idPath, "temp" );
            desc2.putReference( idnull, ref1 );
        executeAction( idslct, desc2, DialogModes.NO );       
        }
    
    function mkPath(){
         var idMk = charIDToTypeID( "Mk  " );
            var desc10 = new ActionDescriptor();
            var idnull = charIDToTypeID( "null" );
                var ref3 = new ActionReference();
                var idPath = charIDToTypeID( "Path" );
                ref3.putClass( idPath );
            desc10.putReference( idnull, ref3 );
            var idAt = charIDToTypeID( "At  " );
                var ref4 = new ActionReference();
                var idPath = charIDToTypeID( "Path" );
                var idPath = charIDToTypeID( "Path" );
                var idvectorMask = stringIDToTypeID( "vectorMask" );
                ref4.putEnumerated( idPath, idPath, idvectorMask );
            desc10.putReference( idAt, ref4 );
            var idUsng = charIDToTypeID( "Usng" );
                var ref5 = new ActionReference();
                var idPath = charIDToTypeID( "Path" );
                var idOrdn = charIDToTypeID( "Ordn" );
                var idTrgt = charIDToTypeID( "Trgt" );
                ref5.putEnumerated( idPath, idOrdn, idTrgt );
            desc10.putReference( idUsng, ref5 );
        executeAction( idMk, desc10, DialogModes.NO );       
        }
    
function mkNewPathInfo(){
    var pathArray = new Array();
    var piArray = new Array();
    for(var i=0;i<newPointArray.length;i++){
         piArray[i] = new PathPointInfo;
         piArray[i].kind = PointKind.CORNERPOINT; 
         piArray[i].anchor = newPointArray[i];
         piArray[i].leftDirection = newPointArray[i];
         piArray[i].rightDirection = newPointArray[i];
        }//end loop
    pathArray[0] = new SubPathInfo();
    pathArray[0].operation = ShapeOperation.SHAPEXOR;
    pathArray[0].closed = true;
    pathArray[0].entireSubPath = piArray;  
    return pathArray;
    };//end function      

function changeRes(res){
        var idImgS = charIDToTypeID( "ImgS" );
        var desc15 = new ActionDescriptor();
        var idRslt = charIDToTypeID( "Rslt" );
        var idRsl = charIDToTypeID( "#Rsl" );
        desc15.putUnitDouble( idRslt, idRsl, res );
    executeAction( idImgS, desc15, DialogModes.NO );
    }

function savePrefs(){
    var newName = prompt ('Enter New Profile Name', '', 'Save Preferences');
    var prefGood = true;
    for(var i=0;i<prefsDrop.items.length;i++){
        if(prefsDrop.items[i].text == newName){
            newName = prompt ('That preset exists\nPlease choose a unique name','', 'Save Preferences' );
            i=0
            }
        }
        prefsDrop.add('item',newName)  
xmlPref.prefs.arrows.appendChild (XML('<arrow preset="'+ newName+ '"><leng>'+dlg.aLgp.ALen.text+'</leng><aWidth>'+dlg.aWgp.AWid.text+'</aWidth><concave>'+dlg.aConGp.ACon.text+'</concave></arrow>'));
        writeXMLFile (xmlFile, xmlPref) 

    }

//===============READ/WRITE functions========================================
//=========================================================================
 function readXMLFile(file) {
	if (!file.exists) {
		alert( "Cannot find file: " + deodeURI(file.absoluteURI));
		}
	else{
		file.encoding = "UTF8";
		file.lineFeed = "unix";
		file.open("r", "TEXT", "????");
		var str = file.read();
		file.close();

		return new XML(str);
		};
};

function writeXMLFile(file, xml) {
	if (!(xml instanceof XML)) {
		alert( "Bad XML parameter");
		};
	else{
		file.encoding = "UTF8";
		file.open("w", "TEXT", "????");
		//unicode signature, this is UTF16 but will convert to UTF8 "EF BB BF"
		file.write("\uFEFF");
		file.lineFeed = "unix";
		file.write(xml.toXMLString());
		file.close();
		};
	};
 //=========================================================================

 

This topic has been closed for replies.

1 reply

JJMack
Adobe Expert
October 23, 2020

IMO the change to the line tool in 2021 is a disaster. Chuck can you create a visible line shape layer. Adobe might as well disable disable shape option as well as  pixel.  I get  no stroke with the line shape tool do you get one?.

JJMack
Chuck Uebele
Adobe Expert
October 23, 2020

JJ, yes, I get a stroke. 

Inspiring
May 17, 2021

It seems like I have a problem. I delete all preferences and now I do get a stroke.  It still I bummer I can set not arrows  a percentage of line weight,.


Under Windows 10, Photoshop CC allows the user to set the width and length of a line's arrowhead by percentage (%), but under macOS one has to constantly calculate appropriate widths and lengths as one changes the line's width. At least under macOS one can now specify a line width with pixels, something that was missing for no practical reason for a long time, too.

 

Without percentages, this is incredibly tedious - and it worked for two decades in Photoshop CS and older. Anyone who marks up images with lines with arrowheads needs that feature back.

 

Please update!