Custom rounded rectangle function

Adobe Community Professional ,
Jul 18, 2022 Jul 18, 2022

Copy link to clipboard

Copied

Ok... So just like any petulant, stubborn child.... I don't like being told what I can't do... And  I've decided i'm no longer going to simply accept that we can't set our own custom corner radii when creating a roundedRectangle... Only being able to change "horizontalRadius" and "verticalRadius" isn't enough, because unless you use the same value for each, the corners will not really be round and the shape won't resemble a rounded rectangle, but rather a bloated and/or stretched rounded rectangle.

 

To that end... Here's a function i just threw together that lets you create a rectangle at whatever dimensions you want, and lets you pass in an array of "corner radii" listed clockwise from top left to bottom left. So, if you need a rectangle at [0,0] and 200pt wide and 400pt tall with rounded corners of the radii [20,10,25,50] for whatever reason, you could create one with this function call:

 

 

 

customRoundedRectangle(app.activeDocument, 0,0,200,400,[20,10,25,50]);

 

 

 

the first argument is the "parent" object. The container in which you want the rectangle to be created. This could be the document, a layer, a group, a compound path.. (though i think this will need some refinement to properly handle making something inside a clipping mask.. otherwise the pathfinder logic won't work properly, i suspect.. I'll get to it). The next 4 arguments are the standard rectangle arguments (top, left, width, height). Then the last argument is the array of corner radii, again sorted clockwise from top left.

 

Here's the code:

 

 

 

//Custom Rounded Rectangle Generator
//The built in roundedRectangle() method only allows for changing
//"verticalRadius" and "horizontalRadius".. But if you use different
//values for these, the corners are not truly round... they'll be asymptotic
//and the values given will apply to both left and right sides or top and bottom
//so you can't change the corners individually
.
//This customRoundedRectangle() function allows for each corner to have a separate
//corner radius that is truly rounded and maintains the integrity of the rectangle's existing side

//author: William Dowling
//email: illustrator.dev.pro@gmail.com
//github: https://www.github.com/wdjsdev
    //this project on githuh: https://github.com/wdjsdev/public_illustrator_scripts/blob/master/custom_rounded_rectangle.js
//linkedin: https://www.linkedin.com/in/william-dowling-4537449a/
//Adobe Discussion Forum Post that initiated this: https://community.adobe.com/t5/illustrator-discussions/rounded-rectangle/td-p/13076613

//*******//

//Did you find this useful? Would you like to buy me a cup (or a pot) of coffee to say thanks?
//paypal.me/illustratordev
//<3

//Do you have some work to do, but you have more money than time/skill?
//Send me an email or a dm! I'll see what we can do to help each other..

//*******//

