jinlin
2024-09-26 7ec9326ce00d08f9d957981f2edff6df26f24a28
web/public/SVGOrigin/Method-Draw-master/method-draw/src/svgcanvas.js
@@ -39,10 +39,10 @@
  // This fixes $(...).attr() to work as expected with SVG elements.
  // Does not currently use *AttributeNS() since we rarely need that.
  // See http://api.jquery.com/attr/ for basic documentation of .attr()
  // Additional functionality:
  // Additional functionality:
  // - When getting attributes, a string that's a number is return as type number.
  // - If an array is supplied as first parameter, multiple values are returned
  // as an object with values for each given attributes
@@ -72,7 +72,7 @@
            obj[aname] = attr;
          }
          return obj;
        } else if(typeof key === "object") {
          // Setting attributes form object
          for(var v in key) {
@@ -93,7 +93,7 @@
    }
    return this;
  };
}());
// Class: SvgCanvas
@@ -117,7 +117,7 @@
var curConfig = {
  show_outside_canvas: true,
  selectNew: true,
  dimensions: [640, 480]
  dimensions: [200, 200]
};
// Update config with new one if given
@@ -183,8 +183,8 @@
// Function: setIdPrefix
// Changes the ID prefix to the given value
//
// Parameters:
// p - String with the new prefix
// Parameters:
// p - String with the new prefix
canvas.setIdPrefix = function(p) {
  idprefix = p;
};
@@ -238,7 +238,7 @@
// Function: addSvgElementFromJson
// Create a new SVG element based on the given object keys/values and add it to the current layer
// The element will be ran through cleanupElement before being returned
// The element will be ran through cleanupElement before being returned
//
// Parameters:
// data - Object with the following keys/values:
@@ -342,7 +342,7 @@
      var elems = cmd.elements();
      canvas.pathActions.clear();
      call("changed", elems);
      var cmdType = cmd.type();
      var isApply = (eventType == EventTypes.AFTER_APPLY);
      if (cmdType == MoveElementCommand.type()) {
@@ -360,7 +360,7 @@
        } else {
          if (!isApply) restoreRefElems(cmd.elem);
        }
        if(cmd.elem.tagName === 'use') {
          setUseData(cmd.elem);
        }
@@ -374,8 +374,8 @@
        if (values["stdDeviation"]) {
          canvas.setBlurOffsets(cmd.elem.parentNode, values["stdDeviation"]);
        }
        // Remove & Re-add hack for Webkit (issue 775)
        // Remove & Re-add hack for Webkit (issue 775)
        //if(cmd.elem.tagName === 'use' && svgedit.browser.isWebkit()) {
        //  var elem = cmd.elem;
        //  if(!elem.getAttribute('x') && !elem.getAttribute('y')) {
@@ -462,9 +462,9 @@
      }
    }
  }
  var childs = elem.getElementsByTagName('*');
  if(childs.length) {
    for(var i = 0, l = childs.length; i < l; i++) {
      restoreRefElems(childs[i]);
@@ -484,55 +484,55 @@
// Object to contain image data for raster images that were found encodable
var encodableImages = {},
  // String with image URL of last loadable image
  last_good_img_url = curConfig.imgPath + 'logo.png',
  // Array with current disabled elements (for in-group editing)
  disabled_elems = [],
  // Object with save options
  save_options = {round_digits: 5},
  // Boolean indicating whether or not a draw action has been started
  started = false,
  // String with an element's initial transform attribute value
  start_transform = null,
  // String indicating the current editor mode
  current_mode = "select",
  // String with the current direction in which an element is being resized
  current_resize_mode = "none",
  // Object with IDs for imported files, to see if one was already added
  import_ids = {};
// Current text style properties
var cur_text = all_properties.text,
  // Current general properties
  cur_properties = cur_shape,
  // Array with selected elements' Bounding box object
//  selectedBBoxes = new Array(1),
  // The DOM element that was just selected
  justSelected = null,
  // DOM element for selection rectangle drawn by the user
  rubberBox = null,
  // Array of current BBoxes (still needed?)
  curBBoxes = [],
  // Object to contain all included extensions
  extensions = {},
  // Canvas point for the most recent right click
  lastClickPoint = null,
  // Map of deleted reference elements
  removedElements = {}
@@ -558,14 +558,14 @@
// Function: addExtension
// Add an extension to the editor
//
//
// Parameters:
// name - String with the ID of the extension
// ext_func - Function supplied by the extension with its data
this.addExtension = function(name, ext_func) {
  if(!(name in extensions)) {
    // Provide private vars/funcs here. Is there a better way to do this?
    if($.isFunction(ext_func)) {
    var ext = ext_func($.extend(canvas.getPrivateMethods(), {
      svgroot: svgroot,
@@ -582,7 +582,7 @@
    console.log('Cannot add extension "' + name + '", an extension by that name already exists"');
  }
};
// This method rounds the incoming value to the nearest value based on the current_zoom
var round = this.round = function(val) {
  return parseInt(val*current_zoom)/current_zoom;
@@ -590,10 +590,10 @@
// This method sends back an array or a NodeList full of elements that
// intersect the multi-select rubber-band-box on the current_layer only.
//
// Since the only browser that supports the SVG DOM getIntersectionList is Opera,
//
// Since the only browser that supports the SVG DOM getIntersectionList is Opera,
// we need to provide an implementation here.  We brute-force it for now.
//
//
// Reference:
// Firefox does not implement getIntersectionList(), see https://bugzilla.mozilla.org/show_bug.cgi?id=501421
// Webkit does not implement getIntersectionList(), see https://bugs.webkit.org/show_bug.cgi?id=11274
@@ -601,12 +601,12 @@
  if (rubberBox == null) { return null; }
  var parent = current_group || getCurrentDrawing().getCurrentLayer();
  if(!curBBoxes.length) {
    // Cache all bboxes
    curBBoxes = getVisibleElementsAndBBoxes(parent);
  }
  var resultList = null;
  try {
    resultList = parent.getIntersectionList(rect, null);
@@ -614,16 +614,16 @@
  if (resultList == null || typeof(resultList.item) != "function") {
    resultList = [];
    if(!rect) {
      var rubberBBox = rubberBox.getBBox();
      var bb = {};
      for(var o in rubberBBox) {
        bb[o] = rubberBBox[o] / current_zoom;
      }
      rubberBBox = bb;
    } else {
      var rubberBBox = rect;
    }
@@ -635,8 +635,8 @@
      }
    }
  }
  // addToSelection expects an array, but it's ok to pass a NodeList
  // because using square-bracket notation is allowed:
  // addToSelection expects an array, but it's ok to pass a NodeList
  // because using square-bracket notation is allowed:
  // http://www.w3.org/TR/DOM-Level-2-Core/ecma-script-binding.html
  return resultList;
};
@@ -644,34 +644,34 @@
// TODO(codedread): Migrate this into svgutils.js
// Function: getStrokedBBox
// Get the bounding box for one or more stroked and/or transformed elements
//
//
// Parameters:
// elems - Array with DOM elements to check
//
//
// Returns:
// A single bounding box object
getStrokedBBox = this.getStrokedBBox = function(elems) {
  if(!elems) elems = getVisibleElements();
  if(!elems.length) return false;
  // Make sure the expected BBox is returned if the element is a group
  var getCheckedBBox = function(elem) {
    try {
      // TODO: Fix issue with rotated groups. Currently they work
      // fine in FF, but not in other browsers (same problem mentioned
      // in Issue 339 comment #2).
      var bb = svgedit.utilities.getBBox(elem);
      var angle = svgedit.utilities.getRotationAngle(elem);
      if ((angle && angle % 90) ||
          svgedit.math.hasMatrixTransform(svgedit.transformlist.getTransformList(elem))) {
        // Accurate way to get BBox of rotated element in Firefox:
        // Put element in group and get its BBox
        var good_bb = false;
        // Get the BBox from the raw path for these elements
        var elemNames = ['ellipse','path','line','polyline','polygon'];
        if(elemNames.indexOf(elem.tagName) >= 0) {
@@ -684,10 +684,10 @@
            bb = good_bb = canvas.convertToPath(elem, true);
          }
        }
        if(!good_bb) {
          // Must use clone else FF freaks out
          var clone = elem.cloneNode(true);
          var clone = elem.cloneNode(true);
          var g = document.createElementNS(svgns, "g");
          var parent = elem.parentNode;
          parent.appendChild(g);
@@ -695,17 +695,17 @@
          bb = svgedit.utilities.bboxToObj(g.getBBox());
          parent.removeChild(g);
        }
        // Old method: Works by giving the rotated BBox,
        // this is (unfortunately) what Opera and Safari do
        // natively when getting the BBox of the parent group
//            var angle = angle * Math.PI / 180.0;
//            var rminx = Number.MAX_VALUE, rminy = Number.MAX_VALUE,
//            var rminx = Number.MAX_VALUE, rminy = Number.MAX_VALUE,
//              rmaxx = Number.MIN_VALUE, rmaxy = Number.MIN_VALUE;
//            var cx = round(bb.x + bb.width/2),
//              cy = round(bb.y + bb.height/2);
//            var pts = [ [bb.x - cx, bb.y - cy],
//            var pts = [ [bb.x - cx, bb.y - cy],
//                  [bb.x + bb.width - cx, bb.y - cy],
//                  [bb.x + bb.width - cx, bb.y + bb.height - cy],
//                  [bb.x - cx, bb.y + bb.height - cy] ];
@@ -717,23 +717,23 @@
//              var theta = Math.atan2(y,x) + angle;
//              x = round(r * Math.cos(theta) + cx);
//              y = round(r * Math.sin(theta) + cy);
//
//
//              // now set the bbox for the shape after it's been rotated
//              if (x < rminx) rminx = x;
//              if (y < rminy) rminy = y;
//              if (x > rmaxx) rmaxx = x;
//              if (y > rmaxy) rmaxy = y;
//            }
//
//
//            bb.x = rminx;
//            bb.y = rminy;
//            bb.width = rmaxx - rminx;
//            bb.height = rmaxy - rminy;
      }
      return bb;
    } catch(e) {
    } catch(e) {
      console.log(elem, e);
    }
    }
  };
  var full_bb;
@@ -742,18 +742,18 @@
    if(!this.parentNode) return;
    full_bb = getCheckedBBox(this);
  });
  // This shouldn't ever happen...
  if(full_bb == null) return null;
  // full_bb doesn't include the stoke, so this does no good!
//    if(elems.length == 1) return full_bb;
  var max_x = full_bb.x + full_bb.width;
  var max_y = full_bb.y + full_bb.height;
  var min_x = full_bb.x;
  var min_y = full_bb.y;
  // FIXME: same re-creation problem with this function as getCheckedBBox() above
  var getOffset = function(elem) {
    var sw = elem.getAttribute("stroke-width");
@@ -773,10 +773,10 @@
      bboxes.push(cur_bb);
    }
  });
  full_bb.x = min_x;
  full_bb.y = min_y;
  $.each(elems, function(i, elem) {
    var cur_bb = bboxes[i];
    // ensure that elem is really an element node
@@ -786,7 +786,7 @@
      max_y = Math.max(max_y, cur_bb.y + cur_bb.height + offset);
    }
  });
  full_bb.width = max_x - min_x;
  full_bb.height = max_y - min_y;
  return full_bb;
@@ -830,7 +830,7 @@
// * bbox - The element's BBox as retrieved from getStrokedBBox
var getVisibleElementsAndBBoxes = this.getVisibleElementsAndBBoxes = function(parent) {
  if(!parent) parent = $(svgcontent).children(); // Prevent layers from being included
  var contentElems = [];
  $(parent).children().each(function(i, elem) {
    try {
@@ -870,7 +870,7 @@
      new_el.setAttributeNS(attr.namespaceURI, attr.nodeName, attr.nodeValue);
    }
  });
  // Opera's "d" value needs to be reset for Opera/Win/non-EN
  // Also needed for webkit (else does not keep curved segments on clone)
  if(svgedit.browser.isWebkit() && el.nodeName == 'path') {
@@ -891,7 +891,7 @@
        break;
    }
  });
  if($(el).data('gsvg')) {
    $(new_el).data('gsvg', new_el.firstChild);
  } else if($(el).data('symbol')) {
@@ -915,7 +915,7 @@
  getId = c.getId = function() { return getCurrentDrawing().getId(); };
  getNextId = c.getNextId = function() { return getCurrentDrawing().getNextId(); };
  // Function: call
  // Run the callback function associated with the given event
  //
@@ -927,14 +927,14 @@
      return events[event](this, arg);
    }
  };
  // Function: bind
  // Attaches a callback function to an event
  //
  // Parameters:
  // event - String indicating the name of the event
  // f - The callback function to bind to the event
  //
  //
  // Return:
  // The previous event
  c.bind = function(event, f) {
@@ -942,7 +942,7 @@
    events[event] = f;
    return old;
  };
}(canvas));
// Function: canvas.prepareSvg
@@ -972,13 +972,13 @@
}
// Function: ffClone
// Hack for Firefox bugs where text element features aren't updated or get
// Hack for Firefox bugs where text element features aren't updated or get
// messed up. See issue 136 and issue 137.
// This function clones the element and re-selects it
// TODO: Test for this bug on load and add it to "support" object instead of
// This function clones the element and re-selects it
// TODO: Test for this bug on load and add it to "support" object instead of
// browser sniffing
//
// Parameters:
// Parameters:
// elem - The (text) DOM element to clone
var ffClone = function(elem) {
  if(!svgedit.browser.isGecko()) return elem;
@@ -1016,7 +1016,7 @@
  var bbox = svgedit.utilities.getBBox(elem);
  var cx = bbox.x+bbox.width/2, cy = bbox.y+bbox.height/2;
  var tlist = getTransformList(elem);
  // only remove the real rotational transform if present (i.e. at index=0)
  if (tlist.numberOfItems > 0) {
    var xform = tlist.getItem(0);
@@ -1038,7 +1038,7 @@
  else if (tlist.numberOfItems == 0) {
    elem.removeAttribute("transform");
  }
  if (!preventUndo) {
    // we need to undo it, then redo it so it can be undo-able! :)
    // TODO: figure out how to make changes to transform list undo-able cross-browser?
@@ -1057,7 +1057,7 @@
};
// Function: recalculateAllSelectedDimensions
// Runs recalculateDimensions on the selected elements,
// Runs recalculateDimensions on the selected elements,
// adding the changes to a single batch command
var recalculateAllSelectedDimensions = this.recalculateAllSelectedDimensions = function() {
  var text = (current_resize_mode == "none" ? "position" : "size");
@@ -1080,9 +1080,9 @@
};
// this is how we map paths to our preferred relative segment types
var pathMap = [0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a',
var pathMap = [0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a',
          'H', 'h', 'V', 'v', 'S', 's', 'T', 't'];
// Debug tool to easily see the current matrix in the browser's console
var logMatrix = function(m) {
  console.log([m.a,m.b,m.c,m.d,m.e,m.f]);
@@ -1106,7 +1106,7 @@
      assignAttributes(selected, changes, 1000, true);
    }
    box = svgedit.utilities.getBBox(selected);
  for(var i = 0; i < 2; i++) {
    var type = i === 0 ? 'fill' : 'stroke';
    var attrVal = selected.getAttribute(type);
@@ -1114,15 +1114,15 @@
      if(m.a < 0 || m.d < 0) {
        var grad = getRefElem(attrVal);
        var newgrad = grad.cloneNode(true);
        if(m.a < 0) {
          //flip x
          var x1 = newgrad.getAttribute('x1');
          var x2 = newgrad.getAttribute('x2');
          newgrad.setAttribute('x1', -(x1 - 1));
          newgrad.setAttribute('x2', -(x2 - 1));
        }
        }
        if(m.d < 0) {
          //flip y
          var y1 = newgrad.getAttribute('y1');
@@ -1134,7 +1134,7 @@
        findDefs().appendChild(newgrad);
        selected.setAttribute(type, 'url(#' + newgrad.id + ')');
      }
      // Not really working :(
//      if(selected.tagName === 'path') {
//        reorientGrads(selected, m);
@@ -1146,8 +1146,8 @@
  var elName = selected.tagName;
  if(elName === "g" || elName === "text" || elName === "use") {
    // if it was a translate, then just update x,y
    if (m.a == 1 && m.b == 0 && m.c == 0 && m.d == 1 &&
      (m.e != 0 || m.f != 0) )
    if (m.a == 1 && m.b == 0 && m.c == 0 && m.d == 1 &&
      (m.e != 0 || m.f != 0) )
    {
      // [T][M] = [M][T']
      // therefore [T'] = [M_inv][T][M]
@@ -1165,7 +1165,7 @@
      chlist.appendItem(mt);
    }
  }
  // now we have a set of changes and an applied reduced transform list
  // we apply the changes directly to the DOM
  switch (elName)
@@ -1173,7 +1173,7 @@
    case "foreignObject":
    case "rect":
    case "image":
      // Allow images to be inverted (give them matrix when flipped)
      if(elName === 'image' && (m.a < 0 || m.d < 0)) {
        // Convert to matrix
@@ -1184,10 +1184,10 @@
        chlist.appendItem(mt);
      } else {
        var pt1 = remap(changes.x,changes.y);
        changes.width = scalew(changes.width);
        changes.height = scaleh(changes.height);
        changes.x = pt1.x + Math.min(0,changes.width);
        changes.y = pt1.y + Math.min(0,changes.height);
        changes.width = Math.abs(changes.width);
@@ -1201,7 +1201,7 @@
      changes.cy = c.y;
      changes.rx = scalew(changes.rx);
      changes.ry = scaleh(changes.ry);
      changes.rx = Math.abs(changes.rx);
      changes.ry = Math.abs(changes.ry);
      finishUp();
@@ -1225,7 +1225,7 @@
      changes.y1 = pt1.y;
      changes.x2 = pt2.x;
      changes.y2 = pt2.y;
    case "text":
      var tspan = selected.querySelectorAll('tspan');
      var i = tspan.length
@@ -1272,7 +1272,7 @@
      selected.setAttribute("points", pstr);
      break;
    case "path":
      var segList = selected.pathSegList;
      var len = segList.numberOfItems;
      changes.d = new Array(len);
@@ -1293,7 +1293,7 @@
          sweepFlag: seg.sweepFlag
        };
      }
      var len = changes.d.length,
        firstseg = changes.d[0],
        currentpt = remap(firstseg.x,firstseg.y);
@@ -1330,7 +1330,7 @@
          seg.r2 = scaleh(seg.r2);
        }
      } // for each segment
      var dstr = "";
      var len = changes.d.length;
      for (var i = 0; i < len; ++i) {
@@ -1359,7 +1359,7 @@
            dstr += seg.x1 + "," + seg.y1 + " " + seg.x2 + "," + seg.y2 + " " +
               seg.x + "," + seg.y + " ";
            break;
          case 9: // relative quad (q)
          case 9: // relative quad (q)
          case 8: // absolute quad (Q)
            dstr += seg.x1 + "," + seg.y1 + " " + seg.x + "," + seg.y + " ";
            break;
@@ -1389,14 +1389,14 @@
// ty - The translation's y value
var updateClipPath = function(attr, tx, ty) {
  var path = getRefElem(attr).firstChild;
  var cp_xform = getTransformList(path);
  var newxlate = svgroot.createSVGTransform();
  newxlate.setTranslate(tx, ty);
  cp_xform.appendItem(newxlate);
  // Update clipPath's dimensions
  recalculateDimensions(path);
}
@@ -1407,13 +1407,13 @@
// Parameters:
// selected - The DOM element to recalculate
//
// Returns:
// Returns:
// Undo command object with the resulting change
var recalculateDimensions = this.recalculateDimensions = function(selected) {
  if (selected == null) return null;
  var tlist = getTransformList(selected);
  // remove any unnecessary transforms
  if (tlist && tlist.numberOfItems > 0) {
    var k = tlist.numberOfItems;
@@ -1438,13 +1438,13 @@
    // End here if all it has is a rotation
    if(tlist.numberOfItems === 1 && getRotationAngle(selected)) return null;
  }
  // if this element had no transforms, we are done
  if (!tlist || tlist.numberOfItems == 0) {
    selected.removeAttribute("transform");
    return null;
  }
  // TODO: Make this work for more than 2
  if (tlist) {
    var k = tlist.numberOfItems;
@@ -1463,23 +1463,23 @@
      tlist.removeItem(mxs[1][1]);
      tlist.insertItemBefore(m_new, mxs[1][1]);
    }
    // combine matrix + translate
    k = tlist.numberOfItems;
    if(k >= 2 && tlist.getItem(k-2).type === 1 && tlist.getItem(k-1).type === 2) {
      var mt = svgroot.createSVGTransform();
      var m = matrixMultiply(
        tlist.getItem(k-2).matrix,
        tlist.getItem(k-2).matrix,
        tlist.getItem(k-1).matrix
      );
      );
      mt.setMatrix(m);
      tlist.removeItem(k-2);
      tlist.removeItem(k-2);
      tlist.appendItem(mt);
    }
  }
  // If it still has a single [M] or [R][M], return null too (prevents BatchCommand from being returned).
  switch ( selected.tagName ) {
    // Ignore these elements, as they can absorb the [M]
@@ -1496,13 +1496,13 @@
        return null;
      }
  }
  // Grouped SVG element
  // Grouped SVG element
  var gsvg = $(selected).data('gsvg');
  // we know we have some transforms, so set up return variable
  // we know we have some transforms, so set up return variable
  var batchCmd = new BatchCommand("Transform");
  // store initial values that will be affected by reducing the transform list
  var changes = {}, initial = null, attrs = [];
  switch (selected.tagName)
