Skip to main content
Participant
September 25, 2026
Question

Allow option in radial repeat, to have each instance remain upright, like numerals on a clock, rather than rotate, so object in the 6:00 position isn't upside down

  • September 25, 2026
  • 5 replies
  • 35 views

Is there an option in radial repeat, to have each instance remain upright, like numerals on a clock, rather than rotate, so object in the 6:00 position isn't upside down

    5 replies

    m1b
    Community Expert
    Community Expert
    September 26, 2026

    ​@ronn57685899 I have a script that should help. It’s called “Unrotate Selected Items 2.js”. I tested it and it worked, but you have to “expand” the radial repeat first.

    I’ll put the code here as well because the original thread is crowded:

    /**
    * @file Unrotate Selected Items 2.js
    *
    * Makes an attempt to remove any recorded rotation
    * that can be derived from the selected items.
    * Finds rotation either in the item's BBAccumRotation
    * tag or the item's matrix.
    *
    * @author m1b
    * @version 2025-02-17
    * @discussion https://community.adobe.com/t5/illustrator-discussions/script-to-return-or-restore-rotation-of-multiple-selected-objects/m-p/15146692
    */
    (function () {

    var doc = app.activeDocument;

    var items = getItems({
    from: doc.selection,
    getPageItems: true,
    getGroupItems: true,
    });

    if (0 === items.length)
    return alert('Please select some items to rotate and try again.');

    for (var i = 0; i < items.length; i++)
    unrotate2(items[i]);

    })();

    /**
    * Attempt to return a page item to unrotated state.
    * Modified by renél80416020 to apply rotation
    * to group items and compound path items correctly.
    * Important: will only work if the item has a valid
    * BBAccumRotation tag or a matrix that is ammenable
    * to derivation of the rotation.
    * @author m1b and renél80416020
    * @version 2025-02-14
    * @param {PageItem} item - the item to unrotate.
    * @return {Number} - the amount of rotation, in degrees.
    */
    function unrotate2(item) {

    var grp = false, trs = false;

    var accumRotation = undefined,
    rotationTag;

    if (item.hasOwnProperty('tags')) {

    if (item.typename == "GroupItem" || item.typename == "CompoundPathItem") {
    if (item.typename == "CompoundPathItem") {
    rotationTag = getThing(item.pathItems[0].tags, 'name', 'BBAccumRotation');
    trs = true;
    }

    else {
    rotationTag = getThing(item.pageItems[0].tags, 'name', 'BBAccumRotation');
    grp = true;
    }
    }
    else rotationTag = getThing(item.tags, 'name', 'BBAccumRotation');

    if (rotationTag)
    accumRotation = rotationTag.value * 180 / Math.PI;
    }

    if (
    undefined == accumRotation
    && item.hasOwnProperty('matrix')
    )
    // derive rotation from the matrix
    accumRotation = getRotationFromMatrix(item.matrix);

    if (undefined == accumRotation)
    return;

    // rotate the item
    item.rotate(- accumRotation);

    if (rotationTag) {
    // update tag
    var pi;

    if (grp)
    for (var i = 0; i < item.pageItems.length; i++) {
    pi = item.pageItems[i];
    rotationTag = getThing(pi.tags, 'name', 'BBAccumRotation');
    if (rotationTag != undefined) { rotationTag.value = 0; }
    }

    if (trs) {
    pi = item.pathItems[0];
    rotationTag = getThing(pi.tags, 'name', 'BBAccumRotation');
    if (rotationTag != undefined) { rotationTag.value = 0; }
    rotationTag = getThing(item.tags, 'name', 'BBAccumRotation');
    if (rotationTag != undefined) { rotationTag.value = 0; }
    }

    if (!trs && !grp)
    rotationTag.value = 0;

    }

    };

    /**
    * Returns a thing with matching property.
    * If `key` is undefined, evaluate the object itself.
    * @author m1b
    * @version 2024-04-21
    * @param {Array|Collection} things - the things to look through.
    * @param {String} [key] - the property name (default: undefined).
    * @param {*} value - the value to match.
    */
    function getThing(things, key, value) {

    for (var i = 0, obj; i < things.length; i++)
    if ((undefined == key ? things[i] : things[i][key]) == value)
    return things[i];

    };

    /**
    * Returns the rotation amount, in degrees,
    * of the given (not skewed) matrix.
    * @author m1b
    * @version 2023-10-20
    * @param {Matrix} matrix - an Illustrator Matrix.
    * @returns {Number}
    */
    function getRotationFromMatrix(matrix) {

    if (!matrix.hasOwnProperty('mValueA'))
    throw new Error('getRotationFromMatrix: bad `matrix` supplied.');

    // scaling factors
    var scaleX = Math.sqrt(matrix.mValueA * matrix.mValueA + matrix.mValueC * matrix.mValueC);
    // scaleY = Math.sqrt(matrix.mValueB * matrix.mValueB + matrix.mValueD * matrix.mValueD);

    // rotation angle
    var radians = Math.acos(matrix.mValueA / scaleX),
    degrees = radians * (180 / Math.PI);

    return Math.round(degrees * 1000) / 1000;

    };

    /** ------------------------------------------------------------------- *
    * GET ITEMS *
    * -------------------------------------------------------------------- *
    * @author m1b *
    * @version 2024-03-01 *
    * -------------------------------------------------------------------- *
    * Collects page items from a `from` source, eg. a Document, Layer, *
    * GroupItem, or Array. Will look inside group items up to `maxDepth`. *
    * Search can be filtered using `filter` function. Note that the *
    * filter function is evaluated last in the filtering process. *
    * -------------------------------------------------------------------- *
    * Example 1. Get all items in document: *
    * *
    * var myItems = getItems({ from: app.activeDocument }); *
    * *
    * -------------------------------------------------------------------- *
    * Example 2. Get all selected items except groups: *
    * *
    * var myItems = getItems({ *
    * from: app.activeDocument.selection, *
    * getGroupItems: false, *
    * }); *
    * *
    * -------------------------------------------------------------------- *
    * Example 3. Using `filter` function to choose item type: *
    * *
    * var myItems = getItems({ *
    * from: app.activeDocument, *
    * filter: function (item) { *
    * return ( *
    * 'PathItem' === item.typename *
    * || 'CompoundPathItem' === item.typename *
    * ); *
    * } *
    * }); *
    * *
    * -------------------------------------------------------------------- *
    * Example 4. Using `filter` function: *
    * *
    * var myItems = getItems({ *
    * from: app.activeDocument, *
    * filter: onlyPngLinks *
    * }); *
    * *
    * function onlyPngLinks(item, depth) { *
    * return ( *
    * 'PlacedItem' === item.typename *
    * && '.png' === item.file.name.slice(-4).toLowerCase() *
    * ); *
    * }; *
    * *
    * -------------------------------------------------------------------- *
    * Example 4. Using the `filter` function for custom collecting: *
    * *
    * This example bypasses the normal returned array and instead *
    * captures items in an "external" array `itemsByDepth`. *
    * *
    * var itemsByDepth = []; *
    * *
    * function getItemsByDepth(item, depth) { *
    * if (undefined == itemsByDepth[depth]) *
    * itemsByDepth[depth] = []; *
    * itemsByDepth[depth].push(item); *
    * }; *
    * *
    * getItems({ *
    * from: app.activeDocument, *
    * filter: getItemsByDepth *
    * }); *
    * *
    * -------------------------------------------------------------------- *
    * @param {Object} options - parameters
    * @param {PageItem|Array<PageItem>|Document|Layer} options.from - the thing(s) to look in, eg. a selection.
    * @param {Function} [options.filter] - function that, given a found item, must return true (default: no filtering).
    * @param {Boolean} [options.getPageItems] - whether to include page items in returned items (default: true).
    * @param {Boolean} [options.getGroupItems] - whether to include GroupItems in returned items (default: true).
    * @param {Boolean} [options.getLayers] - whether to include Layers in returned items (default: false).
    * @param {Boolean} [options.getHiddenItems] - whether to include hidden items in returned items (default: true).
    * @param {Boolean} [options.getLockedItems] - whether to include locked items in returned items (default: true).
    * @param {Boolean} [options.getGuideItems] - whether to include guide items in returned items (default: false).
    * @param {Number} [options.maxDepth] - deepest folder level (recursion depth limit) (default: 99).
    * @param {Boolean} [options.returnFirstMatch] - whether to return only the first found item (default: false).
    * @param {Number} [depth] - the current depth (private).
    * @returns {Array|PageItem} - all the found items in a flat array, or the first found item if `returnFirstMatch`.
    */
    function getItems(options, depth) {

    // defaults
    options = options || {};

    var found = [],
    depth = depth || 0,
    items = options.from;

    if (!options.initialized)
    // once-off initialization
    if (!initialize())
    return [];

    itemsLoop:
    for (var i = 0, item, len = items.length; i < len; i++) {

    item = items[i];

    if (
    false === excludeFilter(item)
    && true === includeFilter(item)
    ) {
    // item found!
    found.push(item);

    if (options.returnFirstMatch)
    break itemsLoop;
    }

    if (
    'GroupItem' !== item.constructor.name
    && 'Layer' !== item.typename
    )
    // only items with children from here
    continue itemsLoop;

    if (
    excludeHidden(item)
    || excludeLocked(item)
    )
    // don't look into excluded containers
    continue itemsLoop;

    if (depth >= options.maxDepth)
    // don't go deeper
    continue itemsLoop;

    // set up for the next depth
    options.from = item.pageItems;

    // look inside
    found = found.concat(getItems(options, depth + 1));

    }

    // this level done
    if (true == options.returnFirstMatch)
    return found[0];
    else
    return found;

    /**
    * Returns true when the item should be not be found.
    * @param {PageItem|Layer} item
    * @returns {Boolean}
    */
    function excludeFilter(item) {

    return (

    isAlreadyFound(item)

    // is hidden
    || excludeHidden(item)

    // is locked
    || excludeLocked(item)

    // is guide
    || (
    false === options.getGuideItems
    && true === item.guides
    )

    // is layer
    || (
    false === options.getLayers
    && 'Layer' === item.typename
    )

    // is group item
    || (
    false === options.getGroupItems
    && 'GroupItem' === item.typename
    )

    // is page item
    || (
    false === options.getPageItems
    && 'GroupItem' !== item.typename
    && undefined != item.uuid
    )

    );

    };

    /**
    * Returns true when the item should be included.
    * @param {PageItem|Layer} item
    * @returns {Boolean}
    */
    function includeFilter(item) {

    return (
    undefined == options.filter
    || options.filter(item, depth)
    );

    };

    /**
    * Returns true when the item should
    * be excluded because it is hidden.
    * @param {PageItem|Layer} item
    * @returns {Boolean}
    */
    function excludeHidden(item) {

    return (
    false === options.getHiddenItems
    && (
    true === item.hidden
    || false === item.visible
    )
    );

    };

    /**
    * Returns true when the item should
    * be excluded because it is locked.
    * @param {PageItem|Layer} item
    * @returns {Boolean}
    */
    function excludeLocked(item) {

    return (
    false === options.getLockedItems
    && true === item.locked
    );

    };

    /**
    * Returns true if item was already
    * found, and marks item as found,
    * to avoid finding same item twice.
    * @param {PageItem|Layer} item
    * @returns {Boolean}
    */
    function isAlreadyFound(item) {

    var uuid = item.hasOwnProperty('uuid')
    ? item.uuid
    : item.typename + item.zOrderPosition,

    isFound = !!options.isFound[uuid];

    options.isFound[uuid] = true;

    return isFound;

    }

    /**
    * Returns the initialised `options` object.
    * @returns {Object}
    */
    function initialize() {

    // make a new object, so we don't pollute the original
    options = {
    initialized: true,
    depth: 0,
    isFound: {},
    filter: options.filter,
    getPageItems: false !== options.getPageItems,
    getGroupItems: false !== options.getGroupItems,
    getLayers: true === options.getLayers,
    getHiddenItems: false !== options.getHiddenItems,
    getLockedItems: false !== options.getLockedItems,
    getGuideItems: true === options.getGuideItems,
    maxDepth: options.maxDepth,
    returnFirstMatch: options.returnFirstMatch,
    };

    if (
    undefined == options.maxDepth
    || !options.maxDepth instanceof Number
    )
    options.maxDepth = 99;

    // items is a single layer
    if ('Layer' === items.typename)
    items = [items];

    // items is a document
    else if ('Document' === items.constructor.name) {

    var layers = items.layers;
    items = [];

    for (var i = 0; i < layers.length; i++)
    items.push(layers[i]);

    }

    else if ('Array' !== items.constructor.name)
    items = [items];

    return items.length > 0;

    };

    };

     

    Kurt Gold
    Community Expert
    Community Expert
    September 25, 2026


    Yes, it would be a long overdue enhancement.

    Another adjustable workaround may be using (brush) objects on a path object (see sample Illustrator file below).

    NED - GER

    Dave Creamer of IDEAS
    Community Expert
    Community Expert
    September 25, 2026

    After rotating the numbers, select the “upside-down” numbers and use the Transform Each option to reflect on the X and Y axis. 

     

    David Creamer: Community Expert (ACI and ACE 1995-2023)
    Ton Frederiks
    Community Expert
    Community Expert
    September 25, 2026

    ​@ronn57685899 As a workaround: Create a Symbol and use an instance as the object in the Radial Repeat. When you have the result you want, select Object > Expand… > Object (uncheck Fill). You can select the Symbol instances and select from the Symbol panel menu: Reset Transformation. This will set them upright.

    Harshika Verma
    Community Manager
    Community Manager
    September 25, 2026

    Hi ronn57685899,

    Thank you for reaching out. I assume you're looking for a similar functionality mentioned in this UserVoice. If so, please upvote the request and add your comments.

    I am also keeping the discussion open for our experts for any suggestions.

    Thanks,

    Harshika