omniJS test - adding an alternative autoLayout

Here’s a rough draft of a test script (works with Sierra and iOS 10+ only on current OG7.4 and iOS OG 3 test builds).

If you run it on macOS from something like Script Editor, it should:

  1. Apply a classic Reingold Tilford ‘Tidy’ tree layout (parents centered over children) to any hierarchical diagram in an (OG7.4 test) front canvas (using the direction and Level/Peer gap settings in the built-in autolayout)
  2. Cycle the autolayout settings on to the next layout direction for cyclical testing of each direction
  3. Place in the clipboard a URL version of the script which seems to run OK on iOS OG3 test builds too.

No guarantees, and not for use with real data - just a testing toy, and may be broken soon by fixes in coming builds.

Click on disclosure triangle below to see code:

JavaScript for Automation source code, which runs omniJS code and also creates an omniJS URL
    (() => {
        'use strict';

        // Ver .056
        // Works with: OG7 7.4 test (v179.5 r290823)
        // SIERRA+ macOS or iOS 10+ only - this draft is in ES6 JS


        // DRAFT OF A 'TIDY' (REINGOLD TILFORD) AUTO-LAYOUT FOR NESTED DIAGRAMS
        // (an alternative to the OmniGraffle Graphviz 'Dot' layout,
        //  in which parent nodes are not child-centered)

        // Includes a JS calque of Andrew Kennedy's classic paper 'Drawing Trees'
        // Journal of Functional Programming 6 (3): 527-534, May 1996

        // Rough draft - purely illustrative - no claims or guarantees
        // use with caution, and not on real data.

        // Rob Trew June 27 2017

        // GENERIC JS FUNCTIONS --------------------------------------------------

        // A list of functions applied to a list of arguments
        // <*> :: [(a -> b)] -> [a] -> [b]
        const ap = (fs, xs) => //
            [].concat.apply([], fs.map(f => //
                [].concat.apply([], xs.map(x => [f(x)]))));

        // show :: a -> String
        const show = (...x) =>
            JSON.stringify.apply(
                null, x.length > 1 ? [x[0], null, x[1]] : x
            );

        // OMNIGRAFFLE JXA FUNCTIONS ---------------------------------------------

        // evaluateOmniJS :: (Options -> OG Maybe JSON String) -> {KeyValues}
        //      -> Maybe JSON String
        const evaluateOmniJS = (f, dctOptions) => {
            const
                a = Application.currentApplication(),
                sa = (a.includeStandardAdditions = true, a);

            ap([sa.openLocation, sa.setTheClipboardTo], //
                ['omnigraffle:///omnijs-run?script=' +
                    encodeURIComponent(
                        '(' + f.toString() + ')(' +
                        (dctOptions && Object.keys(dctOptions)
                            .length > 0 ? show(dctOptions) : '') + ')'
                    )
                ]);
            // Possible harvest of return value from canvasbackground.userData
            const
                og = Application('OmniGraffle'),
                ws = (og.activate(), og.windows.where({
                    _not: [{
                        name: {
                            _beginsWith: 'Automation Console'
                        }
                    }]
                })),
                mw = ws.length > 0 ? {
                    just: ws.at(0),
                    nothing: false
                } : {
                    nothing: true
                },
                mResult = mw.nothing ? mw : (() => {
                    const v = mw.just.canvas()
                        .canvasbackground.userDataItems.byName('omniJSON')
                        .value()
                    return (v === undefined || v === null) ? ({
                        nothing: true,
                        msg: "No JSON found in " +
                            ".canvasbackground.userDataItems.byName('omniJSON')"
                    }) : {
                        just: v,
                        nothing: false
                    };
                })();
            return mResult.nothing ? mResult : (
                mw.just.canvas()
                .canvasbackground.userDataItems.byName('omniJSON')
                .value = null,
                mResult
            );
        };

        // ogFrontDoc :: {useExisting : Bool, templateName: String} -> OG.Document
        const ogFrontDoc = dctOptions => {
            const
                options = dctOptions || {},
                optTemplate = options.templateName,
                og = Application('OmniGraffle'),
                xs = og.availableTemplates(),
                strTemplate = (optTemplate && elem(optTemplate, xs)) ? (
                    optTemplate
                ) : xs[0],
                ds = og.documents,
                d = options.useExisting && ds.length > 0 ? (
                    ds.at(0)
                ) : (() => {
                    return (
                        ds.push(og.Document({
                            template: strTemplate
                        })),
                        ds.at(0)
                    );
                })();
            return (
                og.activate(),
                d
            );
        };

        // OMNIJS FUNCTIONS to be run with evaluateOmniJS()  (above) -------------

        // tidyLayout :: Options -> OG ()
        const tidyLayout = options => {
            // GENERIC FUNCTIONS -------------------------------------------------

            // (++) :: [a] -> [a] -> [a]
            const append = (xs, ys) => xs.concat(ys);

            // concat :: [[a]] -> [a] | [String] -> String
            const concat = xs =>
                xs.length > 0 ? (() => {
                    const unit = typeof xs[0] === 'string' ? '' : [];
                    return unit.concat.apply(unit, xs);
                })() : [];

            // cons :: a -> [a] -> [a]
            const cons = (x, xs) => [x].concat(xs);

            // 2 or more arguments
            // curry :: Function -> Function
            const curry = (f, ...args) => {
                const go = xs => xs.length >= f.length ? (f.apply(null, xs)) :
                    function () {
                        return go(xs.concat(Array.from(arguments)));
                    };
                return go([].slice.call(args, 1));
            };

            // elem :: Eq a => a -> [a] -> Bool
            const elem = (x, xs) => xs.indexOf(x) !== -1;

            // elemIndex :: Eq a => a -> [a] -> Maybe Int
            const elemIndex = (x, xs) => {
                const i = xs.indexOf(x);
                return {
                    nothing: i === -1,
                    just: i
                };
            };

            // foldl :: (b -> a -> b) -> b -> [a] -> b
            const foldl = (f, a, xs) => xs.reduce(f, a);

            // fst :: [a, b] -> a
            const fst = pair => pair.length === 2 ? pair[0] : undefined;

            // isNull :: [a] | String -> Bool
            const isNull = xs =>
                Array.isArray(xs) || typeof xs === 'string' ? (
                    xs.length < 1
                ) : undefined;

            // log :: a -> IO ()
            const log = (...args) => {
                console.log(
                    args
                    .map(show)
                    .join(' -> ')
                );
            };

            // map :: (a -> b) -> [a] -> [b]
            const map = (f, xs) => xs.map(f);

            // max :: Ord a => a -> a -> a
            const max = (a, b) => b > a ? b : a;

            // maximumBy :: (a -> a -> Ordering) -> [a] -> a
            const maximumBy = (f, xs) =>
                xs.reduce((a, x) => a === undefined ? x : (
                    f(x, a) > 0 ? x : a
                ), undefined);

            // min :: Ord a => a -> a -> a
            const min = (a, b) => b < a ? b : a;

            // negate :: Num a => a -> a
            const negate = n => -n;

            // nextInCycleMay :: [a] -> a -> Maybe a
            const nextInCycleMay = (xs, x) => {
                const mbIndex = elemIndex(x, xs);
                return mbIndex.nothing ? (
                    isNull(xs) ? mbIndex : {
                        just: xs[0]
                    }
                ) : ({
                    just: takeCycle(mbIndex.just + 2, xs)[mbIndex.just + 1],
                    nothing: false
                });
            };

            // read :: Read a => String -> a
            const read = s => JSON.parse(s);

            // replicate :: Int -> a -> [a]
            const replicate = (n, x) =>
                Array.from({
                    length: n
                }, () => x);

            // reverse :: [a] -> [a]
            const reverse = xs =>
                typeof xs === 'string' ? (
                    xs.split('')
                    .reverse()
                    .join('')
                ) : xs.slice(0)
                .reverse();

            // show :: a -> String
            const show = (...x) =>
                JSON.stringify.apply(
                    null, x.length > 1 ? [x[0], null, x[1]] : x
                );

            // snd :: (a, b) -> b
            const snd = tpl => Array.isArray(tpl) ? tpl[1] : undefined;

            // take :: Int -> [a] -> [a]
            const take = (n, xs) => xs.slice(0, n);

            // First n members of a repeating cycle of xs
            // takeCycle :: Int -> [a] -> [a]
            const takeCycle = (n, xs) => {
                const lng = xs.length;
                return take(n,
                    (lng >= n ? xs : concat(replicate(Math.ceil(n / lng), xs)))
                );
            };

            // unconsMay :: [a] -> Maybe (a, [a])
            const unconsMay = xs => xs.length > 0 ? {
                just: [xs[0], xs.slice(1)],
                nothing: false
            } : {
                nothing: true
            };

            // unzip :: [(a,b)] -> ([a],[b])
            const unzip = xys =>
                xys.reduceRight(([xs, ys], [x, y]) => [
                    [x].concat(xs), [y].concat(ys)
                ], [
                    [],
                    []
                ]);

            // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
            const zipWith = (f, xs, ys) =>
                Array.from({
                    length: Math.min(xs.length, ys.length)
                }, (_, i) => f(xs[i], ys[i]));

            // TEXT NEST FUNCTIONS -----------------------------------------------

            // textNest :: OGoutlineNodes -> Node {text: String, nest:[Node]}
            const textNest = xs => {
                const go = (xs, iLevel) =>
                    xs.length ? xs.map(x => {
                        const
                            g = x.graphic,
                            rect = g.geometry;
                        return {
                            id: g.id,
                            level: iLevel,
                            text: g.text,
                            xywh: ['x', 'y', 'width', 'height']
                                .reduce((a, k) => (a[k] = rect[k], a), {}),
                            nest: x.children.length > 0 ? (
                                go(x.children, iLevel + 1)
                            ) : []
                        };
                    }) : [];
                return go(xs, 1);
            };

            // Level order iteration, collecting values by supplied keys

            // levelOrderTraversal :: [String] -> Node -> [[a]]
            const levelOrderTraversal = (ks, dctNode) => {
                const go = queue => {
                    const mbht = unconsMay(queue);
                    return mbht.nothing ? [] : (() => {
                        const [h, t] = mbht.just;
                        return cons(
                            foldl((a, k) => (a[k] = h[k], a), {}, ks),
                            go(isNull(h.nest) ? (
                                t
                            ) : append(t, h.nest)) // Nest deferred to later.
                        );
                    })();
                };
                return go([dctNode]);
            };

            // levelHeights :: Bool -> Node -> Dictionary
            const levelHeights = (blnHoriz, dctNode) => {
                const k = blnHoriz ? 'width' : 'height';
                return foldl((a, x) => {
                    const level = x.level;
                    return (a[level] = max(a[level] || 0, x.xywh[k]), a)
                }, {}, levelOrderTraversal(['level', 'xywh'], dctNode));
            };

            // CANVAS ------------------------------------------------------------
            const cnv = document.windows[0].selection.canvas;

            // LAYOUT DIRECTION AND GAP SETTINGS ---------------------------------

            // layoutSettings :: OG () ->
            //      {direction: String, levelGap: Real, peerGap: Real}
            const layoutSettings = blnCycle => {
                const cnv = document.windows[0].selection.canvas; //??? remove

                // Temporary fix for a glitch in the Enum
                // tmp :: String -> String
                const tmp = k => ({
                        Left: 'Top',
                        Top: 'Bottom',
                        Right: 'Left',
                        Bottom: 'Right'
                    })[k],
                    // directionName :: OG HierarchicalDirection -> String
                    directionName = d =>
                    d < HierarchicalDirection[tmp('Right')] ? (
                        d < HierarchicalDirection[tmp('Top')] ? (
                            'Left'
                        ) : 'Top'
                    ) : d < HierarchicalDirection[tmp('Bottom')] ? (
                        'Right'
                    ) : 'Bottom',
                    layout = cnv.layoutInfo;

                return (
                    layout.automaticLayout = false, // Lest it undo ours.
                    layout.type = LayoutType.Hierarchical, // Remove ???
                    // DIRECTION CYCLING FOR TESTING --- /// REMOVE ???

                    blnCycle && (layout.direction =
                        HierarchicalDirection[tmp(nextInCycleMay(
                                ['Top', 'Bottom', 'Left', 'Right'],
                                directionName(layout.direction))
                            .just)]), {
                        direction: directionName(layout.direction),
                        levelGap: layout.rankSeparation * 72, // Points per inch.
                        peerGap: layout.objectSeparation * 72,
                        type: layout.type
                    });
            };

            // A positioned tree with its root at zero.
            // (A standard textNest decorated with geometry, id, level index)

            // layoutDesign :: Node {text: String, nest:[Node]} ->
            //           Node {text: String, nest:[Node], x:Real}
            const layoutDesign = (keyDimension, peerGap, dctNode) => {

                // REINGOLD TILFORD CODE ADAPTED FROM KENNEDY --------------------

                // TREE-DRAWING FUNCTIONS ----------------------------------------
                // A JavaScript adaption of Andrew Kennedy's classic (ML) paper
                // 'Drawing Trees'
                // Journal of Functional Programming 6 (3): 527-534, May 1996

                // This version mainly differs from Kennedy in allowing for nodes
                // of variable height and width.
                // keyDimension is either 'width' (or vertical trees)
                // or 'height' for horizontal trees.
                // peerGap should be in the same units as height/width

                // X POSITIONS - relative to parents
                // The x position of a node is relative to its parent
                // (y positions are properties of nesting layers - ignored here)

                // A sub-tree moves with any x-delta applied to its parent node

                // Node {text: String, nest:[Node], x:Real}
                // moveTree :: Real -> Node -> Node
                const moveTree = (rd, dctNode) =>
                    Object.assign({}, dctNode, {
                        rx: dctNode.rx + rd
                    });

                // EXTENTS - Lists of pairs of x positions

                // The head of the list is the pair for the top level of the tree,
                // and thus downward.

                // Each pair holds the left (mininum) and right (maximum) x values
                // for its level
                // Extent [(leftMost :: Real, rightMost :: Real)]

                // Applying an x-axis delta to all the pairs in an extent:

                // moveExtent :: Real -> Extent -> Extent
                const moveExtent = (rd, pairs) =>
                    map(([p, q]) => [p + rd, q + rd], pairs);

                // The left most positions of one extent,
                // combined with the rightmost positions of an adject extent:

                // merge :: Extent -> Extent -> Extent
                const merge = (ps, qs) => {
                    const
                        mbp = unconsMay(ps),
                        mbq = mbp.nothing ? mbp : unconsMay(qs);
                    return mbp.nothing ? (
                        qs
                    ) : mbq.nothing ? (
                        ps
                    ) : (() => {
                        const [ph, pt] = mbp.just, [qh, qt] = mbq.just;
                        return cons([fst(ph), snd(qh)], merge(pt, qt));
                    })();
                };

                // Merging a pair of adjacent extents lifting to merging
                // a longer series of adjacent extents:

                // mergeList :: [Extent] -> Extent
                const mergeList = es => foldl(merge, [], es);

                // What is the *minimum* distance between two given extents ?
                // (Proliferating descendants may push them further apart)

                // fit :: Extent -> Extent -> Real
                const fit = (ps, qs) => {
                    const
                        mbp = unconsMay(ps),
                        mbq = mbp.nothing ? mbp : unconsMay(qs);
                    return (mbp.nothing || mbq.nothing) ? 0 : (() => {
                        const [ph, pt] = mbp.just, [qh, qt] = mbq.just;
                        return max((snd(ph) - fst(qh)), fit(pt, qt));
                    })();
                };

                // How far leftward could each extent go ?

                // fitlistl :: [Extent] -> [Real]
                const fitlistl = es => {
                    const fl = (a, ees) => {
                        const mbees = unconsMay(ees);
                        return mbees.nothing ? [] : (() => {
                            const [e, es] = mbees.just,
                                d = fit(a, e);
                            return cons(d, fl(merge(a, moveExtent(d, e)), es));
                        })();
                    };
                    return fl([], es);
                };

                // How far rightward could each extent go ?

                // fitlistr :: [Extent] -> [Real]
                const fitlistr = es => {
                    const fr = (a, ees) => {
                        const mbees = unconsMay(ees);
                        return mbees.nothing ? [] : (() => {
                            const [e, es] = mbees.just,
                                d = negate(fit(e, a));
                            return cons(d, fr(merge(moveExtent(d, e), a), es));
                        })();
                    }
                    return reverse(fr([], reverse(es)));
                };

                // What are the mean positions,
                // mid way between left and right constraints,
                // for each extent ?

                // fitList :: [Extent] -> [Real]
                const fitList = es =>
                    zipWith(
                        (x, y) => (x + y) / 2,
                        fitlistl(es),
                        fitlistr(es)
                    );

                // design :: Node -> (Node, [Extent])
                const design = node => {
                    const
                        w2 = (node.xywh[keyDimension] + peerGap) / 2,
                        [trees, extents] = unzip(
                            map(design, node.nest)
                        ),
                        positions = fitList(extents),
                        resultExtent = cons([-w2, w2], mergeList(
                            zipWith(moveExtent, positions, extents)
                        )),
                        resultTree = Object.assign({}, node, {
                            nest: zipWith(moveTree, positions, trees),
                            rx: 0
                        });
                    return [resultTree, resultExtent];
                };
                return fst(design(dctNode));
            };

            // UPDATED RENDER ----------------------------------------------------

            // Subtree positions updated from geometry in a given key of nodes
            // positionUpdated :: Canvas -> String -> Bool, Node -> Node
            const positionsUpdated = curry((cnv, key, blnAcross, dctNode) => {
                const
                    dct = dctNode[key],
                    [x, y, width, height] = map(
                        k => dct[k], ['x', 'y', 'width', 'height']
                    ),
                    g = cnv.graphicWithId(dctNode.id),
                    mgs = g.magnets;

                // Make sure that each node has the right two magnets for
                // direction of tree growth. (NS for vertical, EW for horizontal)
                return (
                    (g.magnets = (blnAcross ? (
                        [new Point(1.00, 0.00), new Point(-1.00, 0.00)]
                    ) : [new Point(0.00, 1.00), new Point(0.00, -1.00)])),
                    g.geometry = new Rect(x, y, width, height),
                    map(positionsUpdated(cnv, key, blnAcross), dctNode.nest),
                    dctNode
                );
            });

            // newPositions :: Dictionary -> Dictionary ->
            //                                  Dictionary -> Node -> Node
            const newPositions = (settings, dctLevels, posn, dctNode) => {
                // Direction, levelGap, peerGap
                const
                    blnVert = elem(settings.direction, ['Top', 'Bottom']),
                    blnMarked = elem(settings.direction, ['Bottom', 'Right']),
                    [keyOrth, keyAxial, dimOrth, dimAxial] = blnVert ? (
                        ['width', 'height', 'x', 'y']
                    ) : ['height', 'width', 'y', 'x'],
                    cs = dctNode.nest,
                    iLevel = dctNode.level,
                    rankDepth = dctLevels[iLevel];
                return {
                    id: dctNode.id,
                    level: iLevel,
                    text: dctNode.text,
                    rx: dctNode.rx,
                    geo: posn,
                    nest: cs.length < 1 ? [] : (() => {
                        const
                            origin = posn[dimOrth] + (posn[keyOrth] / 2),
                            pLevelTop = blnMarked ? (
                                ((posn[dimAxial] + posn[keyAxial]) - rankDepth) -
                                settings.levelGap
                            ) : posn[dimAxial] + rankDepth + settings.levelGap;
                        return map(c => {
                            // Updated geo and recursively updated descendants.
                            const
                                cxywh = c.xywh,
                                w = cxywh[keyOrth],
                                h = cxywh[keyAxial];
                            return newPositions(settings, dctLevels, {
                                x: blnVert ? (
                                    (origin + c.rx) - (w / 2)
                                ) : (pLevelTop - (blnMarked ? h : 0)),
                                y: blnVert ? (
                                    pLevelTop - (blnMarked ? h : 0)
                                ) : ((origin + c.rx) - (w / 2)),
                                rx: c.rx,
                                width: cxywh.width,
                                height: cxywh.height
                            }, c);
                        }, cs)
                    })()
                };
            };

            // MAIN --------------------------------------------------------------
            console.clear();
            const
                lstTextNest = textNest(cnv.outlineRoot.children),
                // Direction, levelGap, peerGap
                settings = layoutSettings(), // TEST VERSION ROTATES DIRECTION ???
                blnHoriz = elem(settings.direction, ['Left', 'Right']),
                jsoLayout = layoutDesign(
                    blnHoriz ? 'height' : 'width',
                    settings.peerGap,
                    isNull(lstTextNest) ? {
                        text: 'Root',
                        nest: []
                    } : lstTextNest[0]
                );

            const strJSON = show(
                positionsUpdated(
                    cnv, 'geo', blnHoriz, newPositions(
                        settings,
                        levelHeights(blnHoriz, jsoLayout),
                        jsoLayout.xywh,
                        jsoLayout
                    )
                ), 2
            );

            // WHILE TESTING, ROTATE TO NEXT DIRECTION ??? REMOVE LATER
            const dctNew = layoutSettings(true);
            return (
                cnv.background.setUserData('omniJSON', strJSON),
                strJSON
            );
        };

        // TEST: APPLY REINGOLD TILFORD 'TIDY' AUTOLAYOUT TO FRONT CANVAS
        // USING DIRECTION AND LEVEL/PEER GAP SETTINGS IN OG7 --------------------
        // THIS TEST VERSION ALSO MOVES THE OG7 LAYOUT DIRECTION ON TO THE
        // NEXT POSITION FOR CYCLICAL TESTING AROUND THE FOUR QUADRANTS
        const
            og = Application('OmniGraffle'),
            d = ogFrontDoc({
                useExisting: true
            }),
            a = Application.currentApplication(),
            sa = (a.includeStandardAdditions = true, a);

        // const strClip = (
        const strJSON = evaluateOmniJS(tidyLayout, {
            k: "someOption" // Not used
        });
        // );
        // );

        // TESTING -- RETURN FOCUS TO THE CODE EDITOR
        // (Atom, running the Script plugin to execute JXA)
        // Application('Atom')
        //     .activate(); // Remove after testing ???
        // sa.setTheClipboardTo(strClip);
        // return strClip;
        //return strJSON;
    })();

