diff --git a/pd/nw/dialog_prefs.html b/pd/nw/dialog_prefs.html
index 07a857749594c6722cde64de2092bf787ecd773b..8308b66de68e7e468aaca4a9b2c98675cfdc218c 100644
--- a/pd/nw/dialog_prefs.html
+++ b/pd/nw/dialog_prefs.html
@@ -415,9 +415,16 @@ select {
             </select>
             <br/>
             <label data-i18n="[title]prefs.gui.grid.show_grid_tt">
-              <input type="checkbox" id="show_grid" name="show_grid">
+              <input type="checkbox" id="show_grid" name="show_grid"
+                     onchange="grid_toggle(this.checked)">
               <span data-i18n="prefs.gui.grid.show_grid"></span>
             </label>
+	    <select id="grid_size">
+              <option value="5">5</option>
+              <option value="10">10</option>
+              <option value="20">20</option>
+              <option value="25">25</option>
+            </select>
             <br/>
             <label data-i18n="[title]prefs.gui.zoom.save_zoom_tt">
               <input type="checkbox" id="save_zoom" name="save_zoom">
@@ -783,6 +790,7 @@ function apply(save_prefs) {
     pdgui.pdsend("pd gui-prefs",
         get_gui_preset(),
         get_bool_elem("show_grid"),
+	document.getElementById("grid_size").value,
         get_bool_elem("save_zoom"),
         get_bool_elem("browser_doc"),
         get_bool_elem("browser_path"),
@@ -796,7 +804,8 @@ function apply(save_prefs) {
         get_bool_elem("browser_path")
     );
     // Update the grid on all open windows.
-    pdgui.update_grid(get_bool_elem("show_grid"));
+    pdgui.update_grid(get_bool_elem("show_grid"),
+        document.getElementById("grid_size").value);
 
     // Send the startup config data to Pd
     pdgui.pdsend.apply(null, ["pd path-dialog", startup_use_stdpath, startup_verbose].concat(get_path_array()));
@@ -1035,13 +1044,18 @@ function midi_prefs_callback(attrs) {
     pdgui.resize_window(pd_object_callback);
 }
 
+function grid_toggle(checked) {
+    document.getElementById("grid_size").disabled = !checked;
+    document.getElementById("show_grid").checked = checked;
+}
+
 function autopatch_yoffset_toggle(checked) {
     document.getElementById("autopatch_yoffset_value").disabled = !checked;
     document.getElementById("autopatch_yoffset").checked = checked;
 }
 
-function gui_prefs_callback(name, show_grid, save_zoom, browser_doc, browser_path,
-    browser_init, autopatch_yoffset) {
+function gui_prefs_callback(name, show_grid, grid_size, save_zoom, browser_doc,
+    browser_path, browser_init, autopatch_yoffset) {
     var s = document.getElementById("gui_preset");
 
     // ag: scan the css subdir for user-defined styles
@@ -1071,6 +1085,8 @@ function gui_prefs_callback(name, show_grid, save_zoom, browser_doc, browser_pat
         }
     }
     document.getElementById("show_grid").checked = !!show_grid;
+    document.getElementById("grid_size").value = grid_size;
+    grid_toggle(!!show_grid);
     document.getElementById("save_zoom").checked = !!save_zoom;
     document.getElementById("browser_doc").checked = !!browser_doc;
     document.getElementById("browser_path").checked = !!browser_path;
diff --git a/pd/nw/locales/de/translation.json b/pd/nw/locales/de/translation.json
index 73ccfc8b2cad4fb9d7911edb1f2ac4d748a75394..c2b72f7e3c0479f87ec9297a2cd5601c938582c7 100644
--- a/pd/nw/locales/de/translation.json
+++ b/pd/nw/locales/de/translation.json
@@ -432,8 +432,8 @@
         "footgun": "Fusspistole"
       },
       "grid": {
-        "show_grid": "Gitter-Hintergrund im Edit-Modus",
-        "show_grid_tt": "Gitter-Hintergrund im Edit-Modus anzeigen"
+        "show_grid": "Am Gitter ausrichten (experimentell)",
+        "show_grid_tt": "Ausrichten am Gitter im Edit-Modus"
       },
       "zoom": {
         "save_zoom": "Speichern/Laden der Vergrößerung im Patch",
diff --git a/pd/nw/locales/en/translation.json b/pd/nw/locales/en/translation.json
index ee65f1aa393cb47133587fdb9a875daddab2284d..7c9ac0e47444773d8cd7601a63a37e4819d527dd 100644
--- a/pd/nw/locales/en/translation.json
+++ b/pd/nw/locales/en/translation.json
@@ -432,8 +432,8 @@
         "footgun": "Footgun"
       },
       "grid": {
-        "show_grid": "grid background in edit mode",
-        "show_grid_tt": "Show the grid background in edit mode"
+        "show_grid": "snap to grid (experimental)",
+        "show_grid_tt": "Snap to the grid in edit mode"
       },
       "zoom": {
         "save_zoom": "save/load zoom level with patch",
diff --git a/pd/nw/locales/fr/translation.json b/pd/nw/locales/fr/translation.json
index ccdc3bc4b7524cabb3a5cd4a4a2b8ac2f9b74667..b397705d8a46942e62e67caa8732c8c2b9954162 100644
--- a/pd/nw/locales/fr/translation.json
+++ b/pd/nw/locales/fr/translation.json
@@ -432,8 +432,8 @@
         "footgun":   "Footgun"
       },
       "grid": {
-        "show_grid": "Fond de grille en mode Édition",
-        "show_grid_tt": "Afficher l'arrière-plan de la grille en mode Édition"
+        "show_grid": "Aligner sur la grille (expérimental)",
+        "show_grid_tt": "Accrocher à la grille en mode Édition"
       },
       "zoom": {
         "save_zoom":    "Sauver/Charger niveau zoom avec patch",
diff --git a/pd/nw/pdgui.js b/pd/nw/pdgui.js
index dd8d0bb349d523196f6e5797decdad137e47a87d..367550ee2e98c437510eabcd2e949b88f643f8f6 100644
--- a/pd/nw/pdgui.js
+++ b/pd/nw/pdgui.js
@@ -1374,36 +1374,146 @@ function menu_send(name) {
     }
 }
 
-/*
-ico@vt.edu 20200907: added svg tiled background to reflect edit mode and
-integrated it into the canvas_set_editmode below.
+// Set the grid background position to adjust for the viewBox of the svg.
+// We do this separately and before setting the background so we can call this
+// when the scroll view needs to be adjusted.
+function get_grid_coords(cid, svg_elem) {
+    var vbox = svg_elem.getAttribute("viewBox").split(" "),
+        dx = 0, dy = 0;
+    // First two values of viewBox are x-origin and y-origin. Pd allows
+    // negative coordinates-- for example, the user can drag an object at
+    // (0, 0) 12 pixels to the left to arrive at (-12, 0). To accommodate this
+    // with the svg backend, we would adjust the x-origin to be -12 so that
+    // the user can view it (possibly by scrolling). These adjustments are
+    // all handled with gui_canvas_get_scroll.
+    //
+    // For the background image css property, everything is based on
+    // CSS DOM positioning. CSS doesn't really know anything about the SVG
+    // viewport-- it only knows that an SVG element is of a certain size and
+    // (in our case) has its top-left corner at the top-left corner of the
+    // window. So when we change the viewBox to have negative origin indices,
+    // we have to adjust the origin of the grid in the opposite direction
+    // For example, if our new x-origin for the svg viewBox is -12, we make
+    // the x-origin for the background image "12px". This adjustment positions
+    // the grid *as if* if extended 12 more pixels to the left of its
+    // container.
+    if (vbox[0] < 0) {
+        dx = 0 - vbox[0];
+    }
+    if (vbox[1] < 0) {
+        dy = 0 - vbox[1];
+    }
+    return { x: dx, y: dy };
+}
+
+function create_svg_lock(cid) {
+    var zoom = patchwin[cid].zoomLevel,
+        size;
+    // adjust for zoom level
+    size = 1 / Math.pow(1.2, zoom) * 24;
+    return "url('data:image/svg+xml;utf8," +
+        encodeURIComponent(['<svg xmlns="http://www.w3.org/2000/svg"',
+                 ['width="', size, 'px"'].join(""),
+                 ['height="', size, 'px"'].join(""),
+                 'viewBox="0 0 486.866 486.866"',
+            '>',
+              '<path fill="#bbb" d="',
+                'M393.904,214.852h-8.891v-72.198c0-76.962-61.075-141.253',
+                '-137.411-142.625c-2.084-0.038-6.254-0.038-8.338,0',
+                'C162.927,1.4,101.853,65.691,101.853,142.653v1.603c0,16.182,',
+                '13.118,29.3,29.3,29.3c16.182,0,29.299-13.118,29.299-29.3',
+                'v-1.603',
+                'c0-45.845,37.257-83.752,82.98-83.752s82.981,37.907,82.981,',
+                '83.752v72.198H92.963c-13.702,0-24.878,14.139-24.878,',
+                '31.602v208.701',
+                'c0,17.44,11.176,31.712,24.878,31.712h300.941c13.703,0,',
+                '24.878-14.271,24.878-31.712V246.452',
+                'C418.783,228.989,407.607,214.852,393.904,214.852z M271.627,',
+                '350.591v63.062c0,7.222-6.046,13.332-13.273,13.332h-29.841',
+                'c-7.228,0-13.273-6.11-13.273-13.332v-63.062c-7.009-6.9-11.09',
+                '-16.44-11.09-26.993c0-19.999,15.459-37.185,35.115-37.977',
+                'c2.083-0.085,6.255-0.085,8.337,0c19.656,0.792,35.115,17.978,',
+                '35.115,37.977C282.717,334.149,278.637,343.69,271.627,350.591z',
+              '"/>',
+            '</svg>',
+        "')",
+    ].join(" "));
+}
+
+// Background for edit mode. Currently, we use a grid if snap-to-grid
+// functionality is turned on in the GUI preferences. If not, we just use
+// the same grid with a lower opacity. That way the edit mode is always
+// visually distinct from run mode.
+var create_editmode_bg = function(cid, svg_elem) {
+    var data, cell_data_str, opacity_str, grid, size, pos;
+    grid = showgrid[cid];
+    size = gridsize[cid];
+    pos = get_grid_coords(cid, svg_elem);
+    // if snap-to-grid isn't turned on, just use cell size of 10 and make the
+    // grid partially transparent
+    size = grid ? size : 10;
+    opacity_str = '"' + (grid ? 1 : 0.4) + '"';
+    cell_data_str = ['"', "M", size, 0, "L", 0, 0, 0, size, '"'].join(" ");
+
+    data = ['<svg xmlns="http://www.w3.org/2000/svg" ',
+                'width="1000" height="1000" ',
+                'opacity=', opacity_str, '>',
+              '<defs>',
+                '<pattern id="cell" patternUnits="userSpaceOnUse" ',
+                         'width="', size, '" height="', size, '">',
+                  '<path fill="none" stroke="#ddd" stroke-width="1" ',
+                        'd=', cell_data_str,'/>',
+                '</pattern>',
+                '<pattern id="grid" patternUnits="userSpaceOnUse" ',
+                    'width="100" height="100" x="', pos.x, '" y="', pos.y, '">',
+                  '<rect width="500" height="500" fill="url(#cell)" />',
+                  '<path fill="none" stroke="#bbb" stroke-width="1" ',
+                        'd="M 500 0 L 0 0 0 500"/>',
+                '</pattern>',
+              '</defs>',
+              '<rect width="1000" height="1000" fill="url(#grid)" />',
+            '</svg>'
+        ].join(" ");
+    // make sure to encode the data so we obey all the rules with our data URL
+    return "url('data:image/svg+xml;utf8," + encodeURIComponent(data) + "')";
+}
 
-LATER: consider adding an interim version that reflects only the ctrl button press
-*/
-var gui_editmode_svg_background = "url(\"data:image/svg+xml,%3Csvg " +
-        "xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 " +
-        " 100 100'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%239C92AC' fill-opacity" +
-        "='0.4'%3E%3Cpath opacity='.5' d='M96 95h4v1h-4v4h-1v-4h-9v4h-1v-4h-9v4h-1" +
-        "v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4H0v-" +
-        "1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9" +
-        "H0v-1h15v-9H0v-1h15V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9" +
-        "V0h1v15h9V0h1v15h9V0h1v15h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v" +
-        "1h-4v9h4v1h-4v9h4v1h-4v9zm-1 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h" +
-        "9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-1" +
-        "0 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9" +
-        "h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-" +
-        "10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9" +
-        "h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9" +
-        "v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h" +
-        "9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h" +
-        "9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-1" +
-        "0 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-" +
-        "9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm1" +
-        "0 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9" +
-        "h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9" +
-        "h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0" +
-        "h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9z'/%3E%3Cpath d" +
-        "='M6 5V0H5v5H0v1h5v94h1V6h94V5H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E\")";
+function set_bg(cid, data_url, bg_pos, repeat) {
+    var style = patchwin[cid].window.document.body.style;
+    style.setProperty("background-image", data_url);
+    style.setProperty("background-position", bg_pos);
+    style.setProperty("background-repeat", repeat);
+}
+
+function set_editmode_bg(cid, svg_elem, state)
+{
+    var offset, zoom;
+    if (!state) {
+        set_bg(cid, "none", "0% 0%", "repeat");
+    } else if (showgrid[cid]) {
+        // Show a grid in editmode if we're snapping to grid
+        set_bg(cid, create_editmode_bg(cid, svg_elem), "0% 0%", "repeat");
+    } else {
+        // Otherwise show a little lock in the top right corner of the patch
+        // adjusting for zoom level
+        zoom = patchwin[cid].zoomLevel;
+        offset = 1 / Math.pow(1.2, zoom) * 5;
+        offset = offset + "px";
+        set_bg(cid, create_svg_lock(cid),
+            ["right", offset, "top", offset].join(" "),
+            "no-repeat");
+    }
+}
+
+function update_svg_background(cid, svg_elem) {
+    var bg = patchwin[cid].window.document.body.style
+        .getPropertyValue("background-image");
+    // Quick hack-- we just check whether the background has been drawn. If
+    // it has we assume we're in editmode.
+    if (bg !== "none") {
+        set_editmode_bg(cid, svg_elem, 1);
+    }
+}
 
 // requires nw.js API (Menuitem)
 function canvas_set_editmode(cid, state) {
@@ -1411,15 +1521,13 @@ function canvas_set_editmode(cid, state) {
         w.set_editmode_checkbox(state !== 0 ? true : false);
         if (state !== 0) {
             patchsvg.classList.add("editmode");
-            if (showgrid[cid]) {
-                //post("editmode:" + gui_editmode_svg_background);
-                patchwin[cid].window.document.body.style.setProperty
-                ("background-image", gui_editmode_svg_background);
-            }
+            // For now, we just change the opacity of the background grid
+            // depending on whether snap-to-grid is turned on. This way
+            // edit mode is always visually distinct.
+            set_editmode_bg(cid, patchsvg, true);
         } else {
             patchsvg.classList.remove("editmode");
-            patchwin[cid].window.document.body.style.setProperty("background-image",
-                "none");  
+            set_editmode_bg(cid, patchsvg, false);
         }
     });
 }
@@ -1437,21 +1545,19 @@ function canvas_query_editmode(cid) {
 
 exports.canvas_query_editmode = canvas_query_editmode;
 
-function update_grid(grid) {
+function update_grid(grid, grid_size_value) {
     // Update the grid background of all canvas windows when the corresponding
     // option in the gui prefs changes.
-    var bg = grid != 0 ? gui_editmode_svg_background : "none";
     for (var cid in patchwin) {
+        showgrid[cid] = grid !== 0;
+        gridsize[cid] = grid_size_value;
         gui(cid).get_elem("patchsvg", function(patchsvg, w) {
             var editmode = patchsvg.classList.contains("editmode");
             if (editmode) {
-                patchwin[cid].window.document.body.style.setProperty
-                ("background-image", bg);
+                set_editmode_bg(cid, patchsvg, true);
             }
         });
     }
-    // Also update the showgrid flags.
-    set_showgrid(grid);
 }
 
 exports.update_grid = update_grid;
@@ -1713,6 +1819,7 @@ var scroll = {},
     font = {},
     doscroll = {},
     showgrid = {},
+    gridsize = {},
     last_loaded, // last loaded canvas
     last_focused, // last focused canvas (doesn't include Pd window or dialogs)
     loading = {},
@@ -1723,12 +1830,6 @@ var scroll = {},
     var patchwin = {}; // object filled with cid: [Window object] pairs
     var dialogwin = {}; // object filled with did: [Window object] pairs
 
-var set_showgrid = function(grid) {
-    for (var cid in showgrid) {
-        showgrid[cid] = grid;
-    }
-}
-
 exports.get_patchwin = function(name) {
     return patchwin[name];
 }
@@ -1888,7 +1989,9 @@ function create_window(cid, type, width, height, xpos, ypos, attr_array) {
 }
 
 // create a new canvas
-function gui_canvas_new(cid, width, height, geometry, grid, zoom, editmode, name, dir, dirty_flag, warid, hide_scroll, hide_menu, has_toplevel_scalars, cargs) {
+function gui_canvas_new(cid, width, height, geometry, grid, grid_size_value,
+    zoom, editmode, name, dir, dirty_flag, warid, hide_scroll, hide_menu,
+    has_toplevel_scalars, cargs) {
     // hack for buggy tcl popups... should go away for node-webkit
     //reset_ctrl_on_popup_window
     
@@ -1916,6 +2019,7 @@ function gui_canvas_new(cid, width, height, geometry, grid, zoom, editmode, name
     font[cid] = 10;
     doscroll[cid] = 0;
     showgrid[cid] = grid != 0;
+    gridsize[cid] = grid_size_value;
     toplevel_scalars[cid] = has_toplevel_scalars;
     // geometry is just the x/y screen offset "+xoff+yoff"
     geometry = geometry.slice(1);   // remove the leading "+"
@@ -6162,10 +6266,10 @@ function gui_midi_properties(gfxstub, sys_indevs, sys_outdevs,
     }
 }
 
-function gui_gui_properties(dummy, name, show_grid, save_zoom, browser_doc, browser_path,
+function gui_gui_properties(dummy, name, show_grid, grid_size, save_zoom, browser_doc, browser_path,
     browser_init, autopatch_yoffset) {
     if (dialogwin["prefs"] !== null) {
-        dialogwin["prefs"].window.gui_prefs_callback(name, show_grid, save_zoom,
+        dialogwin["prefs"].window.gui_prefs_callback(name, show_grid, grid_size, save_zoom,
             browser_doc, browser_path, browser_init, autopatch_yoffset);
     }
 }
@@ -6739,6 +6843,11 @@ function do_getscroll(cid, checkgeom) {
             width: width,
             height: height
         });
+        // Now update the svg's background if we're in edit mode. This adds
+        // a new background image to the body of the document each time.
+        // So if there is a performance regression with do_getscroll when
+        // in editmode, this could be the culprit.
+        update_svg_background(cid, svg_elem);
     });
 }
 
diff --git a/pd/src/g_editor.c b/pd/src/g_editor.c
index ef71817264cea7a3ee78a7b194252418b37c434b..d12c47d8dc0b923af8024e2cf92490712120ee39 100644
--- a/pd/src/g_editor.c
+++ b/pd/src/g_editor.c
@@ -2799,6 +2799,9 @@ void canvas_init_menu(t_canvas *x)
     gui_vmess("gui_menu_font_set_initial_size", "xi", x, x->gl_font);
 }
 
+extern int sys_snaptogrid; /* whether we are snapping to grid or not */
+extern int sys_gridsize;
+
 void canvas_vis(t_canvas *x, t_floatarg f)
 {
     //fprintf(stderr,"canvas_vis .x%zx %f\n", (t_int)x, f);
@@ -2809,7 +2812,6 @@ void canvas_vis(t_canvas *x, t_floatarg f)
 
     t_gobj *g;
     t_int properties;
-    extern int sys_grid;
 
     int flag = (f != 0);
     if (flag)
@@ -2865,12 +2867,13 @@ void canvas_vis(t_canvas *x, t_floatarg f)
                We may need to expand this to include scalars, as well. */
             canvas_create_editor(x);
             canvas_args_to_string(argsbuf, x);
-            gui_vmess("gui_canvas_new", "xiisiiissiiiiis",
+            gui_vmess("gui_canvas_new", "xiisiiiissiiiiis",
                 x,
                 (int)(x->gl_screenx2 - x->gl_screenx1),
                 (int)(x->gl_screeny2 - x->gl_screeny1),
                 geobuf,
-                sys_grid,
+                sys_snaptogrid,
+		sys_gridsize,
                 x->gl_zoom,
                 x->gl_edit,
                 x->gl_name->s_name,
@@ -3637,6 +3640,8 @@ static int canvas_upx, canvas_upy;
 
 static int ctrl_runmode_warned;
 
+static int snap_got_anchor;
+
     /* mouse click */
 void canvas_doclick(t_canvas *x, int xpos, int ypos, int which,
     int mod, int doit)
@@ -3646,6 +3651,10 @@ void canvas_doclick(t_canvas *x, int xpos, int ypos, int which,
        to array_motion so that we can update corresponding send when
        the array has been changed */
     array_garray = NULL;
+
+    /* reset the snap_got_anchor variable so the the snap_to_grid feature
+       can find its anchor object before it starts to displace a selection */
+    snap_got_anchor = 0;
     //post("canvas_doclick %d", doit);
 
     t_gobj *y;
@@ -5767,15 +5776,77 @@ void canvas_key(t_canvas *x, t_symbol *s, int ac, t_atom *av)
 extern void graph_checkgop_rect(t_gobj *z, t_glist *glist,
     int *xp1, int *yp1, int *xp2, int *yp2);
 
+    /* We get the bbox for the object under the mouse the first time around,
+       then cache its offset from the mouse for future motion messages.
+       Since there are cases where snapping to a grid can move the object
+       relative to the mouse pointer, we can't rely on our "anchor" object to
+       always be directly under the mouse coordinates. */
+static void snap_get_anchor_xy(t_canvas *x, int *gobj_x, int *gobj_y)
+{
+    t_selection *s = x->gl_editor->e_selection;
+    int x1, y1, x2, y2;
+    while (s)
+    {
+        if (canvas_hitbox(x, s->sel_what, x->gl_editor->e_xwas,
+            x->gl_editor->e_ywas, &x1, &y1, &x2, &y2))
+        {
+            *gobj_x = x1;
+            *gobj_y = y1;
+            return;
+        }
+	s = s->sel_next;
+    }
+    bug("canvas_get_snap_offset");
+}
 
+int anchor_xoff;
+int anchor_yoff;
+
+static void canvas_snap_to_grid(t_canvas *x, int xwas, int ywas, int xnew,
+    int ynew, int *dx, int *dy)
+{
+    int gsize = sys_gridsize;
+        /* If we're snapping to grid, we need an initial delta to align
+           the object under the mouse to the given gridlines. We keep
+           that in the variables below, which will have no affect after
+           our initial grid adjustment. */
+    int snap_dx = 0, snap_dy = 0;
+    if (!snap_got_anchor)
+    {
+        int obx = xnew, oby = ynew, xsign, ysign;
+        snap_get_anchor_xy(x, &obx, &oby);
+            /* First, get the distance the selection should be displaced
+               in order to align the anchor object with a grid line. */
+
+        snap_dx = ((obx + gsize / 2 * (obx < 0 ? -1 : 1)) / gsize) * gsize - obx;
+        snap_dy = ((oby + gsize / 2 * (oby < 0 ? -1 : 1)) / gsize) * gsize - oby;
+        obx = obx / gsize * gsize;
+        oby = oby / gsize * gsize;
+        anchor_xoff = xnew - obx;
+        anchor_yoff = ynew - oby;
+        snap_got_anchor = 1;
+    }
+    *dx = ((xnew - anchor_xoff + gsize / 2) / gsize) * gsize -
+        ((xwas - anchor_xoff + gsize / 2) / gsize) * gsize + snap_dx;
+    *dy = ((ynew - anchor_yoff + gsize / 2) / gsize) * gsize -
+        ((ywas - anchor_yoff + gsize / 2) / gsize) * gsize + snap_dy;
+}
 
 static void delay_move(t_canvas *x)
 {
-    canvas_displaceselection(x,
-        x->gl_editor->e_xnew - x->gl_editor->e_xwas,
-        x->gl_editor->e_ynew - x->gl_editor->e_ywas);
-    x->gl_editor->e_xwas = x->gl_editor->e_xnew;
-    x->gl_editor->e_ywas = x->gl_editor->e_ynew;
+    int dx, dy;
+    int xwas = x->gl_editor->e_xwas, ywas = x->gl_editor->e_ywas,
+        xnew = x->gl_editor->e_xnew, ynew = x->gl_editor->e_ynew;
+    if (sys_snaptogrid)
+        canvas_snap_to_grid(x, xwas, ywas, xnew, ynew, &dx, &dy);
+    else
+    {
+        dx = xnew - xwas;
+        dy = ynew - ywas;
+    }
+    canvas_displaceselection(x, dx, dy);
+    x->gl_editor->e_xwas = xnew;
+    x->gl_editor->e_ywas = ynew;
 }
 
 void canvas_motion(t_canvas *x, t_floatarg xpos, t_floatarg ypos,
diff --git a/pd/src/m_glob.c b/pd/src/m_glob.c
index 6fca411ca59a08070c0c2b2fd9ee2a0da4f41eb7..db3b842cf9959e36dd41d02028bf5c50109a2937 100644
--- a/pd/src/m_glob.c
+++ b/pd/src/m_glob.c
@@ -79,13 +79,14 @@ static void glob_perf(t_pd *dummy, float f)
     sys_perf = (f != 0);
 }
 
-extern int sys_grid, sys_zoom, sys_browser_doc, sys_browser_path, sys_browser_init,
+extern int sys_snaptogrid, sys_gridsize, sys_zoom, sys_browser_doc, sys_browser_path, sys_browser_init,
     sys_autopatch_yoffset;
 extern t_symbol *sys_gui_preset;
 static void glob_gui_prefs(t_pd *dummy, t_symbol *s, int argc, t_atom *argv)
 {
     sys_gui_preset = atom_getsymbolarg(0, argc--, argv++);
-    sys_grid = !!atom_getintarg(0, argc--, argv++);
+    sys_snaptogrid = !!atom_getintarg(0, argc--, argv++);
+    sys_gridsize = atom_getintarg(0, argc--, argv++);
     sys_zoom = !!atom_getintarg(0, argc--, argv++);
     sys_browser_doc = !!atom_getintarg(0, argc--, argv++);
     sys_browser_path = !!atom_getintarg(0, argc--, argv++);
@@ -96,10 +97,11 @@ static void glob_gui_prefs(t_pd *dummy, t_symbol *s, int argc, t_atom *argv)
 /* just the gui-preset, the save-zoom toggle and various help browser options for now */
 static void glob_gui_properties(t_pd *dummy)
 {
-    gui_vmess("gui_gui_properties", "xsiiiiii",
+    gui_vmess("gui_gui_properties", "xsiiiiiii",
         dummy,
         sys_gui_preset->s_name,
-        sys_grid,
+        sys_snaptogrid,
+	sys_gridsize,
         sys_zoom,
         sys_browser_doc,
         sys_browser_path,
diff --git a/pd/src/s_file.c b/pd/src/s_file.c
index 0f0b18f258c23d9ad266a71eb86638fcb0ccd320..e78cabcc4ff0ae6ec6db62c4c6765ab6d7548de7 100644
--- a/pd/src/s_file.c
+++ b/pd/src/s_file.c
@@ -42,7 +42,7 @@
 #define snprintf sprintf_s
 #endif
 
-int sys_defeatrt, sys_autopatch_yoffset, sys_grid = 1, sys_zoom, sys_browser_doc = 1,
+int sys_defeatrt, sys_autopatch_yoffset, sys_snaptogrid = 1, sys_gridsize = 10, sys_zoom, sys_browser_doc = 1,
     sys_browser_path, sys_browser_init;
 t_symbol *sys_flags = &s_;
 void sys_doflags( void);
@@ -671,7 +671,9 @@ void sys_loadpreferences( void)
     if (sys_getpreference("defeatrt", prefbuf, MAXPDSTRING))
         sscanf(prefbuf, "%d", &sys_defeatrt);
     if (sys_getpreference("showgrid", prefbuf, MAXPDSTRING))
-        sscanf(prefbuf, "%d", &sys_grid);
+        sscanf(prefbuf, "%d", &sys_snaptogrid);
+    if (sys_getpreference("gridsize", prefbuf, MAXPDSTRING))
+        sscanf(prefbuf, "%d", &sys_gridsize);
     if (sys_getpreference("savezoom", prefbuf, MAXPDSTRING))
         sscanf(prefbuf, "%d", &sys_zoom);
     if (sys_getpreference("browser_doc", prefbuf, MAXPDSTRING))
@@ -819,8 +821,10 @@ void glob_savepreferences(t_pd *dummy)
     sys_putpreference("nloadlib", buf1);
     sprintf(buf1, "%d", sys_defeatrt);
     sys_putpreference("defeatrt", buf1);
-    sprintf(buf1, "%d", sys_grid);
+    sprintf(buf1, "%d", sys_snaptogrid);
     sys_putpreference("showgrid", buf1);
+    sprintf(buf1, "%d", sys_gridsize);
+    sys_putpreference("gridsize", buf1);
     sprintf(buf1, "%d", sys_zoom);
     sys_putpreference("savezoom", buf1);
     sprintf(buf1, "%d", sys_browser_doc);