Connect selected shapes (in selection order) as tree, cycle, or chain


#1

Three variants of a Keyboard Maestro macro for macOS OmniGraffle 7:

(KM macros feel somehow lighter and more flexible than the omniJS plugin mechanism, which is really only needed for iOS – not the platform on which I personally use OmniGraffle).

Source code: omniJS + JavaScript for Automation

(() => {
    // Rob Trew 2019
    // Ver 0.01

    // OmniGraffle 7:
    // Toggle connections between several selected
    // shapes, in the order of selection,
    // in one of the following patterns:
    //
    // - Cyclical (last selected is connected to the first)
    // - Linear (chain of connections from first to last)
    // - Tree (First selected connected to each of rest)
    //
    // The toggle has 3 stages:
    // 1. Connected, with arrow directions in selection order,
    // 2. arrow directions reversed,
    // 3. connections cleared.

    // Connection pattern for toggling:
    const
        connectionPattern = {
            cyclic: 0,
            linear: 1,
            tree: 3
        } ['tree']

    // OMNI JS CODE ---------------------------------------
    const omniJSContext = iPattern => {
        const dctConnectorStyle = {
            headType: 'FilledArrow',
            strokeColor: Color.RGB(1, 0, 0)
        };

        // main :: IO ()
        const main = () => {
            // Uncomment the code for the connection pattern chosen
            // (cyclical, linear, or tree)
            const
                connectionType = Boolean(iPattern) ? (
                    iPattern !== 1 ? (
                        tripleToggleTree
                    ) : tripleToggleSeries(false)
                ) : tripleToggleSeries(true);
            const
                seln = document.windows[0].selection,
                shapes = seln.solids;
            return 1 < shapes.length ? (
                connectionType(seln.canvas)(shapes)
            ) : 'Select more than one shape';
        };

        // OMNIGRAFFLE LINK-TOGGLING ----------------------

        // tripleToggleTree :: Canvas -> [Shape] -> IO String
        const tripleToggleTree = canvas => solids => {
            // First item of `solids` connected *to*
            // all the remaining shapes in list
            // -> connected *from* all remaining shapes
            // -> disconnected from all remaining shapes.
            const
                root = solids[0],
                following = solids[1],
                f = 0 < existingFromTo(root)(following).length ? (
                    a => b => (
                        unLinkedFromTo(a)(b),
                        linkedFromTo(canvas)(b)(a)
                    )
                ) : 0 < existingFromTo(following)(root).length ? (
                    flip(unLinkedFromTo)
                ) : linkedFromTo(canvas);
            return solids.slice(1).flatMap(f(root));
        };

        // tripleToggleSequences :: Canvas -> Bool -> Bool ->
        //                                  [Shape] -> IO String
        const tripleToggleSeries = blnCycle => canvas => solids => {
            // Solids connected in series ,
            // -> reversed connected in series,
            // -> disconnected,
            // and series closed by return to start if blnCycle is true.
            const
                start = solids[0],
                following = solids[1],
                f = 0 < existingFromTo(start)(following).length ? (
                    a => b => (
                        unLinkedFromTo(a)(b),
                        linkedFromTo(canvas)(b)(a)
                    )
                ) : 0 < existingFromTo(following)(start).length ? (
                    flip(unLinkedFromTo)
                ) : linkedFromTo(canvas);

            return zipWith(f)(solids)(
                solids.slice(1).concat(
                    blnCycle ? [start] : []
                )
            ).join(', ');
        };

        // existingFromTo :: Solid -> Solid -> [Line]
        const existingFromTo = a => b => {
            // An array of any connections running from a to b.
            const strID = a.id;
            return b.incomingLines.filter(
                x => strID === x.tail.id
            );
        };

        // linkedFromTo :: Shape -> Shape -> IO [String]
        const linkedFromTo = canvas => a => b => {
            // A new connecting line from shape a to shape b.
            const line = canvas.connect(a, b);
            return (
                Object.assign(line, dctConnectorStyle),
                line.setUserData('quickLink', '1'),
                [a.text + ' -> ' + b.text]
            );
        };

        // unLinkedFromTo :: Shape -> Shape -> IO [String]
        const unLinkedFromTo = a => b =>
            // Any connection from a to b removed.
            existingFromTo(a)(b).map(
                x => (
                    x.remove(), [a.text + ' <- x -> ' + b.text]
                )
            );

        // GENERIC ----------------------------------------
        // https://github.com/RobTrew/prelude-jxa

        // flip :: (a -> b -> c) -> b -> a -> c
        const flip = f =>
            x => y => f(y)(x);

        // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
        const zipWith = f => xs => ys =>
            xs.slice(
                0, Math.min(xs.length, ys.length)
            ).map((x, i) => f(x)(ys[i]));

        // MAIN -------------------------------------------
        return main()
    };


    // JXA CODE -------------------------------------------

    // omniJSWithArgs :: Function -> [...OptionalArgs] -> a
    function omniJSWithArgs(f) {
        return Application('OmniGraffle')
            .evaluateJavascript(
                `(${f})(${Array.from(arguments)
                .slice(1).map(JSON.stringify)})`
            );
    };

    // return omniJSContext();
    return omniJSWithArgs(
        omniJSContext,
        connectionPattern
    );
})();