Optimized getBBoxWithTransform when rotation multiple of 90

This feature was in previous release, but broken. This update uses
standard, faster bounding box calculation for simple shapes when angle
is multiple of 90.
Updated tests. Fixed two tests that were broken in Safari.
master
Flint O'Brien 2016-04-28 12:11:54 -04:00
parent 7db3b22c58
commit 3230520d67
2 changed files with 66 additions and 20 deletions

View File

@ -783,6 +783,35 @@ svgedit.utilities.convertToPath = function(elem, attrs, addSvgElementFromJson, p
}; };
// 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 // Function: getBBoxWithTransform
// Get bounding box that includes any transforms. // Get bounding box that includes any transforms.
// //
@ -806,10 +835,12 @@ svgedit.utilities.getBBoxWithTransform = function(elem, addSvgElementFromJson, p
var tlist = svgedit.transformlist.getTransformList(elem); var tlist = svgedit.transformlist.getTransformList(elem);
var angle = svgedit.utilities.getRotationAngleFromTransformList(tlist); var angle = svgedit.utilities.getRotationAngleFromTransformList(tlist);
var hasMatrixTransform = svgedit.math.hasMatrixTransform(tlist);
if (angle || svgedit.math.hasMatrixTransform(tlist)) { if (angle || hasMatrixTransform) {
var good_bb = false; var good_bb = false;
if (bBoxCanBeOptimizedOverNativeGetBBox(angle, hasMatrixTransform)) {
// Get the BBox from the raw path for these elements // Get the BBox from the raw path for these elements
// TODO: why ellipse and not circle // TODO: why ellipse and not circle
var elemNames = ['ellipse', 'path', 'line', 'polyline', 'polygon']; var elemNames = ['ellipse', 'path', 'line', 'polyline', 'polygon'];
@ -823,6 +854,7 @@ svgedit.utilities.getBBoxWithTransform = function(elem, addSvgElementFromJson, p
bb = good_bb = svgedit.utilities.getBBoxOfElementAsPath(elem, addSvgElementFromJson, pathActions); bb = good_bb = svgedit.utilities.getBBoxOfElementAsPath(elem, addSvgElementFromJson, pathActions);
} }
} }
}
if (!good_bb) { if (!good_bb) {

View File

@ -32,9 +32,11 @@
} }
return elem; return elem;
} }
var mockAddSvgElementFromJsonCallCount = 0;
function mockAddSvgElementFromJson( json) { function mockAddSvgElementFromJson( json) {
var elem = mockCreateSVGElement( json) var elem = mockCreateSVGElement( json)
svgroot.appendChild( elem) svgroot.appendChild( elem)
mockAddSvgElementFromJsonCallCount++;
return elem return elem
} }
var mockPathActions = { var mockPathActions = {
@ -82,7 +84,8 @@
setup: function() { setup: function() {
// We're reusing ID's so we need to do this for transforms. // We're reusing ID's so we need to do this for transforms.
svgedit.transformlist.resetListMap(); svgedit.transformlist.resetListMap();
svgedit.path.init(null) svgedit.path.init(null);
mockAddSvgElementFromJsonCallCount = 0;
}, },
teardown: function() { teardown: function() {
} }
@ -109,6 +112,7 @@
svgroot.appendChild( elem) svgroot.appendChild( elem)
bbox = getBBoxWithTransform(elem, mockAddSvgElementFromJson, mockPathActions) bbox = getBBoxWithTransform(elem, mockAddSvgElementFromJson, mockPathActions)
deepEqual(bbox, {"x": 0, "y": 1, "width": 2, "height": 2 }); deepEqual(bbox, {"x": 0, "y": 1, "width": 2, "height": 2 });
equal( mockAddSvgElementFromJsonCallCount, 0);
svgroot.removeChild( elem); svgroot.removeChild( elem);
elem = mockCreateSVGElement({ elem = mockCreateSVGElement({
@ -118,6 +122,7 @@
svgroot.appendChild( elem); svgroot.appendChild( elem);
bbox = getBBoxWithTransform( elem, mockAddSvgElementFromJson, mockPathActions) bbox = getBBoxWithTransform( elem, mockAddSvgElementFromJson, mockPathActions)
deepEqual( bbox, { "x": 0, "y": 1, "width": 5, "height": 10}); deepEqual( bbox, { "x": 0, "y": 1, "width": 5, "height": 10});
equal( mockAddSvgElementFromJsonCallCount, 0);
svgroot.removeChild( elem); svgroot.removeChild( elem);
elem = mockCreateSVGElement({ elem = mockCreateSVGElement({
@ -127,6 +132,7 @@
svgroot.appendChild( elem); svgroot.appendChild( elem);
bbox = getBBoxWithTransform( elem, mockAddSvgElementFromJson, mockPathActions) bbox = getBBoxWithTransform( elem, mockAddSvgElementFromJson, mockPathActions)
deepEqual( bbox, { "x": 0, "y": 1, "width": 5, "height": 5}); deepEqual( bbox, { "x": 0, "y": 1, "width": 5, "height": 5});
equal( mockAddSvgElementFromJsonCallCount, 0);
svgroot.removeChild( elem); svgroot.removeChild( elem);
elem = mockCreateSVGElement({ elem = mockCreateSVGElement({
@ -141,6 +147,7 @@
svgroot.appendChild( g); svgroot.appendChild( g);
bbox = getBBoxWithTransform( elem, mockAddSvgElementFromJson, mockPathActions) bbox = getBBoxWithTransform( elem, mockAddSvgElementFromJson, mockPathActions)
deepEqual( bbox, { "x": 0, "y": 1, "width": 5, "height": 10}); deepEqual( bbox, { "x": 0, "y": 1, "width": 5, "height": 10});
equal( mockAddSvgElementFromJsonCallCount, 0);
svgroot.removeChild( g); svgroot.removeChild( g);
}); });
@ -172,6 +179,7 @@
close( bbox.y, 15, EPSILON); close( bbox.y, 15, EPSILON);
close( bbox.width, 20, EPSILON); close( bbox.width, 20, EPSILON);
close( bbox.height, 10, EPSILON); close( bbox.height, 10, EPSILON);
equal( mockAddSvgElementFromJsonCallCount, 1);
svgroot.removeChild( elem); svgroot.removeChild( elem);
var rect = {x: 10, y: 10, width: 10, height: 20}; var rect = {x: 10, y: 10, width: 10, height: 20};
@ -182,12 +190,14 @@
'attr': { 'id': 'rect2', 'x': rect.x, 'y': rect.y, 'width': rect.width, 'height': rect.height, 'transform': 'rotate(' + angle + ' ' + origin.x + ',' + origin.y + ')'} 'attr': { 'id': 'rect2', 'x': rect.x, 'y': rect.y, 'width': rect.width, 'height': rect.height, 'transform': 'rotate(' + angle + ' ' + origin.x + ',' + origin.y + ')'}
}); });
svgroot.appendChild( elem); svgroot.appendChild( elem);
mockAddSvgElementFromJsonCallCount = 0;
bbox = getBBoxWithTransform( elem, mockAddSvgElementFromJson, mockPathActions); bbox = getBBoxWithTransform( elem, mockAddSvgElementFromJson, mockPathActions);
var r2 = rotateRect( rect, angle, origin); var r2 = rotateRect( rect, angle, origin);
close( bbox.x, r2.x, EPSILON, 'rect2 x is ' + r2.x); close( bbox.x, r2.x, EPSILON, 'rect2 x is ' + r2.x);
close( bbox.y, r2.y, EPSILON, 'rect2 y is ' + r2.y); close( bbox.y, r2.y, EPSILON, 'rect2 y is ' + r2.y);
close( bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width); close( bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width);
close( bbox.height, r2.height, EPSILON, 'rect2 height is ' + r2.height); close( bbox.height, r2.height, EPSILON, 'rect2 height is ' + r2.height);
equal( mockAddSvgElementFromJsonCallCount, 0);
svgroot.removeChild( elem); svgroot.removeChild( elem);
@ -202,11 +212,13 @@
}); });
g.appendChild( elem); g.appendChild( elem);
svgroot.appendChild( g); svgroot.appendChild( g);
mockAddSvgElementFromJsonCallCount = 0;
bbox = getBBoxWithTransform( g, mockAddSvgElementFromJson, mockPathActions); bbox = getBBoxWithTransform( g, mockAddSvgElementFromJson, mockPathActions);
close( bbox.x, r2.x, EPSILON, 'rect2 x is ' + r2.x); close( bbox.x, r2.x, EPSILON, 'rect2 x is ' + r2.x);
close( bbox.y, r2.y, EPSILON, 'rect2 y is ' + r2.y); close( bbox.y, r2.y, EPSILON, 'rect2 y is ' + r2.y);
close( bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width); close( bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width);
close( bbox.height, r2.height, EPSILON, 'rect2 height is ' + r2.height); close( bbox.height, r2.height, EPSILON, 'rect2 height is ' + r2.height);
equal( mockAddSvgElementFromJsonCallCount, 0);
svgroot.removeChild( g); svgroot.removeChild( g);
@ -215,12 +227,14 @@
'attr': { 'id': 'ellipse1', 'cx': '100', 'cy': '100', 'rx': '50', 'ry': '50', 'transform': 'rotate(45 100,100)'} 'attr': { 'id': 'ellipse1', 'cx': '100', 'cy': '100', 'rx': '50', 'ry': '50', 'transform': 'rotate(45 100,100)'}
}); });
svgroot.appendChild( elem); svgroot.appendChild( elem);
mockAddSvgElementFromJsonCallCount = 0;
bbox = getBBoxWithTransform( elem, mockAddSvgElementFromJson, mockPathActions); bbox = getBBoxWithTransform( elem, mockAddSvgElementFromJson, mockPathActions);
// TODO: the BBox algorithm is using the bezier control points to calculate the bounding box. Should be 50, 50, 100, 100. // TODO: the BBox algorithm is using the bezier control points to calculate the bounding box. Should be 50, 50, 100, 100.
close( bbox.x, 45.111, EPSILON); ok( bbox.x > 45 && bbox.x <= 50);
close( bbox.y, 45.111, EPSILON); ok( bbox.y > 45 && bbox.y <= 50);
close( bbox.width, 109.777, EPSILON); ok( bbox.width >= 100 && bbox.width < 110);
close( bbox.height, 109.777, EPSILON); ok( bbox.height >= 100 && bbox.height < 110);
equal( mockAddSvgElementFromJsonCallCount, 1);
svgroot.removeChild( elem); svgroot.removeChild( elem);
}); });
@ -311,10 +325,10 @@
svgroot.appendChild( elem); svgroot.appendChild( elem);
bbox = getBBoxWithTransform( elem, mockAddSvgElementFromJson, mockPathActions) bbox = getBBoxWithTransform( elem, mockAddSvgElementFromJson, mockPathActions)
// TODO: the BBox algorithm is using the bezier control points to calculate the bounding box. Should be 50, 50, 100, 100. // TODO: the BBox algorithm is using the bezier control points to calculate the bounding box. Should be 50, 50, 100, 100.
close( bbox.x, 45.111 + tx, EPSILON); ok( bbox.x > 45 + tx && bbox.x <= 50 + tx);
close( bbox.y, 45.111 + ty, EPSILON); ok( bbox.y > 45 + ty && bbox.y <= 50 + ty);
close( bbox.width, 109.777, EPSILON); ok( bbox.width >= 100 && bbox.width < 110);
close( bbox.height, 109.777, EPSILON); ok( bbox.height >= 100 && bbox.height < 110);
svgroot.removeChild( elem); svgroot.removeChild( elem);
}); });