@@ -1544,7 +1544,7 @@
      changes["d"] = selected.getAttribute("d");
      break;
  } // switch on element type to get initial values
  if(attrs.length) {
    changes = $(selected).attr(attrs);
    $.each(changes, function(attr, val) {
@@ -1557,8 +1557,8 @@
      y: $(gsvg).attr('y') || 0
    };
  }
  // if we haven't created an initial array in polygon/polyline/path, then
  // if we haven't created an initial array in polygon/polyline/path, then
  // make a copy of initial values and include the transform
  if (initial == null) {
    initial = $.extend(true, {}, changes);
@@ -1568,7 +1568,7 @@
  }
  // save the start transform value too
  initial["transform"] = start_transform ? start_transform : "";
  // if it's a regular group, we have special processing to flatten transforms
  if ((selected.tagName == "g" && !gsvg) || selected.tagName == "a") {
    var box = svgedit.utilities.getBBox(selected),
@@ -1576,8 +1576,8 @@
      newcenter = transformPoint(box.x+box.width/2, box.y+box.height/2,
              transformListToTransform(tlist).matrix),
      m = svgroot.createSVGMatrix();
    // temporarily strip off the rotate and save the old center
    var gangle = getRotationAngle(selected);
    if (gangle) {
@@ -1609,17 +1609,17 @@
    }
    // first, if it was a scale then the second-last transform will be it
    if (N >= 3 && tlist.getItem(N-2).type == 3 &&
      tlist.getItem(N-3).type == 2 && tlist.getItem(N-1).type == 2)
    if (N >= 3 && tlist.getItem(N-2).type == 3 &&
      tlist.getItem(N-3).type == 2 && tlist.getItem(N-1).type == 2)
    {
      operation = 3; // scale
      // if the children are unrotated, pass the scale down directly
      // otherwise pass the equivalent matrix() down directly
      var tm = tlist.getItem(N-3).matrix,
        sm = tlist.getItem(N-2).matrix,
        tmn = tlist.getItem(N-1).matrix;
      var children = selected.childNodes;
      var c = children.length;
      while (c--) {
@@ -1649,7 +1649,7 @@
//              childTlist.appendItem(translateOrigin);
//            }
//          }
          var angle = getRotationAngle(child);
          var old_start_transform = start_transform;
          var childxforms = [];
@@ -1664,10 +1664,10 @@
          // if not rotated or skewed, push the [T][S][-T] down to the child
          else {
            // update the transform list with translate,scale,translate
            // slide the [T][S][-T] from the front to the back
            // [T][S][-T][M] = [M][T2][S2][-T2]
            // (only bringing [-T] to the right of [M])
            // [T][S][-T][M] = [T][S][M][-T2]
            // [-T2] = [M_inv][-T][M]
@@ -1676,7 +1676,7 @@
            var t2 = svgroot.createSVGMatrix();
            t2.e = -t2n.e;
            t2.f = -t2n.f;
            // [T][S][-T][M] = [M][T2][S2][-T2]
            // [S2] = [T2_inv][M_inv][T][S][-T][M][-T2_inv]
            var s2 = matrixMultiply(t2.inverse(), m.inverse(), tm, sm, tmn, m, t2n.inverse());
@@ -1697,8 +1697,8 @@
//            logMatrix(scale.matrix);
          } // not rotated
          batchCmd.addSubCommand( recalculateDimensions(child) );
          // TODO: If any <use> have this group as a parent and are
          // referencing this child, then we need to impose a reverse
          // TODO: If any <use> have this group as a parent and are
          // referencing this child, then we need to impose a reverse
          // scale on it so that when it won't get double-translated
//            var uses = selected.getElementsByTagNameNS(svgns, "use");
//            var href = "#"+child.id;
@@ -1728,19 +1728,19 @@
      e2t.setMatrix(m);
      tlist.clear();
      tlist.appendItem(e2t);
    }
    // next, check if the first transform was a translate
    }
    // next, check if the first transform was a translate
    // if we had [ T1 ] [ M ] we want to transform this into [ M ] [ T2 ]
    // therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ]
    else if ( (N == 1 || (N > 1 && tlist.getItem(1).type != 3)) &&
      tlist.getItem(0).type == 2)
    else if ( (N == 1 || (N > 1 && tlist.getItem(1).type != 3)) &&
      tlist.getItem(0).type == 2)
    {
      operation = 2; // translate
      var T_M = transformListToTransform(tlist).matrix;
      tlist.removeItem(0);
      var M_inv = transformListToTransform(tlist).matrix.inverse();
      var M2 = matrixMultiply( M_inv, T_M );
      tx = M2.e;
      ty = M2.f;
@@ -1748,13 +1748,13 @@
        // we pass the translates down to the individual children
        var children = selected.childNodes;
        var c = children.length;
        var clipPaths_done = [];
        while (c--) {
          var child = children.item(c);
          if (child.nodeType == 1) {
            // Check if child has clip-path
            if(child.getAttribute('clip-path')) {
              // tx, ty
@@ -1762,12 +1762,12 @@
              if(clipPaths_done.indexOf(attr) === -1) {
                updateClipPath(attr, tx, ty);
                clipPaths_done.push(attr);
              }
              }
            }
            var old_start_transform = start_transform;
            start_transform = child.getAttribute("transform");
            var childTlist = getTransformList(child);
            // some children might not have a transform (<metadata>, <defs>, etc)
            if (childTlist) {
@@ -1779,7 +1779,7 @@
                childTlist.appendItem(newxlate);
              }
              batchCmd.addSubCommand( recalculateDimensions(child) );
              // If any <use> have this group as a parent and are
              // If any <use> have this group as a parent and are
              // referencing this child, then impose a reverse translate on it
              // so that when it won't get double-translated
              var uses = selected.getElementsByTagNameNS(svgns, "use");
@@ -1798,9 +1798,9 @@
            }
          }
        }
        clipPaths_done = [];
        start_transform = old_start_transform;
      }
    }
@@ -1817,18 +1817,18 @@
          var old_start_transform = start_transform;
          start_transform = child.getAttribute("transform");
          var childTlist = getTransformList(child);
          if (!childTlist) continue;
          var em = matrixMultiply(m, transformListToTransform(childTlist).matrix);
          var e2m = svgroot.createSVGTransform();
          e2m.setMatrix(em);
          childTlist.clear();
          childTlist.appendItem(e2m,0);
          batchCmd.addSubCommand( recalculateDimensions(child) );
          start_transform = old_start_transform;
          // Convert stroke
          // TODO: Find out if this should actually happen somewhere else
          var sw = child.getAttribute("stroke-width");
@@ -1855,9 +1855,9 @@
      if (tlist.numberOfItems == 0) {
        selected.removeAttribute("transform");
      }
      return null;
      return null;
    }
    // if it was a translate, put back the rotate at the new center
    if (operation == 2) {
      if (gangle) {
@@ -1865,7 +1865,7 @@
          x: oldcenter.x + first_m.e,
          y: oldcenter.y + first_m.f
        };
        var newRot = svgroot.createSVGTransform();
        newRot.setRotate(gangle,newcenter.x,newcenter.y);
        if(tlist.numberOfItems) {
@@ -1914,7 +1914,7 @@
          }
        }
      }
      if (gangle) {
        if(tlist.numberOfItems) {
          tlist.insertItemBefore(rnew, 0);
@@ -1934,9 +1934,9 @@
    // but we still may need to recalculate them (see issue 595).
    // TODO: Figure out how to get BBox from these elements in case they
    // have a rotation transform
    if(!box && selected.tagName != 'path') return null;
    var m = svgroot.createSVGMatrix(),
      // temporarily strip off the rotate and save the old center
@@ -1945,7 +1945,7 @@
      var oldcenter = {x: box.x+box.width/2, y: box.y+box.height/2},
      newcenter = transformPoint(box.x+box.width/2, box.y+box.height/2,
              transformListToTransform(tlist).matrix);
      var a = angle * Math.PI / 180;
      if ( Math.abs(a) > (1.0e-10) ) {
        var s = Math.sin(a)/(1 - Math.cos(a));
@@ -1965,11 +1965,11 @@
        }
      }
    }
    // 2 = translate, 3 = scale, 4 = rotate, 1 = matrix imposition
    var operation = 0;
    var N = tlist.numberOfItems;
    // Check if it has a gradient with userSpaceOnUse, in which case
    // adjust it by recalculating the matrix transform.
    // TODO: Make this work in Webkit using svgedit.transformlist.SVGTransformList