1 Like

Here as a draft plugin, for testing use only.

NB this version is compatible with Sierra onwards only – if there is interest I will convert it to an ES5 JS form which can run on some earlier macOS builds. This draft is written in ES6 JavaScript.

Aims to supplement the built-in GraphViz ‘dot’ layout with a tidier (Reingold Tilford) layout of trees. (Parent nodes centered over their children).

If there is more than one tree on a canvas, all are tidied unless the root nodes of one of more trees are selected, in which case tidying is restricted to the selected trees.

  • Fix applies a Reingold Tilford tree layout using the current OG7.4 (Test) layout settings, adjusting the node magnets if necessary
  • Cycle moves through all four tree directions in the order top -> left -> right -> bottom
  • Toggle alternates between the two more common directions (top ⇄ left)

(There is also an About action, not shown here).

As an early experiment in assembling omniJS plugins, it shows how you can derive multiple actions from a single library function, by passing varying parameters to that function. Most of the code is in the library – each action is a light wrapper.

For example, this is Tidy:
var _ = (function () {
    var
        // activeSeln :: OG () -> OG Selection
        activeSeln = function () {
            return document.windows[0].selection;
        },
        simpleTidy = new PlugIn.Action(function (selection) {
            var oSeln = selection !== undefined ? selection : activeSeln();
            return (
                this.tidyTreeLib.tidyLayout(oSeln, []),
                oSeln.canvas.layoutInfo.direction
            );
        });

    return (
        simpleTidy.validate = function (selection) {
            const cnv = (selection !== undefined ? selection : activeSeln())
                .canvas;
            return (cnv !== null) && (cnv.outlineRoot.children
                .reduce(function (a, x) {
                    return a + x.children.length;
                }, 0)) > 0;
        },
        simpleTidy
    );
})();
_;

