From f633ddeb117be63d75b5d50c37a2394ba757f500 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Sun, 22 Nov 2009 00:50:09 +0000 Subject: [PATCH] Fix Issue 111: Finally got the math right for group transformations including translation, rotation, resizing. Still broken: Undo for transforming groups. Still massively broken: Ungrouping (Issue 204) git-svn-id: http://svg-edit.googlecode.com/svn/trunk@964 eee81c28-f429-11dd-99c0-75d572ba1ddd --- editor/svgcanvas.js | 328 +++++++++++++++----------------------------- 1 file changed, 109 insertions(+), 219 deletions(-) diff --git a/editor/svgcanvas.js b/editor/svgcanvas.js index ee5eebc3..dc090076 100644 --- a/editor/svgcanvas.js +++ b/editor/svgcanvas.js @@ -1,10 +1,11 @@ /* TODOs for TransformList: - * See if I can transfer scales at the end of the tlist down to the children - * Groups of scaled groups have selector box sizing problems - * Fix rotating of resized groups (need to re-center?) - * Ensure ungrouping works (surely broken) + * Ensure undo works properly + * rotations seem to be getting transferred down to the children upon undo + * Ensure rotation works properly (extract the correct rotational center now?) + * Ensure resized/rotated groups show the proper located and sized selector box + * Ensure ungrouping works */ /* TODOs for Localizing: @@ -1304,11 +1305,6 @@ function BatchCommand(text) { than a simple element. Thus its transform list looks like the following: [ Translate] [ Rotate ] [ SkewX/SkewY ] [ Scale ] - - all translates can be removed by moving the element's children - - all rotations can be collapsed down to one rotation - - all scales can be collapsed down to one scale (we cannot simply resize the children - because the child of a may be another group - and that may be rotated!) - This means a complex element has a reduced transform list as: [ Rotate ] [ SkewX/SkewY ] [ Scale ] @@ -1319,14 +1315,6 @@ function BatchCommand(text) { transform list looks like: [ Parent Rotate ] [ Parent SkewX/SkewY ] [ Parent Scale ] [ Rotate ] [ SkewX/SkewY ] [ Scale ] - THINGS TO FIGURE OUT: - - 1) It's not clear to me yet what happens when you want to rotate an element with the - above type of transform list. - - 2) It's also not clear to me if we need to calculate the rotation angle of the element - differently (nor what we should display as the element's rotation angle). - */ // this is how we map paths to our preferred relative segment types @@ -1344,9 +1332,8 @@ function BatchCommand(text) { // TODO: use suspendRedraw() and unsuspendRedraw() around this function var recalculateDimensions = function(selected) { if (selected == null) return null; - // if this element had no transforms, we are done + var tlist = canvas.getTransformList(selected); - if (tlist.numberOfItems == 0) return null; // remove any stray identity transforms if (tlist && tlist.numberOfItems > 0) { @@ -1359,6 +1346,9 @@ function BatchCommand(text) { } } + // if this element had no transforms, we are done + if (tlist.numberOfItems == 0) return null; + // we know we have some transforms, so set up return variable var batchCmd = new BatchCommand("Transform"); @@ -1441,48 +1431,113 @@ function BatchCommand(text) { initial["transform"] = start_transform ? start_transform : ""; // reduce the transform list here... - var box = canvas.getBBox(selected); - var origcenter = {x: (box.x+box.width/2), y: (box.y+box.height/2)}; - var newcenter = {x: origcenter.x, y: origcenter.y}; - var tx = 0.0, ty = 0.0, sx = 1.0, sy = 1.0, r = 0.0; - // if it's a group, we have special reduction loops to flatten transforms + // if it's a group, we have special processing to flatten transforms if (selected.tagName == "g") { - // loop through transforms and accumulate translation and scaling - var mat = svgroot.createSVGMatrix(); - n = tlist.numberOfItems; - while (n--) { - var xform = tlist.getItem(n); + + var angle = 0; + var sx = 1, sy = 1; + var tx = 0, ty = 0; + var oldcx = 0, oldcy = 0, newcx = 0, newcy = 0; + var opType = 0; + + /* + The current concatenated matrix will be of the form: + + [ T ] [ R,oldc ] [ S ] [ T,s ] [ S,new ] [ - T,s ] + + which can be simply represented as: - // if it's a scale, we accumulate it + | x' | | a c e | | x | + | y' | = | b d f | * | y | + | 1 | | 0 0 1 | | 1 | + + where: a = A*cos(r), c = -C*sin(r) + b = A*sin(r), d = C*cos(r) + e,f are the translation required to recenter and properly scale it. + A is the total x scale factor + C is the total y scale factor + + We always want to reduce the new transformation matrix to: + + [ R,newc ] [ S ] + + which will be of the form: + + | x' | | a c g | | x + tx | + | y' | = | b d h | * | y + ty | + | 1 | | 0 0 1 | | 1 | + + where: tx,ty are appropriate translations on the children so that the effect + is identical to the original concatenated matrix above + g,h are translations to recenter the rotation. + + We can get a, b, c, d, e, f from the actual tlist matrix. + We can calculate g,h from the new bounding box. + + Thus, we solve for tx,ty and we get: + + tx = ( (e-g)*cos(r) + (f-h)*sin(r) ) / A + ty = ( -(e-g)*sin(r) + (e-g)*cos(r) ) / C + */ + + // First, we quickly extract the factors: + var N = tlist.numberOfItems; + var i = N; + while (i--) { + var xform = tlist.getItem(i); if (xform.type == 3) { - // update the frame of reference - mat = matrixMultiply(xform.matrix, mat); - - // accumulate the scale values + // extract scale factors var sobj = transformToObj(xform); sx *= sobj.sx; sy *= sobj.sy; - tlist.removeItem(n); } - // if it's a rotation, adjust the frame of reference else if (xform.type == 4) { -// mat = matrixMultiply(xform.matrix, mat); - r = xform.angle; - tlist.removeItem(n); - } - // accumulate the transformed translation - else if (xform.type == 2) { - // determine the translation based on the accumulated transformation thus far - var tobj = transformToObj(xform); - var t = transformPoint(tobj.tx, tobj.ty, mat.inverse()); - // accumulate translation values - tx += t.x; - ty += t.y; - tlist.removeItem(n); + var robj = transformToObj(xform); + // extract angle and old center + angle = robj.angle; + oldcx = robj.cx; + oldcy = robj.cy; + newcx = oldcx; + newcy = oldcy; } } + + // now find the new transformed bbox so we can determine what the + // new center of rotation should be + var origm = transformListToTransform(tlist).matrix; + if (angle != 0) { + var c = transformPoint(oldcx,oldcy,origm); + newcx = c.x; + newcy = c.y; + } + + // now get e,f and calculate g,h + var e = origm.e, + f = origm.f; + var rad = angle * Math.PI / 180; + var g = newcx * (1 - Math.cos(rad)) + newcy * Math.sin(rad); + h = newcy * (1 - Math.cos(rad)) - newcx * Math.sin(rad); + + // now actually calculate the new scale factors + var tx = ( (e-g)*Math.cos(rad) + (f-h)*Math.sin(rad) ) / sx, + ty = ( -(e-g)*Math.sin(rad) + (f-h)*Math.cos(rad) ) / sy; + + // now we can remove all transforms from the list and create our new transforms + tlist.clear(); + if (angle) { + var rot = svgroot.createSVGTransform(); + rot.setRotate(angle,newcx,newcy); + tlist.appendItem(rot); + } + + if (sx != 1 || sy != 1) { + var scale = svgroot.createSVGTransform(); + scale.setScale(sx,sy); + tlist.appendItem(scale); + } + // force the accumulated translation down to the children if (tx != 0 || ty != 0) { // we pass the translates down to the individual children @@ -1499,29 +1554,13 @@ function BatchCommand(text) { } } } - - // now append the single scale to the end of this group's tlist - // NOTE: we can't force this down to the children because they - // might be rotated on a different frame of reference than the scale - if (sx != 1 || sy != 1) { - var newscale = svgroot.createSVGTransform(); - newscale.setScale(sx,sy); - tlist.appendItem(newscale); - } - - if (r != 0.0) { - // get new bbox - var box = canvas.getBBox(selected); - // transform the center point by any remaining scale transforms - var cx = (box.x+box.width/2)*sx, - cy = (box.y+box.height/2)*sy; - var newrot = svgroot.createSVGTransform(); - newrot.setRotate(r,cx,cy); - tlist.insertItemBefore(newrot,0); - } } // else, it's a non-group else { + var box = canvas.getBBox(selected); + var origcenter = {x: (box.x+box.width/2), y: (box.y+box.height/2)}; + var newcenter = {x: origcenter.x, y: origcenter.y}; + // This pass loop in reverse order and removes any translates or scales. // Once we hit our first rotate(), we will only remove translates. var bRemoveTransform = true; @@ -1782,155 +1821,6 @@ function BatchCommand(text) { batchCmd.addSubCommand(new ChangeElementCommand(selected, initial)); return batchCmd; - // ----- - // TODO: once all functionality has been restored to the above function code then - // remove the below (old) function code - - var box = canvas.getBBox(selected); - - // if we have not moved/resized, then immediately leave - var xform = selected.getAttribute("transform"); - var bScaleMatrix = false; - var tlist = selected.transform.baseVal; - var t = tlist.numberOfItems; - while (t--) { - var xform = tlist.getItem(t); - if (xform.type == 3) { - bScaleMatrix = xform.matrix; - break; - } - } - - // Flipping points should only occur for elements without regular x,y vals - var multiPoints = (selected.getAttribute('x') === null); - - // after this point, we have some change to this element - - var remap = function(x,y) { - // Prevent division by 0 - if(!box.height) box.height = 1; - if(!box.width) box.width = 1; - - var new_x = (((x-box.x)/box.width)*selectedBBox.width + selectedBBox.x); - var new_y = (((y-box.y)/box.height)*selectedBBox.height + selectedBBox.y); - - if(multiPoints && bScaleMatrix) { - if(bScaleMatrix.a < 0) { - new_x = selectedBBox.x + selectedBBox.width - (new_x - selectedBBox.x); - } - if(bScaleMatrix.d < 0) { - new_y = selectedBBox.y + selectedBBox.height - (new_y - selectedBBox.y); - } - } - - return {x:new_x, y:new_y}; - }; - - var scalew = function(w) {return (w*selectedBBox.width/box.width);} - var scaleh = function(h) {return (h*selectedBBox.height/box.height);} - - var batchCmd = new BatchCommand("Transform"); - - // if there was a rotation transform, re-set it, otherwise empty out the transform attribute - var angle = canvas.getRotationAngle(selected); - if (angle) { - // this is our old center upon which we have rotated the shape - var tr_x = round(box.x + box.width/2), - tr_y = round(box.y + box.height/2); - var cx = null, cy = null; - - // if this was a resize, find the new cx,cy - if (bScaleMatrix) { - var alpha = angle * Math.PI / 180.0; - - // rotate new opposite corners of bbox by angle at old center - var dx = selectedBBox.x - tr_x, - dy = selectedBBox.y - tr_y, - r = Math.sqrt(dx*dx + dy*dy), - theta = Math.atan2(dy,dx) + alpha; - var left = r * Math.cos(theta) + tr_x, - top = r * Math.sin(theta) + tr_y; - - dx += selectedBBox.width; - dy += selectedBBox.height; - r = Math.sqrt(dx*dx + dy*dy); - theta = Math.atan2(dy,dx) + alpha; - var right = r * Math.cos(theta) + tr_x, - bottom = r * Math.sin(theta) + tr_y; - - // now find mid-point of line between top-left and bottom-right to find new center - cx = round(left + (right-left)/2); - cy = round(top + (bottom-top)/2); - - // now that we know the center and the axis-aligned width/height, calculate the x,y - selectedBBox.x = round(cx - selectedBBox.width/2), - selectedBBox.y = round(cy - selectedBBox.height/2); - } - // if it was not a resize, then it was a translation only - else { - var tx = selectedBBox.x - box.x, - ty = selectedBBox.y - box.y; - cx = tr_x + tx; - cy = tr_y + ty; - } - - var rotate = ["rotate(", angle, " ", cx, ",", cy, ")"].join(''); - selected.setAttribute("transform", rotate); - // if we were rotated, store just the old rotation (not other transforms) on the - // undo stack - var changes = {}; - changes["transform"] = ["rotate(", angle, " ", tr_x, ",", tr_y, ")"].join(''); - batchCmd.addSubCommand(new ChangeElementCommand(selected, changes)); - setPointContainerTransform(rotate); - } - else { - // This fixes Firefox 2- behavior - which does not reset values when the attribute has - // been removed, see https://bugzilla.mozilla.org/show_bug.cgi?id=320622 - selected.setAttribute("transform", ""); - selected.removeAttribute("transform"); - setPointContainerTransform(""); - } - - // if it's a group, transfer the transform attribute to each child element - // and recursively call recalculateDimensions() - if (selected.tagName == "g") { - var children = selected.childNodes; - var i = children.length; - while (i--) { - var child = children.item(i); - if (child.nodeType == 1) { - try { - var childBox = child.getBBox(); - // TODO: to fix the rotation problem, we must account for the - // child's rotation in the bbox adjustment - - // If the child is rotated at all, we should figure out the rotated - // bbox before the group's transform, remap all four corners of the bbox - // via the group's transform, then determine the new angle and the new center - /* - var childAngle = canvas.getRotationAngle(child) * Math.PI / 180.0; - var left = childBox.x - gcx, - top = childBox.y - gcy, - right = childBox.x + childBox.width - gcx, - bottom = childBox.y + childBox.height - gcy; - - var ptTopLeft = remap(left,top), - ptTopRight = remap(right,top), - ptBottomLeft = remap(left,bottom), - ptBottomRight = remap(right,bottom); - */ - var pt = remap(childBox.x,childBox.y), - w = scalew(childBox.width), - h = scaleh(childBox.height); - childBox.x = pt.x; childBox.y = pt.y; - childBox.width = w; childBox.height = h; - batchCmd.addSubCommand(recalculateDimensions(child));//, childBox)); - } catch(e) {} - } - } - return batchCmd; - } - }; // public events @@ -2588,7 +2478,7 @@ function BatchCommand(text) { // clear out selection and set it to the new list canvas.clearSelection(); - // TODO: fix this, need to suppliy rect to getIntersectionList() + // FIXME: fix this, need to supply rect to getIntersectionList() canvas.addToSelection(getIntersectionList()); /*