@@ -1992,14 +1992,14 @@
      }
    }
    // first, if it was a scale of a non-skewed element, then the second-last
    // first, if it was a scale of a non-skewed element, then the second-last
    // transform will be the [S]
    // if we had [M][T][S][T] we want to extract the matrix equivalent of
    // [T][S][T] and push it down to the element
    if (N >= 3 && tlist.getItem(N-2).type == 3 &&
      tlist.getItem(N-3).type == 2 && tlist.getItem(N-1).type == 2)
      // Removed this so a <use> with a given [T][S][T] would convert to a matrix.
    if (N >= 3 && tlist.getItem(N-2).type == 3 &&
      tlist.getItem(N-3).type == 2 && tlist.getItem(N-1).type == 2)
      // Removed this so a <use> with a given [T][S][T] would convert to a matrix.
      // Is that bad?
      //  && selected.nodeName != "use"
    {
@@ -2019,12 +2019,12 @@
      tlist.appendItem(e2t);
      // reset the matrix so that the element is not re-mapped
      m = svgroot.createSVGMatrix();
    } // if we had [R][T][S][-T][M], then this was a rotated matrix-element
    } // if we had [R][T][S][-T][M], then this was a rotated matrix-element
    // if we had [T1][M] we want to transform this into [M][T2]
    // therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ] and we can push [T2]
    // therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ] and we can push [T2]
    // down to the element
    else if ( (N == 1 || (N > 1 && tlist.getItem(1).type != 3)) &&
      tlist.getItem(0).type == 2)
    else if ( (N == 1 || (N > 1 && tlist.getItem(1).type != 3)) &&
      tlist.getItem(0).type == 2)
    {
      operation = 2; // translate
      var oldxlate = tlist.getItem(0).matrix,
@@ -2069,7 +2069,7 @@
      if (angle) {
        var newRot = svgroot.createSVGTransform();
        newRot.setRotate(angle,newcenter.x,newcenter.y);
        if(tlist.numberOfItems) {
          tlist.insertItemBefore(newRot, 0);
        } else {
@@ -2081,12 +2081,12 @@
      }
      return null;
    }
    // if it was a translate or resize, we need to remap the element and absorb the xform
    if (operation == 1 || operation == 2 || operation == 3) {
      remapElement(selected,changes,m);
    } // if we are remapping
    // if it was a translate, put back the rotate at the new center
    if (operation == 2) {
      if (angle) {
@@ -2119,7 +2119,7 @@
      var rnew_inv = rnew.matrix.inverse();
      var m_inv = m.inverse();
      var extrat = matrixMultiply(m_inv, rnew_inv, rold, m);
      remapElement(selected,changes,extrat);
      if (angle) {
        if(tlist.numberOfItems) {
@@ -2135,9 +2135,9 @@
  if (tlist.numberOfItems == 0) {
    selected.removeAttribute("transform");
  }
  batchCmd.addSubCommand(new ChangeElementCommand(selected, initial));
  return batchCmd;
};
@@ -2148,7 +2148,7 @@
// Function: clearSelection
// Clears the selection.  The 'selected' handler is then called.
// Parameters:
// Parameters:
// noCall - Optional boolean that when true does not call the "selected" handler
var clearSelection = this.clearSelection = function(noCall) {
  if (selectedElements[0] != null) {
@@ -2177,9 +2177,9 @@
  if (elemsToAdd.length == 0) { return; }
  // find the first null in our selectedElements array
  var j = 0;
  while (j < selectedElements.length) {
    if (selectedElements[j] == null) {
    if (selectedElements[j] == null) {
      break;
    }
    ++j;
@@ -2192,7 +2192,7 @@
    if (!elem || !svgedit.utilities.getBBox(elem)) continue;
    if(elem.tagName === 'a' && elem.childNodes.length === 1) {
      // Make "a" element's child be the selected element
      // Make "a" element's child be the selected element
      elem = elem.firstChild;
    }
@@ -2205,7 +2205,7 @@
//      if (j == 0) selectedBBoxes[0] = svgedit.utilities.getBBox(elem);
      j++;
      var sel = selectorManager.requestSelector(elem);
      if (selectedElements.length > 1) {
        sel.showGrips(false);
      }
@@ -2220,12 +2220,12 @@
  selectedElements.sort(function(a,b) {
    if(a && b && a.compareDocumentPosition) {
      return 3 - (b.compareDocumentPosition(a) & 6);
      return 3 - (b.compareDocumentPosition(a) & 6);
    } else if(a == null) {
      return 1;
    }
  });
  // Make sure first elements are not null
  while(selectedElements[0] == null) selectedElements.shift(0);
};
@@ -2285,10 +2285,10 @@
// Function: getMouseTarget
// Gets the desired element from a mouse event
//
//
// Parameters:
// evt - Event object from the mouse event
//
//
// Returns:
// DOM element we want
var getMouseTarget = this.getMouseTarget = function(evt) {
@@ -2296,33 +2296,33 @@
    return null;
  }
  var mouse_target = evt.target;
  // if it was a <use>, Opera and WebKit return the SVGElementInstance
  if (mouse_target.correspondingUseElement) mouse_target = mouse_target.correspondingUseElement;
  // for foreign content, go up until we find the foreignObject
  // WebKit browsers set the mouse target to the svgcanvas div
  if ([mathns, htmlns].indexOf(mouse_target.namespaceURI) >= 0 &&
    mouse_target.id != "svgcanvas")
  // WebKit browsers set the mouse target to the svgcanvas div
  if ([mathns, htmlns].indexOf(mouse_target.namespaceURI) >= 0 &&
    mouse_target.id != "svgcanvas")
  {
    while (mouse_target.nodeName != "foreignObject") {
      mouse_target = mouse_target.parentNode;
      if(!mouse_target) return svgroot;
    }
  }
  // Get the desired mouse_target with jQuery selector-fu
  // If it's root-like, select the root
  var current_layer = getCurrentDrawing().getCurrentLayer();
  if([svgroot, container, svgcontent, current_layer].indexOf(mouse_target) >= 0) {
    return svgroot;
  }
  var $target = $(mouse_target);
  // If it's a selection grip, return the grip parent
  if($target.closest('#selectorParentGroup').length) {
    // While we could instead have just returned mouse_target,
    // While we could instead have just returned mouse_target,
    // this makes it easier to indentify as being a selector grip
    return selectorManager.selectorParentGroup;
  }
@@ -2330,8 +2330,8 @@
  while (mouse_target.parentNode && mouse_target.parentNode !== (current_group || current_layer)) {
    mouse_target = mouse_target.parentNode;
  }
//
//
//  // go up until we hit a child of a layer
//  while (mouse_target.parentNode.parentNode.tagName == 'g') {
//    mouse_target = mouse_target.parentNode;
@@ -2341,7 +2341,7 @@
//  if (mouse_target.nodeName.toLowerCase() == "div") {
//    mouse_target = svgroot;
//  }
  return mouse_target;
};
@@ -2359,7 +2359,7 @@
      maxx: null,
      maxy: null
    };
  // - when we are in a create mode, the element is added to the canvas
  //   but the action is not recorded until mousing up
  // - when we are in select mode, select the element, remember the position
@@ -2374,7 +2374,7 @@
    var pt = transformPoint( evt.pageX, evt.pageY, root_sctm ),
      mouse_x = pt.x * current_zoom,
      mouse_y = pt.y * current_zoom;
    evt.preventDefault();
@@ -2382,15 +2382,15 @@
      current_mode = "select";
      lastClickPoint = pt;
    }
    var x = mouse_x / current_zoom,
      y = mouse_y / current_zoom,
      mouse_target = getMouseTarget(evt);
    if(mouse_target.tagName === 'a' && mouse_target.childNodes.length === 1) {
      mouse_target = mouse_target.firstChild;
    }
    // real_x/y ignores grid-snap value
    var real_x = r_start_x = start_x = x;
    var real_y = r_start_y = start_y = y;
@@ -2402,9 +2402,9 @@
      start_y = snapToGrid(start_y);
    }
    // if it is a selector grip, then it must be a single element selected,
    // if it is a selector grip, then it must be a single element selected,
    // set the mouse_target to that and update the mode to rotate/resize
    if (mouse_target == selectorManager.selectorParentGroup && selectedElements[0] != null) {
      var grip = evt.target;
      var griptype = elData(grip, "type");
@@ -2420,7 +2420,7 @@
      }
      mouse_target = selectedElements[0];
    }
    start_transform = mouse_target.getAttribute("transform");
    var tlist = getTransformList(mouse_target);
    switch (current_mode) {
@@ -2428,11 +2428,11 @@
        started = true;
        current_resize_mode = "none";
        if(right_click) started = false;
        if (mouse_target != svgroot) {
          // if this element is not yet selected, clear selection and select it
          if (selectedElements.indexOf(mouse_target) == -1) {
            // only clear selection if shift is not pressed (otherwise, add
            // only clear selection if shift is not pressed (otherwise, add
            // element to selection)
            if (!evt.shiftKey) {
              // No need to do the call here as it will be done on addToSelection
@@ -2443,7 +2443,7 @@
            pathActions.clear();
          }
          // else if it's a path, go into pathedit mode in mouseup
          if(!right_click) {
            // insert a dummy transform so if the element(s) are moved it will have
            // a transform to use for its translate
@@ -2466,11 +2466,11 @@
          }
          r_start_x *= current_zoom;
          r_start_y *= current_zoom;
//          console.log('p',[evt.pageX, evt.pageY]);
//          console.log('c',[evt.clientX, evt.clientY]);
//          console.log('o',[evt.offsetX, evt.offsetY]);
//          console.log('p',[evt.pageX, evt.pageY]);
//          console.log('c',[evt.clientX, evt.clientY]);
//          console.log('o',[evt.offsetX, evt.offsetY]);
//          console.log('s',[start_x, start_y]);
          assignAttributes(rubberBox, {
            'x': r_start_x,
            'y': r_start_y,
@@ -2480,7 +2480,7 @@
          }, 100);
        }
        break;
      case "zoom":
      case "zoom":
        started = true;
        if (rubberBox == null) {
          rubberBox = selectorManager.getRubberBandBox();
@@ -2497,7 +2497,7 @@
        started = true;
        start_x = x;
        start_y = y;
        // Getting the BBox from the selection box, since we know we
        // want to orient around it
        init_bbox = svgedit.utilities.getBBox($('#selectedBox0')[0]);
@@ -2509,7 +2509,7 @@
        // append three dummy transforms to the tlist so that
        // we can translate,scale,translate in mousemove
        var pos = getRotationAngle(mouse_target)?1:0;
        if(hasMatrixTransform(tlist)) {
          tlist.insertItemBefore(svgroot.createSVGTransform(), pos);
          tlist.insertItemBefore(svgroot.createSVGTransform(), pos);
@@ -2518,7 +2518,7 @@
          tlist.appendItem(svgroot.createSVGTransform());
          tlist.appendItem(svgroot.createSVGTransform());
          tlist.appendItem(svgroot.createSVGTransform());
          if(svgedit.browser.supportsNonScalingStroke()) {
            //Handle crash for newer Webkit: https://code.google.com/p/svg-edit/issues/detail?id=904
            //Chromium issue: https://code.google.com/p/chromium/issues/detail?id=114625
@@ -2701,27 +2701,27 @@
        // This could occur in an extension
        break;
    }
    var ext_result = runExtensions("mouseDown", {
      event: evt,
      start_x: start_x,
      start_y: start_y,
      selectedElements: selectedElements
    }, true);
    $.each(ext_result, function(i, r) {
      if(r && r.started) {
        started = true;
      }
    });
    if (current_mode) {
      document.getElementById("workarea").className =
      document.getElementById("workarea").className =
        (current_mode == "resize")
        ? evt.target.style.cursor
        : current_mode
      }
  };
  // in this function we do not record any state changes yet (but we do update
  // any elements that are still being created, moved or resized on the canvas)
  var mouseMove = function(evt) {
@@ -2743,23 +2743,23 @@
    }
    evt.preventDefault();
    switch (current_mode)
    {
      case "select":
        // we temporarily use a translate on the element(s) being dragged
        // this transform is removed upon mousing up and the element is
        // this transform is removed upon mousing up and the element is
        // relocated to the new location
        if (selectedElements[0] !== null) {
          var dx = x - start_x;
          var dy = y - start_y;
          if(curConfig.gridSnapping){
            dx = snapToGrid(dx);
            dy = snapToGrid(dy);
          }
          if(evt.shiftKey) {
          if(evt.shiftKey) {
            var xya = snapToAngle(start_x,start_y,x,y); x=xya.x; y=xya.y;
         }
          if (dx != 0 || dy != 0) {
@@ -2790,13 +2790,13 @@
              } else {
                tlist.appendItem(xform);
              }
              // update our internal bbox that we're tracking while dragging
              selectorManager.requestSelector(selected).resize();
            }
            //duplicate only once
            // alt drag = create a clone and save the reference
            // alt drag = create a clone and save the reference
            if(evt.altKey) {
              //clone doesn't exist yet
              if (!canvas.addClones) {
@@ -2812,14 +2812,14 @@
                window.addEventListener("keyup", canvas.removeClones)
              }
            }
            call("transition", selectedElements);
          }
        }
        break;
      case "multiselect":
@@ -2839,7 +2839,7 @@
        var elemsToRemove = [], elemsToAdd = [],
          newList = getIntersectionList(),
          len = selectedElements.length;
        for (var i = 0; i < len; ++i) {
          var ind = newList.indexOf(selectedElements[i]);
          if (ind == -1) {
@@ -2849,16 +2849,16 @@
            newList[ind] = null;
          }
        }
        len = newList.length;
        for (i = 0; i < len; ++i) { if (newList[i]) elemsToAdd.push(newList[i]); }
        if (elemsToRemove.length > 0)
        if (elemsToRemove.length > 0)
          canvas.removeFromSelection(elemsToRemove);
        if (elemsToAdd.length > 0)
        if (elemsToAdd.length > 0)
          addToSelection(elemsToAdd);
        break;
      case "resize":
        // we track the resize bounding box and translate/scale the selected element
@@ -2866,10 +2866,10 @@
        // the shape's coordinates
        var tlist = getTransformList(selected),
          hasMatrix = hasMatrixTransform(tlist),
          box = hasMatrix ? init_bbox : svgedit.utilities.getBBox(selected),
          box = hasMatrix ? init_bbox : svgedit.utilities.getBBox(selected),
          left=box.x, top=box.y, width=box.width,
          height=box.height, dx=(x-start_x), dy=(y-start_y);
        if(curConfig.gridSnapping){
          dx = snapToGrid(dx);
          dy = snapToGrid(dy);
@@ -2893,24 +2893,24 @@
        }
        if(current_resize_mode.indexOf("e")==-1 && current_resize_mode.indexOf("w")==-1) {
          dx = 0;
        }
        }
        var ts = null,
          tx = 0, ty = 0,
          sy = height ? (height+dy)/height : 1,
          sy = height ? (height+dy)/height : 1,
          sx = width ? (width+dx)/width : 1;
        // if we are dragging on the north side, then adjust the scale factor and ty
        if(current_resize_mode.indexOf("n") >= 0) {
          sy = height ? (height-dy)/height : 1;
          ty = height;
        }
        // if we dragging on the east side, then adjust the scale factor and tx
        if(current_resize_mode.indexOf("w") >= 0) {
          sx = width ? (width-dx)/width : 1;
          tx = width;
        }
        // update the transform list with translate,scale,translate
        var translateOrigin = svgroot.createSVGTransform(),
          scale = svgroot.createSVGTransform(),
@@ -2929,7 +2929,7 @@
          else sy = sx;
        }
        scale.setScale(sx,sy);
        translateBack.setTranslate(left+tx,top+ty);
        if(hasMatrix) {
          var diff = angle?1:0;
@@ -2944,9 +2944,9 @@
        }
        selectorManager.requestSelector(selected).resize();
        call("transition", selectedElements);
        break;
      case "zoom":
        real_x *= current_zoom;
@@ -2956,7 +2956,7 @@
          'y': Math.min(r_start_y*current_zoom, real_y),
          'width': Math.abs(real_x - r_start_x*current_zoom),
          'height': Math.abs(real_y - r_start_y*current_zoom)
        },100);
        },100);
        break;
      case "text":
        assignAttributes(shape,{
@@ -2971,10 +2971,10 @@
        }
        var x2 = x;
        var y2 = y;
        var y2 = y;
        if(evt.shiftKey) { var xya = snapToAngle(start_x,start_y,x2,y2); x2=xya.x; y2=xya.y; }
        shape.setAttributeNS(null, "x2", x2);
        shape.setAttributeNS(null, "y2", y2);
        break;
@@ -2998,11 +2998,11 @@
        }
        if (evt.altKey){
          w *=2;
          h *=2;
          h *=2;
          new_x = start_x - w/2;
          new_y = start_y - h/2;
        }
        if(curConfig.gridSnapping){
          w = snapToGrid(w);
          h = snapToGrid(h);
@@ -3016,7 +3016,7 @@
          'x': new_x,
          'y': new_y
        },1000);
        break;
      case "circle":
        var c = $(shape).attr(["cx", "cy"]);
@@ -3042,7 +3042,7 @@
        if (evt.shiftKey) {
          ry = rx
          cy = (y > start_y) ? start_y + rx : start_y - rx
        }
        if (evt.altKey) {
          cx = start_x
@@ -3072,7 +3072,7 @@
      case "pathedit":
        x *= current_zoom;
        y *= current_zoom;
        if(curConfig.gridSnapping){
          x = snapToGrid(x);
          y = snapToGrid(y);
@@ -3091,7 +3091,7 @@
          var xya = snapToAngle(x1,y1,x,y);
          x=xya.x; y=xya.y;
        }
        if(rubberBox && rubberBox.getAttribute('display') !== 'none') {
          real_x *= current_zoom;
          real_y *= current_zoom;
@@ -3100,10 +3100,10 @@
            'y': Math.min(r_start_y*current_zoom, real_y),
            'width': Math.abs(real_x - r_start_x*current_zoom),
            'height': Math.abs(real_y - r_start_y*current_zoom)
          },100);
          },100);
        }
        pathActions.mouseMove(evt, x, y);
        break;
      case "textedit":
        x *= current_zoom;
@@ -3116,13 +3116,13 @@
//              'height': Math.abs(y-start_y)
//            },100);
//          }
        textActions.mouseMove(mouse_x, mouse_y);
        break;
      case "rotate":
        var box = svgedit.utilities.getBBox(selected),
          cx = box.x + box.width/2,
          cx = box.x + box.width/2,
          cy = box.y + box.height/2,
          m = getMatrix(selected),
          center = transformPoint(cx,cy,m);
@@ -3150,7 +3150,7 @@
      default:
        break;
    }
    runExtensions("mouseMove", {
      event: evt,
      mouse_x: mouse_x,
@@ -3159,16 +3159,16 @@
    });
  }; // mouseMove()
  /* mouseover mode
  var mouseOver = function(evt) {
    if(canvas.spaceKey || evt.button === 1 || current_mode != "select") return;
    evt.stopPropagation();
    mouse_target = getMouseTarget(evt);
    if (svghover.lastChild) svghover.removeChild(svghover.lastChild);
    if (mouse_target.id == "svgroot") return
    switch (mouse_target.nodeName) {
      case "polyline":
@@ -3176,7 +3176,7 @@
      case "path":
      case "ellipse":
      case "rect":
          var clone = mouse_target.cloneNode(true);
          var clone = mouse_target.cloneNode(true);
          clone.setAttribute("stroke", "#c00")
          clone.setAttribute("stroke-width", "1")
          clone.setAttribute("stroke-opacity", "1")
@@ -3184,13 +3184,13 @@
          clone.setAttribute("fill", "none")
          hover_group.appendChild(clone);
      break;
      default:
      break;
    }
  }
  */
  // - in create mode, the element's opacity is set properly, we create an InsertElementCommand
  //   and store it on the Undo stack
  // - in move/resize mode, the element's attributes which were affected by the move/resize are
@@ -3256,7 +3256,7 @@
              cur_text.font_family = selected.getAttribute("font-family");
            }
            selectorManager.requestSelector(selected).showGrips(true);
            // This shouldn't be necessary as it was done on mouseDown...
//              call("selected", [selected]);
          }
@@ -3264,8 +3264,8 @@
          recalculateAllSelectedDimensions();
          // if it was being dragged/resized
          r_start_x = r_start_x;
          r_start_y = r_start_y;
          r_start_x = r_start_x;
          r_start_y = r_start_y;
          var difference_x = Math.abs(real_x-r_start_x);
          var difference_y = Math.abs(real_y-r_start_y);