//arguments:
//parent: the parent container object inside which to create the rectangle 
//y: y (top) coordinate of the rectangle (Number, in points)
//x: x (left) coordinate of the rectangle  (Number, in points)
//w: width of the rectangle (Number, in points)
//h: height of the rectangle (Number, in points)
//r: array of radii clockwise from top left corner (array)
function customRoundedRectangle(parent, y, x, w, h, r) {

    ///////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////
    //function calls//
    ///////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////
    if (!app.documents.length) {
        alert("Please open a document and try again.");
        return;
    }

    function makeRect()
    {
        var doc = app.activeDocument;
        var swatches = doc.swatches;
        var blackSwatch;
        try {
            blackSwatch = swatches["Black"];
        } catch (error) {
            blackSwatch = swatches.add();
            blackSwatch.name = "Black";
            blackSwatch.color = new CMYKColor();
            blackSwatch.color.black = 100;
        }

        //draw the base rectangle
        var rect = parent.pathItems.rectangle(y, x, w, h);
        rect.filled = true;
        rect.fillColor = blackSwatch.color;
        rect.stroked = false;
        var rectData = { w: rect.width, h: rect.height, l: rect.left, t: rect.top, r: rect.left + rect.width, b: rect.top - rect.height };
        var cp = parent.compoundPathItems.add();
        //create an ellipse for each radius
        r.forEach(function (radius, i) {
            var rad = radius * 2;
            var ellipse = cp.pathItems.ellipse(y, x, rad, rad);
            ellipse.filled = true;
            ellipse.fillColor = blackSwatch.color;
            ellipse.stroked = false;
            switch (i) {
                case 1:
                    ellipse.left += rectData.w - rad;
                    break;
                case 2:
                    ellipse.left += rectData.w - rad;
                    ellipse.top -= rectData.h - rad;
                    break;
                case 3:
                    ellipse.top -= rectData.h - rad;
            }
        });

        //load pathfinder actions into actions panel
        createAction("pathfinder", getActionString());

        doc.selection = null;
        cp.selected = rect.selected = true;
        app.doScript("divide", "pathfinder");

        //look for the "corner"s of the rectangle...
        //the part that's outside our desired radius
        afc(doc.selection[0], "pageItems").forEach(function (item) {
            var area = Math.abs(item.area);
            var calcArea = item.width * item.height;
            if (area < (calcArea * .6)) {
                item.remove();
            }
        });

        //all that's left should be the base rectangle and
        //the ellipses for the corner radii. unite them all
        //together into a single pathItem.
        app.doScript("unite", "pathfinder");

        //remove pathfinder actions from actions panel
        removeAction("pathfinder");

        return app.selection[0];

        //end of script

    }
        








    


    ///////////////////////////////////////////////////////////////////////////
    //dependencies:
    ///////////////////////////////////////////////////////////////////////////



    //array from container
    //get all elements of the type "crit" inside the container
    //and return as a standard javascript array
    function afc(container, crit) {
        var result = [];
        var items;
        if (!crit || crit === "any") {
            items = container.pageItems;
        } else {
            items = container[crit];
        }
        for (var x = 0; x < items.length; x++) {
            result.push(items[x])
        }
        return result;
    }

    //Array.forEach() prototype for easily looping arrays
    //this helps eliminate scope issues because variables
    //remain contained inside the function block. almost
    //kind of imitating "let" in modern javascript.
    //Plus, you don't need to worry about pre-declaring
    //variables outside the loop to prevent mrap/parm errors.
    //args:
    //callback: the function to call for each element
    //startPos: the index to start at (defaults to 0)
    //inc: the increment to move by (defaults to 1)
    Array.prototype.forEach = function (callback, startPos, inc) {
        if (!inc) inc = 1;
        if (!startPos) startPos = 0;
        for (var i = startPos; i < this.length; i += inc)
            callback(this[i], i, this);
    };


    //create and load a new action
    function createAction(name, actionString) {
        var documentsPath = "~/Documents/Adobe_Script_Helpers/";
        var dest = Folder(documentsPath);
        if (!dest.exists) {
            dest.create();
        }

        var actionFile = new File(decodeURI(dest + "/" + name + ".aia"));

        actionFile.open("w");
        actionFile.write(actionString);
        actionFile.close();

        //load the action
        app.loadAction(actionFile);
    }


    //remove all instances of an action with a given name
    function removeAction(actionName) {
        var localValid = true;

        while (localValid) {
            try {
                app.unloadAction(actionName, "");
            }
            catch (e) {
                localValid = false;
            }
        }
    }


    function getActionString() {
        //pathfinder action string. this will create an
        //action set containing all of the pathfinder options.
        //this is much more reliable than app.executeMenuCommand()
        //which uses "live pathfinder" effects which don't always work
        //and need to be expanded.
        var pathfinderActionString = [
            "/version 3",
            "/name [ 10",
            "	7061746866696e646572",
            "]",
            "/isOpen 1",
            "/actionCount 10",
            "/action-1 {",
            "	/name [ 5",
            "		756e697465",
            "	]",
            "	/keyIndex 0",
            "	/colorIndex 0",
            "	/isOpen 0",
            "	/eventCount 1",
            "	/event-1 {",
            "		/useRulersIn1stQuadrant 0",
            "		/internalName (ai_plugin_pathfinder)",
            "		/localizedName [ 10",
            "			5061746866696e646572",
            "		]",
            "		/isOpen 0",
            "		/isOn 1",
            "		/hasDialog 0",
            "		/parameterCount 1",
            "		/parameter-1 {",
            "			/key 1851878757",
            "			/showInPalette 4294967295",
            "			/type (enumerated)",
            "			/name [ 3",
            "				416464",
            "			]",
            "			/value 0",
            "		}",
            "	}",
            "}",
            "/action-2 {",
            "	/name [ 11",
            "		6d696e75735f66726f6e74",
            "	]",
            "	/keyIndex 0",
            "	/colorIndex 0",
            "	/isOpen 0",
            "	/eventCount 1",
            "	/event-1 {",
            "		/useRulersIn1stQuadrant 0",
            "		/internalName (ai_plugin_pathfinder)",
            "		/localizedName [ 10",
            "			5061746866696e646572",
            "		]",
            "		/isOpen 0",
            "		/isOn 1",
            "		/hasDialog 0",
            "		/parameterCount 1",
            "		/parameter-1 {",
            "			/key 1851878757",
            "			/showInPalette 4294967295",
            "			/type (enumerated)",
            "			/name [ 8",
            "				5375627472616374",
            "			]",
            "			/value 3",
            "		}",
            "	}",
            "}",
            "/action-3 {",
            "	/name [ 9",
            "		696e74657273656374",
            "	]",
            "	/keyIndex 0",
            "	/colorIndex 0",
            "	/isOpen 0",
            "	/eventCount 1",
            "	/event-1 {",
            "		/useRulersIn1stQuadrant 0",
            "		/internalName (ai_plugin_pathfinder)",
            "		/localizedName [ 10",
            "			5061746866696e646572",
            "		]",
            "		/isOpen 0",
            "		/isOn 1",
            "		/hasDialog 0",
            "		/parameterCount 1",
            "		/parameter-1 {",
            "			/key 1851878757",
            "			/showInPalette 4294967295",
            "			/type (enumerated)",
            "			/name [ 9",
            "				496e74657273656374",
            "			]",
            "			/value 1",
            "		}",
            "	}",
            "}",
            "/action-4 {",
            "	/name [ 7",
            "		6578636c756465",
            "	]",
            "	/keyIndex 0",
            "	/colorIndex 0",
            "	/isOpen 1",
            "	/eventCount 1",
            "	/event-1 {",
            "		/useRulersIn1stQuadrant 0",
            "		/internalName (ai_plugin_pathfinder)",
            "		/localizedName [ 10",
            "			5061746866696e646572",
            "		]",
            "		/isOpen 0",
            "		/isOn 1",
            "		/hasDialog 0",
            "		/parameterCount 1",
            "		/parameter-1 {",
            "			/key 1851878757",
            "			/showInPalette 4294967295",
            "			/type (enumerated)",
            "			/name [ 7",
            "				4578636c756465",
            "			]",
            "			/value 2",
            "		}",
            "	}",
            "}",
            "/action-5 {",
            "	/name [ 6",
            "		646976696465",
            "	]",
            "	/keyIndex 0",
            "	/colorIndex 0",
            "	/isOpen 1",
            "	/eventCount 1",
            "	/event-1 {",
            "		/useRulersIn1stQuadrant 0",
            "		/internalName (ai_plugin_pathfinder)",
            "		/localizedName [ 10",
            "			5061746866696e646572",
            "		]",
            "		/isOpen 0",
            "		/isOn 1",
            "		/hasDialog 0",
            "		/parameterCount 1",
            "		/parameter-1 {",
            "			/key 1851878757",
            "			/showInPalette 4294967295",
            "			/type (enumerated)",
            "			/name [ 6",
            "				446976696465",
            "			]",
            "			/value 5",
            "		}",
            "	}",
            "}",
            "/action-6 {",
            "	/name [ 4",
            "		7472696d",
            "	]",
            "	/keyIndex 0",
            "	/colorIndex 0",
            "	/isOpen 1",
            "	/eventCount 1",
            "	/event-1 {",
            "		/useRulersIn1stQuadrant 0",
            "		/internalName (ai_plugin_pathfinder)",
            "		/localizedName [ 10",
            "			5061746866696e646572",
            "		]",
            "		/isOpen 0",
            "		/isOn 1",
            "		/hasDialog 0",
            "		/parameterCount 1",
            "		/parameter-1 {",
            "			/key 1851878757",
            "			/showInPalette 4294967295",
            "			/type (enumerated)",
            "			/name [ 4",
            "				5472696d",
            "			]",
            "			/value 7",
            "		}",
            "	}",
            "}",
            "/action-7 {",
            "	/name [ 5",
            "		6d65726765",
            "	]",
            "	/keyIndex 0",
            "	/colorIndex 0",
            "	/isOpen 1",
            "	/eventCount 1",
            "	/event-1 {",
            "		/useRulersIn1stQuadrant 0",
            "		/internalName (ai_plugin_pathfinder)",
            "		/localizedName [ 10",
            "			5061746866696e646572",
            "		]",
            "		/isOpen 0",
            "		/isOn 1",
            "		/hasDialog 0",
            "		/parameterCount 1",
            "		/parameter-1 {",
            "			/key 1851878757",
            "			/showInPalette 4294967295",
            "			/type (enumerated)",
            "			/name [ 5",
            "				4d65726765",
            "			]",
            "			/value 8",
            "		}",
            "	}",
            "}",
            "/action-8 {",
            "	/name [ 4",
            "		63726f70",
            "	]",
            "	/keyIndex 0",
            "	/colorIndex 0",
            "	/isOpen 1",
            "	/eventCount 1",
            "	/event-1 {",
            "		/useRulersIn1stQuadrant 0",
            "		/internalName (ai_plugin_pathfinder)",
            "		/localizedName [ 10",
            "			5061746866696e646572",
            "		]",
            "		/isOpen 0",
            "		/isOn 1",
            "		/hasDialog 0",
            "		/parameterCount 1",
            "		/parameter-1 {",
            "			/key 1851878757",
            "			/showInPalette 4294967295",
            "			/type (enumerated)",
            "			/name [ 4",
            "				43726f70",
            "			]",
            "			/value 9",
            "		}",
            "	}",
            "}",
            "/action-9 {",
            "	/name [ 7",
            "		6f75746c696e65",
            "	]",
            "	/keyIndex 0",
            "	/colorIndex 0",
            "	/isOpen 1",
            "	/eventCount 1",
            "	/event-1 {",
            "		/useRulersIn1stQuadrant 0",
            "		/internalName (ai_plugin_pathfinder)",
            "		/localizedName [ 10",
            "			5061746866696e646572",
            "		]",
            "		/isOpen 0",
            "		/isOn 1",
            "		/hasDialog 0",
            "		/parameterCount 1",
            "		/parameter-1 {",
            "			/key 1851878757",
            "			/showInPalette 4294967295",
            "			/type (enumerated)",
            "			/name [ 7",
            "				4f75746c696e65",
            "			]",
            "			/value 6",
            "		}",
            "	}",
            "}",
            "/action-10 {",
            "	/name [ 10",
            "		6d696e75735f6261636b",
            "	]",
            "	/keyIndex 0",
            "	/colorIndex 0",
            "	/isOpen 1",
            "	/eventCount 1",
            "	/event-1 {",
            "		/useRulersIn1stQuadrant 0",
            "		/internalName (ai_plugin_pathfinder)",
            "		/localizedName [ 10",
            "			5061746866696e646572",
            "		]",
            "		/isOpen 0",
            "		/isOn 1",
            "		/hasDialog 0",
            "		/parameterCount 1",
            "		/parameter-1 {",
            "			/key 1851878757",
            "			/showInPalette 4294967295",
            "			/type (enumerated)",
            "			/name [ 10",
            "				4d696e7573204261636b",
            "			]",
            "			/value 4",
            "		}",
            "	}",
            "}"
        ].join("\n");

        return pathfinderActionString;
    }


    //this is the function call that actually
    //does the work. I wrapped the logic in a function
    //to avoid variable scope issues and accessing
    //Array.forEach before it was defined.
    //But i still want the function logic to be at the
    //top of the file so it's easier to read and
    //the dependencies at the bottom and out of the way. 
    makeRect();
}

