Bug 582865 - Provide a shared "shape" module for Point, Rect, etc. [r=mossop a=gavin]
authorEdward Lee <edilee@mozilla.com>
Thu, 12 Aug 2010 02:51:03 -0700
changeset 49686 cdfff833edf962ceb2025de491aea29438e27df2
parent 49635 90b492f96ceb41c9927e429506806ce7f570913b (current diff)
parent 49685 098f406926090b2b912d3f25f5484cf513bcafb2 (diff)
child 49687 08ca624c8616ba1582993eccaa0bce29f693dad9
child 50342 7e54fcbcd2afccec273d2db1ac9843af2ed61ce7
push idunknown
push userunknown
push dateunknown
reviewersmossop, gavin
bugs582865
milestone2.0b4pre
Bug 582865 - Provide a shared "shape" module for Point, Rect, etc. [r=mossop a=gavin]
toolkit/content/Geometry.jsm
toolkit/content/Makefile.in
toolkit/content/tests/browser/Makefile.in
new file mode 100644
--- /dev/null
+++ b/toolkit/content/Geometry.jsm
@@ -0,0 +1,364 @@
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Roy Frostig <rfrostig@mozilla.com>
+ *   Ben Combee <bcombee@mozilla.com>
+ *   Matt Brubeck <mbrubeck@mozilla.com>
+ *   Benjamin Stover <bstover@mozilla.com>
+ *   Michael Yoshitaka Erlewine <mitcho@mitcho.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+let EXPORTED_SYMBOLS = ["Point", "Rect"];
+
+/**
+ * Simple Point class.
+ *
+ * Any method that takes an x and y may also take a point.
+ */
+function Point(x, y) {
+  this.set(x, y);
+}
+
+Point.prototype = {
+  clone: function clone() {
+    return new Point(this.x, this.y);
+  },
+
+  set: function set(x, y) {
+    this.x = x;
+    this.y = y;
+    return this;
+  },
+
+  equals: function equals(x, y) {
+    return this.x == x && this.y == y;
+  },
+
+  toString: function toString() {
+    return "(" + this.x + "," + this.y + ")";
+  },
+
+  map: function map(f) {
+    this.x = f.call(this, this.x);
+    this.y = f.call(this, this.y);
+    return this;
+  },
+
+  add: function add(x, y) {
+    this.x += x;
+    this.y += y;
+    return this;
+  },
+
+  subtract: function subtract(x, y) {
+    this.x -= x;
+    this.y -= y;
+    return this;
+  },
+
+  scale: function scale(s) {
+    this.x *= s;
+    this.y *= s;
+    return this;
+  },
+
+  isZero: function() {
+    return this.x == 0 && this.y == 0;
+  }
+};
+
+(function() {
+  function takePointOrArgs(f) {
+    return function(arg1, arg2) {
+      if (arg2 === undefined)
+        return f.call(this, arg1.x, arg1.y);
+      else
+        return f.call(this, arg1, arg2);
+    };
+  }
+
+  for each (let f in ['add', 'subtract', 'equals', 'set'])
+    Point.prototype[f] = takePointOrArgs(Point.prototype[f]);
+})();
+
+
+/**
+ * Rect is a simple data structure for representation of a rectangle supporting
+ * many basic geometric operations.
+ *
+ * NOTE: Since its operations are closed, rectangles may be empty and will report
+ * non-positive widths and heights in that case.
+ */
+
+function Rect(x, y, w, h) {
+  this.left = x;
+  this.top = y;
+  this.right = x + w;
+  this.bottom = y + h;
+};
+
+Rect.fromRect = function fromRect(r) {
+  return new Rect(r.left, r.top, r.right - r.left, r.bottom - r.top);
+};
+
+Rect.prototype = {
+  get x() { return this.left; },
+  get y() { return this.top; },
+  get width() { return this.right - this.left; },
+  get height() { return this.bottom - this.top; },
+  set x(v) {
+    let diff = this.left - v;
+    this.left = v;
+    this.right -= diff;
+  },
+  set y(v) {
+    let diff = this.top - v;
+    this.top = v;
+    this.bottom -= diff;
+  },
+  set width(v) { this.right = this.left + v; },
+  set height(v) { this.bottom = this.top + v; },
+
+  isEmpty: function isEmpty() {
+    return this.left >= this.right || this.top >= this.bottom;
+  },
+
+  setRect: function(x, y, w, h) {
+    this.left = x;
+    this.top = y;
+    this.right = x+w;
+    this.bottom = y+h;
+
+    return this;
+  },
+
+  setBounds: function(l, t, r, b) {
+    this.top = t;
+    this.left = l;
+    this.bottom = b;
+    this.right = r;
+
+    return this;
+  },
+
+  equals: function equals(other) {
+    return other != null &&
+            (this.isEmpty() && other.isEmpty() ||
+            this.top == other.top &&
+            this.left == other.left &&
+            this.bottom == other.bottom &&
+            this.right == other.right);
+  },
+
+  clone: function clone() {
+    return new Rect(this.left, this.top, this.right - this.left, this.bottom - this.top);
+  },
+
+  center: function center() {
+    if (this.isEmpty())
+      throw "Empty rectangles do not have centers";
+    return new Point(this.left + (this.right - this.left) / 2,
+                          this.top + (this.bottom - this.top) / 2);
+  },
+
+  copyFrom: function(other) {
+    this.top = other.top;
+    this.left = other.left;
+    this.bottom = other.bottom;
+    this.right = other.right;
+
+    return this;
+  },
+
+  translate: function(x, y) {
+    this.left += x;
+    this.right += x;
+    this.top += y;
+    this.bottom += y;
+
+    return this;
+  },
+
+  toString: function() {
+    return "[" + this.x + "," + this.y + "," + this.width + "," + this.height + "]";
+  },
+
+  /** return a new rect that is the union of that one and this one */
+  union: function(other) {
+    return this.clone().expandToContain(other);
+  },
+
+  contains: function(other) {
+    if (other.isEmpty()) return true;
+    if (this.isEmpty()) return false;
+
+    return (other.left >= this.left &&
+            other.right <= this.right &&
+            other.top >= this.top &&
+            other.bottom <= this.bottom);
+  },
+
+  intersect: function(other) {
+    return this.clone().restrictTo(other);
+  },
+
+  intersects: function(other) {
+    if (this.isEmpty() || other.isEmpty())
+      return false;
+
+    let x1 = Math.max(this.left, other.left);
+    let x2 = Math.min(this.right, other.right);
+    let y1 = Math.max(this.top, other.top);
+    let y2 = Math.min(this.bottom, other.bottom);
+    return x1 < x2 && y1 < y2;
+  },
+
+  /** Restrict area of this rectangle to the intersection of both rectangles. */
+  restrictTo: function restrictTo(other) {
+    if (this.isEmpty() || other.isEmpty())
+      return this.setRect(0, 0, 0, 0);
+
+    let x1 = Math.max(this.left, other.left);
+    let x2 = Math.min(this.right, other.right);
+    let y1 = Math.max(this.top, other.top);
+    let y2 = Math.min(this.bottom, other.bottom);
+    // If width or height is 0, the intersection was empty.
+    return this.setRect(x1, y1, Math.max(0, x2 - x1), Math.max(0, y2 - y1));
+  },
+
+  /** Expand this rectangle to the union of both rectangles. */
+  expandToContain: function expandToContain(other) {
+    if (this.isEmpty()) return this.copyFrom(other);
+    if (other.isEmpty()) return this;
+
+    let l = Math.min(this.left, other.left);
+    let r = Math.max(this.right, other.right);
+    let t = Math.min(this.top, other.top);
+    let b = Math.max(this.bottom, other.bottom);
+    return this.setRect(l, t, r-l, b-t);
+  },
+
+  /**
+   * Expands to the smallest rectangle that contains original rectangle and is bounded
+   * by lines with integer coefficients.
+   */
+  expandToIntegers: function round() {
+    this.left = Math.floor(this.left);
+    this.top = Math.floor(this.top);
+    this.right = Math.ceil(this.right);
+    this.bottom = Math.ceil(this.bottom);
+    return this;
+  },
+
+  scale: function scale(xscl, yscl) {
+    this.left *= xscl;
+    this.right *= xscl;
+    this.top *= yscl;
+    this.bottom *= yscl;
+    return this;
+  },
+
+  map: function map(f) {
+    this.left = f.call(this, this.left);
+    this.top = f.call(this, this.top);
+    this.right = f.call(this, this.right);
+    this.bottom = f.call(this, this.bottom);
+    return this;
+  },
+
+  /** Ensure this rectangle is inside the other, if possible. Preserves w, h. */
+  translateInside: function translateInside(other) {
+    let offsetX = (this.left < other.left ? other.left - this.left :
+        (this.right > other.right ? other.right - this.right : 0));
+    let offsetY = (this.top < other.top ? other.top - this.top :
+        (this.bottom > other.bottom ? other.bottom - this.bottom : 0));
+    return this.translate(offsetX, offsetY);
+  },
+
+  /** Subtract other area from this. Returns array of rects whose union is this-other. */
+  subtract: function subtract(other) {
+    let r = new Rect(0, 0, 0, 0);
+    let result = [];
+    other = other.intersect(this);
+    if (other.isEmpty())
+      return [this.clone()];
+
+    // left strip
+    r.setBounds(this.left, this.top, other.left, this.bottom);
+    if (!r.isEmpty())
+      result.push(r.clone());
+    // inside strip
+    r.setBounds(other.left, this.top, other.right, other.top);
+    if (!r.isEmpty())
+      result.push(r.clone());
+    r.setBounds(other.left, other.bottom, other.right, this.bottom);
+    if (!r.isEmpty())
+      result.push(r.clone());
+    // right strip
+    r.setBounds(other.right, this.top, this.right, this.bottom);
+    if (!r.isEmpty())
+      result.push(r.clone());
+
+    return result;
+  },
+
+  /**
+   * Blends two rectangles together.
+   * @param rect Rectangle to blend this one with
+   * @param scalar Ratio from 0 (returns a clone of this rect) to 1 (clone of rect).
+   * @return New blended rectangle.
+   */
+  blend: function blend(rect, scalar) {
+    return new Rect(
+      this.left   + (rect.left   - this.left  ) * scalar,
+      this.top    + (rect.top    - this.top   ) * scalar,
+      this.width  + (rect.width  - this.width ) * scalar,
+      this.height + (rect.height - this.height) * scalar);
+  },
+
+  /**
+   * Grows or shrinks the rectangle while keeping the center point.
+   * Accepts single multipler, or separate for both axes.
+   */
+  inflate: function inflate(xscl, yscl) {
+    let xAdj = (this.width * xscl - this.width) / 2;
+    let s = (arguments.length > 1) ? yscl : xscl;
+    let yAdj = (this.height * s - this.height) / 2;
+    this.left -= xAdj;
+    this.right += xAdj;
+    this.top -= yAdj;
+    this.bottom += yAdj;
+    return this;
+  }
+};
--- a/toolkit/content/Makefile.in
+++ b/toolkit/content/Makefile.in
@@ -78,16 +78,17 @@ ifdef MOZ_TOOLKIT_SEARCH
 DEFINES += -DMOZ_TOOLKIT_SEARCH
 endif
 
 ifdef ENABLE_TESTS
 DIRS += tests
 endif
 
 EXTRA_JS_MODULES = \
+  Geometry.jsm \
   InlineSpellChecker.jsm \
   PopupNotifications.jsm \
   $(NULL)
 
 EXTRA_PP_JS_MODULES = \
   debug.js \
   LightweightThemeConsumer.jsm \
   Services.jsm \
--- a/toolkit/content/tests/browser/Makefile.in
+++ b/toolkit/content/tests/browser/Makefile.in
@@ -48,14 +48,15 @@ DIRS = \
   data \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_TEST_FILES = \
   $(warning browser_keyevents_during_autoscrolling.js disabled due to frequent timeouts (bug 567950)) \
   browser_bug295977_autoscroll_overflow.js \
+  browser_Geometry.js \
   browser_save_resend_postdata.js \
   browser_Services.js \
   $(NULL)
 
 libs:: $(_BROWSER_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_Geometry.js
@@ -0,0 +1,140 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Benjamin Stover <bstover@mozilla.com> (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Components.utils.import("resource://gre/modules/Geometry.jsm");
+
+function test() {
+  ok(Rect, "Rect class exists");
+  for (var fname in tests) {
+    tests[fname]();
+  }
+}
+
+let tests = {
+  testGetDimensions: function() {
+    let r = new Rect(5, 10, 100, 50);
+    ok(r.left == 5, "rect has correct left value");
+    ok(r.top == 10, "rect has correct top value");
+    ok(r.right == 105, "rect has correct right value");
+    ok(r.bottom == 60, "rect has correct bottom value");
+    ok(r.width == 100, "rect has correct width value");
+    ok(r.height == 50, "rect has correct height value");
+    ok(r.x == 5, "rect has correct x value");
+    ok(r.y == 10, "rect has correct y value");
+  },
+
+  testIsEmpty: function() {
+    let r = new Rect(0, 0, 0, 10);
+    ok(r.isEmpty(), "rect with nonpositive width is empty");
+    let r = new Rect(0, 0, 10, 0);
+    ok(r.isEmpty(), "rect with nonpositive height is empty");
+    let r = new Rect(0, 0, 10, 10);
+    ok(!r.isEmpty(), "rect with positive dimensions is not empty");
+  },
+
+  testRestrictTo: function() {
+    let r1 = new Rect(10, 10, 100, 100);
+    let r2 = new Rect(50, 50, 100, 100);
+    r1.restrictTo(r2);
+    ok(r1.equals(new Rect(50, 50, 60, 60)), "intersection is non-empty");
+
+    let r1 = new Rect(10, 10, 100, 100);
+    let r2 = new Rect(120, 120, 100, 100);
+    r1.restrictTo(r2);
+    ok(r1.isEmpty(), "intersection is empty");
+
+    let r1 = new Rect(10, 10, 100, 100);
+    let r2 = new Rect(0, 0, 0, 0);
+    r1.restrictTo(r2);
+    ok(r1.isEmpty(), "intersection of rect and empty is empty");
+
+    let r1 = new Rect(0, 0, 0, 0);
+    let r2 = new Rect(0, 0, 0, 0);
+    r1.restrictTo(r2);
+    ok(r1.isEmpty(), "intersection of empty and empty is empty");
+  },
+
+  testExpandToContain: function() {
+    let r1 = new Rect(10, 10, 100, 100);
+    let r2 = new Rect(50, 50, 100, 100);
+    r1.expandToContain(r2);
+    ok(r1.equals(new Rect(10, 10, 140, 140)), "correct expandToContain on intersecting rectangles");
+
+    let r1 = new Rect(10, 10, 100, 100);
+    let r2 = new Rect(120, 120, 100, 100);
+    r1.expandToContain(r2);
+    ok(r1.equals(new Rect(10, 10, 210, 210)), "correct expandToContain on non-intersecting rectangles");
+
+    let r1 = new Rect(10, 10, 100, 100);
+    let r2 = new Rect(0, 0, 0, 0);
+    r1.expandToContain(r2);
+    ok(r1.equals(new Rect(10, 10, 100, 100)), "expandToContain of rect and empty is rect");
+
+    let r1 = new Rect(10, 10, 0, 0);
+    let r2 = new Rect(0, 0, 0, 0);
+    r1.expandToContain(r2);
+    ok(r1.isEmpty(), "expandToContain of empty and empty is empty");
+  },
+
+  testSubtract: function testSubtract() {
+    function equals(rects1, rects2) {
+      return rects1.length == rects2.length && rects1.every(function(r, i) {
+        return r.equals(rects2[i]);
+      });
+    }
+
+    let r1 = new Rect(0, 0, 100, 100);
+    let r2 = new Rect(500, 500, 100, 100);
+    ok(equals(r1.subtract(r2), [r1]), "subtract area outside of region yields same region");
+
+    let r1 = new Rect(0, 0, 100, 100);
+    let r2 = new Rect(-10, -10, 50, 120);
+    ok(equals(r1.subtract(r2), [new Rect(40, 0, 60, 100)]), "subtracting vertical bar from edge leaves one rect");
+
+    let r1 = new Rect(0, 0, 100, 100);
+    let r2 = new Rect(-10, -10, 120, 50);
+    ok(equals(r1.subtract(r2), [new Rect(0, 40, 100, 60)]), "subtracting horizontal bar from edge leaves one rect");
+
+    let r1 = new Rect(0, 0, 100, 100);
+    let r2 = new Rect(40, 40, 20, 20);
+    ok(equals(r1.subtract(r2), [
+      new Rect(0, 0, 40, 100),
+      new Rect(40, 0, 20, 40),
+      new Rect(40, 60, 20, 40),
+      new Rect(60, 0, 40, 100)]),
+      "subtracting rect in middle leaves union of rects");
+  },
+};