@@ -3292,7 +3292,7 @@
              }
            }
          } // no change in mouse position
          // Remove non-scaling stroke
          if(svgedit.browser.supportsNonScalingStroke()) {
            var elem = selectedElements[0];
@@ -3401,7 +3401,7 @@
        element = null;
        // continue to be set to true so that mouseMove happens
        started = true;
        var res = pathActions.mouseUp(evt, element, mouse_x, mouse_y);
        element = res.element;
        keep = res.keep;
@@ -3421,7 +3421,7 @@
        element = null;
        current_mode = "select";
        var batchCmd = canvas.undoMgr.finishUndoableChange();
        if (!batchCmd.isEmpty()) {
        if (!batchCmd.isEmpty()) {
          addCommandToHistory(batchCmd);
        }
        // perform recalculation to weed out any stray identity transforms that might get stuck
@@ -3432,13 +3432,13 @@
        // This could occur in an extension
        break;
    }
    var ext_result = runExtensions("mouseUp", {
      event: evt,
      mouse_x: mouse_x,
      mouse_y: mouse_y
    }, true);
    $.each(ext_result, function(i, r) {
      if(r) {
        keep = r.keep || keep;
@@ -3446,37 +3446,37 @@
        started = r.started || started;
      }
    });
    if (!keep && element != null) {
      getCurrentDrawing().releaseId(getId());
      element.parentNode.removeChild(element);
      element = null;
      var t = evt.target;
      // if this element is in a group, go up until we reach the top-level group
      // if this element is in a group, go up until we reach the top-level group
      // just below the layer groups
      // TODO: once we implement links, we also would have to check for <a> elements
      while (t.parentNode.parentNode.tagName == "g") {
        t = t.parentNode;
      }
      // if we are not in the middle of creating a path, and we've clicked on some shape,
      // if we are not in the middle of creating a path, and we've clicked on some shape,
      // then go to Select mode.
      // WebKit returns <div> when the canvas is clicked, Firefox/Opera return <svg>
      if ( (current_mode != "path" || !drawn_path) &&
        t.parentNode.id != "selectorParentGroup" &&
        t.id != "svgcanvas" && t.id != "svgroot")
        t.id != "svgcanvas" && t.id != "svgroot")
      {
        // switch into "select" mode if we've clicked on an element
        canvas.setMode("select");
        selectOnly([t], true);
      }
    } else if (element != null) {
      canvas.addedNew = true;
      if(useUnit) svgedit.units.convertAttrs(element);
      var ani_dur = .2, c_ani;
      if(opac_ani.beginElement && element.getAttribute('opacity') != cur_shape.opacity) {
        c_ani = $(opac_ani).clone().attr({
@@ -3490,7 +3490,7 @@
      } else {
        ani_dur = 0;
      }
      // Ideally this would be done on the endEvent of the animation,
      // but that doesn't seem to be supported in Webkit
      setTimeout(function() {
@@ -3508,14 +3508,14 @@
        // we create the insert command that is stored on the stack
        // undo means to call cmd.unapply(), redo means to call cmd.apply()
        addCommandToHistory(new InsertElementCommand(element));
        call("changed",[element]);
      }, ani_dur * 1000);
    }
    start_transform = null;
  };
  var dblClick = function(evt) {
    var evt_target = evt.target;
    var parent = evt_target.parentNode;
@@ -3523,16 +3523,16 @@
    var tagName = mouse_target.tagName;
    if(parent === current_group) return;
    if(tagName === 'text' && current_mode !== 'textedit') {
      var pt = transformPoint( evt.pageX, evt.pageY, root_sctm );
      textActions.select(mouse_target, pt.x, pt.y);
    }
    if((tagName === "g" || tagName === "a") && getRotationAngle(mouse_target)) {
      // TODO: Allow method of in-group editing without having to do
      // TODO: Allow method of in-group editing without having to do
      // this (similar to editing rotated paths)
      // Ungroup and regroup
      pushGroupProperties(mouse_target);
      mouse_target = selectedElements[0];
@@ -3542,7 +3542,7 @@
    if(current_group) {
      leaveContext();
    }
    if((parent.tagName !== 'g' && parent.tagName !== 'a') ||
      parent === getCurrentDrawing().getCurrentLayer() ||
      mouse_target === selectorManager.selectorParentGroup)
@@ -3558,12 +3558,12 @@
    e.preventDefault();
    return false;
  };
  // Added mouseup to the container here.
  // TODO(codedread): Figure out why after the Closure compiler, the window mouseup is ignored.
  $(container).mousedown(mouseDown).mousemove(mouseMove).click(handleLinkInCanvas).dblclick(dblClick).mouseup(mouseUp);
//  $(window).mouseup(mouseUp);
  $(container).bind("mousewheel DOMMouseScroll", function(e){
    if(!e.shiftKey) return;
    e.preventDefault();
@@ -3589,14 +3589,14 @@
      if (e.detail > 0) {
        bbox.factor = .5;
      } else if (e.detail < 0) {
        bbox.factor = 2;
      }
        bbox.factor = 2;
      }
    }
    if(!bbox.factor) return;
    call("zoomed", bbox);
  });
}());
// Function: preventClickDefault
@@ -3621,11 +3621,11 @@
  var matrix;
  var last_x, last_y;
  var allow_dbl;
  function setCursor(index) {
    var empty = (textinput.value === "");
    $(textinput).focus();
    if(!arguments.length) {
      if(empty) {
        index = 0;
@@ -3634,7 +3634,7 @@
        index = textinput.selectionEnd;
      }
    }
    var charbb;
    charbb = chardata[index];
    if(!empty) {
@@ -3650,7 +3650,7 @@
      });
      cursor = getElem("selectorParentGroup").appendChild(cursor);
    }
    if(!blinker) {
      blinker = setInterval(function() {
        var show = (cursor.getAttribute('display') === 'none');
@@ -3658,11 +3658,11 @@
      }, 600);
    }
    var start_pt = ptToScreen(charbb.x, textbb.y);
    var end_pt = ptToScreen(charbb.x, (textbb.y + textbb.height));
    assignAttributes(cursor, {
      x1: start_pt.x,
      y1: start_pt.y,
@@ -3671,20 +3671,20 @@
      visibility: 'visible',
      display: 'inline'
    });
    if(selblock) selblock.setAttribute('d', 'M 0 0');
  }
  function setSelection(start, end, skipInput) {
    if(start === end) {
      setCursor(end);
      return;
    }
    if(!skipInput) {
      textinput.setSelectionRange(start, end);
    }
    selblock = getElem("text_selectblock");
    if (!selblock) {
@@ -3698,30 +3698,30 @@
      getElem("selectorParentGroup").appendChild(selblock);
    }
    var startbb = chardata[start];
    var endbb = chardata[end];
    cursor.setAttribute('visibility', 'hidden');
    var tl = ptToScreen(startbb.x, textbb.y),
      tr = ptToScreen(startbb.x + (endbb.x - startbb.x), textbb.y),
      bl = ptToScreen(startbb.x, textbb.y + textbb.height),
      br = ptToScreen(startbb.x + (endbb.x - startbb.x), textbb.y + textbb.height);
    var dstr = "M" + tl.x + "," + tl.y
          + " L" + tr.x + "," + tr.y
          + " " + br.x + "," + br.y
          + " " + bl.x + "," + bl.y + "z";
    assignAttributes(selblock, {
      d: dstr,
      'display': 'inline'
    });
  }
  function getIndexFromPoint(mouse_x, mouse_y) {
    // Position cursor here
    var pt = svgroot.createSVGPoint();
@@ -3748,62 +3748,62 @@
    }
    return charpos;
  }
  function setCursorFromPoint(mouse_x, mouse_y) {
    setCursor(getIndexFromPoint(mouse_x, mouse_y));
  }
  function setEndSelectionFromPoint(x, y, apply) {
    var i1 = textinput.selectionStart;
    var i2 = getIndexFromPoint(x, y);
    var start = Math.min(i1, i2);
    var end = Math.max(i1, i2);
    setSelection(start, end, !apply);
  }
  function screenToPt(x_in, y_in) {
    var out = {
      x: x_in,
      y: y_in
    }
    out.x /= current_zoom;
    out.y /= current_zoom;
    out.y /= current_zoom;
    if(matrix) {
      var pt = transformPoint(out.x, out.y, matrix.inverse());
      out.x = pt.x;
      out.y = pt.y;
    }
    return out;
  }
  }
  function ptToScreen(x_in, y_in) {
    var out = {
      x: x_in,
      y: y_in
    }
    if(matrix) {
      var pt = transformPoint(out.x, out.y, matrix);
      out.x = pt.x;
      out.y = pt.y;
    }
    out.x *= current_zoom;
    out.y *= current_zoom;
    return out;
  }
  function hideCursor() {
    if(cursor) {
      cursor.setAttribute('visibility', 'hidden');
    }
  }
  function selectAll(evt) {
    setSelection(0, curtext.textContent.length);
    $(this).unbind(evt);
@@ -3811,25 +3811,25 @@
  function selectWord(evt) {
    if(!allow_dbl || !curtext) return;
    var ept = transformPoint( evt.pageX, evt.pageY, root_sctm ),
      mouse_x = ept.x * current_zoom,
      mouse_y = ept.y * current_zoom;
    var pt = screenToPt(mouse_x, mouse_y);
    var index = getIndexFromPoint(pt.x, pt.y);
    var str = curtext.textContent;
    var first = str.substr(0, index).replace(/[a-z0-9]+$/i, '').length;
    var m = str.substr(index).match(/^[a-z0-9]+/i);
    var last = (m?m[0].length:0) + index;
    setSelection(first, last);
    // Set tripleclick
    $(evt.target).click(selectAll);
    setTimeout(function() {
      $(evt.target).unbind('click', selectAll);
    }, 300);
  }
  return {
@@ -3843,27 +3843,27 @@
    },
    mouseDown: function(evt, mouse_target, start_x, start_y) {
      var pt = screenToPt(start_x, start_y);
      textinput.focus();
      setCursorFromPoint(pt.x, pt.y);
      last_x = start_x;
      last_y = start_y;
      // TODO: Find way to block native selection
    },
    mouseMove: function(mouse_x, mouse_y) {
      var pt = screenToPt(mouse_x, mouse_y);
      setEndSelectionFromPoint(pt.x, pt.y);
    },
    },
    mouseUp: function(evt, mouse_x, mouse_y) {
      var pt = screenToPt(mouse_x, mouse_y);
      setEndSelectionFromPoint(pt.x, pt.y, true);
      // TODO: Find a way to make this work: Use transformed BBox instead of evt.target
      // TODO: Find a way to make this work: Use transformed BBox instead of evt.target
//        if(last_x === mouse_x && last_y === mouse_y
//          && !svgedit.math.rectsIntersect(transbb, {x: pt.x, y: pt.y, width:0, height:0})) {
//          textActions.toSelectMode(true);
//          textActions.toSelectMode(true);
//        }
      if(
@@ -3883,21 +3883,21 @@
      allow_dbl = false;
      current_mode = "textedit";
      selectorManager.requestSelector(curtext).showGrips(false);
      // Make selector group accept clicks
      var sel = selectorManager.requestSelector(curtext).selectorRect;
      textActions.init();
      $(curtext).css('cursor', 'text');
      if(!arguments.length) {
        setCursor();
      } else {
        var pt = screenToPt(x, y);
        setCursorFromPoint(pt.x, pt.y);
      }
      setTimeout(function() {
        allow_dbl = true;
      }, 300);
@@ -3909,11 +3909,11 @@
      if(selblock) $(selblock).attr('display','none');
      if(cursor) $(cursor).attr('visibility','hidden');
      $(curtext).css('cursor', 'move');
      if(selectElem) {
        clearSelection();
        $(curtext).css('cursor', 'move');
        call("selected", [curtext]);
        addToSelection([curtext], true);
      }
@@ -3921,11 +3921,11 @@
        // No content, so delete
        canvas.deleteSelectedElements();
      }
      $(textinput).blur();
      curtext = false;
//        if(svgedit.browser.supportsEditableText()) {
//          curtext.removeAttribute('editable');
//        }
@@ -3946,47 +3946,47 @@
//          curtext.select();
//          return;
//        }
      if(!curtext.parentNode) {
        // Result of the ffClone, need to get correct element
        curtext = selectedElements[0];
        selectorManager.requestSelector(curtext).showGrips(false);
      }
      var str = curtext.textContent;
      var len = str.length;
      var xform = curtext.getAttribute('transform');
      textbb = svgedit.utilities.getBBox(curtext);
      matrix = xform?getMatrix(curtext):null;
      chardata = Array(len);
      textinput.focus();
      $(curtext).unbind('dblclick', selectWord).dblclick(selectWord);
      if(!len) {
        var end = {x: textbb.x + (textbb.width/2), width: 0};
      }
      for(var i=0; i<len; i++) {
        var start = curtext.getStartPositionOfChar(i);
        var end = curtext.getEndPositionOfChar(i);
        if(!svgedit.browser.supportsGoodTextCharPos()) {
          var offset = canvas.contentW * current_zoom;
          start.x -= offset;
          end.x -= offset;
          start.x /= current_zoom;
          end.x /= current_zoom;
        }
        // Get a "bbox" equivalent for each character. Uses the
        // bbox data of the actual text for y, height purposes
        // TODO: Decide if y, width and height are actually necessary
        chardata[i] = {
          x: start.x,
@@ -3995,7 +3995,7 @@
          height: textbb.height
        };
      }
      // Add a last bbox for cursor at end of text
      chardata.push({
        x: end.x,
@@ -4010,11 +4010,11 @@
// Group: Path edit functions
// Functions relating to editing path elements
var pathActions = canvas.pathActions = function() {
  var subpath = false;
  var current_path;
  var newPoint, firstCtrl;
  function resetD(p) {
    p.setAttribute("d", pathActions.convertPath(p));
  }
@@ -4050,9 +4050,9 @@
      }
      // TODO: Correct this:
      pathActions.canDeleteNodes = true;
      pathActions.closed_subpath = this.subpathIsClosed(this.selected_pts[0]);
      call("selected", grips);
    }
@@ -4062,7 +4062,7 @@
    stretchy = null;
  this.lastCtrlPoint = [0, 0];
  // This function converts a polyline (created by the fh_path tool) into
  // a path element and coverts every three line segments into a single bezier
  // curve in an attempt to smooth out the free-hand
@@ -4071,13 +4071,13 @@
    var N = points.numberOfItems;
    if (N >= 4) {
      // loop through every 3 points and convert to a cubic bezier curve segment
      //
      // NOTE: this is cheating, it means that every 3 points has the potential to
      //
      // NOTE: this is cheating, it means that every 3 points has the potential to
      // be a corner instead of treating each point in an equal manner.  In general,
      // this technique does not look that good.
      //
      //
      // I am open to better ideas!
      //
      //
      // Reading:
      // - http://www.efg2.com/Lab/Graphics/Jean-YvesQueinecBezierCurves.htm
      // - http://www.codeproject.com/KB/graphics/BezierSpline.aspx?msg=2956963
@@ -4090,7 +4090,7 @@
        var ct1 = points.getItem(i);
        var ct2 = points.getItem(i+1);
        var end = points.getItem(i+2);
        // if the previous segment had a control point, we want to smooth out
        // the control points on both sides
        if (prevCtlPt) {
@@ -4103,9 +4103,9 @@
            ct1 = newpts[1];
          }
        }
        d.push([ct1.x,ct1.y,ct2.x,ct2.y,end.x,end.y].join(','));
        curpos = end;
        prevCtlPt = ct2;
      }
@@ -4137,12 +4137,12 @@
      if(current_mode === "path") {
        mouse_x = start_x;
        mouse_y = start_y;
        var x = mouse_x/current_zoom,
          y = mouse_y/current_zoom,
          stretchy = getElem("path_stretch_line");
        newPoint = [x, y];
        newPoint = [x, y];
        if(curConfig.gridSnapping){
          x = snapToGrid(x);
          y = snapToGrid(y);
@@ -4163,9 +4163,9 @@
        stretchy.setAttribute("display", "inline");
        this.stretchy = stretchy;
        var keep = null;
        // if pts array is empty, create path element with M at current point
        if (!drawn_path) {
          d_attr = "M" + x + "," + y + " ";
@@ -4200,15 +4200,15 @@
              break;
            }
          }
          // get path element that we are in the process of creating
          var id = getId();
          // Remove previous path object if previously created
          svgedit.path.removePath_(id);
          var newpath = getElem(id);
          var len = seglist.numberOfItems;
          // if we clicked on an existing point, then we are done this path, commit it
          // (i,i+1) are the x,y that were clicked on
@@ -4224,7 +4224,7 @@
              var abs_y = seglist.getItem(0).y;
              var grip_x = svgedit.path.first_grip ? svgedit.path.first_grip[0]/current_zoom : seglist.getItem(0).x;
              var grip_y = svgedit.path.first_grip ? svgedit.path.first_grip[1]/current_zoom : seglist.getItem(0).y;
              var s_seg = stretchy.pathSegList.getItem(1);
              if(s_seg.pathSegType === 4) {
@@ -4247,7 +4247,7 @@
              return keep;
            }
            $(stretchy).remove();
            // this will signal to commit the path
            element = newpath;
            drawn_path = null;
@@ -4257,7 +4257,7 @@
              if(svgedit.path.path.matrix) {
                remapElement(newpath, {}, svgedit.path.path.matrix.inverse());
              }
              var new_d = newpath.getAttribute("d");
              var orig_d = $(svgedit.path.path.elem).attr("d");
              $(svgedit.path.path.elem).attr("d", orig_d + new_d);
@@ -4286,7 +4286,7 @@
            var lastx = last.x, lasty = last.y;
            if(evt.shiftKey) { var xya = snapToAngle(lastx,lasty,x,y); x=xya.x; y=xya.y; }
            // Use the segment defined by stretchy
            var s_seg = stretchy.pathSegList.getItem(1);
            if(s_seg.pathSegType === 4) {
@@ -4301,12 +4301,12 @@
                s_seg.y2 / current_zoom
              );
            }
            drawn_path.pathSegList.appendItem(newseg);
            x *= current_zoom;
            y *= current_zoom;
            // update everything to the latest point
            stretchy.setAttribute('d', ['M', x, y, x, y].join(' '));
            var pointGrip1 = svgedit.path.addCtrlGrip('1c1');
@@ -4337,20 +4337,20 @@
        }
        return;
      }
      // TODO: Make sure current_path isn't null at this point
      if(!svgedit.path.path) return;
      svgedit.path.path.storeD();
      var id = evt.target.id;
      if (id.substr(0,14) == "pathpointgrip_") {
        // Select this point
        var cur_pt = svgedit.path.path.cur_pt = parseInt(id.substr(14));
        svgedit.path.path.dragging = [start_x, start_y];
        var seg = svgedit.path.path.segs[cur_pt];
        // only clear selection if shift is not pressed (otherwise, add
        // only clear selection if shift is not pressed (otherwise, add
        // node to selection)
        if (!evt.shiftKey) {
          if(svgedit.path.path.selected_pts.length <= 1 || !seg.selected) {
@@ -4364,7 +4364,7 @@
        }
      } else if(id.indexOf("ctrlpointgrip_") == 0) {
        svgedit.path.path.dragging = [start_x, start_y];
        var parts = id.split('_')[1].split('c');
        var cur_pt = parts[0]-0;
        var ctrl_num = parts[1]-0;
@@ -4467,7 +4467,7 @@
        if(!drawn_path) return;
        var seglist = drawn_path.pathSegList;
        var index = seglist.numberOfItems - 1;
        var pointGrip1 = svgedit.path.addCtrlGrip('1c1');
        var pointGrip1 = svgedit.path.addCtrlGrip('1c1');
        var pointGrip2 = svgedit.path.addCtrlGrip('0c2');
        if(newPoint) {
@@ -4484,20 +4484,20 @@
          var pt_x = newPoint[0];
          var pt_y = newPoint[1];
          // set curve
          var seg = seglist.getItem(index);
          var cur_x = mouse_x / current_zoom;
          var cur_y = mouse_y / current_zoom;
          var alt_x = (is_linked) ?  (pt_x + (pt_x - cur_x)) : current_pointGrip2_x;
          var alt_y = (is_linked) ?  (pt_y + (pt_y - cur_y)) : current_pointGrip2_y;
          pointGrip2.setAttribute('cx', alt_x * current_zoom);
          pointGrip2.setAttribute('cy', alt_y * current_zoom);
          pointGrip2.setAttribute('display', 'inline');
          var ctrlLine = svgedit.path.getCtrlLine(1);
          var ctrlLine2 = svgedit.path.getCtrlLine(2);
          assignAttributes(ctrlLine, {
@@ -4507,7 +4507,7 @@
            y2: pt_y * current_zoom,
            display: 'inline'
          });
          assignAttributes(ctrlLine2, {
            x1: alt_x * current_zoom,
@@ -4522,11 +4522,11 @@
            firstCtrl = [mouse_x, mouse_y];
          } else {
            var last_x, last_y;
            var last = seglist.getItem(index - 1);
            var last_x = last.x;
            var last_y = last.y
            if(last.pathSegType === 6) {
              last_x += (last_x - last.x2);
              last_y += (last_y - last.y2);
@@ -4552,7 +4552,7 @@
            if(prev.pathSegType === 6) {
              var prev_x = this.lastCtrlPoint[0]/current_zoom || prev.x + (prev.x - prev.x2);
              var prev_y = this.lastCtrlPoint[1]/current_zoom || prev.y + (prev.y - prev.y2);
              svgedit.path.replacePathSeg(6, 1, [mouse_x, mouse_y, prev_x * current_zoom, prev_y * current_zoom, lastgripx, lastgripy], stretchy);
              svgedit.path.replacePathSeg(6, 1, [mouse_x, mouse_y, prev_x * current_zoom, prev_y * current_zoom, lastgripx, lastgripy], stretchy);
            } else if(firstCtrl) {
              svgedit.path.replacePathSeg(6, 1, [mouse_x, mouse_y, firstCtrl[0], firstCtrl[1], mouse_x, mouse_y], stretchy);
            } else {
@@ -4592,7 +4592,7 @@
          if(!seg.next && !seg.prev) return;
          var item = seg.item;
          var rbb = rubberBox.getBBox();
          var pt = svgedit.path.getGripPt(seg);
          var pt_bb = {
            x: pt.x,
@@ -4600,7 +4600,7 @@
            width: 0,
            height: 0
          };
          var sel = svgedit.math.rectsIntersect(rbb, pt_bb);
          this.select(sel);
@@ -4609,7 +4609,7 @@
        });
      }
    },
    },
    mouseUp: function(evt, element, mouse_x, mouse_y) {
      var lastpointgrip = getElem('ctrlpointgrip_1c1');
      var firstpointgrip = getElem('ctrlpointgrip_0c2');
@@ -4639,34 +4639,34 @@
          element: element
        }
      }
      // Edit mode
      if (svgedit.path.path.dragging) {
        var last_pt = svgedit.path.path.cur_pt;
        svgedit.path.path.dragging = false;
        svgedit.path.path.dragctrl = false;
        svgedit.path.path.update();
        if(hasMoved) {
          svgedit.path.path.endChanges("Move path point(s)");
        }
        }
        if(!evt.shiftKey && !hasMoved) {
          svgedit.path.path.selectPt(last_pt);
        }
        }
      }
      else if(rubberBox && rubberBox.getAttribute('display') != 'none') {
        // Done with multi-node-select
        rubberBox.setAttribute("display", "none");
        if(rubberBox.getAttribute('width') <= 2 && rubberBox.getAttribute('height') <= 2) {
          pathActions.toSelectMode(evt.target);
        }
      // else, move back to select mode
      // else, move back to select mode
      } else {
        pathActions.toSelectMode(evt.target);
      }