//sample function call:
// customRoundedRectangle(app.activeDocument, 0, 0, 200, 400, [20, 10, 25, 50]);

 

 

 

 

TOPICS
Scripting , Tools

Views

140

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Adobe Community Professional ,
Jul 19, 2022 Jul 19, 2022

Copy link to clipboard

Copied

Hi William, thanks for sharing!

 

I've got an error though, r.forEach is not a function on line 67. Moving the function definition above the function call takes care of the error. I don't remember having to do that before. Can you restart illustrator and give it another try? 

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Adobe Community Professional ,
Jul 19, 2022 Jul 19, 2022

Copy link to clipboard

Copied

hmmm. strange..? I thought javascript treated functions as priority and "hoists" them to the top when the interpreter runs regardless of where it is in the code..

 

I've never had that problem before... (definitely had it with variables.. but not functions..) perhaps prototypes work differently than regular functions? I'll reorganize it and update the post and the github page.

 

Thanks for letting me know. 😃

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Adobe Community Professional ,
Jul 19, 2022 Jul 19, 2022

Copy link to clipboard

Copied

Ok. I wrapped up the main logic in a function and then placed that function call at the bottom to ensure everything is declared and defined before the logic runs.

 

I wanted to keep the main logic at the top for readability, and then just kind of bury the dependencies below. In my own workflow, all those dependencies exist in a master "utilities container" that i include in every script, so these kinds of scope issues are never a problem because all this logic simply gets added to the beginning of every script. But unfortunately that's not a super portable solution, so I combined everything so this function would be self sufficient. And now i learned something new. haha. or relearned something i learned before and forgot becasue it didn't affect me. 

 

