This approach (source below) still contains some redundancy (could be made more efficient by fusing sortOn with groupBy, to reduce the number of times that any given shape is queried for its ptwShape value).
Nevertheless, it is already a bit more linear, and takes 20 seconds here with 4000 shapes (creating 2000 pair groups);
It calls an omniJS function from JXA, so you can run it from Script Editor etc.
(() => {
'use strict';
// OG JS Context (omniJS) -----------------------------------------------
const ogJSContext = () => {
// Tuple (,) :: a -> b -> (a, b)
const Tuple = (a, b) => ({
type: 'Tuple',
'0': a,
'1': b
});
// compare :: a -> a -> Ordering
const compare = (a, b) => a < b ? -1 : (a > b ? 1 : 0);
// flatten :: NestedList a -> [a]
const flatten = t =>
Array.isArray(t) ? (
[].concat.apply([], t.map(flatten))
) : t;
// Typical usage: groupBy(on(eq, f), xs)
// groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
const groupBy = (f, xs) => {
const dct = xs.slice(1)
.reduce((a, x) => {
const h = a.active.length > 0 ? a.active[0] : undefined;
return h !== undefined && f(h, x) ? {
active: a.active.concat([x]),
sofar: a.sofar
} : {
active: [x],
sofar: a.sofar.concat([a.active])
};
}, {
active: xs.length > 0 ? [xs[0]] : [],
sofar: []
});
return dct.sofar.concat(dct.active.length > 0 ? [dct.active] : []);
};
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) => xs.map(f);
// mappendComparing :: [((a -> b), Bool)] -> (a -> a -> Ordering)
const mappendComparing = fboolPairs =>
(x, y) => fboolPairs.reduce(
(ord, fb) => {
const f = fb[0];
return ord !== 0 ? (
ord
) : fb[1] ? (
compare(f(x), f(y))
) : compare(f(y), f(x));
}, 0
);
// sortBy :: (a -> a -> Ordering) -> [a] -> [a]
const sortBy = (f, xs) =>
xs.slice()
.sort(f);
// Sort a list by comparing the results of a key function applied to each
// element. sortOn f is equivalent to sortBy (comparing f), but has the
// performance advantage of only evaluating f once for each element in
// the input list. This is called the decorate-sort-undecorate paradigm,
// or Schwartzian transform.
// Elements are arranged from from lowest to highest.
// sortOn :: Ord b => (a -> b) -> [a] -> [a]
// sortOn :: Ord b => [((a -> b), Bool)] -> [a] -> [a]
const sortOn = (f, xs) => {
// Functions and matching bools derived from argument f
// which may be a single key function, or a list of key functions
// each of which may or may not be followed by a direction bool.
const fsbs = unzip(
flatten([f])
.reduceRight((a, x) =>
typeof x === 'boolean' ? {
asc: x,
fbs: a.fbs
} : {
asc: true,
fbs: [
[x, a.asc]
].concat(a.fbs)
}, {
asc: true,
fbs: []
})
.fbs
),
[fs, bs] = [fsbs[0], fsbs[1]],
iLast = fs.length;
// decorate-sort-undecorate
return sortBy(mappendComparing(
// functions that access pre-calculated values by position
// in the decorated ('Schwartzian') version of xs
zip(fs.map((_, i) => x => x[i]), bs)
), xs.map( // xs decorated with precalculated key function values
x => fs.reduceRight(
(a, g) => [g(x)].concat(a), [
x
])))
.map(x => x[iLast]); // undecorated version of data, post sort.
};
// showJSON :: a -> String
const showJSON = x => JSON.stringify(x, null, 2);
// unzip :: [(a,b)] -> ([a],[b])
const unzip = xys =>
xys.reduce(
(a, x) => Tuple.apply(null, [0, 1].map(
i => a[i].concat(x[i])
)),
Tuple([], [])
);
// zip :: [a] -> [b] -> [(a, b)]
const zip = (xs, ys) =>
xs.slice(0, Math.min(xs.length, ys.length))
.map((x, i) => Tuple(x, ys[i]));
// MAIN --------------------------------------------------------------
const
sorted = sortOn(
g => g.userData['ptwShape'],
canvases[0].graphics
),
inGroups = groupBy(
(a, b) => a.userData['ptwShape'] === b.userData['ptwShape'],
sorted
);
return showJSON(
map(xs => {
const
grp = new Group(xs),
k = xs[0].name;
return (
grp.name = k,
grp.setUserData('ptwGroup', 'system'),
grp.locked = true,
k
);
},
inGroups
)
);
};
// Javascript for Automation JS Context (JXA) ----------------------------
return Application('OmniGraffle')
.evaluateJavascript(
'(' + ogJSContext + ')()'
);
})();