@@ -4686,12 +4686,12 @@
      svgedit.path.path.show(false);
      current_path = false;
      clearSelection();
      if(svgedit.path.path.matrix) {
        // Rotated, so may need to re-calculate the center
        svgedit.path.recalcRotatedPath();
      }
      if(selPath) {
        call("selected", [elem]);
        addToSelection([elem], true);
@@ -4715,14 +4715,14 @@
      } // going into pathedit mode
      else {
        current_path = target;
      }
      }
    },
    reorient: function() {
      var elem = selectedElements[0];
      if(!elem) return;
      var angle = getRotationAngle(elem);
      if(angle == 0) return;
      var batchCmd = new BatchCommand("Reorient path");
      var changes = {
        d: elem.getAttribute('d'),
@@ -4731,18 +4731,18 @@
      batchCmd.addSubCommand(new ChangeElementCommand(elem, changes));
      clearSelection();
      this.resetOrientation(elem);
      addCommandToHistory(batchCmd);
      // Set matrix to null
      svgedit.path.getPath_(elem).show(false).matrix = null;
      svgedit.path.getPath_(elem).show(false).matrix = null;
      this.clear();
      addToSelection([elem], true);
      call("changed", selectedElements);
    },
    clear: function(remove) {
      current_path = null;
      if (drawn_path) {
@@ -4764,11 +4764,11 @@
      tlist.clear();
      path.removeAttribute("transform");
      var segList = path.pathSegList;
      // Opera/win/non-EN throws an error here.
      // TODO: Find out why!
      // Presumed fixed in Opera 10.5, so commented out for now
//      try {
        var len = segList.numberOfItems;
//      } catch(err) {
@@ -4794,7 +4794,7 @@
        });
        svgedit.path.replacePathSeg(type, i, pts, path);
      }
      reorientGrads(path, m);
@@ -4814,16 +4814,16 @@
        y: seg.item.y,
        type: seg.type
      };
    },
    },
    linkControlPoints: function(linkPoints) {
      svgedit.path.setLinkControlPoints(linkPoints);
    },
    clonePathNode: function() {
      svgedit.path.path.storeD();
      var sel_pts = svgedit.path.path.selected_pts;
      var segs = svgedit.path.path.segs;
      var i = sel_pts.length;
      var nums = [];
@@ -4833,7 +4833,7 @@
        nums.push(pt + i);
        nums.push(pt + i + 1);
      }
      svgedit.path.path.init().addPtsToSelection(nums);
      svgedit.path.path.endChanges("Clone path node(s)");
@@ -4842,14 +4842,14 @@
      var sel_pts = svgedit.path.path.selected_pts;
      // Only allow one selected node for now
      if(sel_pts.length !== 1) return;
      var elem = svgedit.path.path.elem;
      var list = elem.pathSegList;
      var len = list.numberOfItems;
      var index = sel_pts[0];
      var open_pt = null;
      var start_item = null;
@@ -4869,7 +4869,7 @@
          return false;
        }
      });
      if(open_pt == null) {
        // Single path, so close last seg
        open_pt = svgedit.path.path.segs.length - 1;
@@ -4877,10 +4877,10 @@
      if(open_pt !== false) {
        // Close this path
        // Create a line going to the previous "M"
        var newseg = elem.createSVGPathSegLinetoAbs(start_item.x, start_item.y);
        var closer = elem.createSVGPathSegClosePath();
        if(open_pt == svgedit.path.path.segs.length) {
          list.appendItem(newseg);
@@ -4889,30 +4889,30 @@
          svgedit.path.insertItemBefore(elem, closer, open_pt);
          svgedit.path.insertItemBefore(elem, newseg, open_pt);
        }
        svgedit.path.path.init().selectPt(open_pt+1);
        return;
      }
      // M 1,1 L 2,2 L 3,3 L 1,1 z // open at 2,2
      // M 2,2 L 3,3 L 1,1
      // M 1,1 L 2,2 L 1,1 z M 4,4 L 5,5 L6,6 L 5,5 z
      // M 1,1 L 2,2 L 1,1 z [M 4,4] L 5,5 L(M)6,6 L 5,5 z
      // M 1,1 L 2,2 L 1,1 z M 4,4 L 5,5 L6,6 L 5,5 z
      // M 1,1 L 2,2 L 1,1 z [M 4,4] L 5,5 L(M)6,6 L 5,5 z
      var seg = svgedit.path.path.segs[index];
      if(seg.mate) {
        list.removeItem(index); // Removes last "L"
        list.removeItem(index); // Removes the "Z"
        svgedit.path.path.init().selectPt(index - 1);
        return;
      }
      var last_m, z_seg;
      // Find this sub-path's closing point and remove
      for(var i=0; i<list.numberOfItems; i++) {
        var item = list.getItem(i);
@@ -4931,26 +4931,26 @@
          break;
        }
      }
      var num = (index - last_m) - 1;
      while(num--) {
        svgedit.path.insertItemBefore(elem, list.getItem(last_m), z_seg);
      }
      var pt = list.getItem(last_m);
      // Make this point the new "M"
      svgedit.path.replacePathSeg(2, last_m, [pt.x, pt.y]);
      var i = index
      svgedit.path.path.init().selectPt(0);
    },
    deletePathNode: function() {
      if(!pathActions.canDeleteNodes) return;
      svgedit.path.path.storeD();
      var sel_pts = svgedit.path.path.selected_pts;
      var i = sel_pts.length;
@@ -4958,12 +4958,12 @@
        var pt = sel_pts[i];
        svgedit.path.path.deleteSeg(pt);
      }
      // Cleanup
      var cleanup = function() {
        var segList = svgedit.path.path.elem.pathSegList;
        var len = segList.numberOfItems;
        var remItems = function(pos, count) {
          while(count--) {
            segList.removeItem(pos);
@@ -4971,7 +4971,7 @@
        }
        if(len <= 1) return true;
        while(len--) {
          var item = segList.getItem(len);
          if(item.pathSegType === 1) {
@@ -4990,12 +4990,12 @@
          } else if(item.pathSegType === 2) {
            if(len > 0) {
              var prev_type = segList.getItem(len-1).pathSegType;
              // Path has M M
              // Path has M M
              if(prev_type === 2) {
                remItems(len-1, 1);
                cleanup();
                break;
              // Entire path ends with Z M
              // Entire path ends with Z M
              } else if(prev_type === 1 && segList.numberOfItems-1 === len) {
                remItems(len, 1);
                cleanup();
@@ -5003,23 +5003,23 @@
              }
            }
          }
        }
        }
        return false;
      }
      cleanup();
      // Completely delete a path with 1 or 0 segments
      if(svgedit.path.path.elem.pathSegList.numberOfItems <= 1) {
        canvas.setMode("select")
        canvas.deleteSelectedElements();
        return;
      }
      svgedit.path.path.init();
      svgedit.path.path.clearSelection();
      // TODO: Find right way to select point now
      // path.selectPt(sel_pt);
      if(window.opera) { // Opera repaints incorrectly
@@ -5034,14 +5034,14 @@
    moveNode: function(attr, newValue) {
      var sel_pts = svgedit.path.path.selected_pts;
      if(!sel_pts.length) return;
      svgedit.path.path.storeD();
      // Get first selected point
      var seg = svgedit.path.path.segs[sel_pts[0]];
      var diff = {x:0, y:0};
      diff[attr] = newValue - seg.item[attr];
      seg.move(diff.x, diff.y);
      svgedit.path.path.endChanges("Move path point");
    },
@@ -5057,7 +5057,7 @@
        if(item.pathSegType === 2) {
          last_m = item;
        }
        if(item.pathSegType === 1) {
          var prev = segList.getItem(i-1);
          if(prev.x != last_m.x || prev.y != last_m.y) {
@@ -5068,7 +5068,7 @@
            pathActions.fixEnd(elem);
            break;
          }
        }
      }
      if(svgedit.browser.isWebkit()) resetD(elem);
