Bug 1411645 - Shapes highlighter: resize on one axis in transform mode for polygon and ellipse. r=pbro draft
authorMike Park <mikeparkms@gmail.com>
Wed, 01 Nov 2017 11:19:13 -0400
changeset 697759 3c94b4e21127396447831a07f176c17f8f549463
parent 696443 ca87f2b2c3404e3bf119313363751bec50010bf3
child 740189 e8c68c8835d7b48b44159dbb3051ffebb58081ea
push id89078
push userbmo:mpark@mozilla.com
push dateTue, 14 Nov 2017 15:18:49 +0000
reviewerspbro
bugs1411645
milestone58.0a1
Bug 1411645 - Shapes highlighter: resize on one axis in transform mode for polygon and ellipse. r=pbro MozReview-Commit-ID: 9Td88QCRPQg
devtools/client/inspector/test/browser.ini
devtools/client/inspector/test/browser_inspector_highlighter-cssshape_07.js
devtools/server/actors/highlighters/shapes.js
devtools/server/actors/utils/shapes-utils.js
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -79,16 +79,17 @@ skip-if = os == "mac" # Full keyboard na
 [browser_inspector_highlighter-cssgrid_01.js]
 [browser_inspector_highlighter-cssgrid_02.js]
 [browser_inspector_highlighter-cssshape_01.js]
 [browser_inspector_highlighter-cssshape_02.js]
 [browser_inspector_highlighter-cssshape_03.js]
 [browser_inspector_highlighter-cssshape_04.js]
 [browser_inspector_highlighter-cssshape_05.js]
 [browser_inspector_highlighter-cssshape_06.js]
+[browser_inspector_highlighter-cssshape_07.js]
 [browser_inspector_highlighter-cssshape_iframe_01.js]
 [browser_inspector_highlighter-csstransform_01.js]
 [browser_inspector_highlighter-csstransform_02.js]
 [browser_inspector_highlighter-embed.js]
 [browser_inspector_highlighter-eyedropper-clipboard.js]
 subsuite = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
 [browser_inspector_highlighter-eyedropper-csp.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_07.js
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test that shapes are updated correctly for scaling on one axis in transform mode.
+
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html";
+const HIGHLIGHTER_TYPE = "ShapesHighlighter";
+const SHAPE_IDS = ["#polygon-transform", "#ellipse"];
+
+add_task(function* () {
+  let inspector = yield openInspectorForURL(TEST_URL);
+  let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
+  let {testActor} = inspector;
+
+  yield testOneDimScale(testActor, helper);
+
+  helper.finalize();
+});
+
+function* testOneDimScale(testActor, helper) {
+  for (let shape of SHAPE_IDS) {
+    info(`Displaying ${shape}`);
+    yield helper.show(shape, {mode: "cssClipPath", transformMode: true});
+    let { mouse } = helper;
+
+    let { top, left, width, height } = 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);
+      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;
+    }
+    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 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.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 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.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 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.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 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.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
+  };
+}
--- a/devtools/server/actors/highlighters/shapes.js
+++ b/devtools/server/actors/highlighters/shapes.js
@@ -521,63 +521,72 @@ class ShapesHighlighter extends AutoRefr
       // 3) Scale each point by multiplying by the scaling proportion.
       // 4) Translate the shape back such that the anchor is in its original position.
 
       let { bb } = this[_dragging];
       let { minX, minY, maxX, maxY } = bb;
       let { width, height } = this.zoomAdjustedDimensions;
 
       // How much points on each axis should be translated before scaling
-      let transX = (type === "scale-se" || type === "scale-ne") ?
+      let transX = (type === "scale-se" || type === "scale-ne" || type === "scale-e") ?
       minX / 100 * width : maxX / 100 * width;
-      let transY = (type === "scale-se" || type === "scale-sw") ?
+      let transY = (type === "scale-se" || type === "scale-sw" || type === "scale-s") ?
       minY / 100 * height : maxY / 100 * height;
 
       let { percentX, percentY } = this.convertPageCoordsToPercent(x, y);
       let { percentX: percentPageX,
           percentY: percentPageY } = this.convertPageCoordsToPercent(pageX, pageY);
       // distance from original click to current mouse position, in %
-      let distanceX = (type === "scale-se" || type === "scale-ne") ?
+      let distanceX = (type === "scale-se" || type === "scale-ne" || type === "scale-e") ?
       percentPageX - percentX : percentX - percentPageX;
-      let distanceY = (type === "scale-se" || type === "scale-sw") ?
+      let distanceY = (type === "scale-se" || type === "scale-sw" || type === "scale-s") ?
       percentPageY - percentY : percentY - percentPageY;
 
       // scale = 1 + proportion of distance to bounding box width/height of shape
       let scaleX = 1 + distanceX / (maxX - minX);
       let scaleY = 1 + distanceY / (maxY - minY);
       let scale = (scaleX + scaleY) / 2;
+      let axis = "xy";
+      if (type === "scale-e" || type === "scale-w") {
+        scale = scaleX;
+        axis = "x";
+      } else if (type === "scale-n" || type === "scale-s") {
+        scale = scaleY;
+        axis = "y";
+      }
 
       if (this.shapeType === "polygon") {
-        this._scalePolygon(pageX, pageY, transX, transY, scale);
+        this._scalePolygon(pageX, pageY, transX, transY, scale, axis);
       } else if (this.shapeType === "circle") {
         this._scaleCircle(pageX, pageY, transX, transY, scale);
       } else if (this.shapeType === "ellipse") {
-        this._scaleEllipse(pageX, pageY, transX, transY, scale);
+        this._scaleEllipse(pageX, pageY, transX, transY, scale, axis);
       } else if (this.shapeType === "inset") {
         this._scaleInset(pageX, pageY, transX, transY, scale);
       }
     }
   }
 
   /**
    * Scale a polygon depending on mouse position after clicking on a corner handle.
    * @param {Number} pageX the x coordinate of the mouse
    * @param {Number} pageY the y coordinate of the mouse
    * @param {Number} transX the number of pixels to translate on the x axis before scaling
    * @param {Number} transY the number of pixels to translate on the y axis before scaling
    * @param {Number} scale the proportion to scale by
+   * @param {String} axis the axis to scale on. "x", "y", or "xy" for both.
    */
-  _scalePolygon(pageX, pageY, transX, transY, scale) {
+  _scalePolygon(pageX, pageY, transX, transY, scale, axis) {
     let { pointsInfo } = this[_dragging];
 
     let polygonDef = (this.fillRule) ? `${this.fillRule}, ` : "";
     polygonDef += pointsInfo.map(point => {
       let { unitX, unitY, valueX, valueY, ratioX, ratioY } = point;
       let [newX, newY] = scalePoint(valueX, valueY, transX * ratioX,
-                                    transY * ratioY, scale);
+                                    transY * ratioY, scale, axis);
       return `${newX}${unitX} ${newY}${unitY}`;
     }).join(", ");
     polygonDef = (this.geometryBox) ? `polygon(${polygonDef}) ${this.geometryBox}` :
                                       `polygon(${polygonDef})`;
 
     this.currentNode.style.setProperty(this.property, polygonDef, "important");
   }
 
@@ -608,23 +617,24 @@ class ShapesHighlighter extends AutoRefr
 
   /**
    * Scale an ellipse depending on mouse position after clicking on a corner handle.
    * @param {Number} pageX the x coordinate of the mouse
    * @param {Number} pageY the y coordinate of the mouse
    * @param {Number} transX the number of pixels to translate on the x axis before scaling
    * @param {Number} transY the number of pixels to translate on the y axis before scaling
    * @param {Number} scale the proportion to scale by
+   * @param {String} axis the axis to scale on. "x", "y", or "xy" for both.
    */
-  _scaleEllipse(pageX, pageY, transX, transY, scale) {
+  _scaleEllipse(pageX, pageY, transX, transY, scale, axis) {
     let { unitX, unitY, unitRX, unitRY, valueX, valueY,
           ratioX, ratioY, ratioRX, ratioRY } = this[_dragging];
 
     let [newCx, newCy] = scalePoint(valueX, valueY, transX * ratioX,
-                                    transY * ratioY, scale);
+                                    transY * ratioY, scale, axis);
     // As part of scaling, the center is translated to be tangent to the lines y=0 & x=0.
     // To get the new radii, we scale the new center back to that point and get the
     // distances to the line x=0 and y=0.
     let newRx = `${Math.abs((newCx / ratioX - transX) * ratioRX)}${unitRX}`;
     let newRy = `${Math.abs((newCy / ratioY - transY) * ratioRY)}${unitRY}`;
     newCx = `${newCx}${unitX}`;
     newCy = `${newCy}${unitY}`;
 
@@ -1077,16 +1087,20 @@ class ShapesHighlighter extends AutoRefr
       let centerY = (minY + maxY) / 2;
 
       const points = [
         { pointName: "translate", x: centerX, y: centerY, cursor: "move" },
         { pointName: "scale-se", x: maxX, y: maxY, cursor: "nwse-resize" },
         { pointName: "scale-ne", x: maxX, y: minY, cursor: "nesw-resize" },
         { pointName: "scale-sw", x: minX, y: maxY, cursor: "nesw-resize" },
         { pointName: "scale-nw", x: minX, y: minY, cursor: "nwse-resize" },
+        { pointName: "scale-n", x: centerX, y: minY, cursor: "ns-resize" },
+        { pointName: "scale-s", x: centerX, y: maxY, cursor: "ns-resize" },
+        { pointName: "scale-e", x: maxX, y: centerY, cursor: "ew-resize" },
+        { pointName: "scale-w", x: minX, y: centerY, cursor: "ew-resize" }
       ];
 
       for (let { pointName, x, y, cursor } of points) {
         if (point === pointName) {
           this._drawHoverMarker([[x, y]]);
           this.setCursor(cursor);
         }
       }
@@ -1221,24 +1235,31 @@ class ShapesHighlighter extends AutoRefr
     let { width, height } = this.zoomAdjustedDimensions;
     let zoom = getCurrentZoom(this.win);
     let clickRadiusX = BASE_MARKER_SIZE / zoom * 100 / width;
     let clickRadiusY = BASE_MARKER_SIZE / zoom * 100 / height;
 
     let centerX = (minX + maxX) / 2;
     let centerY = (minY + maxY) / 2;
 
-    const points = [
+    let points = [
       { point: "translate", x: centerX, y: centerY },
       { point: "scale-se", x: maxX, y: maxY },
       { point: "scale-ne", x: maxX, y: minY },
       { point: "scale-sw", x: minX, y: maxY },
       { point: "scale-nw", x: minX, y: minY },
     ];
 
+    if (this.shapeType === "polygon" || this.shapeType === "ellipse") {
+      points.push({ point: "scale-n", x: centerX, y: minY },
+                  { point: "scale-s", x: centerX, y: maxY },
+                  { point: "scale-e", x: maxX, y: centerY },
+                  { point: "scale-w", x: minX, y: centerY });
+    }
+
     for (let { point, x, y } of points) {
       if (pageX >= x - clickRadiusX && pageX <= x + clickRadiusX &&
           pageY >= y - clickRadiusY && pageY <= y + clickRadiusY) {
         return point;
       }
     }
 
     return "";
@@ -1874,16 +1895,20 @@ class ShapesHighlighter extends AutoRefr
     boundingBox.setAttribute("width", maxX - minX);
     boundingBox.setAttribute("height", maxY - minY);
     boundingBox.removeAttribute("hidden");
 
     let centerX = (minX + maxX) / 2;
     let centerY = (minY + maxY) / 2;
     let markerPoints = [[centerX, centerY], [minX, minY],
                         [maxX, minY], [minX, maxY], [maxX, maxY]];
+    if (this.shapeType === "polygon" || this.shapeType === "ellipse") {
+      markerPoints.push([minX, centerY], [maxX, centerY],
+                        [centerX, minY], [centerX, maxY]);
+    }
     this._drawMarkers(markerPoints, width, height, zoom);
 
     if (this.shapeType === "polygon") {
       let points = this.coordinates.map(point => point.join(",")).join(" ");
 
       let polygonEl = this.getElement("polygon");
       polygonEl.setAttribute("points", points);
       polygonEl.removeAttribute("hidden");
--- a/devtools/server/actors/utils/shapes-utils.js
+++ b/devtools/server/actors/utils/shapes-utils.js
@@ -125,21 +125,22 @@ const roundTo = (value, 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) => {
-  let newX = (x - transX) * scale + transX;
-  let newY = (y - transY) * scale + transY;
+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;