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-75d572ba1dddmaster
parent
1bd107b58b
commit
f633ddeb11
|
@ -1,10 +1,11 @@
|
||||||
/*
|
/*
|
||||||
TODOs for TransformList:
|
TODOs for TransformList:
|
||||||
|
|
||||||
* See if I can transfer scales at the end of the tlist down to the children
|
* Ensure undo works properly
|
||||||
* Groups of scaled groups have selector box sizing problems
|
* rotations seem to be getting transferred down to the children upon undo
|
||||||
* Fix rotating of resized groups (need to re-center?)
|
* Ensure rotation works properly (extract the correct rotational center now?)
|
||||||
* Ensure ungrouping works (surely broken)
|
* Ensure resized/rotated groups show the proper located and sized selector box
|
||||||
|
* Ensure ungrouping works
|
||||||
*/
|
*/
|
||||||
/*
|
/*
|
||||||
TODOs for Localizing:
|
TODOs for Localizing:
|
||||||
|
@ -1304,11 +1305,6 @@ function BatchCommand(text) {
|
||||||
than a simple element. Thus its transform list looks like the following:
|
than a simple element. Thus its transform list looks like the following:
|
||||||
[ Translate] [ Rotate ] [ SkewX/SkewY ] [ Scale ]
|
[ 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 <g> may be another group - and that <g> may be rotated!)
|
|
||||||
|
|
||||||
This means a complex element has a reduced transform list as:
|
This means a complex element has a reduced transform list as:
|
||||||
[ Rotate ] [ SkewX/SkewY ] [ Scale ]
|
[ Rotate ] [ SkewX/SkewY ] [ Scale ]
|
||||||
|
|
||||||
|
@ -1319,14 +1315,6 @@ function BatchCommand(text) {
|
||||||
transform list looks like:
|
transform list looks like:
|
||||||
[ Parent Rotate ] [ Parent SkewX/SkewY ] [ Parent Scale ] [ Rotate ] [ SkewX/SkewY ] [ Scale ]
|
[ 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
|
// 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
|
// TODO: use suspendRedraw() and unsuspendRedraw() around this function
|
||||||
var recalculateDimensions = function(selected) {
|
var recalculateDimensions = function(selected) {
|
||||||
if (selected == null) return null;
|
if (selected == null) return null;
|
||||||
// if this element had no transforms, we are done
|
|
||||||
var tlist = canvas.getTransformList(selected);
|
var tlist = canvas.getTransformList(selected);
|
||||||
if (tlist.numberOfItems == 0) return null;
|
|
||||||
|
|
||||||
// remove any stray identity transforms
|
// remove any stray identity transforms
|
||||||
if (tlist && tlist.numberOfItems > 0) {
|
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
|
// we know we have some transforms, so set up return variable
|
||||||
var batchCmd = new BatchCommand("Transform");
|
var batchCmd = new BatchCommand("Transform");
|
||||||
|
|
||||||
|
@ -1441,46 +1431,111 @@ function BatchCommand(text) {
|
||||||
initial["transform"] = start_transform ? start_transform : "";
|
initial["transform"] = start_transform ? start_transform : "";
|
||||||
|
|
||||||
// reduce the transform list here...
|
// 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") {
|
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);
|
|
||||||
|
|
||||||
// if it's a scale, we accumulate it
|
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:
|
||||||
|
|
||||||
|
| 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) {
|
if (xform.type == 3) {
|
||||||
// update the frame of reference
|
// extract scale factors
|
||||||
mat = matrixMultiply(xform.matrix, mat);
|
|
||||||
|
|
||||||
// accumulate the scale values
|
|
||||||
var sobj = transformToObj(xform);
|
var sobj = transformToObj(xform);
|
||||||
sx *= sobj.sx;
|
sx *= sobj.sx;
|
||||||
sy *= sobj.sy;
|
sy *= sobj.sy;
|
||||||
tlist.removeItem(n);
|
|
||||||
}
|
}
|
||||||
// if it's a rotation, adjust the frame of reference
|
|
||||||
else if (xform.type == 4) {
|
else if (xform.type == 4) {
|
||||||
// mat = matrixMultiply(xform.matrix, mat);
|
var robj = transformToObj(xform);
|
||||||
r = xform.angle;
|
// extract angle and old center
|
||||||
tlist.removeItem(n);
|
angle = robj.angle;
|
||||||
|
oldcx = robj.cx;
|
||||||
|
oldcy = robj.cy;
|
||||||
|
newcx = oldcx;
|
||||||
|
newcy = oldcy;
|
||||||
}
|
}
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// force the accumulated translation down to the 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, it's a non-group
|
||||||
else {
|
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.
|
// This pass loop in reverse order and removes any translates or scales.
|
||||||
// Once we hit our first rotate(), we will only remove translates.
|
// Once we hit our first rotate(), we will only remove translates.
|
||||||
var bRemoveTransform = true;
|
var bRemoveTransform = true;
|
||||||
|
@ -1782,155 +1821,6 @@ function BatchCommand(text) {
|
||||||
batchCmd.addSubCommand(new ChangeElementCommand(selected, initial));
|
batchCmd.addSubCommand(new ChangeElementCommand(selected, initial));
|
||||||
|
|
||||||
return batchCmd;
|
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
|
// public events
|
||||||
|
@ -2588,7 +2478,7 @@ function BatchCommand(text) {
|
||||||
|
|
||||||
// clear out selection and set it to the new list
|
// clear out selection and set it to the new list
|
||||||
canvas.clearSelection();
|
canvas.clearSelection();
|
||||||
// TODO: fix this, need to suppliy rect to getIntersectionList()
|
// FIXME: fix this, need to supply rect to getIntersectionList()
|
||||||
canvas.addToSelection(getIntersectionList());
|
canvas.addToSelection(getIntersectionList());
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
Loading…
Reference in New Issue