@@ -5080,7 +5080,7 @@
      var curx = 0, cury = 0;
      var d = "";
      var last_m = null;
      for (var i = 0; i < len; ++i) {
        var seg = segList.getItem(i);
        // if these properties are not in the segment, set them to zero
@@ -5090,10 +5090,10 @@
          y1 = seg.y1 || 0,
          x2 = seg.x2 || 0,
          y2 = seg.y2 || 0;
        var type = seg.pathSegType;
        var letter = pathMap[type]['to'+(toRel?'Lower':'Upper')+'Case']();
        var addToD = function(pnts, more, last) {
          var str = '';
          var more = more?' '+more.join(' '):'';
@@ -5103,7 +5103,7 @@
          });
          d += letter + pnts.join(' ') + more + last;
        }
        switch (type) {
          case 1: // z,Z closepath (Z/z)
            d += "z";
@@ -5143,12 +5143,12 @@
            y -= cury;
          case 5: // relative line (l)
          case 3: // relative move (m)
            // If the last segment was a "z", this must be relative to
            // If the last segment was a "z", this must be relative to
            if(last_m && segList.getItem(i-1).pathSegType === 1 && !toRel) {
              curx = last_m[0];
              cury = last_m[1];
            }
          case 19: // relative smooth quad (t)
            if(toRel) {
              curx += x;
@@ -5160,7 +5160,7 @@
              cury = y;
            }
            if(type === 3) last_m = [curx, cury];
            addToD([[x,y]]);
            break;
          case 6: // absolute cubic (C)
@@ -5181,7 +5181,7 @@
          case 8: // absolute quad (Q)
            x -= curx; x1 -= curx;
            y -= cury; y1 -= cury;
          case 9: // relative quad (q)
          case 9: // relative quad (q)
            if(toRel) {
              curx += x;
              cury += y;
@@ -5241,23 +5241,23 @@
// Function: removeUnusedDefElems
// Looks at DOM elements inside the <defs> to see if they are referred to,
// removes them from the DOM if they are not.
//
//
// Returns:
// The amount of elements that were removed
var removeUnusedDefElems = this.removeUnusedDefElems = function() {
  var defs = svgcontent.getElementsByTagNameNS(svgns, "defs");
  if(!defs || !defs.length) return 0;
//  if(!defs.firstChild) return;
  var defelem_uses = [],
    numRemoved = 0;
  var attrs = ['fill', 'stroke', 'filter', 'marker-start', 'marker-mid', 'marker-end'];
  var alen = attrs.length;
  var all_els = svgcontent.getElementsByTagNameNS(svgns, '*');
  var all_len = all_els.length;
  for(var i=0; i<all_len; i++) {
    var el = all_els[i];
    for(var j = 0; j < alen; j++) {
@@ -5268,14 +5268,14 @@
        }
      }
    }
    // gradients can refer to other gradients
    var href = getHref(el);
    if (href && href.indexOf('#') === 0) {
      defelem_uses.push(href.substr(1));
    }
  };
  var defelems = $(defs).find("linearGradient, radialGradient, filter, marker, svg, symbol");
    defelem_ids = [],
    i = defelems.length;
@@ -5294,34 +5294,34 @@
}
// Function: svgCanvasToString
// Main function to set up the SVG content for output
// Main function to set up the SVG content for output
//
// Returns:
// Returns:
// String containing the SVG image for output
this.svgCanvasToString = function() {
  // keep calling it until there are none to remove
  while (removeUnusedDefElems() > 0) {};
  pathActions.clear(true);
  // Keep SVG-Edit comment on top
  $.each(svgcontent.childNodes, function(i, node) {
    if(i && node.nodeType === 8 && node.data.indexOf('Created with') >= 0) {
      svgcontent.insertBefore(node, svgcontent.firstChild);
    }
  });
  // Move out of in-group editing mode
  if(current_group) {
    leaveContext();
    selectOnly([current_group]);
  }
  //hide grid, otherwise shows a black canvas
  $('#canvasGrid').attr('display', 'none');
  var naked_svgs = [];
  // Unwrap gsvg if it has no special attributes (only id and style)
  $(svgcontent).find('g:data(gsvg)').each(function() {
    var attrs = this.attributes;
@@ -5339,25 +5339,25 @@
    }
  });
  var output = this.svgToString(svgcontent, 0);
  // Rewrap gsvg
  if(naked_svgs.length) {
    $(naked_svgs).each(function() {
      groupSvgElem(this);
    });
  }
  return output;
};
// Function: svgToString
// Sub function ran on each SVG element to convert it to a string as desired
//
// Parameters:
//
// Parameters:
// elem - The SVG element to convert
// indent - Integer with the amount of spaces to indent this tag
//
// Returns:
// Returns:
// String with the given element as an SVG tag
this.svgToString = function(elem, indent) {
  var out = new Array(), toXml = svgedit.utilities.toXml;
@@ -5370,13 +5370,13 @@
      attr,
      i,
      childs = elem.childNodes;
    for (var i=0; i<indent; i++) out.push(" ");
    out.push("<"); out.push(elem.nodeName);
    if(elem.id === 'svgcontent') {
      // Process root element separately
      var res = getResolution();
      var vb = "";
      // TODO: Allow this by dividing all values by current baseVal
      // Note that this also means we should properly deal with this on import
@@ -5385,20 +5385,20 @@
//        var unit_m = svgedit.units.getTypeMap()[unit];
//        res.w = svgedit.units.shortFloat(res.w / unit_m)
//        res.h = svgedit.units.shortFloat(res.h / unit_m)
//        vb = ' viewBox="' + [0, 0, res.w, res.h].join(' ') + '"';
//        vb = ' viewBox="' + [0, 0, res.w, res.h].join(' ') + '"';
//        res.w += unit;
//        res.h += unit;
//      }
      if(unit !== "px") {
        res.w = svgedit.units.convertUnit(res.w, unit) + unit;
        res.h = svgedit.units.convertUnit(res.h, unit) + unit;
      }
      out.push(' width="' + res.w + '" height="' + res.h + '"' + vb + ' xmlns="'+svgns+'"');
      var nsuris = {};
      // Check elements for namespaces, add if found
      $(elem).find('*').andSelf().each(function() {
        var el = this;
@@ -5410,22 +5410,22 @@
          }
        });
      });
      var i = attrs.length;
      var attr_names = ['width','height','xmlns','x','y','viewBox','id','overflow'];
      while (i--) {
        attr = attrs.item(i);
        var attrVal = toXml(attr.nodeValue);
        // Namespaces have already been dealt with, so skip
        if(attr.nodeName.indexOf('xmlns:') === 0) continue;
        // only serialize attributes we don't use internally
        if (attrVal != "" && attr_names.indexOf(attr.localName) == -1)
        if (attrVal != "" && attr_names.indexOf(attr.localName) == -1)
        {
          if(!attr.namespaceURI || nsMap[attr.namespaceURI]) {
            out.push(' ');
            out.push(' ');
            out.push(attr.nodeName); out.push("=\"");
            out.push(attrVal); out.push("\"");
          }
@@ -5434,7 +5434,7 @@
    } else {
      // Skip empty defs
      if(elem.nodeName === 'defs' && !elem.firstChild) return;
      var moz_attrs = ['-moz-math-font-style', '_moz-math-font-style'];
      for (var i=attrs.length-1; i>=0; i--) {
        attr = attrs.item(i);
@@ -5444,25 +5444,25 @@
        if (attrVal != "") {
          if(attrVal.indexOf('pointer-events') === 0) continue;
          if(attr.localName === "class" && attrVal.indexOf('se_') === 0) continue;
          out.push(" ");
          out.push(" ");
          if(attr.localName === 'd') attrVal = pathActions.convertPath(elem, true);
          if(!isNaN(attrVal)) {
            attrVal = svgedit.units.shortFloat(attrVal);
          } else if(unit_re.test(attrVal)) {
            attrVal = svgedit.units.shortFloat(attrVal) + unit;
          }
          // Embed images when saving
          // Embed images when saving
          if(save_options.apply
            && elem.nodeName === 'image'
            && elem.nodeName === 'image'
            && attr.localName === 'href'
            && save_options.images
            && save_options.images === 'embed')
            && save_options.images === 'embed')
          {
            var img = encodableImages[attrVal];
            if(img) attrVal = img;
          }
          // map various namespaces to our fixed namespace prefixes
          // (the default xmlns attribute itself does not get a prefix)
          if(!attr.namespaceURI || attr.namespaceURI == svgns || nsMap[attr.namespaceURI]) {
@@ -5477,7 +5477,7 @@
      out.push(">");
      indent++;
      var bOneLine = false;
      for (var i=0; i<childs.length; i++)
      {
        var child = childs.item(i);
@@ -5525,7 +5525,7 @@
// Function: embedImage
// Converts a given image file to a data URL when possible, then runs a given callback
//
// Parameters:
// Parameters:
// val - String with the path/URL of the image
// callback - Optional function to run when image data is found, supplies the
// result (data URL or false) as first parameter.
@@ -5567,7 +5567,7 @@
// This function also includes the XML prolog.  Clients of the SvgCanvas bind their save
// function to the 'saved' event.
//
// Returns:
// Returns:
// Nothing
this.save = function(opts) {
  // remove the selected outline before serializing
@@ -5575,7 +5575,7 @@
  // Update save options if provided
  if(opts) $.extend(save_options, opts);
  save_options.apply = true;
  // no need for doctype, see http://jwatt.org/svg/authoring/#doctype-declaration
  var str = this.svgCanvasToString();
  if (svgedit.browser.supportsBlobs()) {
@@ -5589,15 +5589,15 @@
};
// Function: rasterExport
// Generates a PNG Data URL based on the current image, then calls "exported"
// Generates a PNG Data URL based on the current image, then calls "exported"
// with an object including the string and any issues found
this.rasterExport = function() {
  // remove the selected outline before serializing
  clearSelection();
  // Check for known CanVG issues
  // Check for known CanVG issues
  var issues = [];
  // Selector and notice
  var issue_list = {
    'feGaussianBlur': uiStrings.exportNoBlur,
@@ -5605,12 +5605,12 @@
    '[stroke-dasharray]': uiStrings.exportNoDashArray
  };
  var content = $(svgcontent);
  // Add font/text check if Canvas Text API is not implemented
  if(!("font" in $('<canvas>')[0].getContext('2d'))) {
    issue_list['text'] = uiStrings.exportNoText;
  }
  $.each(issue_list, function(sel, descr) {
    if(content.find(sel).length) {
      issues.push(descr);
@@ -5634,7 +5634,7 @@
// Function: randomizeIds
// This function determines whether to use a nonce in the prefix, when
// generating IDs for future documents in SVG-Edit.
//
//
//  Parameters:
//   an opional boolean, which, if true, adds a nonce to the prefix. Thus
//     svgCanvas.randomizeIds()  <==> svgCanvas.randomizeIds(true)
@@ -5662,11 +5662,11 @@
  //
  // <marker id='se_marker_end_svg_7'/>
  // <polyline id='svg_7' se:connector='svg_1 svg_6' marker-end='url(#se_marker_end_svg_7)'/>
  //
  //
  // Problem #1: if svg_1 gets renamed, we do not update the polyline's se:connector attribute
  // Problem #2: if the polyline svg_7 gets renamed, we do not update the marker id nor the polyline's marker-end attribute
  var ref_elems = ["filter", "linearGradient", "pattern", "radialGradient", "symbol", "textPath", "use"];
  svgedit.utilities.walkTree(g, function(n) {
    // if it's an element node
    if (n.nodeType == 1) {
@@ -5679,7 +5679,7 @@
        }
        ids[n.id]["elem"] = n;
      }
      // now search for all attributes on this element that might refer
      // to other elements
      $.each(ref_attrs,function(i,attr) {
@@ -5697,7 +5697,7 @@
          }
        }
      });
      // check xlink:href now
      var href = svgedit.utilities.getHref(n);
      // TODO: what if an <image> or <a> element refers to an element internally?
@@ -5711,20 +5711,20 @@
          }
          ids[refid]["hrefs"].push(n);
        }
      }
      }
    }
  });
  // in ids, we now have a map of ids, elements and attributes, let's re-identify
  for (var oldid in ids) {
    if (!oldid) continue;
    var elem = ids[oldid]["elem"];
    if (elem) {
      var newid = getNextId();
      // assign element its new id
      elem.id = newid;
      // remap all url() attributes
      var attrs = ids[oldid]["attrs"];
      var j = attrs.length;
@@ -5732,7 +5732,7 @@
        var attr = attrs[j];
        attr.ownerElement.setAttribute(attr.name, "url(#" + newid + ")");
      }
      // remap all href attributes
      var hreffers = ids[oldid]["hrefs"];
      var k = hreffers.length;
@@ -5748,11 +5748,11 @@
// Assigns reference data for each use element
var setUseData = this.setUseData = function(parent) {
  var elems = $(parent);
  if(parent.tagName !== 'use') {
    elems = elems.find('use');
  }
  elems.each(function() {
    var id = getHref(this).substr(1);
    var ref_elem = getElem(id);
@@ -5774,38 +5774,38 @@
      return (this.tagName.indexOf('Gradient') >= 0);
    });
  }
  elems.each(function() {
    var grad = this;
    if($(grad).attr('gradientUnits') === 'userSpaceOnUse') {
      // TODO: Support more than one element with this ref by duplicating parent grad
      var elems = $(svgcontent).find('[fill="url(#' + grad.id + ')"],[stroke="url(#' + grad.id + ')"]');
      if(!elems.length) return;
      // get object's bounding box
      var bb = svgedit.utilities.getBBox(elems[0]);
      // This will occur if the element is inside a <defs> or a <symbol>,
      // in which we shouldn't need to convert anyway.
      if(!bb) return;
      if(grad.tagName === 'linearGradient') {
        var g_coords = $(grad).attr(['x1', 'y1', 'x2', 'y2']);
        // If has transform, convert
        var tlist = grad.gradientTransform.baseVal;
        if(tlist && tlist.numberOfItems > 0) {
          var m = transformListToTransform(tlist).matrix;
          var pt1 = transformPoint(g_coords.x1, g_coords.y1, m);
          var pt2 = transformPoint(g_coords.x2, g_coords.y2, m);
          g_coords.x1 = pt1.x;
          g_coords.y1 = pt1.y;
          g_coords.x2 = pt2.x;
          g_coords.y2 = pt2.y;
          grad.removeAttribute('gradientTransform');
        }
        $(grad).attr({
          x1: (g_coords.x1 - bb.x) / bb.width,
          y1: (g_coords.y1 - bb.y) / bb.height,
@@ -5814,26 +5814,26 @@
        });
        grad.removeAttribute('gradientUnits');
      } else {
        // Note: radialGradient elements cannot be easily converted
        // Note: radialGradient elements cannot be easily converted
        // because userSpaceOnUse will keep circular gradients, while
        // objectBoundingBox will x/y scale the gradient according to
        // its bbox.
        // its bbox.
        // For now we'll do nothing, though we should probably have
        // the gradient be updated as the element is moved, as
        // the gradient be updated as the element is moved, as
        // inkscape/illustrator do.
//                var g_coords = $(grad).attr(['cx', 'cy', 'r']);
//
//
//            $(grad).attr({
//              cx: (g_coords.cx - bb.x) / bb.width,
//              cy: (g_coords.cy - bb.y) / bb.height,
//              r: g_coords.r
//            });
//
//
//                grad.removeAttribute('gradientUnits');
      }
    }
  });
@@ -5846,19 +5846,19 @@
    elem = selectedElements[0];
  }
  var $elem = $(elem);
  var batchCmd = new BatchCommand();
  var ts;
  if($elem.data('gsvg')) {
    // Use the gsvg as the new group
    var svg = elem.firstChild;
    var pt = $(svg).attr(['x', 'y']);
    $(elem.firstChild.firstChild).unwrap();
    $(elem).removeData('gsvg');
    var tlist = getTransformList(elem);
    var xform = svgroot.createSVGTransform();
    xform.setTranslate(pt.x, pt.y);
@@ -5867,61 +5867,61 @@
    call("selected", [elem]);
  } else if($elem.data('symbol')) {
    elem = $elem.data('symbol');
    ts = $elem.attr('transform');
    var pos = $elem.attr(['x','y']);
    var vb = elem.getAttribute('viewBox');
    if(vb) {
      var nums = vb.split(' ');
      pos.x -= +nums[0];
      pos.y -= +nums[1];
    }
    // Not ideal, but works
    ts += " translate(" + (pos.x || 0) + "," + (pos.y || 0) + ")";
    var prev = $elem.prev();
    // Remove <use> element
    batchCmd.addSubCommand(new RemoveElementCommand($elem[0], $elem[0].nextSibling, $elem[0].parentNode));
    $elem.remove();
    // See if other elements reference this symbol
    var has_more = $(svgcontent).find('use:data(symbol)').length;
    var g = svgdoc.createElementNS(svgns, "g");
    var childs = elem.childNodes;
    for(var i = 0; i < childs.length; i++) {
      g.appendChild(childs[i].cloneNode(true));
    }
    // Duplicate the gradients for Gecko, since they weren't included in the <symbol>
    if(svgedit.browser.isGecko()) {
      var dupeGrads = $(findDefs()).children('linearGradient,radialGradient,pattern').clone();
      $(g).append(dupeGrads);
    }
    if (ts) {
      g.setAttribute("transform", ts);
    }
    var parent = elem.parentNode;
    uniquifyElems(g);
    // Put the dupe gradients back into <defs> (after uniquifying them)
    if(svgedit.browser.isGecko()) {
      $(findDefs()).append( $(g).find('linearGradient,radialGradient,pattern') );
    }
    // now give the g itself a new id
    g.id = getNextId();
    prev.after(g);
    if(parent) {
      if(!has_more) {
        // remove symbol/svg element
@@ -5931,39 +5931,39 @@
      }
      batchCmd.addSubCommand(new InsertElementCommand(g));
    }
    setUseData(g);
    if(svgedit.browser.isGecko()) {
      convertGradients(findDefs());
    } else {
      convertGradients(g);
    }
    // recalculate dimensions on the top-level children so that unnecessary transforms
    // are removed
    svgedit.utilities.walkTreePost(g, function(n){try{recalculateDimensions(n)}catch(e){console.log(e)}});
    // Give ID for any visible element missing one
    $(g).find(visElems).each(function() {
      if(!this.id) this.id = getNextId();
    });
    selectOnly([g]);
    var cm = pushGroupProperties(g, true);
    if(cm) {
      batchCmd.addSubCommand(cm);
    }
    addCommandToHistory(batchCmd);
  } else {
    console.log('Unexpected element to ungroup:', elem);
  }
}
//
//
// Function: setSvgString
// This function sets the current drawing as the input SVG XML.
//
@@ -5985,7 +5985,7 @@
    var nextSibling = svgcontent.nextSibling;
    var oldzoom = svgroot.removeChild(svgcontent);
    batchCmd.addSubCommand(new RemoveElementCommand(oldzoom, nextSibling, svgroot));
    // set new svg document
    // If DOM3 adoptNode() available, use it. Otherwise fall back to DOM2 importNode()
    if(svgdoc.adoptNode) {
@@ -5994,12 +5994,12 @@
    else {
      svgcontent = svgdoc.importNode(newDoc.documentElement, true);
    }
    svgroot.appendChild(svgcontent);
    var content = $(svgcontent);
    canvas.current_drawing_ = new svgedit.draw.Drawing(svgcontent, idprefix);
    // retrieve or set the nonce
    var nonce = getCurrentDrawing().getNonce();
    if (nonce) {
@@ -6007,7 +6007,7 @@
    } else {
      call("unsetnonce");
    }
    // change image href vals if possible
    content.find('image').each(function() {
      var image = this;
@@ -6026,14 +6026,14 @@
      // Add to encodableImages if it loads
      canvas.embedImage(val);
    });
    // Wrap child SVGs in group elements
    content.find('svg').each(function() {
      // Skip if it's in a <defs>
      if($(this).closest('defs').length) return;
      uniquifyElems(this);
      // Check if it already has a gsvg group
      var pa = this.parentNode;
      if(pa.childNodes.length === 1 && pa.nodeName === 'g') {
@@ -6043,29 +6043,29 @@
        groupSvgElem(this);
      }
    });
    // Put all paint elems in defs
    content.find('linearGradient, radialGradient, pattern').appendTo(findDefs());
    // Set ref element for <use> elements
    // TODO: This should also be done if the object is re-added through "redo"
    setUseData(content);
    convertGradients(content[0]);
    // recalculate dimensions on the top-level children so that unnecessary transforms
    // are removed
    svgedit.utilities.walkTreePost(svgcontent, function(n){try{recalculateDimensions(n)}catch(e){console.log(e)}});
    var attrs = {
      id: 'svgcontent',
      overflow: curConfig.show_outside_canvas?'visible':'hidden'
    };
    var percs = false;
    // determine proper size
    if (content.attr("viewBox")) {
      var vb = content.attr("viewBox").split(' ');
@@ -6077,9 +6077,9 @@
      $.each(['width', 'height'], function(i, dim) {
        // Set to 100 if not given
        var val = content.attr(dim);
        if(!val) val = '100%';
        if((val+'').substr(-1) === "%") {
          // Use user units if percentage given
          percs = true;
@@ -6088,31 +6088,31 @@
        }
      });
    }
    // identify layers
    identifyLayers();
    // Give ID for any visible layer children missing one
    content.children().find(visElems).each(function() {
      if(!this.id) this.id = getNextId();
    });
    // Percentage width/height, so let's base it on visible elements
    if(percs) {
      var bb = getStrokedBBox();
      attrs.width = bb.width + bb.x;
      attrs.height = bb.height + bb.y;
    }
    // Just in case negative numbers are given or
    // Just in case negative numbers are given or
    // result from the percs calculation
    if(attrs.width <= 0) attrs.width = 200;
    if(attrs.height <= 0) attrs.height = 200;
    content.attr(attrs);
    this.contentW = attrs['width'];
    this.contentH = attrs['height'];
    $("#canvas_width").val(this.contentW)
    $("#canvas_height").val(this.contentH)
    var background = $("#canvas_background")
@@ -6131,16 +6131,16 @@
    // update root to the correct size
    var changes = content.attr(["width", "height"]);
    batchCmd.addSubCommand(new ChangeElementCommand(svgroot, changes));
    // reset zoom
    current_zoom = 1;
    // reset transform lists
    svgedit.transformlist.resetListMap();
    clearSelection();
    svgedit.path.clearData();
    svgroot.appendChild(selectorManager.selectorParentGroup);
    addCommandToHistory(batchCmd);
    call("changed", [svgcontent]);
  } catch(e) {
@@ -6165,7 +6165,7 @@
    opts = { alpha: opac };
    opts[refElem.tagName] = refElem;
  }
  }
  else if (color.indexOf("#") === 0) {
    opts = {
      alpha: opac,
@@ -6190,19 +6190,19 @@
//
// Returns:
// This function returns false if the import was unsuccessful, true otherwise.
// TODO:
// TODO:
// * properly handle if namespace is introduced by imported content (must add to svgcontent
// and update all prefixes in the imported node)
// * properly handle recalculating dimensions, recalculateDimensions() doesn't handle
// arbitrary transform lists, but makes some assumptions about how the transform list
// arbitrary transform lists, but makes some assumptions about how the transform list
// was obtained
// * import should happen in top-left of current zoomed viewport
// * import should happen in top-left of current zoomed viewport
this.importSvgString = function(xmlString) {
  try {
    // Get unique ID
    var uid = svgedit.utilities.encode64(xmlString.length + xmlString).substr(0,32);
    var useExisting = false;
    // Look for symbol and make sure symbol exists in image
@@ -6211,18 +6211,18 @@
        useExisting = true;
      }
    }
    var batchCmd = new BatchCommand("Import SVG");
    if(useExisting) {
      var symbol = import_ids[uid].symbol;
      var ts = import_ids[uid].xform;
    } else {
      // convert string into XML document
      var newDoc = svgedit.utilities.text2xml(xmlString);
      this.prepareSvg(newDoc);
      // import new svg document into our document
      var svg;
      // If DOM3 adoptNode() available, use it. Otherwise fall back to DOM2 importNode()
@@ -6232,9 +6232,9 @@
      else {
        svg = svgdoc.importNode(newDoc.documentElement, true);
      }
      uniquifyElems(svg);
      var innerw = convertToNum('width', svg.getAttribute("width")),
        innerh = convertToNum('height', svg.getAttribute("height")),
        innervb = svg.getAttribute("viewBox"),
@@ -6242,32 +6242,32 @@
        vb = innervb ? innervb.split(" ") : [0,0,innerw,innerh];
      for (var j = 0; j < 4; ++j)
        vb[j] = +(vb[j]);
      // TODO: properly handle preserveAspectRatio
      var canvasw = +svgcontent.getAttribute("width"),
        canvash = +svgcontent.getAttribute("height");
      // imported content should be 1/3 of the canvas on its largest dimension
      if (innerh > innerw) {
        var ts = "scale(" + (canvash/3)/vb[3] + ")";
      }
      else {
        var ts = "scale(" + (canvash/3)/vb[2] + ")";
      }
      // Hack to make recalculateDimensions understand how to scale
      ts = "translate(0) " + ts + " translate(0)";
      var symbol = svgdoc.createElementNS(svgns, "symbol");
      var defs = findDefs();
      if(svgedit.browser.isGecko()) {
        // Move all gradients into root for Firefox, workaround for this bug:
        // https://bugzilla.mozilla.org/show_bug.cgi?id=353575
        // TODO: Make this properly undo-able.
        $(svg).find('linearGradient, radialGradient, pattern').appendTo(defs);
      }
      while (svg.firstChild) {
        var first = svg.firstChild;
        symbol.appendChild(first);
@@ -6278,31 +6278,31 @@
        symbol.setAttribute(attr.nodeName, attr.nodeValue);
      }
      symbol.id = getNextId();
      // Store data
      import_ids[uid] = {
        symbol: symbol,
        xform: ts
      }
      findDefs().appendChild(symbol);
      batchCmd.addSubCommand(new InsertElementCommand(symbol));
    }
    var use_el = svgdoc.createElementNS(svgns, "use");
    use_el.id = getNextId();
    setHref(use_el, "#" + symbol.id);
    (current_group || getCurrentDrawing().getCurrentLayer()).appendChild(use_el);
    batchCmd.addSubCommand(new InsertElementCommand(use_el));
    clearSelection();
    use_el.setAttribute("transform", ts);
    recalculateDimensions(use_el);
    $(use_el).data('symbol', symbol).data('ref', symbol);
    addToSelection([use_el]);
    // TODO: Find way to add this in a recalculateDimensions-parsable way
//        if (vb[0] != 0 || vb[1] != 0)
//          ts = "translate(" + (-vb[0]) + "," + (-vb[1]) + ") " + ts;
@@ -6330,7 +6330,7 @@
};
// Function: createLayer
// Creates a new top-level layer in the drawing with the given name, sets the current layer
// Creates a new top-level layer in the drawing with the given name, sets the current layer
// to it, and then clears the selection  This function then calls the 'changed' handler.
// This is an undoable action.
//
@@ -6366,7 +6366,7 @@
    if(ch.localName == 'title') continue;
    new_layer.appendChild(copyElem(ch));
  }
  clearSelection();
  identifyLayers();
@@ -6377,7 +6377,7 @@
};
// Function: deleteCurrentLayer
// Deletes the current layer from the drawing and then clears the selection. This function
// Deletes the current layer from the drawing and then clears the selection. This function
// then calls the 'changed' handler.  This is an undoable action.
this.deleteCurrentLayer = function() {
  var current_layer = getCurrentDrawing().getCurrentLayer();
@@ -6414,11 +6414,11 @@
};
// Function: renameCurrentLayer
// Renames the current layer. If the layer name is not valid (i.e. unique), then this function
// Renames the current layer. If the layer name is not valid (i.e. unique), then this function
// does nothing and returns false, otherwise it returns true. This is an undo-able action.
//
//
// Parameters:
// newname - the new name you want to give the current layer.  This name must be unique
// newname - the new name you want to give the current layer.  This name must be unique
// among all layer names.
//
// Returns:
@@ -6437,14 +6437,14 @@
      }
      var oldname = drawing.getLayerName(i);
      drawing.all_layers[i][0] = svgedit.utilities.toXml(newname);
      // now change the underlying title element contents
      var len = oldLayer.childNodes.length;
      for (var i = 0; i < len; ++i) {
        var child = oldLayer.childNodes.item(i);
        // found the <title> element, now append all the
        if (child && child.tagName == "title") {
          // wipe out old name
          // wipe out old name
          while (child.firstChild) { child.removeChild(child.firstChild); }
          child.textContent = newname;
@@ -6461,14 +6461,14 @@
};
// Function: setCurrentLayerPosition
// Changes the position of the current layer to the new value. If the new index is not valid,
// Changes the position of the current layer to the new value. If the new index is not valid,
// this function does nothing and returns false, otherwise it returns true. This is an
// undo-able action.
//
// Parameters:
// newpos - The zero-based index of the new position of the layer.  This should be between
// 0 and (number of layers - 1)
//
//
// Returns:
// true if the current layer position was changed, false otherwise.
this.setCurrentLayerPosition = function(newpos) {
@@ -6479,7 +6479,7 @@
    }
    // some unknown error condition (current_layer not in all_layers)
    if (oldpos == drawing.getNumLayers()) { return false; }
    if (oldpos != newpos) {
      // if our new position is below us, we need to insert before the node after newpos
      var refLayer = null;
@@ -6495,19 +6495,19 @@
      }
      svgcontent.insertBefore(drawing.current_layer, refLayer);
      addCommandToHistory(new MoveElementCommand(drawing.current_layer, oldNextSibling, svgcontent));
      identifyLayers();
      canvas.setCurrentLayer(drawing.getLayerName(newpos));
      return true;
    }
  }
  return false;
};
// Function: setLayerVisibility
// Sets the visibility of the layer. If the layer name is not valid, this function return
// Sets the visibility of the layer. If the layer name is not valid, this function return
// false, otherwise it returns true. This is an undo-able action.
//
// Parameters:
@@ -6526,17 +6526,17 @@
  } else {
    return false;
  }
  if (layer == drawing.getCurrentLayer()) {
    clearSelection();
    pathActions.clear();
  }
//    call("changed", [selected]);
//    call("changed", [selected]);
  return true;
};
// Function: moveSelectedToLayer
// Moves the selected elements to layername. If the name is not a valid layer name, then false
// Moves the selected elements to layername. If the name is not a valid layer name, then false
// is returned.  Otherwise it returns true. This is an undo-able action.
//
// Parameters:
@@ -6555,9 +6555,9 @@
    }
  }
  if (!layer) return false;
  var batchCmd = new BatchCommand("Move Elements to Layer");
  // loop for each selected element and move it
  var selElems = selectedElements;
  var i = selElems.length;
@@ -6570,9 +6570,9 @@
    layer.appendChild(elem);
    batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldLayer));
  }
  addCommandToHistory(batchCmd);
  return true;
};
@@ -6598,19 +6598,19 @@
    prev.appendChild(ch);
    batchCmd.addSubCommand(new MoveElementCommand(ch, oldNextSibling, drawing.current_layer));
  }
  // Remove current layer
  svgcontent.removeChild(drawing.current_layer);
  if(!skipHistory) {
    clearSelection();
    identifyLayers();
    call("changed", [svgcontent]);
    addCommandToHistory(batchCmd);
  }
  drawing.current_layer = prev;
  return batchCmd;
}
@@ -6622,7 +6622,7 @@
  while($(svgcontent).children('g').length > 1) {
    batchCmd.addSubCommand(canvas.mergeLayer(true));
  }
  clearSelection();
  identifyLayers();
  call("changed", [svgcontent]);
