Merge pull request #103 from gec/getBBox-performance

GetBBox performance improvements
This commit is contained in:
Jeff Schiller
2016-04-28 16:21:12 -07:00
8 changed files with 1597 additions and 325 deletions

View File

@@ -33,7 +33,8 @@ var gripRadius = svgedit.browser.isTouch() ? 10 : 4;
// Parameters:
// id - integer to internally indentify the selector
// elem - DOM element associated with this selector
svgedit.select.Selector = function(id, elem) {
// bbox - Optional bbox to use for initialization (prevents duplicate getBBox call).
svgedit.select.Selector = function(id, elem, bbox) {
// this is the selector's unique number
this.id = id;
@@ -77,7 +78,7 @@ svgedit.select.Selector = function(id, elem) {
'w' : null
};
this.reset(this.selectedElement);
this.reset(this.selectedElement, bbox);
};
@@ -86,10 +87,11 @@ svgedit.select.Selector = function(id, elem) {
//
// Parameters:
// e - DOM element associated with this selector
svgedit.select.Selector.prototype.reset = function(e) {
// bbox - Optional bbox to use for reset (prevents duplicate getBBox call).
svgedit.select.Selector.prototype.reset = function(e, bbox) {
this.locked = true;
this.selectedElement = e;
this.resize();
this.resize(bbox);
this.selectorGroup.setAttribute('display', 'inline');
};
@@ -136,7 +138,8 @@ svgedit.select.Selector.prototype.showGrips = function(show) {
// Function: svgedit.select.Selector.resize
// Updates the selector to match the element's size
svgedit.select.Selector.prototype.resize = function() {
// bbox - Optional bbox to use for resize (prevents duplicate getBBox call).
svgedit.select.Selector.prototype.resize = function(bbox) {
var selectedBox = this.selectorRect,
mgr = selectorManager_,
selectedGrips = mgr.selectorGrips,
@@ -162,7 +165,11 @@ svgedit.select.Selector.prototype.resize = function() {
m.e *= current_zoom;
m.f *= current_zoom;
var bbox = svgedit.utilities.getBBox(selected);
if (!bbox) {
bbox = svgedit.utilities.getBBox(selected);
}
// TODO: svgedit.utilities.getBBox (previous line) already knows to call getStrokedBBox when tagName === 'g'. Remove this?
// TODO: svgedit.utilities.getBBox doesn't exclude 'gsvg' and calls getStrokedBBox for any 'g'. Should getBBox be updated?
if (tagName === 'g' && !$.data(selected, 'gsvg')) {
// The bbox for a group does not include stroke vals, so we
// get the bbox based on its children.
@@ -413,7 +420,8 @@ svgedit.select.SelectorManager.prototype.initGroup = function() {
//
// Parameters:
// elem - DOM element to get the selector for
svgedit.select.SelectorManager.prototype.requestSelector = function(elem) {
// bbox - Optional bbox to use for reset (prevents duplicate getBBox call).
svgedit.select.SelectorManager.prototype.requestSelector = function(elem, bbox) {
if (elem == null) {return null;}
var i,
N = this.selectors.length;
@@ -425,13 +433,13 @@ svgedit.select.SelectorManager.prototype.requestSelector = function(elem) {
for (i = 0; i < N; ++i) {
if (this.selectors[i] && !this.selectors[i].locked) {
this.selectors[i].locked = true;
this.selectors[i].reset(elem);
this.selectors[i].reset(elem, bbox);
this.selectorMap[elem.id] = this.selectors[i];
return this.selectors[i];
}
}
// if we reached here, no available selectors were found, we create one
this.selectors[N] = new svgedit.select.Selector(N, elem);
this.selectors[N] = new svgedit.select.Selector(N, elem, bbox);
this.selectorParentGroup.appendChild(this.selectors[N].selectorGroup);
this.selectorMap[elem.id] = this.selectors[N];
return this.selectors[N];

View File

@@ -601,140 +601,9 @@ var getIntersectionList = this.getIntersectionList = function(rect) {
//
// Returns:
// A single bounding box object
getStrokedBBox = this.getStrokedBBox = function(elems) {
var 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) {
// 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);
if (!bb) {
return null;
}
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) {
bb = good_bb = canvas.convertToPath(elem, true);
} else if (elem.tagName == 'rect') {
// Look for radius
var rx = elem.getAttribute('rx');
var ry = elem.getAttribute('ry');
if (rx || ry) {
bb = good_bb = canvas.convertToPath(elem, true);
}
}
if (!good_bb) {
// Must use clone else FF freaks out
var clone = elem.cloneNode(true);
var g = document.createElementNS(NS.SVG, 'g');
var parent = elem.parentNode;
parent.appendChild(g);
g.appendChild(clone);
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,
// 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],
// [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] ];
// var j = 4;
// while (j--) {
// var x = pts[j][0],
// y = pts[j][1],
// r = Math.sqrt( x*x + y*y );
// 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;
};
var full_bb;
$.each(elems, function() {
if (full_bb) {return;}
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');
var offset = 0;
if (elem.getAttribute('stroke') != 'none' && !isNaN(sw)) {
offset += sw/2;
}
return offset;
};
var bboxes = [];
$.each(elems, function(i, elem) {
var cur_bb = getCheckedBBox(elem);
if (cur_bb) {
var offset = getOffset(elem);
min_x = Math.min(min_x, cur_bb.x - offset);
min_y = Math.min(min_y, cur_bb.y - offset);
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
if (cur_bb && elem.nodeType == 1) {
var offset = getOffset(elem);
max_x = Math.max(max_x, cur_bb.x + cur_bb.width + offset);
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;
return svgedit.utilities.getStrokedBBox(elems, addSvgElementFromJson, pathActions)
};
// Function: getVisibleElements
@@ -1071,7 +940,9 @@ var addToSelection = this.addToSelection = function(elemsToAdd, showGrips) {
var i = elemsToAdd.length;
while (i--) {
var elem = elemsToAdd[i];
if (!elem || !svgedit.utilities.getBBox(elem)) {continue;}
if (!elem) {continue;}
var bbox = svgedit.utilities.getBBox(elem);
if (!bbox) {continue;}
if (elem.tagName === 'a' && elem.childNodes.length === 1) {
// Make "a" element's child be the selected element
@@ -1086,7 +957,7 @@ var addToSelection = this.addToSelection = function(elemsToAdd, showGrips) {
// only the first selectedBBoxes element is ever used in the codebase these days
// if (j == 0) selectedBBoxes[0] = svgedit.utilities.getBBox(elem);
j++;
var sel = selectorManager.requestSelector(elem);
var sel = selectorManager.requestSelector(elem, bbox);
if (selectedElements.length > 1) {
sel.showGrips(false);
@@ -6576,179 +6447,29 @@ this.setSegType = function(new_type) {
this.convertToPath = function(elem, getBBox) {
if (elem == null) {
var elems = selectedElements;
$.each(selectedElements, function(i, elem) {
$.each(elems, function(i, elem) {
if (elem) {canvas.convertToPath(elem);}
});
return;
}
if (!getBBox) {
var batchCmd = new svgedit.history.BatchCommand('Convert element to Path');
}
var attrs = getBBox?{}:{
'fill': cur_shape.fill,
'fill-opacity': cur_shape.fill_opacity,
'stroke': cur_shape.stroke,
'stroke-width': cur_shape.stroke_width,
'stroke-dasharray': cur_shape.stroke_dasharray,
'stroke-linejoin': cur_shape.stroke_linejoin,
'stroke-linecap': cur_shape.stroke_linecap,
'stroke-opacity': cur_shape.stroke_opacity,
'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?
$.each(['marker-start', 'marker-end', 'marker-mid', 'filter', 'clip-path'], function() {
if (elem.getAttribute(this)) {
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) {
parent.insertBefore(path, elem);
if (getBBox) {
return svgedit.utilities.getBBoxOfElementAsPath(elem, addSvgElementFromJson, pathActions)
} else {
parent.appendChild(path);
}
var d = '';
var joinSegs = function(segs) {
$.each(segs, function(j, seg) {
var i;
var l = seg[0], pts = seg[1];
d += l;
for (i = 0; i < pts.length; i+=2) {
d += (pts[i] +','+pts[i+1]) + ' ';
}
});
};
// Possibly the cubed root of 6, but 1.81 works best
var num = 1.81;
var a, rx;
switch (elem.tagName) {
case 'ellipse':
case 'circle':
a = $(elem).attr(['rx', 'ry', 'cx', 'cy']);
var cx = a.cx, cy = a.cy;
rx = a.rx;
ry = a.ry;
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)]],
['C',[(cx+rx/num),(cy-ry), (cx+rx),(cy-ry/num), (cx+rx),(cy)]],
['C',[(cx+rx),(cy+ry/num), (cx+rx/num),(cy+ry), (cx),(cy+ry)]],
['C',[(cx-rx/num),(cy+ry), (cx-rx),(cy+ry/num), (cx-rx),(cy)]],
['Z',[]]
]);
break;
case 'path':
d = elem.getAttribute('d');
break;
case 'line':
a = $(elem).attr(['x1', 'y1', 'x2', 'y2']);
d = 'M'+a.x1+','+a.y1+'L'+a.x2+','+a.y2;
break;
case 'polyline':
case 'polygon':
d = 'M' + elem.getAttribute('points');
break;
case 'rect':
var r = $(elem).attr(['rx', 'ry']);
rx = r.rx;
ry = r.ry;
var b = elem.getBBox();
var x = b.x, y = b.y, w = b.width, h = b.height;
num = 4 - num; // Why? Because!
if (!rx && !ry) {
// Regular rect
joinSegs([
['M',[x, y]],
['L',[x+w, y]],
['L',[x+w, y+h]],
['L',[x, y+h]],
['L',[x, y]],
['Z',[]]
]);
} else {
joinSegs([
['M',[x, y+ry]],
['C',[x, y+ry/num, x+rx/num, y, x+rx, y]],
['L',[x+w-rx, y]],
['C',[x+w-rx/num, y, x+w, y+ry/num, x+w, y+ry]],
['L',[x+w, y+h-ry]],
['C',[x+w, y+h-ry/num, x+w-rx/num, y+h, x+w-rx, y+h]],
['L',[x+rx, y+h]],
['C',[x+rx/num, y+h, x, y+h-ry/num, x, y+h-ry]],
['L',[x, y+ry]],
['Z',[]]
]);
}
break;
default:
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 = svgedit.transformlist.getTransformList(path);
if (svgedit.math.hasMatrixTransform(tlist)) {
pathActions.resetOrientation(path);
}
}
var nextSibling = elem.nextSibling;
batchCmd.addSubCommand(new svgedit.history.RemoveElementCommand(elem, nextSibling, parent));
batchCmd.addSubCommand(new svgedit.history.InsertElementCommand(path));
clearSelection();
elem.parentNode.removeChild(elem);
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);
var bb = false;
try {
bb = path.getBBox();
} catch(e) {
// Firefox fails
}
path.parentNode.removeChild(path);
return bb;
// TODO: Why is this applying attributes from cur_shape, then inside utilities.convertToPath it's pulling addition attributes from elem?
// TODO: If convertToPath is called with one elem, cur_shape and elem are probably the same; but calling with multiple is a bug or cool feature.
var attrs = {
'fill': cur_shape.fill,
'fill-opacity': cur_shape.fill_opacity,
'stroke': cur_shape.stroke,
'stroke-width': cur_shape.stroke_width,
'stroke-dasharray': cur_shape.stroke_dasharray,
'stroke-linejoin': cur_shape.stroke_linejoin,
'stroke-linecap': cur_shape.stroke_linecap,
'stroke-opacity': cur_shape.stroke_opacity,
'opacity': cur_shape.opacity,
'visibility':'hidden'
};
return svgedit.utilities.convertToPath(elem, attrs, addSvgElementFromJson, pathActions, clearSelection, addToSelection, svgedit.history, addCommandToHistory);
}
};

View File

@@ -529,6 +529,447 @@ svgedit.utilities.getBBox = function(elem) {
return ret;
};
// Function: getPathDFromSegments
// Create a path 'd' attribute from path segments.
// Each segment is an array of the form: [singleChar, [x,y, x,y, ...]]
//
// Parameters:
// pathSegments - An array of path segments to be converted
//
// Returns:
// The converted path d attribute.
svgedit.utilities.getPathDFromSegments = function(pathSegments) {
var d = '';
$.each(pathSegments, function(j, seg) {
var i;
var pts = seg[1];
d += seg[0];
for (i = 0; i < pts.length; i+=2) {
d += (pts[i] +','+pts[i+1]) + ' ';
}
});
return d;
};
// Function: getPathDFromElement
// Make a path 'd' attribute from a simple SVG element shape.
//
// Parameters:
// elem - The element to be converted
//
// Returns:
// The path d attribute or undefined if the element type is unknown.
svgedit.utilities.getPathDFromElement = function(elem) {
// Possibly the cubed root of 6, but 1.81 works best
var num = 1.81;
var d, a, rx, ry;
switch (elem.tagName) {
case 'ellipse':
case 'circle':
a = $(elem).attr(['rx', 'ry', 'cx', 'cy']);
var cx = a.cx, cy = a.cy;
rx = a.rx;
ry = a.ry;
if (elem.tagName == 'circle') {
rx = ry = $(elem).attr('r');
}
d = svgedit.utilities.getPathDFromSegments([
['M',[(cx-rx),(cy)]],
['C',[(cx-rx),(cy-ry/num), (cx-rx/num),(cy-ry), (cx),(cy-ry)]],
['C',[(cx+rx/num),(cy-ry), (cx+rx),(cy-ry/num), (cx+rx),(cy)]],
['C',[(cx+rx),(cy+ry/num), (cx+rx/num),(cy+ry), (cx),(cy+ry)]],
['C',[(cx-rx/num),(cy+ry), (cx-rx),(cy+ry/num), (cx-rx),(cy)]],
['Z',[]]
]);
break;
case 'path':
d = elem.getAttribute('d');
break;
case 'line':
a = $(elem).attr(['x1', 'y1', 'x2', 'y2']);
d = 'M'+a.x1+','+a.y1+'L'+a.x2+','+a.y2;
break;
case 'polyline':
d = 'M' + elem.getAttribute('points');
break;
case 'polygon':
d = 'M' + elem.getAttribute('points') + ' Z';
break;
case 'rect':
var r = $(elem).attr(['rx', 'ry']);
rx = r.rx;
ry = r.ry;
var b = elem.getBBox();
var x = b.x, y = b.y, w = b.width, h = b.height;
num = 4 - num; // Why? Because!
if (!rx && !ry) {
// Regular rect
d = svgedit.utilities.getPathDFromSegments([
['M',[x, y]],
['L',[x+w, y]],
['L',[x+w, y+h]],
['L',[x, y+h]],
['L',[x, y]],
['Z',[]]
]);
} else {
d = svgedit.utilities.getPathDFromSegments([
['M',[x, y+ry]],
['C',[x, y+ry/num, x+rx/num, y, x+rx, y]],
['L',[x+w-rx, y]],
['C',[x+w-rx/num, y, x+w, y+ry/num, x+w, y+ry]],
['L',[x+w, y+h-ry]],
['C',[x+w, y+h-ry/num, x+w-rx/num, y+h, x+w-rx, y+h]],
['L',[x+rx, y+h]],
['C',[x+rx/num, y+h, x, y+h-ry/num, x, y+h-ry]],
['L',[x, y+ry]],
['Z',[]]
]);
}
break;
default:
break;
}
return d;
};
// Function: getExtraAttributesForConvertToPath
// Get a set of attributes from an element that is useful for convertToPath.
//
// Parameters:
// elem - The element to be probed
//
// Returns:
// An object with attributes.
svgedit.utilities.getExtraAttributesForConvertToPath = function(elem) {
var attrs = {} ;
// TODO: make this list global so that we can properly maintain it
// TODO: what about @transform, @clip-rule, @fill-rule, etc?
$.each(['marker-start', 'marker-end', 'marker-mid', 'filter', 'clip-path'], function() {
var a = elem.getAttribute(this);
if (a) {
attrs[this] = a;
}
});
return attrs;
};
// Function: getBBoxOfElementAsPath
// Get the BBox of an element-as-path
//
// Parameters:
// elem - The DOM element to be probed
// addSvgElementFromJson - Function to add the path element to the current layer. See canvas.addSvgElementFromJson
// pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
//
// Returns:
// The resulting path's bounding box object.
svgedit.utilities.getBBoxOfElementAsPath = function(elem, addSvgElementFromJson, pathActions) {
var path = addSvgElementFromJson({
'element': 'path',
'attr': svgedit.utilities.getExtraAttributesForConvertToPath(elem)
});
var eltrans = elem.getAttribute('transform');
if (eltrans) {
path.setAttribute('transform', eltrans);
}
var parent = elem.parentNode;
if (elem.nextSibling) {
parent.insertBefore(path, elem);
} else {
parent.appendChild(path);
}
var d = svgedit.utilities.getPathDFromElement(elem);
if (d)
path.setAttribute('d', d);
else
path.parentNode.removeChild(path);
// Get the correct BBox of the new path, then discard it
pathActions.resetOrientation(path);
var bb = false;
try {
bb = path.getBBox();
} catch(e) {
// Firefox fails
}
path.parentNode.removeChild(path);
return bb;
};
// Function: convertToPath
// Convert selected element to a path.
//
// Parameters:
// elem - The DOM element to be converted
// attrs - Apply attributes to new path. see canvas.convertToPath
// addSvgElementFromJson - Function to add the path element to the current layer. See canvas.addSvgElementFromJson
// pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
// clearSelection - see canvas.clearSelection
// addToSelection - see canvas.addToSelection
// history - see svgedit.history
// addCommandToHistory - see canvas.addCommandToHistory
//
// Returns:
// The converted path element or null if the DOM element was not recognized.
svgedit.utilities.convertToPath = function(elem, attrs, addSvgElementFromJson, pathActions, clearSelection, addToSelection, history, addCommandToHistory) {
var batchCmd = new history.BatchCommand('Convert element to Path');
// Any attribute on the element not covered by the passed-in attributes
attrs = $.extend({}, attrs, svgedit.utilities.getExtraAttributesForConvertToPath(elem));
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) {
parent.insertBefore(path, elem);
} else {
parent.appendChild(path);
}
var d = svgedit.utilities.getPathDFromElement(elem);
if (d) {
path.setAttribute('d', d);
// Replace the current element with the converted one
// Reorient if it has a matrix
if (eltrans) {
var tlist = svgedit.transformlist.getTransformList(path);
if (svgedit.math.hasMatrixTransform(tlist)) {
pathActions.resetOrientation(path);
}
}
var nextSibling = elem.nextSibling;
batchCmd.addSubCommand(new history.RemoveElementCommand(elem, nextSibling, parent));
batchCmd.addSubCommand(new history.InsertElementCommand(path));
clearSelection();
elem.parentNode.removeChild(elem);
path.setAttribute('id', id);
path.removeAttribute('visibility');
addToSelection([path], true);
addCommandToHistory(batchCmd);
return path;
} else {
// the elem.tagName was not recognized, so no "d" attribute. Remove it, so we've haven't changed anything.
path.parentNode.removeChild(path);
return null;
}
};
// Function: bBoxCanBeOptimizedOverNativeGetBBox
// Can the bbox be optimized over the native getBBox? The optimized bbox is the same as the native getBBox when
// the rotation angle is a multiple of 90 degrees and there are no complex transforms.
// Getting an optimized bbox can be dramatically slower, so we want to make sure it's worth it.
//
// The best example for this is a circle rotate 45 degrees. The circle doesn't get wider or taller when rotated
// about it's center.
//
// The standard, unoptimized technique gets the native bbox of the circle, rotates the box 45 degrees, uses
// that width and height, and applies any transforms to get the final bbox. This means the calculated bbox
// is much wider than the original circle. If the angle had been 0, 90, 180, etc. both techniques render the
// same bbox.
//
// The optimization is not needed if the rotation is a multiple 90 degrees. The default technique is to call
// getBBox then apply the angle and any transforms.
//
// Parameters:
// angle - The rotation angle in degrees
// hasMatrixTransform - True if there is a matrix transform
//
// Returns:
// True if the bbox can be optimized.
function bBoxCanBeOptimizedOverNativeGetBBox(angle, hasMatrixTransform) {
var angleModulo90 = angle % 90;
var closeTo90 = angleModulo90 < -89.99 || angleModulo90 > 89.99;
var closeTo0 = angleModulo90 > -0.001 && angleModulo90 < 0.001;
return hasMatrixTransform || ! (closeTo0 || closeTo90);
}
// Function: getBBoxWithTransform
// Get bounding box that includes any transforms.
//
// Parameters:
// elem - The DOM element to be converted
// addSvgElementFromJson - Function to add the path element to the current layer. See canvas.addSvgElementFromJson
// pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
//
// Returns:
// A single bounding box object
svgedit.utilities.getBBoxWithTransform = function(elem, addSvgElementFromJson, pathActions) {
// 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);
if (!bb) {
return null;
}
var tlist = svgedit.transformlist.getTransformList(elem);
var angle = svgedit.utilities.getRotationAngleFromTransformList(tlist);
var hasMatrixTransform = svgedit.math.hasMatrixTransform(tlist);
if (angle || hasMatrixTransform) {
var good_bb = false;
if (bBoxCanBeOptimizedOverNativeGetBBox(angle, hasMatrixTransform)) {
// Get the BBox from the raw path for these elements
// TODO: why ellipse and not circle
var elemNames = ['ellipse', 'path', 'line', 'polyline', 'polygon'];
if (elemNames.indexOf(elem.tagName) >= 0) {
bb = good_bb = svgedit.utilities.getBBoxOfElementAsPath(elem, addSvgElementFromJson, pathActions);
} else if (elem.tagName == 'rect') {
// Look for radius
var rx = elem.getAttribute('rx');
var ry = elem.getAttribute('ry');
if (rx || ry) {
bb = good_bb = svgedit.utilities.getBBoxOfElementAsPath(elem, addSvgElementFromJson, pathActions);
}
}
}
if (!good_bb) {
var matrix = svgedit.math.transformListToTransform(tlist).matrix;
bb = svgedit.math.transformBox(bb.x, bb.y, bb.width, bb.height, matrix).aabox;
// Old technique that was exceedingly slow with large documents.
//
// Accurate way to get BBox of rotated element in Firefox:
// Put element in group and get its BBox
//
// Must use clone else FF freaks out
//var clone = elem.cloneNode(true);
//var g = document.createElementNS(NS.SVG, 'g');
//var parent = elem.parentNode;
//parent.appendChild(g);
//g.appendChild(clone);
//var bb2 = svgedit.utilities.bboxToObj(g.getBBox());
//parent.removeChild(g);
}
}
return bb;
};
// TODO: This is problematic with large stroke-width and, for example, a single horizontal line. The calculated BBox extends way beyond left and right sides.
function getStrokeOffsetForBBox(elem) {
var sw = elem.getAttribute('stroke-width');
return (!isNaN(sw) && elem.getAttribute('stroke') != 'none') ? sw/2 : 0;
};
// Function: getStrokedBBox
// Get the bounding box for one or more stroked and/or transformed elements
//
// Parameters:
// elems - Array with DOM elements to check
// addSvgElementFromJson - Function to add the path element to the current layer. See canvas.addSvgElementFromJson
// pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
//
// Returns:
// A single bounding box object
svgedit.utilities.getStrokedBBox = function(elems, addSvgElementFromJson, pathActions) {
if (!elems || !elems.length) {return false;}
var full_bb;
$.each(elems, function() {
if (full_bb) {return;}
if (!this.parentNode) {return;}
full_bb = svgedit.utilities.getBBoxWithTransform(this, addSvgElementFromJson, pathActions);
});
// This shouldn't ever happen...
if (full_bb === undefined) {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;
// If only one elem, don't call the potentially slow getBBoxWithTransform method again.
if (elems.length === 1) {
var offset = getStrokeOffsetForBBox(elems[0]);
min_x -= offset;
min_y -= offset;
max_x += offset;
max_y += offset;
} else {
$.each(elems, function(i, elem) {
var cur_bb = svgedit.utilities.getBBoxWithTransform(elem, addSvgElementFromJson, pathActions);
if (cur_bb) {
var offset = getStrokeOffsetForBBox(elem);
min_x = Math.min(min_x, cur_bb.x - offset);
min_y = Math.min(min_y, cur_bb.y - offset);
// TODO: The old code had this test for max, but not min. I suspect this test should be for both min and max
if (elem.nodeType == 1) {
max_x = Math.max(max_x, cur_bb.x + cur_bb.width + offset);
max_y = Math.max(max_y, cur_bb.y + cur_bb.height + offset);
}
}
});
}
full_bb.x = min_x;
full_bb.y = min_y;
full_bb.width = max_x - min_x;
full_bb.height = max_y - min_y;
return full_bb;
};
// Function: svgedit.utilities.getRotationAngleFromTransformList
// Get the rotation angle of the given transform list.
//
// Parameters:
// tlist - List of transforms
// to_rad - Boolean that when true returns the value in radians rather than degrees
//
// Returns:
// Float with the angle in degrees or radians
svgedit.utilities.getRotationAngleFromTransformList = function(tlist, to_rad) {
if (!tlist) {return 0;} // <svg> elements have no tlist
var N = tlist.numberOfItems;
var i;
for (i = 0; i < N; ++i) {
var xform = tlist.getItem(i);
if (xform.type == 4) {
return to_rad ? xform.angle * Math.PI / 180.0 : xform.angle;
}
}
return 0.0;
};
// Function: svgedit.utilities.getRotationAngle
// Get the rotation angle of the given/selected DOM element
//
@@ -542,16 +983,7 @@ svgedit.utilities.getRotationAngle = function(elem, to_rad) {
var selected = elem || editorContext_.getSelectedElements()[0];
// find the rotation transform (if any) and set it
var tlist = svgedit.transformlist.getTransformList(selected);
if(!tlist) {return 0;} // <svg> elements have no tlist
var N = tlist.numberOfItems;
var i;
for (i = 0; i < N; ++i) {
var xform = tlist.getItem(i);
if (xform.type == 4) {
return to_rad ? xform.angle * Math.PI / 180.0 : xform.angle;
}
}
return 0.0;
return svgedit.utilities.getRotationAngleFromTransformList(tlist, to_rad)
};
// Function getRefElem