omniJS interface to tables – one crash, one bug, in this early build

Finding that code generated by Copy As > JavaScript doesn’t yet manage to rebuild a table which includes gaps, I tried to reconstruct such a table manually through omniJS:

This led to one reproducible crash, and one apparent bug (Graphic.remove() fails on graphics within tables).

Reproducing the crash

In the GUI, we can delete a tile from a table without losing the table’s ability to extend and automatically add cells in either axis.

Table.graphicAt(row, col) returns a null if we reference the coordinate of a gap, but if we try to construct a table that includes a gap through omniJS, using an array that includes a null, or an empty array, we can accidentally bring the house down:

Don’t try this in the Console, for example - it crashes this build: 7.4 (v179.8 r291433)

Table.withRowsColumns(1, 1, [])

Reproducing the bug

( Graphic.remove() fails without error on graphics inside tables )

My second approach to rebuilding the table above with omniJS was to use Graphic.remove() to emulate manual deletion of one or more cells in the GUI.

The code runs fine, and throws no errors, but the graphic is not deleted.

If we first .ungroup() a table, .remove() then works, but our structure has now lost the status and powers of a Table.

Test code below

Paste the following function into the console, and invoke it by entering:
omniJSONContext() followed by the return key.

// omniJSONContext :: OG () -> JSON String
const omniJSONContext = () => {

        // GENERIC FUNCTIONS -------------------------------------------------
        // enumFromTo :: Int -> Int -> [Int]
        const enumFromTo = (m, n) =>
            Array.from({
                length: Math.floor(n - m) + 1
            }, (_, i) => m + i);

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

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

        const
            dctStyle = {
                cornerRadius: 5,
                fillColor: Color.RGB(1, 0.75, 0.75),
                geometry: new Rect(
                    104.72391558679072, -188.10624072930537,
                    131.8110248179886,
                    104.88189071538886
                ),
                text: '-', // if the text is zero length, formatting is lost
                //fontName: "Monaco",
                autosizing: TextAutosizing.Clip,
                textHorizontalAlignment: HorizontalTextAlignment.Center,
                textSize: 15,
                textVerticalPadding: 0
            },
            cnv = document.windows[0].selection.canvas,

            tbl = Table.withRowsColumns(
                3, 3,
                map(
                    n => Object.assign(
                        cnv.newShape(),
                        dctStyle, {
                            text: (n)
                                .toString()
                        }
                    ),
                    enumFromTo(1, 9)
                )
            ),

            oMiddleTile = tbl.graphicAt(1, 1);
            // tbl.ungroup();  // Remove will work if we ungroup, but then,
            // the structure loses the powers and properties of a table.

            oMiddleTile.remove();


        const strJSON = show({
            message: 'Graphic ' + oMiddleTile.id + ' has not been removed ...'
        });

        // Save a return value for JXA to find in Canvas background user-data
        return (
            cnv.background.setUserData('omniJSON', strJSON),
            strJSON
        );
    };

( In the meanwhile, we could emulate this visually by turning the alpha (transparency) down to zero on fill, stroke, and text )

For example, an omniJS function which draws this:

Might apply a secondary ‘invisible’ or ‘hidden’ style to the central tile, like this:

function drawTableWithGap () {

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

        // enumFromTo :: Int -> Int -> [Int]
        const enumFromTo = (m, n) =>
            Array.from({
                length: Math.floor(n - m) + 1
            }, (_, i) => m + i);

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

        const
            cnv = document.windows[0].selection.canvas,

            // STYLE FOR VISIBLE TABLE CELLS
            dctStyle = {
                cornerRadius: 5,
                fillColor: Color.RGB(1, 0.75, 0.75),
                geometry: new Rect(
                    104.72391558679072, -188.10624072930537,
                    131.8110248179886,
                    104.88189071538886
                ),
                text: '-', // if the text is zero length, formatting is lost
                //fontName: "Monaco",
                autosizing: TextAutosizing.Clip,
                textHorizontalAlignment: HorizontalTextAlignment.Center,
                textSize: 15,
                textVerticalPadding: 0
            },

            // ADDITIONAL 'HIDDEN' STYLE FOR GAPS IN THE TABLE
            transparent = Color.RGB(0, 0, 0, 0),
            dctHidden = {
                fillColor: transparent,
                strokeColor: transparent,
                textColor: transparent
            };

            // THREE BY THREE TABLE, WITH A GAP IN THE MIDDLE
            Table.withRowsColumns(
                3, 3,
                map(
                    n => Object.assign(
                        cnv.newShape(),
                        dctStyle, {
                            text: (n)
                                .toString()
                        },
                        n === 5 ? dctHidden : {} // Additional style applied ?
                    ),
                    enumFromTo(1, 9)
                )
            );
    };

If it proves feasible to give this interface some attention, beyond the immediate type-checking to forestall a crash, it would be very good, I think, to allow for the creation of a table from an array which includes one or more null values, corresponding to the gaps which arise in tables in the GUI when cells are deleted.

If we have a concatMap function like

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

and derive, from an existing (GUI-selected) table a flat array of the components, in the order used by the Table.withRowsColumns() constructor, then from a complete 3*3 table we might get a result like the following, in which ‘{}’ is an abbreviation of an OG graphic object.

[
  {},
  {},
  {},
  {},
  {},
  {},
  {},
  {},
  {}
]

from the following code:

        const xs = concatMap(
            row => concatMap(
                col => [tbl.graphicAt(row, col)],
                enumFromTo(0, tbl.columns - 1)
            ),
            enumFromTo(0, tbl.rows - 1)
        );

        const strJSON = JSON.stringify(xs, null, 2);

while from the same table, but with the central tile deleted, we get:

[
  {},
  {},
  {},
  {},
  null,
  {},
  {},
  {},
  {}
]

I think a user’s intuitive expectation might then be that calling Table.withRowsColumns() on an appropriately dimensioned array which includes a null might produce a corresponding table with a gap.