I appreciate you, Carlos.

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Adobe Community Professional ,
Jul 19, 2022 Jul 19, 2022

Copy link to clipboard

Copied

yeah that's what I thought, that functions could be anywhere but I guess not. I think functions inside functions don't follow that rule?

 

now we know I think 🙂

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Adobe Community Professional ,
Jul 19, 2022 Jul 19, 2022

Copy link to clipboard

Copied

Hmm. ok. Some quick google fu and a bit of reading has properly edified me. functions and variables are hoisted at runtime... expressions and properties are not. 

 

I guess technically Array.prototype.forEach = function(){//do thing}; is a PROPERTY of the array object, even though it looks/acts like a function.

 

https://stackoverflow.com/questions/43029490/property-added-to-a-js-object-as-a-prototype-is-hoisted...

 

If i had instead just created a standalone function like this: function forEach(array,myFunc){for(items in array){myFunc(cur item from array)}} then it would be properly hoisted and preclude this type of error. But personally i prefer the prototype and i have no issue using wrapper functions to avoid these situations. Plus, that's an extra argument i gotta type out. who's got time for that?! 😜 (the answer to this question? github copilot... wow.)

 

I have several other improvements to this on the way as well. default values available so that every argument is actually optional. argument sanitization to ensure no runtime errors when the main logic runs. a temporary sandbox layer to use for creating the shapes and doing all the pathfinder stuff just to ensure we don't accidentally mess with any existing artwork. plus a tiny testing framework for experimenting with any possible arguments.

 

stay tuned

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Advisor ,
Jul 19, 2022 Jul 19, 2022

Copy link to clipboard

Copied

I think the problem was that forEach is a not global function, but a property of the prototype object. And like any property, it will not work before it is defined.  That is why polyfills are best put at the top. 

 

var object1 = {};
object1.function1 = function () {alert("");};
object1.function1();  // will work

var object2 = {};
object2.function1();  // will not work
object2.function1 = function () {alert("");};

 

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Adobe Community Professional ,
Jul 19, 2022 Jul 19, 2022

Copy link to clipboard

Copied

exactly right. prototype functions are properties and not functions as i assumed. But i think your example is a better demonstration of the fact that expressions aren't hoisted. writing functions like you've done here makes them technically expressions instead of properties or functions.

 

I intentionally wanted to bury the dependencies so that when you open the code, the meaningful code is right there at the top. I know i find it frustrating when I open up some code i didn't write and i have to search around to figure out where the actual logic occurs. 

 

Using a simple wrapper function for the main logic and putting the function call at the end, after all the dependencies, did the trick nicely and i still get my way in terms of having the main logic front and center.

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Advisor ,
Jul 19, 2022 Jul 19, 2022

Copy link to clipboard

Copied

They are assignment expressions or expression statements.  But I know what you mean.  The only things that gets hoisted is a declaration statement.  I.e.

var a;
function b() {};

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Adobe Community Professional ,
Aug 09, 2022 Aug 09, 2022

Copy link to clipboard

Copied

Hi @DilliamWowling, just saw this. Thanks for sharing! Great script.

- Mark

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Adobe Community Professional ,
Aug 09, 2022 Aug 09, 2022

Copy link to clipboard

Copied

LATEST
Hey, thanks Mark. I appreciate your kind words. I have more improvements in
the pipeline. But unfortunately (or fortunately depending on how you look
at it) at the moment I have more freelance stuff than I know what to do
with. So I gotta prioritize the paid gigs over the handouts.😋

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines