Bug 1418066 - Add rotation to CSS shapes editor transform mode. r=pbro

MozReview-Commit-ID: HJmdhTg5QjL

--HG--
extra : rebase_source : 828235ca723fe3fa8b3e57eda2512ace002aabd3
This commit is contained in:
Mike Park 2017-11-16 15:58:23 -05:00
parent d3c9541764
commit d4fffe9ba5
7 changed files with 715 additions and 420 deletions

View File

@ -27,9 +27,8 @@ function* testTranslate(testActor, helper) {
yield helper.show(shape, {mode: "cssClipPath", transformMode: true});
let { mouse } = helper;
let { top, left, width, height } = yield getBoundingBoxInPx(testActor, helper, shape);
let x = left + width / 2;
let y = top + height / 2;
let { center, width, height } = yield getBoundingBoxInPx(testActor, helper, shape);
let [x, y] = center;
let dx = width / 10;
let dy = height / 10;
@ -40,8 +39,8 @@ function* testTranslate(testActor, helper) {
yield testActor.reflow();
let newBB = yield getBoundingBoxInPx(testActor, helper);
isnot(newBB.top, top, `${shape} translated on y axis`);
isnot(newBB.left, left, `${shape} translated on x axis`);
isnot(newBB.center[0], x, `${shape} translated on y axis`);
isnot(newBB.center[1], y, `${shape} translated on x axis`);
info(`Translating ${shape} back`);
yield mouse.down(x + dx, y + dy, shape);
@ -50,8 +49,8 @@ function* testTranslate(testActor, helper) {
yield testActor.reflow();
newBB = yield getBoundingBoxInPx(testActor, helper, shape);
is(newBB.top, top, `${shape} translated back on y axis`);
is(newBB.left, left, `${shape} translated back on x axis`);
is(newBB.center[0], x, `${shape} translated back on x axis`);
is(newBB.center[1], y, `${shape} translated back on y axis`);
}
}
@ -61,92 +60,94 @@ function* testScale(testActor, helper) {
yield helper.show(shape, {mode: "cssClipPath", transformMode: true});
let { mouse } = helper;
let { top, left, width, height } = yield getBoundingBoxInPx(testActor, helper, shape);
let { nw, width,
height, center } = yield getBoundingBoxInPx(testActor, helper, shape);
// if the top or left edges are not visible, move the shape so it is.
if (top < 0 || left < 0) {
let x = left + width / 2;
let y = top + height / 2;
let dx = Math.max(0, -left);
let dy = Math.max(0, -top);
if (nw[0] < 0 || nw[1] < 0) {
let [x, y] = center;
let dx = Math.max(0, -nw[0]);
let dy = Math.max(0, -nw[1]);
yield mouse.down(x, y, shape);
yield mouse.move(x + dx, y + dy, shape);
yield mouse.up(x + dx, y + dy, shape);
yield testActor.reflow();
left += dx;
top += dy;
nw[0] += dx;
nw[1] += dy;
}
let dx = width / 10;
let dy = height / 10;
info("Scaling from nw");
yield mouse.down(left, top, shape);
yield mouse.move(left + dx, top + dy, shape);
yield mouse.up(left + dx, top + dy, shape);
yield mouse.down(nw[0], nw[1], shape);
yield mouse.move(nw[0] + dx, nw[1] + dy, shape);
yield mouse.up(nw[0] + dx, nw[1] + dy, shape);
yield testActor.reflow();
let nwBB = yield getBoundingBoxInPx(testActor, helper, shape);
isnot(nwBB.top, top, `${shape} top moved down after nw scale`);
isnot(nwBB.left, left, `${shape} left moved right after nw scale`);
isnot(nwBB.nw[0], nw[0], `${shape} nw moved right after nw scale`);
isnot(nwBB.nw[1], nw[1], `${shape} nw moved down after nw scale`);
isnot(nwBB.width, width, `${shape} width reduced after nw scale`);
isnot(nwBB.height, height, `${shape} height reduced after nw scale`);
info("Scaling from ne");
yield mouse.down(nwBB.left + nwBB.width, nwBB.top, shape);
yield mouse.move(nwBB.left + nwBB.width - dx, nwBB.top + dy, shape);
yield mouse.up(nwBB.left + nwBB.width - dx, nwBB.top + dy, shape);
yield mouse.down(nwBB.ne[0], nwBB.ne[1], shape);
yield mouse.move(nwBB.ne[0] - dx, nwBB.ne[1] + dy, shape);
yield mouse.up(nwBB.ne[0] - dx, nwBB.ne[1] + dy, shape);
yield testActor.reflow();
let neBB = yield getBoundingBoxInPx(testActor, helper, shape);
isnot(neBB.top, nwBB.top, `${shape} top moved down after ne scale`);
is(neBB.left, nwBB.left, `${shape} left not moved right after ne scale`);
isnot(neBB.ne[0], nwBB.ne[0], `${shape} ne moved right after ne scale`);
isnot(neBB.ne[1], nwBB.ne[1], `${shape} ne moved down after ne scale`);
isnot(neBB.width, nwBB.width, `${shape} width reduced after ne scale`);
isnot(neBB.height, nwBB.height, `${shape} height reduced after ne scale`);
info("Scaling from sw");
yield mouse.down(neBB.left, neBB.top + neBB.height, shape);
yield mouse.move(neBB.left + dx, neBB.top + neBB.height - dy, shape);
yield mouse.up(neBB.left + dx, neBB.top + neBB.height - dy, shape);
yield mouse.down(neBB.sw[0], neBB.sw[1], shape);
yield mouse.move(neBB.sw[0] + dx, neBB.sw[1] - dy, shape);
yield mouse.up(neBB.sw[0] + dx, neBB.sw[1] - dy, shape);
yield testActor.reflow();
let swBB = yield getBoundingBoxInPx(testActor, helper, shape);
is(swBB.top, neBB.top, `${shape} top not moved down after sw scale`);
isnot(swBB.left, neBB.left, `${shape} left moved right after sw scale`);
isnot(swBB.sw[0], neBB.sw[0], `${shape} sw moved right after sw scale`);
isnot(swBB.sw[1], neBB.sw[1], `${shape} sw moved down after sw scale`);
isnot(swBB.width, neBB.width, `${shape} width reduced after sw scale`);
isnot(swBB.height, neBB.height, `${shape} height reduced after sw scale`);
info("Scaling from se");
yield mouse.down(swBB.left + swBB.width, swBB.top + swBB.height, shape);
yield mouse.move(swBB.left + swBB.width - dx, swBB.top + swBB.height - dy, shape);
yield mouse.up(swBB.left + swBB.width - dx, swBB.top + swBB.height - dy, shape);
yield mouse.down(swBB.se[0], swBB.se[1], shape);
yield mouse.move(swBB.se[0] - dx, swBB.se[1] - dy, shape);
yield mouse.up(swBB.se[0] - dx, swBB.se[1] - dy, shape);
yield testActor.reflow();
let seBB = yield getBoundingBoxInPx(testActor, helper, shape);
is(seBB.top, swBB.top, `${shape} top not moved down after se scale`);
is(seBB.left, swBB.left, `${shape} left not moved right after se scale`);
isnot(seBB.se[0], swBB.se[0], `${shape} se moved right after se scale`);
isnot(seBB.se[1], swBB.se[1], `${shape} se moved down after se scale`);
isnot(seBB.width, swBB.width, `${shape} width reduced after se scale`);
isnot(seBB.height, swBB.height, `${shape} height reduced after se scale`);
}
}
function* getBoundingBoxInPx(testActor, helper, shape = "#polygon") {
let bbTop = parseFloat(yield helper.getElementAttribute("shapes-bounding-box", "y"));
let bbLeft = parseFloat(yield helper.getElementAttribute("shapes-bounding-box", "x"));
let bbWidth = parseFloat(yield helper.getElementAttribute("shapes-bounding-box",
"width"));
let bbHeight = parseFloat(yield helper.getElementAttribute("shapes-bounding-box",
"height"));
let quads = yield testActor.getAllAdjustedQuads(shape);
let { width, height } = quads.content[0].bounds;
let computedStyle = yield helper.highlightedNode.getComputedStyle();
let paddingTop = parseFloat(computedStyle["padding-top"].value);
let paddingLeft = parseFloat(computedStyle["padding-left"].value);
return {
top: paddingTop + height * bbTop / 100,
left: paddingLeft + width * bbLeft / 100,
width: width * bbWidth / 100,
height: height * bbHeight / 100
};
// path is always of form "Mx y Lx y Lx y Lx y Z", where x/y are numbers
let path = yield helper.getElementAttribute("shapes-bounding-box", "d");
let coords = path.replace(/[MLZ]/g, "").split(" ").map((n, i) => {
return i % 2 === 0 ? paddingLeft + width * n / 100 : paddingTop + height * n / 100;
});
let nw = [coords[0], coords[1]];
let ne = [coords[2], coords[3]];
let se = [coords[4], coords[5]];
let sw = [coords[6], coords[7]];
let center = [(nw[0] + se[0]) / 2, (nw[1] + se[1]) / 2];
let shapeWidth = Math.sqrt((ne[0] - nw[0]) ** 2 + (ne[1] - nw[1]) ** 2);
let shapeHeight = Math.sqrt((sw[0] - nw[0]) ** 2 + (sw[1] - nw[1]) ** 2);
return { nw, ne, se, sw, center, width: shapeWidth, height: shapeHeight };
}

View File

@ -26,92 +26,93 @@ function* testOneDimScale(testActor, helper) {
yield helper.show(shape, {mode: "cssClipPath", transformMode: true});
let { mouse } = helper;
let { top, left, width, height } = yield getBoundingBoxInPx(testActor, helper, shape);
let { nw, width, height, center } = yield getBoundingBoxInPx(testActor, helper, shape);
// if the top or left edges are not visible, move the shape so it is.
if (top < 0 || left < 0) {
let x = left + width / 2;
let y = top + height / 2;
let dx = Math.max(0, -left);
let dy = Math.max(0, -top);
if (nw[0] < 0 || nw[1] < 0) {
let [x, y] = center;
let dx = Math.max(0, -nw[0]);
let dy = Math.max(0, -nw[1]);
yield mouse.down(x, y, shape);
yield mouse.move(x + dx, y + dy, shape);
yield mouse.up(x + dx, y + dy, shape);
yield testActor.reflow();
left += dx;
top += dy;
nw[0] += dx;
nw[1] += dy;
}
let dx = width / 10;
let dy = height / 10;
info("Scaling from w");
yield mouse.down(left, top + height / 2, shape);
yield mouse.move(left + dx, top + height / 2, shape);
yield mouse.up(left + dx, top + height / 2, shape);
yield mouse.down(nw[0], center[1], shape);
yield mouse.move(nw[0] + dx, center[1], shape);
yield mouse.up(nw[0] + dx, center[1], shape);
yield testActor.reflow();
let wBB = yield getBoundingBoxInPx(testActor, helper, shape);
is(wBB.top, top, `${shape} top not moved down after w scale`);
isnot(wBB.left, left, `${shape} left moved right after w scale`);
isnot(wBB.nw[0], nw[0], `${shape} nw moved right after w scale`);
is(wBB.nw[1], nw[1], `${shape} nw not moved down after w scale`);
isnot(wBB.width, width, `${shape} width reduced after w scale`);
is(wBB.height, height, `${shape} height not reduced after w scale`);
info("Scaling from e");
yield mouse.down(wBB.left + wBB.width, wBB.top + wBB.height / 2, shape);
yield mouse.move(wBB.left + wBB.width - dx, wBB.top + wBB.height / 2, shape);
yield mouse.up(wBB.left + wBB.width - dx, wBB.top + wBB.height / 2, shape);
yield mouse.down(wBB.ne[0], center[1], shape);
yield mouse.move(wBB.ne[0] - dx, center[1], shape);
yield mouse.up(wBB.ne[0] - dx, center[1], shape);
yield testActor.reflow();
let eBB = yield getBoundingBoxInPx(testActor, helper, shape);
is(eBB.top, wBB.top, `${shape} top not moved down after e scale`);
is(eBB.left, wBB.left, `${shape} left not moved right after e scale`);
isnot(eBB.ne[0], wBB.ne[0], `${shape} ne moved left after e scale`);
is(eBB.ne[1], wBB.ne[1], `${shape} ne not moved down after e scale`);
isnot(eBB.width, wBB.width, `${shape} width reduced after e scale`);
is(eBB.height, wBB.height, `${shape} height not reduced after e scale`);
info("Scaling from s");
yield mouse.down(eBB.left + eBB.width / 2, eBB.top + eBB.height, shape);
yield mouse.move(eBB.left + eBB.width / 2, eBB.top + eBB.height - dy, shape);
yield mouse.up(eBB.left + eBB.width / 2, eBB.top + eBB.height - dy, shape);
yield mouse.down(eBB.center[0], eBB.sw[1], shape);
yield mouse.move(eBB.center[0], eBB.sw[1] - dy, shape);
yield mouse.up(eBB.center[0], eBB.sw[1] - dy, shape);
yield testActor.reflow();
let sBB = yield getBoundingBoxInPx(testActor, helper, shape);
is(sBB.top, eBB.top, `${shape} top not moved down after w scale`);
is(sBB.left, eBB.left, `${shape} left not moved right after w scale`);
is(sBB.sw[0], eBB.sw[0], `${shape} sw not moved right after w scale`);
isnot(sBB.sw[1], eBB.sw[1], `${shape} sw moved down after w scale`);
is(sBB.width, eBB.width, `${shape} width not reduced after w scale`);
isnot(sBB.height, eBB.height, `${shape} height reduced after w scale`);
info("Scaling from n");
yield mouse.down(sBB.left + sBB.width / 2, sBB.top, shape);
yield mouse.move(sBB.left + sBB.width / 2, sBB.top + dy, shape);
yield mouse.up(sBB.left + sBB.width / 2, sBB.top + dy, shape);
yield mouse.down(sBB.center[0], sBB.nw[1], shape);
yield mouse.move(sBB.center[0], sBB.nw[1] + dy, shape);
yield mouse.up(sBB.center[0], sBB.nw[1] + dy, shape);
yield testActor.reflow();
let nBB = yield getBoundingBoxInPx(testActor, helper, shape);
isnot(nBB.top, sBB.top, `${shape} top moved down after n scale`);
is(nBB.left, sBB.left, `${shape} left not moved right after n scale`);
is(nBB.nw[0], sBB.nw[0], `${shape} nw not moved right after n scale`);
isnot(nBB.nw[1], sBB.nw[1], `${shape} nw moved down after n scale`);
is(nBB.width, sBB.width, `${shape} width reduced after n scale`);
isnot(nBB.height, sBB.height, `${shape} height not reduced after n scale`);
}
}
function* getBoundingBoxInPx(testActor, helper, shape = "#polygon") {
let bbTop = parseFloat(yield helper.getElementAttribute("shapes-bounding-box", "y"));
let bbLeft = parseFloat(yield helper.getElementAttribute("shapes-bounding-box", "x"));
let bbWidth = parseFloat(yield helper.getElementAttribute("shapes-bounding-box",
"width"));
let bbHeight = parseFloat(yield helper.getElementAttribute("shapes-bounding-box",
"height"));
let quads = yield testActor.getAllAdjustedQuads(shape);
let { width, height } = quads.content[0].bounds;
let computedStyle = yield helper.highlightedNode.getComputedStyle();
let paddingTop = parseFloat(computedStyle["padding-top"].value);
let paddingLeft = parseFloat(computedStyle["padding-left"].value);
return {
top: paddingTop + height * bbTop / 100,
left: paddingLeft + width * bbLeft / 100,
width: width * bbWidth / 100,
height: height * bbHeight / 100
};
// path is always of form "Mx y Lx y Lx y Lx y Z", where x/y are numbers
let path = yield helper.getElementAttribute("shapes-bounding-box", "d");
let coords = path.replace(/[MLZ]/g, "").split(" ").map((n, i) => {
return i % 2 === 0 ? paddingLeft + width * n / 100 : paddingTop + height * n / 100;
});
let nw = [coords[0], coords[1]];
let ne = [coords[2], coords[3]];
let se = [coords[4], coords[5]];
let sw = [coords[6], coords[7]];
let center = [(nw[0] + se[0]) / 2, (nw[1] + se[1]) / 2];
let shapeWidth = Math.sqrt((ne[0] - nw[0]) ** 2 + (ne[1] - nw[1]) ** 2);
let shapeHeight = Math.sqrt((sw[0] - nw[0]) ** 2 + (sw[1] - nw[1]) ** 2);
return { nw, ne, se, sw, center, width: shapeWidth, height: shapeHeight };
}

View File

@ -610,7 +610,7 @@
/* Shapes highlighter */
:-moz-native-anonymous .shapes-root {
pointer-events: auto;
pointer-events: none;
}
:-moz-native-anonymous .shapes-shape-container {
@ -621,7 +621,8 @@
:-moz-native-anonymous .shapes-polygon,
:-moz-native-anonymous .shapes-ellipse,
:-moz-native-anonymous .shapes-rect,
:-moz-native-anonymous .shapes-bounding-box {
:-moz-native-anonymous .shapes-bounding-box,
:-moz-native-anonymous .shapes-rotate-line {
fill: transparent;
stroke: var(--highlighter-guide-color);
shape-rendering: geometricPrecision;

File diff suppressed because it is too large Load Diff

View File

@ -122,27 +122,9 @@ const roundTo = (value, exp) => {
return +(value[0] + "e" + (value[1] ? (+value[1] + exp) : exp));
};
/**
* Scale a given x/y coordinate pair by translating, multiplying by the given factor,
* then translating back.
* @param {Number} x the x coordinate
* @param {Number} y the y coordinate
* @param {Number} transX the amount to translate the x coord by
* @param {Number} transY the amount ot translate the y coord by
* @param {Number} scale the scaling factor
* @param {String} axis the axis to scale on. "x", "y", or "xy" for both.
* @returns {Array} of the form [newX, newY], containing the coord pair after scaling.
*/
const scalePoint = (x, y, transX, transY, scale, axis = "xy") => {
let newX = (axis === "y") ? x : (x - transX) * scale + transX;
let newY = (axis === "x") ? y : (y - transY) * scale + transY;
return [newX, newY];
};
exports.getDistance = getDistance;
exports.clickedOnEllipseEdge = clickedOnEllipseEdge;
exports.distanceToLine = distanceToLine;
exports.projection = projection;
exports.clickedOnPoint = clickedOnPoint;
exports.roundTo = roundTo;
exports.scalePoint = scalePoint;

View File

@ -15,7 +15,6 @@ const {
getCirclePath,
getUnit
} = require("devtools/server/actors/highlighters/shapes");
const { scalePoint } = require("devtools/server/actors/utils/shapes-utils");
function run_test() {
test_split_coords();
@ -24,7 +23,6 @@ function run_test() {
test_shape_mode_to_css_property_name();
test_get_circle_path();
test_get_unit();
test_scale_point();
run_next_test();
}
@ -167,31 +165,3 @@ function test_get_unit() {
equal(getUnit(expr), expected, desc);
}
}
function test_scale_point() {
const tests = [{
desc: "scalePoint with 0,0",
x: 0, y: 0, transX: 0, transY: 0, scale: 0.9, expected: [0, 0]
}, {
desc: "scalePoint with scale factor 1",
x: 10, y: 10, transX: 100, transY: 100, scale: 1, expected: [10, 10]
}, {
desc: "scalePoint with scale factor 0.9, no translation",
x: 10, y: 20, transX: 0, transY: 0, scale: 0.9, expected: [9, 18]
}, {
desc: "scalePoint with scale factor 0.9, translation",
x: 10, y: 20, transX: 10, transY: 10, scale: 0.9, expected: [10, 19]
}, {
desc: "scalePoint with scale factor 2, negative translation",
x: 20, y: 30, transX: -10, transY: -10, scale: 2, expected: [50, 70]
}, {
desc: "scalePoint with scale factor 2, translation = coordinates",
x: 20, y: 30, transX: 20, transY: 30, scale: 2, expected: [20, 30]
}];
for (let { desc, x, y, transX, transY, scale, expected } of tests) {
let [newX, newY] = scalePoint(x, y, transX, transY, scale);
equal(newX, expected[0], desc + " x");
equal(newY, expected[1], desc + " y");
}
}

View File

@ -44,6 +44,28 @@ const translate = (tx = 0, ty = tx) => [
];
exports.translate = translate;
/**
* Returns a matrix for the rotation given.
* Calling `rotate()` or `rotate(0)` returns a new identity matrix.
*
* @param {Number} [angle = 0]
* The angle, in radians, for which to return a corresponding rotation matrix.
* If unspecified, it will equal `0`.
* @return {Array}
* The new matrix.
*/
const rotate = (angle = 0) => {
let cos = Math.cos(angle);
let sin = Math.sin(angle);
return [
cos, sin, 0,
-sin, cos, 0,
0, 0, 1
];
};
exports.rotate = rotate;
/**
* Returns a new identity matrix.
*
@ -118,6 +140,54 @@ const isIdentity = (M) =>
M[6] === 0 && M[7] === 0 && M[8] === 1;
exports.isIdentity = isIdentity;
/**
* Get the change of basis matrix and inverted change of basis matrix
* for the coordinate system based on the two given vectors, as well as
* the lengths of the two given vectors.
*
* @param {Array} u
* The first vector, serving as the "x axis" of the coordinate system.
* @param {Array} v
* The second vector, serving as the "y axis" of the coordinate system.
* @return {Object}
* { basis, invertedBasis, uLength, vLength }
* basis and invertedBasis are the change of basis matrices. uLength and
* vLength are the lengths of u and v.
*/
const getBasis = (u, v) => {
let uLength = Math.abs(Math.sqrt(u[0] ** 2 + u[1] ** 2));
let vLength = Math.abs(Math.sqrt(v[0] ** 2 + v[1] ** 2));
let basis =
[ u[0] / uLength, v[0] / vLength, 0,
u[1] / uLength, v[1] / vLength, 0,
0, 0, 1 ];
let determinant = 1 / (basis[0] * basis[4] - basis[1] * basis[3]);
let invertedBasis =
[ basis[4] / determinant, -basis[1] / determinant, 0,
-basis[3] / determinant, basis[0] / determinant, 0,
0, 0, 1 ];
return { basis, invertedBasis, uLength, vLength };
};
exports.getBasis = getBasis;
/**
* Convert the given matrix to a new coordinate system, based on the change of basis
* matrix.
*
* @param {Array} M
* The matrix to convert
* @param {Array} basis
* The change of basis matrix
* @param {Array} invertedBasis
* The inverted change of basis matrix
* @return {Array}
* The converted matrix.
*/
const changeMatrixBase = (M, basis, invertedBasis) => {
return multiply(invertedBasis, multiply(M, basis));
};
exports.changeMatrixBase = changeMatrixBase;
/**
* Returns the transformation matrix for the given node, relative to the ancestor passed
* as second argument; considering the ancestor transformation too.