From 1d392a15c8c7314422a565b9c948102c6f662e2f Mon Sep 17 00:00:00 2001 From: Roman Bruckner Date: Tue, 16 Jan 2024 13:06:57 +0100 Subject: [PATCH] refactor(dia.Paper)!: add SVG grid layer (#2458) --- packages/joint-core/demo/paper/src/paper.js | 4 +- .../api/dia/Paper/prototype/clearGrid.html | 3 - .../api/dia/Paper/prototype/drawGrid.html | 7 - .../api/dia/Paper/prototype/setGrid.html | 4 +- packages/joint-core/src/dia/Paper.mjs | 203 +++--------------- packages/joint-core/src/dia/PaperLayer.mjs | 1 + .../joint-core/src/dia/layers/GridLayer.mjs | 176 +++++++++++++++ packages/joint-core/test/jointjs/paper.js | 85 ++++---- packages/joint-core/types/joint.d.ts | 15 +- 9 files changed, 262 insertions(+), 236 deletions(-) delete mode 100644 packages/joint-core/docs/src/joint/api/dia/Paper/prototype/clearGrid.html delete mode 100644 packages/joint-core/docs/src/joint/api/dia/Paper/prototype/drawGrid.html create mode 100644 packages/joint-core/src/dia/layers/GridLayer.mjs diff --git a/packages/joint-core/demo/paper/src/paper.js b/packages/joint-core/demo/paper/src/paper.js index 684be61c4..1eafa7abe 100644 --- a/packages/joint-core/demo/paper/src/paper.js +++ b/packages/joint-core/demo/paper/src/paper.js @@ -367,8 +367,7 @@ $h.on('input change', function() { paper.setDimensions(parseInt($w.val(), 10), parseInt(this.value, 10)); }); $grid.on('input change', function() { - paper.options.gridSize = this.value; - paper.drawGrid(); + paper.setGridSize(this.value); }); $('.range').on('input change', function() { $(this).next().text(this.value); @@ -548,7 +547,6 @@ var gridTypes = { var renderer = _inputRenderer(gridTypes, function(gridOpt) { paper.setGrid(gridOpt); - paper.drawGrid(); }); var $gridTypesOpt = $('.grid-types-opt'); diff --git a/packages/joint-core/docs/src/joint/api/dia/Paper/prototype/clearGrid.html b/packages/joint-core/docs/src/joint/api/dia/Paper/prototype/clearGrid.html deleted file mode 100644 index 525be5e31..000000000 --- a/packages/joint-core/docs/src/joint/api/dia/Paper/prototype/clearGrid.html +++ /dev/null @@ -1,3 +0,0 @@ - -
paper.clearGrid()
-

Hide the current grid.

diff --git a/packages/joint-core/docs/src/joint/api/dia/Paper/prototype/drawGrid.html b/packages/joint-core/docs/src/joint/api/dia/Paper/prototype/drawGrid.html deleted file mode 100644 index 531f824db..000000000 --- a/packages/joint-core/docs/src/joint/api/dia/Paper/prototype/drawGrid.html +++ /dev/null @@ -1,7 +0,0 @@ - -
paper.drawGrid([opt])
-

Draw visual grid lines on the paper. Possible options:

- diff --git a/packages/joint-core/docs/src/joint/api/dia/Paper/prototype/setGrid.html b/packages/joint-core/docs/src/joint/api/dia/Paper/prototype/setGrid.html index 59ff976ed..06eb6be19 100644 --- a/packages/joint-core/docs/src/joint/api/dia/Paper/prototype/setGrid.html +++ b/packages/joint-core/docs/src/joint/api/dia/Paper/prototype/setGrid.html @@ -1,7 +1,7 @@
paper.setGrid(gridOption)
-

Set the type of visual grid on the paper. Note, you still have to call paper.drawGrid() afterwards to draw the grid.

+

Set the type of visual grid on the paper. If a falsy value is provided, the grid is removed.

-
paper.setGrid(); // default pattern (dot) with default settings
+
paper.setGrid(); // removes the grid visual
 paper.setGrid(true); // default pattern (dot) with default settings
 
 paper.setGrid('mesh'); // pre-defined pattern with default settings
diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs
index eff7c8ae7..ab94a937c 100644
--- a/packages/joint-core/src/dia/Paper.mjs
+++ b/packages/joint-core/src/dia/Paper.mjs
@@ -21,7 +21,6 @@ import {
     debounce,
     omit,
     result,
-    merge,
     camelCase,
     cloneDeep,
     invoke,
@@ -46,6 +45,7 @@ import * as connectionPoints from '../connectionPoints/index.mjs';
 import * as anchors from '../anchors/index.mjs';
 
 import $ from '../mvc/Dom/index.mjs';
+import { GridLayer } from './layers/GridLayer.mjs';
 
 const sortingTypes = {
     NONE: 'sorting-none',
@@ -83,6 +83,8 @@ const defaultHighlighting = {
 };
 
 const defaultLayers = [{
+    name: LayersNames.GRID,
+}, {
     name: LayersNames.BACK,
 }, {
     name: LayersNames.CELLS,
@@ -395,7 +397,6 @@ export const Paper = View.extend({
         // Layers (SVGGroups)
         this._layers = {};
 
-        this.setGrid(options.drawGrid);
         this.cloneOptions();
         this.render();
         this._setDimensions();
@@ -556,15 +557,6 @@ export const Paper = View.extend({
                 position: 'absolute',
                 inset: 0
             }
-        }, {
-            namespaceURI: ns.xhtml,
-            tagName: 'div',
-            className: addClassNamePrefix('paper-grid'),
-            selector: 'grid',
-            style: {
-                position: 'absolute',
-                inset: 0
-            }
         }, {
             namespaceURI: ns.svg,
             tagName: 'svg',
@@ -629,7 +621,7 @@ export const Paper = View.extend({
         }
 
         if (options.drawGrid) {
-            this.drawGrid();
+            this.setGrid(options.drawGrid);
         }
 
         return this;
@@ -640,11 +632,20 @@ export const Paper = View.extend({
         V(this.svg).prepend(V.createSVGStyle(css));
     },
 
+    createLayer(name) {
+        switch (name) {
+            case LayersNames.GRID:
+                return new GridLayer({ name, paper: this, patterns: this.constructor.gridPatterns });
+            default:
+                return new PaperLayer({ name });
+        }
+    },
+
     renderLayers: function(layers = defaultLayers) {
         this.removeLayers();
         // TODO: Layers to be read from the graph `layers` attribute
         layers.forEach(({ name, sorted }) => {
-            const layerView = new PaperLayer({ name });
+            const layerView = this.createLayer(name);
             this.layers.appendChild(layerView.el);
             this._layers[name] = layerView;
         });
@@ -681,10 +682,6 @@ export const Paper = View.extend({
 
     update: function() {
 
-        if (this.options.drawGrid) {
-            this.drawGrid();
-        }
-
         if (this._background) {
             this.updateBackgroundImage(this._background);
         }
@@ -1810,7 +1807,7 @@ export const Paper = View.extend({
         }
 
         const { options } = this;
-        const { origin, drawGrid } = options;
+        const { origin } = options;
 
         // setter
         tx || (tx = 0);
@@ -1828,11 +1825,6 @@ export const Paper = View.extend({
         origin.y = oy;
 
         this.trigger('translate', ox, oy);
-
-        if (drawGrid) {
-            this.drawGrid();
-        }
-
         return this;
     },
 
@@ -2639,155 +2631,17 @@ export const Paper = View.extend({
     },
 
     setGridSize: function(gridSize) {
-
         const { options } = this;
         options.gridSize = gridSize;
-
         if (options.drawGrid && !options.drawGridSize) {
             // Do not redraw the grid if the `drawGridSize` is set.
-            this.drawGrid();
-        }
-
-        return this;
-    },
-
-    clearGrid: function() {
-
-        const { childNodes } = this;
-        if (childNodes && childNodes.grid) {
-            childNodes.grid.style.backgroundImage = '';
+            this.getLayerView(LayersNames.GRID).renderGrid();
         }
         return this;
     },
 
-    _getGridRefs: function() {
-
-        if (!this._gridCache) {
-
-            this._gridCache = {
-                root: V('svg', { width: '100%', height: '100%' }, V('defs')),
-                patterns: {},
-                add: function(id, vel) {
-                    V(this.root.node.childNodes[0]).append(vel);
-                    this.patterns[id] = vel;
-                    this.root.append(V('rect', { width: '100%', height: '100%', fill: 'url(#' + id + ')' }));
-                },
-                get: function(id) {
-                    return this.patterns[id];
-                },
-                exist: function(id) {
-                    return this.patterns[id] !== undefined;
-                }
-            };
-        }
-
-        return this._gridCache;
-    },
-
     setGrid: function(drawGrid) {
-
-        this.clearGrid();
-
-        this._gridCache = null;
-        this._gridSettings = [];
-
-        var optionsList = Array.isArray(drawGrid) ? drawGrid : [drawGrid || {}];
-        optionsList.forEach(function(item) {
-            this._gridSettings.push.apply(this._gridSettings, this._resolveDrawGridOption(item));
-        }, this);
-        return this;
-    },
-
-    _resolveDrawGridOption: function(opt) {
-
-        var namespace = this.constructor.gridPatterns;
-        if (isString(opt) && Array.isArray(namespace[opt])) {
-            return namespace[opt].map(function(item) {
-                return assign({}, item);
-            });
-        }
-
-        var options = opt || { args: [{}] };
-        var isArray = Array.isArray(options);
-        var name = options.name;
-
-        if (!isArray && !name && !options.markup) {
-            name = 'dot';
-        }
-
-        if (name && Array.isArray(namespace[name])) {
-            var pattern = namespace[name].map(function(item) {
-                return assign({}, item);
-            });
-
-            var args = Array.isArray(options.args) ? options.args : [options.args || {}];
-
-            defaults(args[0], omit(opt, 'args'));
-            for (var i = 0; i < args.length; i++) {
-                if (pattern[i]) {
-                    assign(pattern[i], args[i]);
-                }
-            }
-            return pattern;
-        }
-
-        return isArray ? options : [options];
-    },
-
-    drawGrid: function(opt) {
-
-        const gridSize = this.options.drawGridSize || this.options.gridSize;
-        if (gridSize <= 1) {
-            return this.clearGrid();
-        }
-
-        var localOptions = Array.isArray(opt) ? opt : [opt];
-
-        var ctm = this.matrix();
-        var refs = this._getGridRefs();
-
-        this._gridSettings.forEach(function(gridLayerSetting, index) {
-
-            var id = 'pattern_' + index;
-            var options = merge(gridLayerSetting, localOptions[index], {
-                sx: ctm.a || 1,
-                sy: ctm.d || 1,
-                ox: ctm.e || 0,
-                oy: ctm.f || 0
-            });
-
-            options.width = gridSize * (ctm.a || 1) * (options.scaleFactor || 1);
-            options.height = gridSize * (ctm.d || 1) * (options.scaleFactor || 1);
-
-            if (!refs.exist(id)) {
-                refs.add(id, V('pattern', { id: id, patternUnits: 'userSpaceOnUse' }, V(options.markup)));
-            }
-
-            var patternDefVel = refs.get(id);
-
-            if (isFunction(options.update)) {
-                options.update(patternDefVel.node.childNodes[0], options);
-            }
-
-            var x = options.ox % options.width;
-            if (x < 0) x += options.width;
-
-            var y = options.oy % options.height;
-            if (y < 0) y += options.height;
-
-            patternDefVel.attr({
-                x: x,
-                y: y,
-                width: options.width,
-                height: options.height
-            });
-        });
-
-        var patternUri = new XMLSerializer().serializeToString(refs.root.node);
-        patternUri = 'url(data:image/svg+xml;base64,' + btoa(patternUri) + ')';
-
-        this.childNodes.grid.style.backgroundImage = patternUri;
-
+        this.getLayerView(LayersNames.GRID).setGrid(drawGrid);
         return this;
     },
 
@@ -3250,10 +3104,10 @@ export const Paper = View.extend({
             color: '#AAAAAA',
             thickness: 1,
             markup: 'rect',
-            update: function(el, opt) {
+            render: function(el, opt) {
                 V(el).attr({
-                    width: opt.thickness * opt.sx,
-                    height: opt.thickness * opt.sy,
+                    width: opt.thickness,
+                    height: opt.thickness,
                     fill: opt.color
                 });
             }
@@ -3262,16 +3116,21 @@ export const Paper = View.extend({
             color: '#AAAAAA',
             thickness: 1,
             markup: 'rect',
-            update: function(el, opt) {
-                var size = opt.sx <= 1 ? opt.thickness * opt.sx : opt.thickness;
-                V(el).attr({ width: size, height: size, fill: opt.color });
+            render: function(el, opt) {
+                V(el).attr({ fill: opt.color });
+            },
+            update: function(el, opt, paper) {
+                const { sx, sy } = paper.scale();
+                const width = sx <= 1 ? opt.thickness : opt.thickness / sx;
+                const height = sy <= 1 ? opt.thickness : opt.thickness / sy;
+                V(el).attr({ width, height });
             }
         }],
         mesh: [{
             color: '#AAAAAA',
             thickness: 1,
             markup: 'path',
-            update: function(el, opt) {
+            render: function(el, opt) {
 
                 var d;
                 var width = opt.width;
@@ -3291,7 +3150,7 @@ export const Paper = View.extend({
             color: '#AAAAAA',
             thickness: 1,
             markup: 'path',
-            update: function(el, opt) {
+            render: function(el, opt) {
 
                 var d;
                 var width = opt.width;
@@ -3311,7 +3170,7 @@ export const Paper = View.extend({
             thickness: 3,
             scaleFactor: 4,
             markup: 'path',
-            update: function(el, opt) {
+            render: function(el, opt) {
 
                 var d;
                 var width = opt.width;
diff --git a/packages/joint-core/src/dia/PaperLayer.mjs b/packages/joint-core/src/dia/PaperLayer.mjs
index 624e3b38d..5a1d85ac3 100644
--- a/packages/joint-core/src/dia/PaperLayer.mjs
+++ b/packages/joint-core/src/dia/PaperLayer.mjs
@@ -2,6 +2,7 @@ import { View } from '../mvc/index.mjs';
 import { addClassNamePrefix } from '../util/util.mjs';
 
 export const LayersNames = {
+    GRID: 'grid',
     CELLS: 'cells',
     BACK: 'back',
     FRONT: 'front',
diff --git a/packages/joint-core/src/dia/layers/GridLayer.mjs b/packages/joint-core/src/dia/layers/GridLayer.mjs
new file mode 100644
index 000000000..5716fb5f8
--- /dev/null
+++ b/packages/joint-core/src/dia/layers/GridLayer.mjs
@@ -0,0 +1,176 @@
+import { PaperLayer } from '../PaperLayer.mjs';
+import {
+    isFunction,
+    isString,
+    defaults,
+    omit,
+    assign,
+    merge,
+} from '../../util/index.mjs';
+import V from '../../V/index.mjs';
+
+export const GridLayer = PaperLayer.extend({
+
+    style: {
+        'pointer-events': 'none'
+    },
+
+    _gridCache: null,
+    _gridSettings: null,
+
+    init() {
+        PaperLayer.prototype.init.apply(this, arguments);
+        const { options: { paper }} = this;
+        this._gridCache = null;
+        this._gridSettings = [];
+        this.listenTo(paper, 'scale translate resize', this.updateGrid);
+    },
+
+    setGrid(drawGrid) {
+        this._gridSettings = this.getGridSettings(drawGrid);
+        this.renderGrid();
+    },
+
+    getGridSettings(drawGrid) {
+        const gridSettings = [];
+        if (drawGrid) {
+            const optionsList = Array.isArray(drawGrid) ? drawGrid : [drawGrid || {}];
+            optionsList.forEach((item) => {
+                gridSettings.push(...this._resolveDrawGridOption(item));
+            });
+        }
+        return gridSettings;
+    },
+
+    removeGrid() {
+        const { _gridCache: grid } = this;
+        if (!grid) return;
+        grid.root.remove();
+        this._gridCache = null;
+    },
+
+    renderGrid() {
+
+        const { options: { paper }} = this;
+        const { _gridSettings: gridSettings } = this;
+
+        this.removeGrid();
+
+        if (gridSettings.length === 0) return;
+
+        const gridSize = paper.options.drawGridSize || paper.options.gridSize;
+        if (gridSize <= 1) {
+            return;
+        }
+
+        const refs = this._getGridRefs();
+
+        gridSettings.forEach((gridLayerSetting, index) => {
+
+            const id = 'pattern_' + index;
+            const options = merge({}, gridLayerSetting);
+            const { scaleFactor = 1 } = options;
+            options.width = gridSize * scaleFactor || 1;
+            options.height = gridSize * scaleFactor || 1;
+
+            let vPattern;
+            if (!refs.exist(id)) {
+                vPattern = V('pattern', { id: id, patternUnits: 'userSpaceOnUse' }, V(options.markup));
+                refs.add(id, vPattern);
+            } else {
+                vPattern = refs.get(id);
+            }
+
+            if (isFunction(options.render)) {
+                options.render(vPattern.node.firstChild, options, paper);
+            }
+            vPattern.attr({
+                width: options.width,
+                height: options.height
+            });
+        });
+
+        refs.root.appendTo(this.el);
+        this.updateGrid();
+    },
+
+    updateGrid() {
+
+        const { _gridCache: grid, _gridSettings: gridSettings, options: { paper }} = this;
+        if (!grid) return;
+        const { root: vSvg, patterns } = grid;
+        const { x, y, width, height } = paper.getArea();
+        vSvg.attr({ x, y, width, height });
+        for (const patternId in patterns) {
+            const vPattern = patterns[patternId];
+            vPattern.attr({ x: -x, y: -y });
+        }
+        gridSettings.forEach((options, index) => {
+            if (isFunction(options.update)) {
+                const vPattern = patterns['pattern_' + index];
+                options.update(vPattern.node.firstChild, options, paper);
+            }
+        });
+    },
+
+    _getGridRefs() {
+        let { _gridCache: grid } = this;
+        if (grid) return grid;
+        const defsVEl = V('defs');
+        const svgVEl = V('svg', { width: '100%', height: '100%' }, [defsVEl]);
+        grid = this._gridCache = {
+            root: svgVEl,
+            patterns: {},
+            add: function(id, patternVEl) {
+                const rectVEl = V('rect', { width: '100%', height: '100%', fill: `url(#${id})` });
+                defsVEl.append(patternVEl);
+                svgVEl.append(rectVEl);
+                this.patterns[id] = patternVEl;
+            },
+            get: function(id) {
+                return this.patterns[id];
+            },
+            exist: function(id) {
+                return this.patterns[id] !== undefined;
+            }
+        };
+        return grid;
+    },
+
+    _resolveDrawGridOption(opt) {
+
+        var namespace = this.options.patterns;
+        if (isString(opt) && Array.isArray(namespace[opt])) {
+            return namespace[opt].map(function(item) {
+                return assign({}, item);
+            });
+        }
+
+        var options = opt || { args: [{}] };
+        var isArray = Array.isArray(options);
+        var name = options.name;
+
+        if (!isArray && !name && !options.markup) {
+            name = 'dot';
+        }
+
+        if (name && Array.isArray(namespace[name])) {
+            var pattern = namespace[name].map(function(item) {
+                return assign({}, item);
+            });
+
+            var args = Array.isArray(options.args) ? options.args : [options.args || {}];
+
+            defaults(args[0], omit(opt, 'args'));
+            for (var i = 0; i < args.length; i++) {
+                if (pattern[i]) {
+                    assign(pattern[i], args[i]);
+                }
+            }
+            return pattern;
+        }
+
+        return isArray ? options : [options];
+    },
+
+});
diff --git a/packages/joint-core/test/jointjs/paper.js b/packages/joint-core/test/jointjs/paper.js
index 290227934..9ef118b72 100644
--- a/packages/joint-core/test/jointjs/paper.js
+++ b/packages/joint-core/test/jointjs/paper.js
@@ -1337,9 +1337,12 @@ QUnit.module('paper', function(hooks) {
 
     QUnit.module('draw grid options', function(hooks) {
 
-        var getGridVel = function(paper) {
-            var image = paper.childNodes.grid.style.backgroundImage.replace(/url\("*|"*\)/g, '').replace('data:image/svg+xml;base64,', '');
-            return image !== 'none' ?  V(atob(image)) : undefined;
+        const getGridSettings = function(paper) {
+            return paper.getLayerView(joint.dia.Paper.Layers.GRID)._gridSettings;
+        };
+
+        const getGridVel = function(paper) {
+            return V(paper.getLayerNode(joint.dia.Paper.Layers.GRID).firstChild);
         };
 
         var preparePaper = function(drawGrid, paperSettings) {
@@ -1384,7 +1387,7 @@ QUnit.module('paper', function(hooks) {
                     gridSize: 1,
                     drawGridSize: 17
                 });
-                const drawGridSpy = sinon.spy(paper, 'drawGrid');
+                const drawGridSpy = sinon.spy(paper.getLayerView(joint.dia.Paper.Layers.GRID), 'renderGrid');
                 paper.setGridSize(5);
                 assert.ok(drawGridSpy.notCalled);
                 drawGridSpy.restore();
@@ -1405,7 +1408,6 @@ QUnit.module('paper', function(hooks) {
 
                 var drawGrid = { color: 'red', thickness: 2 };
                 var paper = preparePaper(drawGrid);
-                paper.drawGrid();
 
                 var svg = getGridVel(paper);
 
@@ -1423,7 +1425,6 @@ QUnit.module('paper', function(hooks) {
 
                 var drawGrid = { markup: '', color: 'red' };
                 var paper = preparePaper(drawGrid);
-                paper.drawGrid();
 
                 var svg = getGridVel(paper);
 
@@ -1447,7 +1448,6 @@ QUnit.module('paper', function(hooks) {
                     }, color: 'red'
                 };
                 var paper = preparePaper(drawGrid);
-                paper.drawGrid();
 
                 var svg = getGridVel(paper);
 
@@ -1474,8 +1474,6 @@ QUnit.module('paper', function(hooks) {
                     origin: { x: -5, y: -5 }
                 });
 
-                paper.drawGrid();
-
                 var svg = getGridVel(paper);
 
                 var patterns = V(svg.node.childNodes[0]).find('pattern');
@@ -1487,7 +1485,7 @@ QUnit.module('paper', function(hooks) {
 
                 assert.deepEqual(
                     { width: redDotAttrs.width, height: redDotAttrs.height, x: redDotAttrs.x, y: redDotAttrs.y },
-                    { width: '10', height: '10', x: '5', y: '5' },
+                    { width: '10', height: '10', x: '-5', y: '-5' },
                     'red dot pattern attrs'
                 );
 
@@ -1498,12 +1496,12 @@ QUnit.module('paper', function(hooks) {
                         x: greenDotAttrs.x,
                         y: greenDotAttrs.y
                     },
-                    { width: '20', height: '20', x: '15', y: '15' },
+                    { width: '20', height: '20', x: '-5', y: '-5' },
                     'green dot pattern attrs'
                 );
             });
 
-            QUnit.test('local options - as an object', function(assert) {
+            QUnit.test('reset grid array - partial', function(assert) {
 
                 var drawGrid = [
                     { color: 'red' },
@@ -1516,7 +1514,7 @@ QUnit.module('paper', function(hooks) {
                     origin: { x: -5, y: -5 }
                 });
 
-                paper.drawGrid({ color: 'pink' });
+                paper.setGrid(joint.util.defaultsDeep([{ color: 'pink' }], drawGrid));
 
                 var svg = getGridVel(paper);
 
@@ -1526,12 +1524,13 @@ QUnit.module('paper', function(hooks) {
 
                 assert.equal(patterns.length, 2);
 
-                var redDot = V(patterns[0].node.childNodes[0]);
+                const [redDot, greenDot] = patterns.map((pattern) => V(pattern.node.firstChild));
+                assert.equal(redDot.attr('fill'), 'pink', 'color updated by reset');
+                assert.equal(greenDot.attr('fill'), 'green', 'not updated by reset');
 
-                assert.equal(redDot.attr('fill'), 'pink', 'color updated by local options');
             });
 
-            QUnit.test('local options - as an array', function(assert) {
+            QUnit.test('reset grid array', function(assert) {
 
                 var drawGrid = [
                     { color: 'red' },
@@ -1544,7 +1543,7 @@ QUnit.module('paper', function(hooks) {
                     origin: { x: -5, y: -5 }
                 });
 
-                paper.drawGrid([{ color: 'black' }, { color: 'pink' }]);
+                paper.setGrid(joint.util.defaultsDeep([{ color: 'black' }, { color: 'pink' }], drawGrid));
 
                 var svg = getGridVel(paper);
 
@@ -1553,14 +1552,13 @@ QUnit.module('paper', function(hooks) {
 
                 assert.equal(svg.node.childNodes.length, 3, 'defs + 2x rect with pattern fill');
                 assert.equal(patterns.length, 2);
-                assert.equal(greenDot.attr('fill'), 'pink', 'color updated by local options');
+                assert.equal(greenDot.attr('fill'), 'pink', 'color updated by reset');
             });
 
             QUnit.test('update mesh', function(assert) {
 
                 var drawGrid = { name: 'mesh', color: 'red', thickness: 2 };
                 var paper = preparePaper(drawGrid);
-                paper.drawGrid();
 
                 var svg = getGridVel(paper);
 
@@ -1569,7 +1567,7 @@ QUnit.module('paper', function(hooks) {
                 assert.equal(patternAttr.stroke, 'red');
                 assert.equal(patternAttr['stroke-width'], '2');
 
-                paper.drawGrid({ color: 'blue', thickness: 1 });
+                paper.setGrid(joint.util.defaults({ color: 'blue', thickness: 1 }, drawGrid));
                 svg = getGridVel(paper);
                 patterns = V(svg.node.childNodes[0]).find('pattern');
                 patternAttr = V(patterns[0].node.childNodes[0]).attr();
@@ -1581,7 +1579,6 @@ QUnit.module('paper', function(hooks) {
 
                 var drawGrid = { name: 'doubleMesh', args: [{ color: 'red', thickness: 2 }] };
                 var paper = preparePaper(drawGrid);
-                paper.drawGrid();
 
                 var svg = getGridVel(paper);
 
@@ -1590,7 +1587,8 @@ QUnit.module('paper', function(hooks) {
                 assert.equal(patternAttr.stroke, 'red');
                 assert.equal(patternAttr['stroke-width'], '2');
 
-                paper.drawGrid({ color: 'blue', thickness: 1 });
+                paper.setGrid(joint.util.defaultsDeep({ args: { color: 'blue', thickness: 1 }}, drawGrid));
+
                 svg = getGridVel(paper);
                 patterns = V(svg.node.childNodes[0]).find('pattern');
                 patternAttr = V(patterns[0].node.childNodes[0]).attr();
@@ -1608,6 +1606,10 @@ QUnit.module('paper', function(hooks) {
                 paper = new joint.dia.Paper();
             });
 
+            hooks.afterEach(function() {
+                paper.remove();
+            });
+
             QUnit.test('set doubleMesh settings', function(assert) {
 
                 var drawGridTestFixtures = [
@@ -1618,41 +1620,42 @@ QUnit.module('paper', function(hooks) {
 
                 var check = function(message) {
 
-                    assert.equal(paper._gridSettings.length, 2);
-                    var firstLayer = paper._gridSettings[0];
+                    const gridSettings = getGridSettings(paper);
+                    assert.equal(gridSettings.length, 2);
+                    var firstLayer = gridSettings[0];
 
                     assert.equal(firstLayer.color, 'red', message + ': color');
                     assert.equal(firstLayer.thickness, 11, message + ': thickness');
                     assert.equal(firstLayer.markup, 'path', message + ': markup');
-                    assert.ok(_.isFunction(firstLayer.update), message + ': update');
+                    assert.ok(_.isFunction(firstLayer.render), message + ': update');
                 };
 
                 paper.setGrid(drawGridTestFixtures[0]);
                 check('args: {}');
 
                 paper.setGrid(drawGridTestFixtures[1]);
-                var secondLayer = paper._gridSettings[1];
+                var secondLayer = getGridSettings(paper)[1];
                 var message = 'args: [{}] - second layer';
                 assert.equal(secondLayer.color, 'black', message + ': color');
                 assert.equal(secondLayer.thickness, 55, message + ': thickness');
                 assert.equal(secondLayer.markup, 'path', message + ': markup');
-                assert.ok(_.isFunction(secondLayer.update), message + ': update');
+                assert.ok(_.isFunction(secondLayer.render), message + ': update');
                 check('args: [{}]');
 
                 paper.setGrid(drawGridTestFixtures[2]);
                 check('no args');
             });
 
-            QUnit.test('update default', function(assert){
+            QUnit.test('render default', function(assert){
 
                 paper.setGrid({ color: 'red', thickness: 11 });
-                assert.propEqual(paper._gridSettings[0], {
+                assert.propEqual(getGridSettings(paper)[0], {
                     color: 'red',
                     thickness: 11,
                     markup: 'rect',
-                    update: {}
+                    render: {}
                 }, 'update default');
-                assert.ok(_.isFunction(paper._gridSettings[0].update));
+                assert.ok(_.isFunction(getGridSettings(paper)[0].render));
             });
 
             QUnit.test('create custom', function(assert) {
@@ -1664,16 +1667,16 @@ QUnit.module('paper', function(hooks) {
                 ];
 
                 paper.setGrid(drawGridTestFixtures[0]);
-                assert.deepEqual(paper._gridSettings[0], { markup: 'rect', update: 'fnc' }, 'custom markup and update');
+                assert.deepEqual(getGridSettings(paper)[0], { markup: 'rect', update: 'fnc' }, 'custom markup and update');
 
                 paper.setGrid(drawGridTestFixtures[1]);
-                assert.ok(_.isArray(paper._gridSettings));
-                assert.deepEqual(paper._gridSettings[0], { markup: 'rect', update: 'fnc' }, 'custom markup and update - first layer');
-                assert.deepEqual(paper._gridSettings[1], { markup: 'rect2', update: 'fnc2' }, 'custom markup and update- second layer');
+                assert.ok(_.isArray(getGridSettings(paper)));
+                assert.deepEqual(getGridSettings(paper)[0], { markup: 'rect', update: 'fnc' }, 'custom markup and update - first layer');
+                assert.deepEqual(getGridSettings(paper)[1], { markup: 'rect2', update: 'fnc2' }, 'custom markup and update- second layer');
 
                 paper.setGrid(drawGridTestFixtures[2]);
-                assert.ok(_.isArray(paper._gridSettings));
-                assert.deepEqual(paper._gridSettings[0], { markup: '' }, 'custom grid - minimal setup');
+                assert.ok(_.isArray(getGridSettings(paper)));
+                assert.deepEqual(getGridSettings(paper)[0], { markup: '' }, 'custom grid - minimal setup');
             });
 
             QUnit.test('initialize gridSettings', function(assert) {
@@ -1681,14 +1684,14 @@ QUnit.module('paper', function(hooks) {
                 var dotDefault = joint.dia.Paper.gridPatterns.dot[0];
 
                 paper.setGrid({ markup: '' });
-                assert.deepEqual(paper._gridSettings[0], { markup: '' }, 'markup only');
+                assert.deepEqual(getGridSettings(paper)[0], { markup: '' }, 'markup only');
 
                 paper.setGrid({ update: 'custom' });
-                assert.propEqual(_.omit(paper._gridSettings[0], 'update'), _.omit(dotDefault, 'update'), 'override update function');
-                assert.equal(paper._gridSettings[0].update, 'custom');
+                assert.propEqual(_.omit(getGridSettings(paper)[0], 'update'), _.omit(dotDefault, 'update'), 'override update function');
+                assert.equal(getGridSettings(paper)[0].update, 'custom');
 
                 paper.setGrid('dot');
-                assert.propEqual(paper._gridSettings[0], dotDefault, 'update');
+                assert.propEqual(getGridSettings(paper)[0], dotDefault, 'update');
 
                 paper.setGrid([{ color: 'red' }, { color: 'black' }]);
             });
diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts
index 8972a330c..809cc930c 100644
--- a/packages/joint-core/types/joint.d.ts
+++ b/packages/joint-core/types/joint.d.ts
@@ -1187,6 +1187,7 @@ export namespace dia {
             BACK = 'back',
             FRONT = 'front',
             TOOLS = 'tools',
+            GRID = 'grid',
         }
 
         type UpdateStats = {
@@ -1518,20 +1519,12 @@ export namespace dia {
 
         drawBackground(opt?: Paper.BackgroundOptions): this;
 
-        drawGrid(opt?: Paper.GridOptions | Paper.GridOptions[]): this;
-
-        clearGrid(): this;
-
         getDefaultLink(cellView: CellView, magnet: SVGElement): Link;
 
         getModelById(id: Cell.ID | Cell): Cell;
 
         setDimensions(width: Paper.Dimension, height: Paper.Dimension): void;
 
-        setGrid(opt?: boolean | string | Paper.GridOptions | Paper.GridOptions[]): this;
-
-        setGridSize(gridSize: number): this;
-
         setInteractivity(value: any): void;
 
         setOrigin(x: number, y: number): this;
@@ -1546,6 +1539,12 @@ export namespace dia {
 
         getPointerArgs(evt: dia.Event): [dia.Event, number, number];
 
+        // grid
+
+        setGrid(opt?: null | boolean | string | Paper.GridOptions | Paper.GridOptions[]): this;
+
+        setGridSize(gridSize: number): this;
+
         // tools
 
         removeTools(): this;