@@ -6637,7 +6637,7 @@
  if(len) {
    for(var i = 0; i < len; i++) {
      var elem = disabled_elems[i];
      var orig = elData(elem, 'orig_opac');
      if(orig !== 1) {
        elem.setAttribute('opacity', orig);
@@ -6663,7 +6663,7 @@
  // Edit inside this group
  current_group = elem;
  // Disable other elements
  $(elem).parentsUntil('#svgcontent').andSelf().siblings().each(function() {
    var opac = this.getAttribute('opacity') || 1;
@@ -6695,7 +6695,7 @@
  // create empty first layer
  canvas.createLayer("Layer 1");
  // clear the undo stack
  canvas.undoMgr.resetUndoStack();
@@ -6729,10 +6729,10 @@
var getResolution = this.getResolution = function() {
//    var vb = svgcontent.getAttribute("viewBox").split(' ');
//    return {'w':vb[2], 'h':vb[3], 'zoom': current_zoom};
  var width = svgcontent.getAttribute("width")/current_zoom;
  var height = svgcontent.getAttribute("height")/current_zoom;
  return {
    'w': width,
    'h': height,
@@ -6789,11 +6789,11 @@
this.setGroupTitle = function(val) {
  var elem = selectedElements[0];
  elem = $(elem).data('gsvg') || elem;
  var ts = $(elem).children('title');
  var batchCmd = new BatchCommand("Set Label");
  if(!val.length) {
    // Remove title element
    var tsNextSibling = ts.nextSibling;
@@ -6829,9 +6829,9 @@
// newtitle - String with the new title
this.setDocumentTitle = function(newtitle) {
  var childs = svgcontent.childNodes, doc_title = false, old_title = '';
  var batchCmd = new BatchCommand("Change Image Title");
  for (var i=0; i<childs.length; i++) {
    if(childs[i].nodeName == 'title') {
      doc_title = childs[i];
@@ -6842,8 +6842,8 @@
  if(!doc_title) {
    doc_title = svgdoc.createElementNS(svgns, "title");
    svgcontent.insertBefore(doc_title, svgcontent.firstChild);
  }
  }
  if(newtitle.length) {
    doc_title.textContent = newtitle;
  } else {
@@ -6869,13 +6869,13 @@
// Function: setResolution
// Changes the document's dimensions to the given size
//
// Parameters:
// x - Number with the width of the new dimensions in user units.
// Parameters:
// x - Number with the width of the new dimensions in user units.
// Can also be the string "fit" to indicate "fit to content"
// y - Number with the height of the new dimensions in user units.
// y - Number with the height of the new dimensions in user units.
//
// Returns:
// Boolean to indicate if resolution change was succesful.
// Boolean to indicate if resolution change was succesful.
// It will fail on "fit to content" option with no content to fit to.
this.setResolution = function(x, y) {
  var res = getResolution();
@@ -6885,7 +6885,7 @@
  if(x == 'fit') {
    // Get bounding box
    var bbox = getStrokedBBox();
    if(bbox) {
      batchCmd = new BatchCommand("Fit Canvas to Content");
      var visEls = getVisibleElements();
@@ -6895,11 +6895,11 @@
        dx.push(bbox.x*-1);
        dy.push(bbox.y*-1);
      });
      var cmd = canvas.moveSelectedElements(dx, dy, true);
      batchCmd.addSubCommand(cmd);
      clearSelection();
      x = Math.round(bbox.width);
      y = Math.round(bbox.height);
    } else {
@@ -6913,17 +6913,17 @@
    x = convertToNum('width', x);
    y = convertToNum('height', y);
    svgcontent.setAttribute('width', x);
    svgcontent.setAttribute('height', y);
    this.contentW = x;
    this.contentH = y;
    batchCmd.addSubCommand(new ChangeElementCommand(svgcontent, {"width":w, "height":h}));
    svgcontent.setAttribute("viewBox", [0, 0, x/current_zoom, y/current_zoom].join(' '));
    batchCmd.addSubCommand(new ChangeElementCommand(svgcontent, {"viewBox": ["0 0", w, h].join(' ')}));
    addCommandToHistory(batchCmd);
    background = document.getElementById("canvas_background");
    if (background) {
@@ -6946,9 +6946,9 @@
// Function: setBBoxZoom
// Sets the zoom level on the canvas-side based on the given value
//
//
// Parameters:
// val - Bounding box object to zoom to or string indicating zoom option
// val - Bounding box object to zoom to or string indicating zoom option
// editor_w - Integer with the editor's workarea box's width
// editor_h - Integer with the editor's workarea box's height
this.setBBoxZoom = function(val, editor_w, editor_h) {
@@ -6957,12 +6957,12 @@
  var calcZoom = function(bb) {
    if(!bb) return false;
    var w_zoom = Math.round((editor_w / bb.width)*100 * spacer)/100;
    var h_zoom = Math.round((editor_h / bb.height)*100 * spacer)/100;
    var h_zoom = Math.round((editor_h / bb.height)*100 * spacer)/100;
    var zoomlevel = Math.min(w_zoom,h_zoom);
    canvas.setZoom(zoomlevel);
    return {'zoom': zoomlevel, 'bbox': bb};
  }
  if(typeof val == 'object') {
    bb = val;
    if(bb.width == 0 || bb.height == 0) {
@@ -7025,7 +7025,7 @@
// Parameters:
// name - String with the new mode to change to
this.setMode = function(name) {
  pathActions.clear();
  textActions.clear();
  $("#workarea").attr("class", name);
@@ -7043,7 +7043,7 @@
// Function: setColor
// Change the current stroke/fill color/gradient value
//
//
// Parameters:
// type - String indicating fill or stroke
// val - The value to set the stroke attribute to
@@ -7073,7 +7073,7 @@
    if (!preventUndo) {
      changeSelectedAttribute(type, val, elems);
      call("changed", elems);
    } else
    } else
      changeSelectedAttributeNoUndo(type, val, elems);
  }
}
@@ -7147,22 +7147,22 @@
      if (grad.getAttribute('x1') != og.getAttribute('x1') ||
        grad.getAttribute('y1') != og.getAttribute('y1') ||
        grad.getAttribute('x2') != og.getAttribute('x2') ||
        grad.getAttribute('y2') != og.getAttribute('y2'))
        grad.getAttribute('y2') != og.getAttribute('y2'))
      {
        continue;
      }
    } else {
      var grad_attrs = $(grad).attr(rad_attrs);
      var og_attrs = $(og).attr(rad_attrs);
      var diff = false;
      $.each(rad_attrs, function(i, attr) {
        if(grad_attrs[attr] != og_attrs[attr]) diff = true;
      });
      if(diff) continue;
    }
    // else could be a duplicate, iterate through stops
    var stops = grad.getElementsByTagNameNS(svgns, "stop");
    var ostops = og.getElementsByTagNameNS(svgns, "stop");
@@ -7178,7 +7178,7 @@
      if (stop.getAttribute('offset') != ostop.getAttribute('offset') ||
        stop.getAttribute('stop-opacity') != ostop.getAttribute('stop-opacity') ||
        stop.getAttribute('stop-color') != ostop.getAttribute('stop-color'))
        stop.getAttribute('stop-color') != ostop.getAttribute('stop-color'))
      {
        break;
      }
@@ -7204,28 +7204,28 @@
        var y1 = grad.getAttribute('y1') || 0;
        var x2 = grad.getAttribute('x2') || 1;
        var y2 = grad.getAttribute('y2') || 0;
        // Convert to USOU points
        x1 = (bb.width * x1) + bb.x;
        y1 = (bb.height * y1) + bb.y;
        x2 = (bb.width * x2) + bb.x;
        y2 = (bb.height * y2) + bb.y;
        // Transform those points
        var pt1 = transformPoint(x1, y1, m);
        var pt2 = transformPoint(x2, y2, m);
        // Convert back to BB points
        var g_coords = {};
        g_coords.x1 = (pt1.x - bb.x) / bb.width;
        g_coords.y1 = (pt1.y - bb.y) / bb.height;
        g_coords.x2 = (pt2.x - bb.x) / bb.width;
        g_coords.y2 = (pt2.y - bb.y) / bb.height;
        var newgrad = grad.cloneNode(true);
        $(newgrad).attr(g_coords);
        newgrad.id = getNextId();
        findDefs().appendChild(newgrad);
        elem.setAttribute(type, 'url(#' + newgrad.id + ')');
@@ -7237,7 +7237,7 @@
// Function: setPaint
// Set a color/gradient to a fill/stroke
//
// Parameters:
// Parameters:
// type - String with "fill" or "stroke"
// paint - The jGraduate paint object to apply
this.setPaint = function(type, paint) {
@@ -7248,13 +7248,13 @@
  cur_properties[type + '_paint'] = p;
  switch ( p.type ) {
    case "solidColor":
      if (p.solidColor != "none" && p.solidColor != "#none") {
        this.setColor(type, "#"+p.solidColor)
      }
      else {
        this.setColor(type, "none");
        var selector = (type == "fill") ? "#fill_color rect" : "#stroke_color rect"
        var selector = (type == "fill") ? "#fill_color rect" : "#stroke_color rect"
        document.querySelector(selector).setAttribute('fill', 'none');
      }
      break;
@@ -7273,7 +7273,7 @@
//  // make a copy
//  var p = new $.jGraduate.Paint(p);
//  this.setStrokeOpacity(p.alpha/100);
//
//
//  // now set the current paint object
//  cur_properties.stroke_paint = p;
//  switch ( p.type ) {
@@ -7283,17 +7283,17 @@
//    case "linearGradient"
//    case "radialGradient"
//      canvas.strokeGrad = p[p.type];
//      setGradient(type);
//      setGradient(type);
//    default:
// //     console.log("none!");
//  }
// };
//
//
// this.setFillPaint = function(p, addGrad) {
//  // make a copy
//  var p = new $.jGraduate.Paint(p);
//  this.setFillOpacity(p.alpha/100, true);
//
//
//  // now set the current paint object
//  cur_properties.fill_paint = p;
//  if (p.type == "solidColor") {
@@ -7301,11 +7301,11 @@
//  }
//  else if(p.type == "linearGradient") {
//    canvas.fillGrad = p.linearGradient;
//    if(addGrad) setGradient();
//    if(addGrad) setGradient();
//  }
//  else if(p.type == "radialGradient") {
//    canvas.fillGrad = p.radialGradient;
//    if(addGrad) setGradient();
//    if(addGrad) setGradient();
//  }
//  else {
// //     console.log("none!");
@@ -7330,7 +7330,7 @@
    return;
  }
  cur_properties.stroke_width = val;
  var elems = [];
  var i = selectedElements.length;
  while (i--) {
@@ -7338,10 +7338,10 @@
    if (elem) {
      if (elem.tagName == "g")
        svgedit.utilities.walkTree(elem, function(e){if(e.nodeName!="g") elems.push(e);});
      else
      else
        elems.push(elem);
    }
  }
  }
  if (elems.length > 0) {
    changeSelectedAttribute("stroke-width", val, elems);
    call("changed", selectedElements);
@@ -7363,10 +7363,10 @@
    if (elem) {
      if (elem.tagName == "g")
        svgedit.utilities.walkTree(elem, function(e){if(e.nodeName!="g") elems.push(e);});
      else
      else
        elems.push(elem);
    }
  }
  }
  if (elems.length > 0) {
    changeSelectedAttribute(attr, val, elems);
    call("changed", selectedElements);
@@ -7427,7 +7427,7 @@
this.getBlur = function(elem) {
  var val = 0;
//    var elem = selectedElements[0];
  if(elem) {
    var filter_url = elem.getAttribute('filter');
    if(filter_url) {
@@ -7444,7 +7444,7 @@
  var cur_command = null;
  var filter = null;
  var filterHidden = false;
  // Function: setBlurNoUndo
  // Sets the stdDeviation blur value on the selected element without being undoable
  //
@@ -7473,12 +7473,12 @@
      canvas.setBlurOffsets(filter, val);
    }
  }
  function finishChange() {
    var bCmd = canvas.undoMgr.finishUndoableChange();
    cur_command.addSubCommand(bCmd);
    addCommandToHistory(cur_command);
    cur_command = null;
    cur_command = null;
    filter = null;
  }
@@ -7509,7 +7509,7 @@
    }
  }
  // Function: setBlur
  // Function: setBlur
  // Adds/updates the blur filter to the selected element
  //
  // Parameters:
@@ -7520,16 +7520,16 @@
      finishChange();
      return;
    }
    // Looks for associated blur, creates one if not found
    var elem = selectedElements[0];
    var elem_id = elem.id;
    filter = getElem(elem_id + '_blur');
    val -= 0;
    var batchCmd = new BatchCommand();
    // Blur found!
    if(filter) {
      if(val === 0) {
@@ -7543,33 +7543,33 @@
          "stdDeviation": val
        }
      });
      filter = addSvgElementFromJson({ "element": "filter",
        "attr": {
          "id": elem_id + '_blur'
        }
      });
      filter.appendChild(newblur);
      findDefs().appendChild(filter);
      batchCmd.addSubCommand(new InsertElementCommand(filter));
    }
    var changes = {filter: elem.getAttribute('filter')};
    if(val === 0) {
      elem.removeAttribute("filter");
      batchCmd.addSubCommand(new ChangeElementCommand(elem, changes));
      return;
    } else {
      changeSelectedAttribute("filter", 'url(#' + elem_id + '_blur)');
      batchCmd.addSubCommand(new ChangeElementCommand(elem, changes));
      canvas.setBlurOffsets(filter, val);
    }
    cur_command = batchCmd;
    canvas.undoMgr.beginUndoableChange("stdDeviation", [filter?filter.firstChild:null]);
    if(complete) {
@@ -7710,18 +7710,18 @@
// Function: setImageURL
// Sets the new image URL for the selected image element. Updates its size if
// a new URL is given
//
//
// Parameters:
// val - String with the image URL/path
this.setImageURL = function(val) {
  var elem = selectedElements[0];
  if(!elem) return;
  var attrs = $(elem).attr(['width', 'height']);
  var setsize = (!attrs.width || !attrs.height);
  var cur_href = getHref(elem);
  // Do nothing if no URL change or size change
  if(cur_href !== val) {
    setsize = true;
@@ -7737,14 +7737,14 @@
  if(setsize) {
    $(new Image()).load(function() {
      var changes = $(elem).attr(['width', 'height']);
      $(elem).attr({
        width: this.width,
        height: this.height
      });
      selectorManager.requestSelector(elem).resize();
      batchCmd.addSubCommand(new ChangeElementCommand(elem, changes));
      addCommandToHistory(batchCmd);
      call("changed", [elem]);
@@ -7756,7 +7756,7 @@
// Function: setLinkURL
// Sets the new link URL for the selected anchor element.
//
//
// Parameters:
// val - String with the link URL/path
this.setLinkURL = function(val) {
@@ -7771,11 +7771,11 @@
      return;
    }
  }
  var cur_href = getHref(elem);
  if(cur_href === val) return;
  var batchCmd = new BatchCommand("Change Link URL");
  setHref(elem, val);
@@ -7789,14 +7789,14 @@
// Function elementAreSame
// Checks if all the selected Elements are the same type
//
//
// Parameters:
// None
this.elementsAreSame = function(elements) {
  if (!elements.length || elements[0] == null) return null
  else {
    var isSameElement = function(el) {
    var isSameElement = function(el) {
      if (el && selectedElements[0])
        return (el.nodeName == selectedElements[0].nodeName);
      else return null;
@@ -7808,7 +7808,7 @@
// Function: setRectRadius
// Sets the rx & ry values to the selected rect element to change its corner radius
//
//
// Parameters:
// val - The new radius
this.setRectRadius = function(val) {
@@ -7830,7 +7830,7 @@
// Wraps the selected element(s) in an anchor element or converts group to one
this.makeHyperlink = function(url) {
  canvas.groupSelectedElements('a', url);
  // TODO: If element is a single "g", convert to "a"
  //  if(selectedElements.length > 1 && selectedElements[1]) {
@@ -7844,7 +7844,7 @@
// Group: Element manipulation
// Function: setSegType
// Sets the new segment type to the selected segment(s).
// Sets the new segment type to the selected segment(s).
//
// Parameters:
// new_type - Integer with the new segment type
@@ -7857,7 +7857,7 @@
// Function: convertToPath
// Convert selected element to a path, or get the BBox of an element-as-path
//
// Parameters:
// Parameters:
// elem - The DOM element to be converted
// getBBox - Boolean on whether or not to only return the path's BBox
//
@@ -7872,11 +7872,11 @@
    });
    return;
  }
  if(!getBBox) {
    var batchCmd = new BatchCommand("Convert element to Path");
  }
  var attrs = getBBox?{}:{
    "fill": cur_shape.fill,
    "fill-opacity": cur_shape.fill_opacity,
@@ -7889,7 +7889,7 @@
    "opacity": cur_shape.opacity,
    "visibility":"hidden"
  };
  // any attribute on the element not covered by the above
  // TODO: make this list global so that we can properly maintain it
  // TODO: what about @transform, @clip-rule, @fill-rule, etc?
@@ -7898,17 +7898,17 @@
      attrs[this] = elem.getAttribute(this);
    }
  });
  var path = addSvgElementFromJson({
    "element": "path",
    "attr": attrs
  });
  var eltrans = elem.getAttribute("transform");
  if(eltrans) {
    path.setAttribute("transform",eltrans);
  }
  var id = elem.id;
  var parent = elem.parentNode;
  if(elem.nextSibling) {
@@ -7916,9 +7916,9 @@
  } else {
    parent.appendChild(path);
  }
  var d = '';
  var joinSegs = function(segs) {
    $.each(segs, function(j, seg) {
      var l = seg[0], pts = seg[1];
@@ -7940,7 +7940,7 @@
    if(elem.tagName == 'circle') {
      rx = ry = $(elem).attr('r');
    }
    joinSegs([
      ['M',[(cx-rx),(cy)]],
      ['C',[(cx-rx),(cy-ry/num), (cx-rx/num),(cy-ry), (cx),(cy-ry)]],
@@ -7997,14 +7997,14 @@
    path.parentNode.removeChild(path);
    break;
  }
  if(d) {
    path.setAttribute('d',d);
  }
  if(!getBBox) {
    // Replace the current element with the converted one
    // Reorient if it has a matrix
    if(eltrans) {
      var tlist = getTransformList(path);
@@ -8012,7 +8012,7 @@
        pathActions.resetOrientation(path);
      }
    }
    var nextSibling = elem.nextSibling;
    batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent));
    batchCmd.addSubCommand(new InsertElementCommand(path));
@@ -8022,9 +8022,9 @@
    path.setAttribute('id', id);
    path.removeAttribute("visibility");
    addToSelection([path], true);
    addCommandToHistory(batchCmd);
  } else {
    // Get the correct BBox of the new path, then discard it
    pathActions.resetOrientation(path);
@@ -8042,8 +8042,8 @@
// Function: changeSelectedAttributeNoUndo
// This function makes the changes to the elements. It does not add the change
// to the history stack.
//
// to the history stack.
//
// Parameters:
// attr - String with the attribute name
// newValue - String or number with the new attribute value
@@ -8099,7 +8099,7 @@
          },0);
        }
        // if this element was rotated, and we changed the position of this element
        // we need to update the rotational transform attribute
        // we need to update the rotational transform attribute
        var angle = getRotationAngle(elem);
        if (angle != 0 && attr != "transform") {
          var tlist = getTransformList(elem);
@@ -8130,7 +8130,7 @@
// If you want to change all selectedElements, ignore the elems argument.
// If you want to change only a subset of selectedElements, then send the
// subset to this function in the elems argument.
//
//
// Parameters:
// attr - String with the attribute name
// newValue - String or number with the new attribute value
@@ -8143,13 +8143,13 @@
  changeSelectedAttributeNoUndo(attr, val, elems);
  var batchCmd = canvas.undoMgr.finishUndoableChange();
  if (!batchCmd.isEmpty()) {
  if (!batchCmd.isEmpty()) {
    addCommandToHistory(batchCmd);
  }
};
// Function: deleteSelectedElements
// Removes all selected elements from the DOM and adds the change to the
// Removes all selected elements from the DOM and adds the change to the
// history stack
this.deleteSelectedElements = function() {
  var batchCmd = new BatchCommand("Delete Elements");
@@ -8161,19 +8161,19 @@
    var parent = selected.parentNode;
    var t = selected;
    // this will unselect the element and remove the selectedOutline
    selectorManager.releaseSelector(t);
    // Remove the path if present.
    svgedit.path.removePath_(t.id);
    // Get the parent if it's a single-child anchor
    if(parent.tagName === 'a' && parent.childNodes.length === 1) {
      t = parent;
      parent = parent.parentNode;
    }
    var nextSibling = t.nextSibling;
    var elem = parent.removeChild(t);
    selectedCopy.push(selected); //for the copy
@@ -8186,7 +8186,7 @@
};
// Function: cutSelectedElements
// Removes all selected elements from the DOM and adds the change to the
// Removes all selected elements from the DOM and adds the change to the
// history stack. Remembers removed elements on the clipboard
// TODO: Combine similar code with deleteSelectedElements
@@ -8216,7 +8216,7 @@
  if (!batchCmd.isEmpty()) addCommandToHistory(batchCmd);
  call("changed", selectedCopy);
  clearSelection();
  canvas.clipBoard = selectedCopy;
};
@@ -8230,10 +8230,10 @@
  var cb = canvas.clipBoard;
  var len = cb.length;
  if(!len) return;
  var pasted = [];
  var batchCmd = new BatchCommand('Paste elements');
  // Move elements to lastClickPoint
  while (len--) {
@@ -8249,9 +8249,9 @@
  }
  svgCanvas.clearSelection();
  setTimeout(function(){selectOnly(pasted)},100);
  addCommandToHistory(batchCmd);
  call("changed", pasted);
}
@@ -8259,12 +8259,12 @@
// Function: groupSelectedElements
// Wraps all the selected elements in a group (g) element
// Parameters:
// Parameters:
// type - type of element to group into, defaults to <g>
this.groupSelectedElements = function(type) {
  if(!type) type = 'g';
  var cmd_str = '';
  switch ( type ) {
    case "a":
      cmd_str = "Make hyperlink";
@@ -8278,9 +8278,9 @@
      cmd_str = "Group Elements";
      break;
  }
  var batchCmd = new BatchCommand(cmd_str);
  // create and insert the group element
  var g = addSvgElementFromJson({
              "element": type,
@@ -8292,24 +8292,24 @@
    setHref(g, url);
  }
  batchCmd.addSubCommand(new InsertElementCommand(g));
  // now move all children into the group
  var i = selectedElements.length;
  while (i--) {
    var elem = selectedElements[i];
    if (elem == null) continue;
    if (elem.parentNode.tagName === 'a' && elem.parentNode.childNodes.length === 1) {
      elem = elem.parentNode;
    }
    var oldNextSibling = elem.nextSibling;
    var oldParent = elem.parentNode;
    g.appendChild(elem);
    batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldParent));
    batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldParent));
  }
  if (!batchCmd.isEmpty()) addCommandToHistory(batchCmd);
  // update selection
  selectOnly([g], true);
};
@@ -8326,27 +8326,27 @@
  var glist = getTransformList(g);
  var m = transformListToTransform(glist).matrix;
  var batchCmd = new BatchCommand("Push group properties");
  // TODO: get all fill/stroke properties from the group that we are about to destroy
  // "fill", "fill-opacity", "fill-rule", "stroke", "stroke-dasharray", "stroke-dashoffset",
  // "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity",
  // "fill", "fill-opacity", "fill-rule", "stroke", "stroke-dasharray", "stroke-dashoffset",
  // "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity",
  // "stroke-width"
  // and then for each child, if they do not have the attribute (or the value is 'inherit')
  // then set the child's attribute
  var i = 0;
  var gangle = getRotationAngle(g);
  var gattrs = $(g).attr(['filter', 'opacity']);
  var gfilter, gblur;
  for(var i = 0; i < len; i++) {
    var elem = children[i];
    if(elem.nodeType !== 1) continue;
    if(gattrs.opacity !== null && gattrs.opacity !== 1) {
      var c_opac = elem.getAttribute('opacity') || 1;
      var new_opac = Math.round((elem.getAttribute('opacity') || 1) * gattrs.opacity * 100)/100;
@@ -8363,7 +8363,7 @@
      } else if(cblur === 0) {
        cblur = gblur;
      }
      // If child has no current filter, get group's filter or clone it.
      if(!orig_cblur) {
        // Set group's filter to use first child's ID
@@ -8379,28 +8379,28 @@
      }
      // Change this in future for different filters
      var suffix = (gfilter.firstChild.tagName === 'feGaussianBlur')?'blur':'filter';
      var suffix = (gfilter.firstChild.tagName === 'feGaussianBlur')?'blur':'filter';
      gfilter.id = elem.id + '_' + suffix;
      changeSelectedAttribute('filter', 'url(#' + gfilter.id + ')', [elem]);
      // Update blur value
      // Update blur value
      if(cblur) {
        changeSelectedAttribute('stdDeviation', cblur, [gfilter.firstChild]);
        canvas.setBlurOffsets(gfilter, cblur);
      }
    }
    var chtlist = getTransformList(elem);
    // Don't process gradient transforms
    if(~elem.tagName.indexOf('Gradient')) chtlist = null;
    // Hopefully not a problem to add this. Necessary for elements like <desc/>
    if(!chtlist) continue;
    // Apparently <defs> can get get a transformlist, but we don't want it to have one!
    if(elem.tagName === 'defs') continue;
    if (glist.numberOfItems) {
      // TODO: if the group's transform is just a rotate, we can always transfer the
      // rotate() down to the children (collapsing consecutive rotates and factoring
@@ -8408,43 +8408,43 @@
      if (gangle && glist.numberOfItems == 1) {
        // [Rg] [Rc] [Mc]
        // we want [Tr] [Rc2] [Mc] where:
        //  - [Rc2] is at the child's current center but has the
        //  - [Rc2] is at the child's current center but has the
        //    sum of the group and child's rotation angles
        //  - [Tr] is the equivalent translation that this child
        //  - [Tr] is the equivalent translation that this child
        //    undergoes if the group wasn't there
        // [Tr] = [Rg] [Rc] [Rc2_inv]
        // get group's rotation matrix (Rg)
        var rgm = glist.getItem(0).matrix;
        // get child's rotation matrix (Rc)
        var rcm = svgroot.createSVGMatrix();
        var cangle = getRotationAngle(elem);
        if (cangle) {
          rcm = chtlist.getItem(0).matrix;
        }
        // get child's old center of rotation
        var cbox = svgedit.utilities.getBBox(elem);
        var ceqm = transformListToTransform(chtlist).matrix;
        var coldc = transformPoint(cbox.x+cbox.width/2, cbox.y+cbox.height/2,ceqm);
        // sum group and child's angles
        var sangle = gangle + cangle;
        // get child's rotation at the old center (Rc2_inv)
        var r2 = svgroot.createSVGTransform();
        r2.setRotate(sangle, coldc.x, coldc.y);
        // calculate equivalent translate
        var trm = matrixMultiply(rgm, rcm, r2.matrix.inverse());
        // set up tlist
        if (cangle) {
          chtlist.removeItem(0);
        }
        if (sangle) {
          if(chtlist.numberOfItems) {
            chtlist.insertItemBefore(r2, 0);
@@ -8464,9 +8464,9 @@
        }
      }
      else { // more complicated than just a rotate
        // transfer the group's transform down to each child and then
        // call recalculateDimensions()
        // call recalculateDimensions()
        var oldxform = elem.getAttribute("transform");
        var changes = {};
        changes["transform"] = oldxform ? oldxform : "";
@@ -8486,16 +8486,16 @@
    }
  }
  // remove transform and make it undo-able
  if (xform) {
    var changes = {};
    changes["transform"] = xform;
    g.setAttribute("transform", "");
    g.removeAttribute("transform");
    g.removeAttribute("transform");
    batchCmd.addSubCommand(new ChangeElementCommand(g, changes));
  }
  if (undoable && !batchCmd.isEmpty()) {
    return batchCmd;
  }
@@ -8523,25 +8523,25 @@
  if(parents_a.length) {
    g = parents_a[0];
  }
  // Look for parent "a"
  if (g.tagName === "g" || g.tagName === "a") {
    var batchCmd = new BatchCommand("Ungroup Elements");
    var cmd = pushGroupProperties(g, true);
    if(cmd) batchCmd.addSubCommand(cmd);
    var parent = g.parentNode;
    var anchor = g.nextSibling;
    var children = new Array(g.childNodes.length);
    var i = 0;
    while (g.firstChild) {
      var elem = g.firstChild;
      var oldNextSibling = elem.nextSibling;
      var oldParent = elem.parentNode;
      // Remove child title elements
      if(elem.tagName === 'title') {
        var nextSibling = elem.nextSibling;
@@ -8549,21 +8549,21 @@
        oldParent.removeChild(elem);
        continue;
      }
      children[i++] = elem = parent.insertBefore(elem, anchor);
      batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldParent));
    }
    // remove the group from the selection
    // remove the group from the selection
    clearSelection();
    // delete the group element (but make undo-able)
    var gNextSibling = g.nextSibling;
    g = parent.removeChild(g);
    batchCmd.addSubCommand(new RemoveElementCommand(g, gNextSibling, parent));
    if (!batchCmd.isEmpty()) addCommandToHistory(batchCmd);
    // update selection
    addToSelection(children);
  }
@@ -8591,7 +8591,7 @@
};
// Function: moveToBottomSelectedElement
// Repositions the selected element to the top in the DOM to appear under
// Repositions the selected element to the top in the DOM to appear under
// other elements
this.moveToBottomSelectedElement = function() {
  var selected = selectedElements.filter(Boolean).reverse();
@@ -8624,7 +8624,7 @@
// Moves the select element up or down the stack, based on the visibly
// intersecting elements
//
// Parameters:
// Parameters:
// dir - String that's either 'Up' or 'Down'
this.moveUpDownSelected = function(dir) {
  var selected = selectedElements.filter(Boolean);
@@ -8648,7 +8648,7 @@
      return false;
    });
    if(!closest) return;
    var t = selected;
    var oldParent = t.parentNode;
    var oldNextSibling = t.nextSibling;
@@ -8664,7 +8664,7 @@
};
// Function: moveSelectedElements
// Moves selected elements on the X/Y axis
// Moves selected elements on the X/Y axis
//
// Parameters:
// dx - Float with the distance to move on the x-axis
@@ -8688,14 +8688,14 @@
    if (selected != null) {
//      if (i==0)
//        selectedBBoxes[0] = svgedit.utilities.getBBox(selected);
//      var b = {};
//      for(var j in selectedBBoxes[i]) b[j] = selectedBBoxes[i][j];
//      selectedBBoxes[i] = b;
      var xform = svgroot.createSVGTransform();
      var tlist = getTransformList(selected);
      // dx and dy could be arrays
      if (dx.constructor == Array) {
//        if (i==0) {
@@ -8716,12 +8716,12 @@
      } else {
        tlist.appendItem(xform);
      }
      var cmd = recalculateDimensions(selected);
      if (cmd) {
        batchCmd.addSubCommand(cmd);
      }
      selectorManager.requestSelector(selected).resize();
    }
  }
@@ -8734,7 +8734,7 @@
};
// Function: cloneSelectedElements
// Create deep DOM copies (clones) of all selected elements and move them slightly
// Create deep DOM copies (clones) of all selected elements and move them slightly
// from their originals
this.cloneSelectedElements = function(x,y, drag) {
  var batchCmd = new BatchCommand("Clone Elements");
@@ -8753,7 +8753,7 @@
  clones = []
  while (i--) {
    // clone each element and replace it within copiedElements
    var elem = copiedElements[i]
    var elem = copiedElements[i]
    var clone = copyElem(copiedElements[i]);
    var parent = (current_group || getCurrentDrawing().getCurrentLayer())
    if (drag) {
@@ -8769,7 +8769,7 @@
    clones.push(clone)
    batchCmd.addSubCommand(new InsertElementCommand(clone));
  }
  if (!batchCmd.isEmpty()) {
    addToSelection(copiedElements.reverse()); // Need to reverse for correct selection-adding
    if (!drag) this.moveSelectedElements(x,y,false);
@@ -8783,7 +8783,7 @@
//
// Parameters:
// type - String with single character indicating the alignment type
// relative_to - String that must be one of the following:
// relative_to - String that must be one of the following:
// "selected", "largest", "smallest", "page"
this.alignSelectedElements = function(type, relative_to) {
  var bboxes = [], angles = [];
@@ -8795,7 +8795,7 @@
    if (selectedElements[i] == null) break;
    var elem = selectedElements[i];
    bboxes[i] = getStrokedBBox([elem]);
    // now bbox is axis-aligned and handles rotation
    switch (relative_to) {
      case 'smallest':
@@ -8874,13 +8874,13 @@
this.contentH = getResolution().h;
// Function: updateCanvas
// Updates the editor canvas width/height/position after a zoom has occurred
// Updates the editor canvas width/height/position after a zoom has occurred
//
// Parameters:
// w - Float with the new width
// h - Float with the new height
//
// Returns:
// Returns:
// Object with the following values:
// * x - The canvas' new x coordinate
// * y - The canvas' new y coordinate
@@ -8904,7 +8904,7 @@
    'y': y,
    "viewBox" : "0 0 " + this.contentW + " " + this.contentH
  });
  assignAttributes(bg, {
    width: svgcontent.getAttribute('width'),
    height: svgcontent.getAttribute('height'),
@@ -8919,9 +8919,9 @@
      'height': '100%'
    });
  }
  selectorManager.selectorParentGroup.setAttribute("transform","translate(" + x + "," + y + ")");
  return {x:x, y:y, old_x:old_x, old_y:old_y, d_x:x - old_x, d_y:y - old_y};
}
@@ -8976,12 +8976,12 @@
          num = 0;
        } else if(num < 0) {
          num = all_elems.length-1;
        }
        }
        elem = all_elems[num];
        break;
      }
      }
    }
  }
  }
  selectOnly([elem], true);
  call("selected", selectedElements);
}
@@ -8989,7 +8989,7 @@
this.clear();
// DEPRECATED: getPrivateMethods
// DEPRECATED: getPrivateMethods
// Since all methods are/should be public somehow, this function should be removed
// Being able to access private methods publicly seems wrong somehow,