and this is the Cycle action:
var _ = (function () {

    var 
        // activeSeln :: OG () -> OG Selection
        activeSeln = function () {
            return document.windows[0].selection;
        },

        cycleTidy = new PlugIn.Action(function (selection) {
            var oSeln = selection !== undefined ? selection : activeSeln();
            return (
                this.tidyTreeLib.tidyLayout(
                    oSeln, ['Top', 'Left', 'Bottom', 'Right']
                ),
                oSeln.canvas.layoutInfo.direction
            );
        });

    return (
        cycleTidy.validate = function (selection) {
            const cnv = (selection !== undefined ? selection : activeSeln())
                .canvas;
            return (cnv !== null) && (cnv.outlineRoot.children
                .reduce(function (a, x) {
                    return a + x.children.length;
                }, 0)) > 0;
        },
        cycleTidy
    );
})();
_;

Built-in Graphviz ‘Dot’ vs. Reingold Tilford Tidy:

Toggling or cycling, with automatic magnet adjustments.

Whereas simply changing to an orthogonal direction with the built-in layout, creates a tangle:

1 Like

Linking OG Edit > Outlining commands with a Fit operation (combining them in one keystroke with something like Keyboard Maestro), allows for quite a good mind-mapping workflow).

The resulting text outline can then be captured with something like this:

PS an omniJS plugin like this can be installed and used on iOS (OG 3 test builds) as well as on macOS:

Very cool!

You got around the state labels with a generic “toggle” action. Well done. Being able to change/update the labels and icons will hopefully be available at some stage so you can visually indicate the change.

Like you, I’d cleaned up my initial prototype into something that basically is a 4-line call into a number of library methods so that each action retains only the unique aspects.

Looks very nice.

1 Like

Updated in

https://drive.google.com/open?id=0B8zHpuZ2eLMzX29nVndiQ204UVk

The earlier version used a workaround for an initial bug in the omniJS layout direction enum.

That bug is now fixed (7.5 test (v181.3 r293907)) (directions and direction names are now correctly paired), so recent versions of macOS OmniGraffle 7.5 test will need this newer version, to enable the Cycle and Toggle macros to behave as expected.

1 Like

It looks as if there may have been a subsequent change in the (sortable) order of the direction enumeration.

Here is a version compatible (for cycling and toggling of tree directions), with current (and beta) OG7 builds.

RTPlugin.omnigrafflejs.zip (28.7 KB)

When I get a moment I’ll make a Github repository and add a few notes on usage etc. (Also need to tidy the code, I notice :-)