toolkit/modules/Geometry.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/modules/Geometry.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,327 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +this.EXPORTED_SYMBOLS = ["Point", "Rect"];
     1.9 +
    1.10 +/**
    1.11 + * Simple Point class.
    1.12 + *
    1.13 + * Any method that takes an x and y may also take a point.
    1.14 + */
    1.15 +this.Point = function Point(x, y) {
    1.16 +  this.set(x, y);
    1.17 +}
    1.18 +
    1.19 +Point.prototype = {
    1.20 +  clone: function clone() {
    1.21 +    return new Point(this.x, this.y);
    1.22 +  },
    1.23 +
    1.24 +  set: function set(x, y) {
    1.25 +    this.x = x;
    1.26 +    this.y = y;
    1.27 +    return this;
    1.28 +  },
    1.29 +
    1.30 +  equals: function equals(x, y) {
    1.31 +    return this.x == x && this.y == y;
    1.32 +  },
    1.33 +
    1.34 +  toString: function toString() {
    1.35 +    return "(" + this.x + "," + this.y + ")";
    1.36 +  },
    1.37 +
    1.38 +  map: function map(f) {
    1.39 +    this.x = f.call(this, this.x);
    1.40 +    this.y = f.call(this, this.y);
    1.41 +    return this;
    1.42 +  },
    1.43 +
    1.44 +  add: function add(x, y) {
    1.45 +    this.x += x;
    1.46 +    this.y += y;
    1.47 +    return this;
    1.48 +  },
    1.49 +
    1.50 +  subtract: function subtract(x, y) {
    1.51 +    this.x -= x;
    1.52 +    this.y -= y;
    1.53 +    return this;
    1.54 +  },
    1.55 +
    1.56 +  scale: function scale(s) {
    1.57 +    this.x *= s;
    1.58 +    this.y *= s;
    1.59 +    return this;
    1.60 +  },
    1.61 +
    1.62 +  isZero: function() {
    1.63 +    return this.x == 0 && this.y == 0;
    1.64 +  }
    1.65 +};
    1.66 +
    1.67 +(function() {
    1.68 +  function takePointOrArgs(f) {
    1.69 +    return function(arg1, arg2) {
    1.70 +      if (arg2 === undefined)
    1.71 +        return f.call(this, arg1.x, arg1.y);
    1.72 +      else
    1.73 +        return f.call(this, arg1, arg2);
    1.74 +    };
    1.75 +  }
    1.76 +
    1.77 +  for each (let f in ['add', 'subtract', 'equals', 'set'])
    1.78 +    Point.prototype[f] = takePointOrArgs(Point.prototype[f]);
    1.79 +})();
    1.80 +
    1.81 +
    1.82 +/**
    1.83 + * Rect is a simple data structure for representation of a rectangle supporting
    1.84 + * many basic geometric operations.
    1.85 + *
    1.86 + * NOTE: Since its operations are closed, rectangles may be empty and will report
    1.87 + * non-positive widths and heights in that case.
    1.88 + */
    1.89 +
    1.90 +this.Rect = function Rect(x, y, w, h) {
    1.91 +  this.left = x;
    1.92 +  this.top = y;
    1.93 +  this.right = x + w;
    1.94 +  this.bottom = y + h;
    1.95 +};
    1.96 +
    1.97 +Rect.fromRect = function fromRect(r) {
    1.98 +  return new Rect(r.left, r.top, r.right - r.left, r.bottom - r.top);
    1.99 +};
   1.100 +
   1.101 +Rect.prototype = {
   1.102 +  get x() { return this.left; },
   1.103 +  get y() { return this.top; },
   1.104 +  get width() { return this.right - this.left; },
   1.105 +  get height() { return this.bottom - this.top; },
   1.106 +  set x(v) {
   1.107 +    let diff = this.left - v;
   1.108 +    this.left = v;
   1.109 +    this.right -= diff;
   1.110 +  },
   1.111 +  set y(v) {
   1.112 +    let diff = this.top - v;
   1.113 +    this.top = v;
   1.114 +    this.bottom -= diff;
   1.115 +  },
   1.116 +  set width(v) { this.right = this.left + v; },
   1.117 +  set height(v) { this.bottom = this.top + v; },
   1.118 +
   1.119 +  isEmpty: function isEmpty() {
   1.120 +    return this.left >= this.right || this.top >= this.bottom;
   1.121 +  },
   1.122 +
   1.123 +  setRect: function(x, y, w, h) {
   1.124 +    this.left = x;
   1.125 +    this.top = y;
   1.126 +    this.right = x+w;
   1.127 +    this.bottom = y+h;
   1.128 +
   1.129 +    return this;
   1.130 +  },
   1.131 +
   1.132 +  setBounds: function(l, t, r, b) {
   1.133 +    this.top = t;
   1.134 +    this.left = l;
   1.135 +    this.bottom = b;
   1.136 +    this.right = r;
   1.137 +
   1.138 +    return this;
   1.139 +  },
   1.140 +
   1.141 +  equals: function equals(other) {
   1.142 +    return other != null &&
   1.143 +            (this.isEmpty() && other.isEmpty() ||
   1.144 +            this.top == other.top &&
   1.145 +            this.left == other.left &&
   1.146 +            this.bottom == other.bottom &&
   1.147 +            this.right == other.right);
   1.148 +  },
   1.149 +
   1.150 +  clone: function clone() {
   1.151 +    return new Rect(this.left, this.top, this.right - this.left, this.bottom - this.top);
   1.152 +  },
   1.153 +
   1.154 +  center: function center() {
   1.155 +    if (this.isEmpty())
   1.156 +      throw "Empty rectangles do not have centers";
   1.157 +    return new Point(this.left + (this.right - this.left) / 2,
   1.158 +                          this.top + (this.bottom - this.top) / 2);
   1.159 +  },
   1.160 +
   1.161 +  copyFrom: function(other) {
   1.162 +    this.top = other.top;
   1.163 +    this.left = other.left;
   1.164 +    this.bottom = other.bottom;
   1.165 +    this.right = other.right;
   1.166 +
   1.167 +    return this;
   1.168 +  },
   1.169 +
   1.170 +  translate: function(x, y) {
   1.171 +    this.left += x;
   1.172 +    this.right += x;
   1.173 +    this.top += y;
   1.174 +    this.bottom += y;
   1.175 +
   1.176 +    return this;
   1.177 +  },
   1.178 +
   1.179 +  toString: function() {
   1.180 +    return "[" + this.x + "," + this.y + "," + this.width + "," + this.height + "]";
   1.181 +  },
   1.182 +
   1.183 +  /** return a new rect that is the union of that one and this one */
   1.184 +  union: function(other) {
   1.185 +    return this.clone().expandToContain(other);
   1.186 +  },
   1.187 +
   1.188 +  contains: function(other) {
   1.189 +    if (other.isEmpty()) return true;
   1.190 +    if (this.isEmpty()) return false;
   1.191 +
   1.192 +    return (other.left >= this.left &&
   1.193 +            other.right <= this.right &&
   1.194 +            other.top >= this.top &&
   1.195 +            other.bottom <= this.bottom);
   1.196 +  },
   1.197 +
   1.198 +  intersect: function(other) {
   1.199 +    return this.clone().restrictTo(other);
   1.200 +  },
   1.201 +
   1.202 +  intersects: function(other) {
   1.203 +    if (this.isEmpty() || other.isEmpty())
   1.204 +      return false;
   1.205 +
   1.206 +    let x1 = Math.max(this.left, other.left);
   1.207 +    let x2 = Math.min(this.right, other.right);
   1.208 +    let y1 = Math.max(this.top, other.top);
   1.209 +    let y2 = Math.min(this.bottom, other.bottom);
   1.210 +    return x1 < x2 && y1 < y2;
   1.211 +  },
   1.212 +
   1.213 +  /** Restrict area of this rectangle to the intersection of both rectangles. */
   1.214 +  restrictTo: function restrictTo(other) {
   1.215 +    if (this.isEmpty() || other.isEmpty())
   1.216 +      return this.setRect(0, 0, 0, 0);
   1.217 +
   1.218 +    let x1 = Math.max(this.left, other.left);
   1.219 +    let x2 = Math.min(this.right, other.right);
   1.220 +    let y1 = Math.max(this.top, other.top);
   1.221 +    let y2 = Math.min(this.bottom, other.bottom);
   1.222 +    // If width or height is 0, the intersection was empty.
   1.223 +    return this.setRect(x1, y1, Math.max(0, x2 - x1), Math.max(0, y2 - y1));
   1.224 +  },
   1.225 +
   1.226 +  /** Expand this rectangle to the union of both rectangles. */
   1.227 +  expandToContain: function expandToContain(other) {
   1.228 +    if (this.isEmpty()) return this.copyFrom(other);
   1.229 +    if (other.isEmpty()) return this;
   1.230 +
   1.231 +    let l = Math.min(this.left, other.left);
   1.232 +    let r = Math.max(this.right, other.right);
   1.233 +    let t = Math.min(this.top, other.top);
   1.234 +    let b = Math.max(this.bottom, other.bottom);
   1.235 +    return this.setRect(l, t, r-l, b-t);
   1.236 +  },
   1.237 +
   1.238 +  /**
   1.239 +   * Expands to the smallest rectangle that contains original rectangle and is bounded
   1.240 +   * by lines with integer coefficients.
   1.241 +   */
   1.242 +  expandToIntegers: function round() {
   1.243 +    this.left = Math.floor(this.left);
   1.244 +    this.top = Math.floor(this.top);
   1.245 +    this.right = Math.ceil(this.right);
   1.246 +    this.bottom = Math.ceil(this.bottom);
   1.247 +    return this;
   1.248 +  },
   1.249 +
   1.250 +  scale: function scale(xscl, yscl) {
   1.251 +    this.left *= xscl;
   1.252 +    this.right *= xscl;
   1.253 +    this.top *= yscl;
   1.254 +    this.bottom *= yscl;
   1.255 +    return this;
   1.256 +  },
   1.257 +
   1.258 +  map: function map(f) {
   1.259 +    this.left = f.call(this, this.left);
   1.260 +    this.top = f.call(this, this.top);
   1.261 +    this.right = f.call(this, this.right);
   1.262 +    this.bottom = f.call(this, this.bottom);
   1.263 +    return this;
   1.264 +  },
   1.265 +
   1.266 +  /** Ensure this rectangle is inside the other, if possible. Preserves w, h. */
   1.267 +  translateInside: function translateInside(other) {
   1.268 +    let offsetX = (this.left <= other.left ? other.left - this.left :
   1.269 +                  (this.right > other.right ? other.right - this.right : 0));
   1.270 +    let offsetY = (this.top <= other.top ? other.top - this.top :
   1.271 +                  (this.bottom > other.bottom ? other.bottom - this.bottom : 0));
   1.272 +    return this.translate(offsetX, offsetY);
   1.273 +  },
   1.274 +
   1.275 +  /** Subtract other area from this. Returns array of rects whose union is this-other. */
   1.276 +  subtract: function subtract(other) {
   1.277 +    let r = new Rect(0, 0, 0, 0);
   1.278 +    let result = [];
   1.279 +    other = other.intersect(this);
   1.280 +    if (other.isEmpty())
   1.281 +      return [this.clone()];
   1.282 +
   1.283 +    // left strip
   1.284 +    r.setBounds(this.left, this.top, other.left, this.bottom);
   1.285 +    if (!r.isEmpty())
   1.286 +      result.push(r.clone());
   1.287 +    // inside strip
   1.288 +    r.setBounds(other.left, this.top, other.right, other.top);
   1.289 +    if (!r.isEmpty())
   1.290 +      result.push(r.clone());
   1.291 +    r.setBounds(other.left, other.bottom, other.right, this.bottom);
   1.292 +    if (!r.isEmpty())
   1.293 +      result.push(r.clone());
   1.294 +    // right strip
   1.295 +    r.setBounds(other.right, this.top, this.right, this.bottom);
   1.296 +    if (!r.isEmpty())
   1.297 +      result.push(r.clone());
   1.298 +
   1.299 +    return result;
   1.300 +  },
   1.301 +
   1.302 +  /**
   1.303 +   * Blends two rectangles together.
   1.304 +   * @param rect Rectangle to blend this one with
   1.305 +   * @param scalar Ratio from 0 (returns a clone of this rect) to 1 (clone of rect).
   1.306 +   * @return New blended rectangle.
   1.307 +   */
   1.308 +  blend: function blend(rect, scalar) {
   1.309 +    return new Rect(
   1.310 +      this.left   + (rect.left   - this.left  ) * scalar,
   1.311 +      this.top    + (rect.top    - this.top   ) * scalar,
   1.312 +      this.width  + (rect.width  - this.width ) * scalar,
   1.313 +      this.height + (rect.height - this.height) * scalar);
   1.314 +  },
   1.315 +
   1.316 +  /**
   1.317 +   * Grows or shrinks the rectangle while keeping the center point.
   1.318 +   * Accepts single multipler, or separate for both axes.
   1.319 +   */
   1.320 +  inflate: function inflate(xscl, yscl) {
   1.321 +    let xAdj = (this.width * xscl - this.width) / 2;
   1.322 +    let s = (arguments.length > 1) ? yscl : xscl;
   1.323 +    let yAdj = (this.height * s - this.height) / 2;
   1.324 +    this.left -= xAdj;
   1.325 +    this.right += xAdj;
   1.326 +    this.top -= yAdj;
   1.327 +    this.bottom += yAdj;
   1.328 +    return this;
   1.329 +  }
   1.330 +};

mercurial