summaryrefslogtreecommitdiff
path: root/browser/devtools/tilt
diff options
context:
space:
mode:
authorwolfbeast <mcwerewolf@gmail.com>2014-05-21 11:38:25 +0200
committerwolfbeast <mcwerewolf@gmail.com>2014-05-21 11:38:25 +0200
commitd25ba7d760b017b038e5aa6c0a605b4a330eb68d (patch)
tree16ec27edc7d5f83986f16236d3a36a2682a0f37e /browser/devtools/tilt
parenta942906574671868daf122284a9c4689e6924f74 (diff)
downloadpalemoon-gre-d25ba7d760b017b038e5aa6c0a605b4a330eb68d.tar.gz
Recommit working copy to repo with proper line endings.
Diffstat (limited to 'browser/devtools/tilt')
-rw-r--r--browser/devtools/tilt/CmdTilt.jsm216
-rw-r--r--browser/devtools/tilt/Makefile.in16
-rw-r--r--browser/devtools/tilt/TiltWorkerCrafter.js280
-rw-r--r--browser/devtools/tilt/TiltWorkerPicker.js186
-rw-r--r--browser/devtools/tilt/moz.build8
-rw-r--r--browser/devtools/tilt/test/Makefile.in63
-rw-r--r--browser/devtools/tilt/test/browser_tilt_01_lazy_getter.js14
-rw-r--r--browser/devtools/tilt/test/browser_tilt_02_notifications-seq.js100
-rw-r--r--browser/devtools/tilt/test/browser_tilt_02_notifications-tabs.js173
-rw-r--r--browser/devtools/tilt/test/browser_tilt_02_notifications.js132
-rw-r--r--browser/devtools/tilt/test/browser_tilt_03_tab_switch.js106
-rw-r--r--browser/devtools/tilt/test/browser_tilt_04_initialization.js57
-rw-r--r--browser/devtools/tilt/test/browser_tilt_05_destruction-esc.js49
-rw-r--r--browser/devtools/tilt/test/browser_tilt_05_destruction-url.js49
-rw-r--r--browser/devtools/tilt/test/browser_tilt_05_destruction.js49
-rw-r--r--browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js131
-rw-r--r--browser/devtools/tilt/test/browser_tilt_arcball-reset.js129
-rw-r--r--browser/devtools/tilt/test/browser_tilt_arcball.js496
-rw-r--r--browser/devtools/tilt/test/browser_tilt_controller.js134
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl01.js155
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl02.js44
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl03.js67
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl04.js124
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl05.js40
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl06.js57
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl07.js58
-rw-r--r--browser/devtools/tilt/test/browser_tilt_gl08.js49
-rw-r--r--browser/devtools/tilt/test/browser_tilt_math01.js59
-rw-r--r--browser/devtools/tilt/test/browser_tilt_math02.js104
-rw-r--r--browser/devtools/tilt/test/browser_tilt_math03.js33
-rw-r--r--browser/devtools/tilt/test/browser_tilt_math04.js49
-rw-r--r--browser/devtools/tilt/test/browser_tilt_math05.js101
-rw-r--r--browser/devtools/tilt/test/browser_tilt_math06.js42
-rw-r--r--browser/devtools/tilt/test/browser_tilt_math07.js49
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking.js54
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking_delete.js76
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking_highlight01-offs.js75
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking_highlight01.js75
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking_highlight02.js70
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking_highlight03.js70
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking_inspector.js61
-rw-r--r--browser/devtools/tilt/test/browser_tilt_picking_miv.js76
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils01.js69
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils02.js21
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils03.js18
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils04.js54
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils05.js100
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils06.js44
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils07.js158
-rw-r--r--browser/devtools/tilt/test/browser_tilt_utils08.js85
-rw-r--r--browser/devtools/tilt/test/browser_tilt_visualizer.js126
-rw-r--r--browser/devtools/tilt/test/browser_tilt_zoom.js91
-rw-r--r--browser/devtools/tilt/test/head.js206
-rw-r--r--browser/devtools/tilt/test/moz.build6
-rw-r--r--browser/devtools/tilt/tilt-gl.js1595
-rw-r--r--browser/devtools/tilt/tilt-math.js2322
-rw-r--r--browser/devtools/tilt/tilt-utils.js612
-rw-r--r--browser/devtools/tilt/tilt-visualizer-style.js46
-rw-r--r--browser/devtools/tilt/tilt-visualizer.js2260
-rw-r--r--browser/devtools/tilt/tilt.js263
60 files changed, 12052 insertions, 0 deletions
diff --git a/browser/devtools/tilt/CmdTilt.jsm b/browser/devtools/tilt/CmdTilt.jsm
new file mode 100644
index 000000000..cc2d9b240
--- /dev/null
+++ b/browser/devtools/tilt/CmdTilt.jsm
@@ -0,0 +1,216 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+this.EXPORTED_SYMBOLS = [ ];
+
+Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
+Components.utils.import("resource://gre/modules/devtools/gcli.jsm");
+Components.utils.import("resource://gre/modules/devtools/Loader.jsm");
+
+// Fetch TiltManager using the current loader, but don't save a
+// reference to it, because it might change with a tool reload.
+// We can clean this up once the command line is loadered.
+Object.defineProperty(this, "TiltManager", {
+ get: function() {
+ return devtools.require("devtools/tilt/tilt").TiltManager;
+ },
+ enumerable: true
+});
+
+/**
+ * 'tilt' command
+ */
+gcli.addCommand({
+ name: 'tilt',
+ description: gcli.lookup("tiltDesc"),
+ manual: gcli.lookup("tiltManual")
+});
+
+
+/**
+ * 'tilt open' command
+ */
+gcli.addCommand({
+ name: 'tilt open',
+ description: gcli.lookup("tiltOpenDesc"),
+ manual: gcli.lookup("tiltOpenManual"),
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ let Tilt = TiltManager.getTiltForBrowser(chromeWindow);
+ if (!Tilt.currentInstance) {
+ Tilt.toggle();
+ }
+ }
+});
+
+
+/**
+ * 'tilt toggle' command
+ */
+gcli.addCommand({
+ name: "tilt toggle",
+ buttonId: "command-button-tilt",
+ buttonClass: "command-button",
+ tooltipText: gcli.lookup("tiltToggleTooltip"),
+ hidden: true,
+ state: {
+ isChecked: function(aTarget) {
+ let browserWindow = aTarget.tab.ownerDocument.defaultView;
+ return !!TiltManager.getTiltForBrowser(browserWindow).currentInstance;
+ },
+ onChange: function(aTarget, aChangeHandler) {
+ let browserWindow = aTarget.tab.ownerDocument.defaultView;
+ let tilt = TiltManager.getTiltForBrowser(browserWindow);
+ tilt.on("change", aChangeHandler);
+ },
+ offChange: function(aTarget, aChangeHandler) {
+ if (aTarget.tab) {
+ let browserWindow = aTarget.tab.ownerDocument.defaultView;
+ let tilt = TiltManager.getTiltForBrowser(browserWindow);
+ tilt.off("change", aChangeHandler);
+ }
+ },
+ },
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ let Tilt = TiltManager.getTiltForBrowser(chromeWindow);
+ Tilt.toggle();
+ }
+});
+
+
+/**
+ * 'tilt translate' command
+ */
+gcli.addCommand({
+ name: 'tilt translate',
+ description: gcli.lookup("tiltTranslateDesc"),
+ manual: gcli.lookup("tiltTranslateManual"),
+ params: [
+ {
+ name: "x",
+ type: "number",
+ defaultValue: 0,
+ description: gcli.lookup("tiltTranslateXDesc"),
+ manual: gcli.lookup("tiltTranslateXManual")
+ },
+ {
+ name: "y",
+ type: "number",
+ defaultValue: 0,
+ description: gcli.lookup("tiltTranslateYDesc"),
+ manual: gcli.lookup("tiltTranslateYManual")
+ }
+ ],
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ let Tilt = TiltManager.getTiltForBrowser(chromeWindow);
+ if (Tilt.currentInstance) {
+ Tilt.currentInstance.controller.arcball.translate([args.x, args.y]);
+ }
+ }
+});
+
+
+/**
+ * 'tilt rotate' command
+ */
+gcli.addCommand({
+ name: 'tilt rotate',
+ description: gcli.lookup("tiltRotateDesc"),
+ manual: gcli.lookup("tiltRotateManual"),
+ params: [
+ {
+ name: "x",
+ type: { name: 'number', min: -360, max: 360, step: 10 },
+ defaultValue: 0,
+ description: gcli.lookup("tiltRotateXDesc"),
+ manual: gcli.lookup("tiltRotateXManual")
+ },
+ {
+ name: "y",
+ type: { name: 'number', min: -360, max: 360, step: 10 },
+ defaultValue: 0,
+ description: gcli.lookup("tiltRotateYDesc"),
+ manual: gcli.lookup("tiltRotateYManual")
+ },
+ {
+ name: "z",
+ type: { name: 'number', min: -360, max: 360, step: 10 },
+ defaultValue: 0,
+ description: gcli.lookup("tiltRotateZDesc"),
+ manual: gcli.lookup("tiltRotateZManual")
+ }
+ ],
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ let Tilt = TiltManager.getTiltForBrowser(chromeWindow);
+ if (Tilt.currentInstance) {
+ Tilt.currentInstance.controller.arcball.rotate([args.x, args.y, args.z]);
+ }
+ }
+});
+
+
+/**
+ * 'tilt zoom' command
+ */
+gcli.addCommand({
+ name: 'tilt zoom',
+ description: gcli.lookup("tiltZoomDesc"),
+ manual: gcli.lookup("tiltZoomManual"),
+ params: [
+ {
+ name: "zoom",
+ type: { name: 'number' },
+ description: gcli.lookup("tiltZoomAmountDesc"),
+ manual: gcli.lookup("tiltZoomAmountManual")
+ }
+ ],
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ let Tilt = TiltManager.getTiltForBrowser(chromeWindow);
+
+ if (Tilt.currentInstance) {
+ Tilt.currentInstance.controller.arcball.zoom(-args.zoom);
+ }
+ }
+});
+
+
+/**
+ * 'tilt reset' command
+ */
+gcli.addCommand({
+ name: 'tilt reset',
+ description: gcli.lookup("tiltResetDesc"),
+ manual: gcli.lookup("tiltResetManual"),
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ let Tilt = TiltManager.getTiltForBrowser(chromeWindow);
+
+ if (Tilt.currentInstance) {
+ Tilt.currentInstance.controller.arcball.reset();
+ }
+ }
+});
+
+
+/**
+ * 'tilt close' command
+ */
+gcli.addCommand({
+ name: 'tilt close',
+ description: gcli.lookup("tiltCloseDesc"),
+ manual: gcli.lookup("tiltCloseManual"),
+ exec: function(args, context) {
+ let chromeWindow = context.environment.chromeDocument.defaultView;
+ let Tilt = TiltManager.getTiltForBrowser(chromeWindow);
+
+ Tilt.destroy(Tilt.currentWindowId);
+ }
+});
diff --git a/browser/devtools/tilt/Makefile.in b/browser/devtools/tilt/Makefile.in
new file mode 100644
index 000000000..e4584f380
--- /dev/null
+++ b/browser/devtools/tilt/Makefile.in
@@ -0,0 +1,16 @@
+# 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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
+ $(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/tilt
diff --git a/browser/devtools/tilt/TiltWorkerCrafter.js b/browser/devtools/tilt/TiltWorkerCrafter.js
new file mode 100644
index 000000000..9884d059a
--- /dev/null
+++ b/browser/devtools/tilt/TiltWorkerCrafter.js
@@ -0,0 +1,280 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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";
+
+/**
+ * Given the initialization data (sizes and information about
+ * each DOM node) this worker sends back the arrays representing
+ * vertices, texture coords, colors, indices and all the needed data for
+ * rendering the DOM visualization mesh.
+ *
+ * Used in the TiltVisualization.Presenter object.
+ */
+self.onmessage = function TWC_onMessage(event)
+{
+ let data = event.data;
+ let maxGroupNodes = parseInt(data.maxGroupNodes);
+ let style = data.style;
+ let texWidth = data.texWidth;
+ let texHeight = data.texHeight;
+ let nodesInfo = data.nodesInfo;
+
+ let mesh = {
+ allVertices: [],
+ groups: [],
+ width: 0,
+ height: 0
+ };
+
+ let vertices;
+ let texCoord;
+ let color;
+ let stacksIndices;
+ let wireframeIndices;
+ let index;
+
+ // seed the random function to get the same values each time
+ // we're doing this to avoid ugly z-fighting with overlapping nodes
+ self.random.seed(0);
+
+ // go through all the dom nodes and compute the verts, texcoord etc.
+ for (let n = 0, len = nodesInfo.length; n < len; n++) {
+
+ // check if we need to start creating a new group
+ if (n % maxGroupNodes === 0) {
+ vertices = []; // recreate the arrays used to construct the 3D mesh data
+ texCoord = [];
+ color = [];
+ stacksIndices = [];
+ wireframeIndices = [];
+ index = 0;
+ }
+
+ let info = nodesInfo[n];
+ let coord = info.coord;
+
+ // calculate the stack x, y, z, width and height coordinates
+ let z = coord.depth + coord.thickness;
+ let y = coord.top;
+ let x = coord.left;
+ let w = coord.width;
+ let h = coord.height;
+
+ // the maximum texture size slices the visualization mesh where needed
+ if (x + w > texWidth) {
+ w = texWidth - x;
+ }
+ if (y + h > texHeight) {
+ h = texHeight - y;
+ }
+
+ x += self.random.next();
+ y += self.random.next();
+ w -= self.random.next() * 0.1;
+ h -= self.random.next() * 0.1;
+
+ let xpw = x + w;
+ let yph = y + h;
+ let zmt = coord.depth;
+
+ let xotw = x / texWidth;
+ let yoth = y / texHeight;
+ let xpwotw = xpw / texWidth;
+ let yphoth = yph / texHeight;
+
+ // calculate the margin fill color
+ let fill = style[info.name] || style.highlight.defaultFill;
+
+ let r = fill[0];
+ let g = fill[1];
+ let b = fill[2];
+ let g10 = r * 1.1;
+ let g11 = g * 1.1;
+ let g12 = b * 1.1;
+ let g20 = r * 0.6;
+ let g21 = g * 0.6;
+ let g22 = b * 0.6;
+
+ // compute the vertices
+ vertices.push(x, y, z, /* front */ // 0
+ x, yph, z, // 1
+ xpw, yph, z, // 2
+ xpw, y, z, // 3
+ // we don't duplicate vertices for the left and right faces, because
+ // they can be reused from the bottom and top faces; we do, however,
+ // duplicate some vertices from front face, because it has custom
+ // texture coordinates which are not shared by the other faces
+ x, y, z, /* front */ // 4
+ x, yph, z, // 5
+ xpw, yph, z, // 6
+ xpw, y, z, // 7
+ x, y, zmt, /* back */ // 8
+ x, yph, zmt, // 9
+ xpw, yph, zmt, // 10
+ xpw, y, zmt); // 11
+
+ // compute the texture coordinates
+ texCoord.push(xotw, yoth,
+ xotw, yphoth,
+ xpwotw, yphoth,
+ xpwotw, yoth,
+ -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0);
+
+ // compute the colors for each vertex in the mesh
+ color.push(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ g10, g11, g12,
+ g10, g11, g12,
+ g10, g11, g12,
+ g10, g11, g12,
+ g20, g21, g22,
+ g20, g21, g22,
+ g20, g21, g22,
+ g20, g21, g22);
+
+ let i = index; // number of vertex points, used to create the indices array
+ let ip1 = i + 1;
+ let ip2 = ip1 + 1;
+ let ip3 = ip2 + 1;
+ let ip4 = ip3 + 1;
+ let ip5 = ip4 + 1;
+ let ip6 = ip5 + 1;
+ let ip7 = ip6 + 1;
+ let ip8 = ip7 + 1;
+ let ip9 = ip8 + 1;
+ let ip10 = ip9 + 1;
+ let ip11 = ip10 + 1;
+
+ // compute the stack indices
+ stacksIndices.unshift(i, ip1, ip2, i, ip2, ip3,
+ ip8, ip9, ip5, ip8, ip5, ip4,
+ ip7, ip6, ip10, ip7, ip10, ip11,
+ ip8, ip4, ip7, ip8, ip7, ip11,
+ ip5, ip9, ip10, ip5, ip10, ip6);
+
+ // compute the wireframe indices
+ if (coord.thickness !== 0) {
+ wireframeIndices.unshift(i, ip1, ip1, ip2,
+ ip2, ip3, ip3, i,
+ ip8, i, ip9, ip1,
+ ip11, ip3, ip10, ip2);
+ }
+
+ // there are 12 vertices in a stack representing a node
+ index += 12;
+
+ // set the maximum mesh width and height to calculate the center offset
+ mesh.width = Math.max(w, mesh.width);
+ mesh.height = Math.max(h, mesh.height);
+
+ // check if we need to save the currently active group; this happens after
+ // we filled all the "slots" in a group or there aren't any remaining nodes
+ if (((n + 1) % maxGroupNodes === 0) || (n === len - 1)) {
+ mesh.groups.push({
+ vertices: vertices,
+ texCoord: texCoord,
+ color: color,
+ stacksIndices: stacksIndices,
+ wireframeIndices: wireframeIndices
+ });
+ mesh.allVertices = mesh.allVertices.concat(vertices);
+ }
+ }
+
+ self.postMessage(mesh);
+ close();
+};
+
+/**
+ * Utility functions for generating random numbers using the Alea algorithm.
+ */
+self.random = {
+
+ /**
+ * The generator function, automatically created with seed 0.
+ */
+ _generator: null,
+
+ /**
+ * Returns a new random number between [0..1)
+ */
+ next: function RNG_next()
+ {
+ return this._generator();
+ },
+
+ /**
+ * From http://baagoe.com/en/RandomMusings/javascript
+ * Johannes Baagoe <baagoe@baagoe.com>, 2010
+ *
+ * Seeds a random generator function with a set of passed arguments.
+ */
+ seed: function RNG_seed()
+ {
+ let s0 = 0;
+ let s1 = 0;
+ let s2 = 0;
+ let c = 1;
+
+ if (arguments.length === 0) {
+ return this.seed(+new Date());
+ } else {
+ s0 = this.mash(" ");
+ s1 = this.mash(" ");
+ s2 = this.mash(" ");
+
+ for (let i = 0, len = arguments.length; i < len; i++) {
+ s0 -= this.mash(arguments[i]);
+ if (s0 < 0) {
+ s0 += 1;
+ }
+ s1 -= this.mash(arguments[i]);
+ if (s1 < 0) {
+ s1 += 1;
+ }
+ s2 -= this.mash(arguments[i]);
+ if (s2 < 0) {
+ s2 += 1;
+ }
+ }
+
+ let random = function() {
+ let t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32
+ s0 = s1;
+ s1 = s2;
+ return (s2 = t - (c = t | 0));
+ };
+ random.uint32 = function() {
+ return random() * 0x100000000; // 2^32
+ };
+ random.fract53 = function() {
+ return random() +
+ (random() * 0x200000 | 0) * 1.1102230246251565e-16; // 2^-53
+ };
+ return (this._generator = random);
+ }
+ },
+
+ /**
+ * From http://baagoe.com/en/RandomMusings/javascript
+ * Johannes Baagoe <baagoe@baagoe.com>, 2010
+ */
+ mash: function RNG_mash(data)
+ {
+ let h, n = 0xefc8249d;
+
+ for (let i = 0, data = data.toString(), len = data.length; i < len; i++) {
+ n += data.charCodeAt(i);
+ h = 0.02519603282416938 * n;
+ n = h >>> 0;
+ h -= n;
+ h *= n;
+ n = h >>> 0;
+ h -= n;
+ n += h * 0x100000000; // 2^32
+ }
+ return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
+ }
+};
diff --git a/browser/devtools/tilt/TiltWorkerPicker.js b/browser/devtools/tilt/TiltWorkerPicker.js
new file mode 100644
index 000000000..d35e7677d
--- /dev/null
+++ b/browser/devtools/tilt/TiltWorkerPicker.js
@@ -0,0 +1,186 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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";
+
+/**
+ * This worker handles picking, given a set of vertices and a ray (calculates
+ * the intersection points and offers back information about the closest hit).
+ *
+ * Used in the TiltVisualization.Presenter object.
+ */
+self.onmessage = function TWP_onMessage(event)
+{
+ let data = event.data;
+ let vertices = data.vertices;
+ let ray = data.ray;
+
+ let intersection = null;
+ let hit = [];
+
+ // calculates the squared distance between two points
+ function dsq(p1, p2) {
+ let xd = p2[0] - p1[0];
+ let yd = p2[1] - p1[1];
+ let zd = p2[2] - p1[2];
+
+ return xd * xd + yd * yd + zd * zd;
+ }
+
+ // check each stack face in the visualization mesh for intersections with
+ // the mouse ray (using a ray picking algorithm)
+ for (let i = 0, len = vertices.length; i < len; i += 36) {
+
+ // the front quad
+ let v0f = [vertices[i], vertices[i + 1], vertices[i + 2]];
+ let v1f = [vertices[i + 3], vertices[i + 4], vertices[i + 5]];
+ let v2f = [vertices[i + 6], vertices[i + 7], vertices[i + 8]];
+ let v3f = [vertices[i + 9], vertices[i + 10], vertices[i + 11]];
+
+ // the back quad
+ let v0b = [vertices[i + 24], vertices[i + 25], vertices[i + 26]];
+ let v1b = [vertices[i + 27], vertices[i + 28], vertices[i + 29]];
+ let v2b = [vertices[i + 30], vertices[i + 31], vertices[i + 32]];
+ let v3b = [vertices[i + 33], vertices[i + 34], vertices[i + 35]];
+
+ // don't do anything with degenerate quads
+ if (!v0f[0] && !v1f[0] && !v2f[0] && !v3f[0]) {
+ continue;
+ }
+
+ // for each triangle in the stack box, check for the intersections
+ if (self.intersect(v0f, v1f, v2f, ray, hit) || // front left
+ self.intersect(v0f, v2f, v3f, ray, hit) || // front right
+ self.intersect(v0b, v1b, v1f, ray, hit) || // left back
+ self.intersect(v0b, v1f, v0f, ray, hit) || // left front
+ self.intersect(v3f, v2b, v3b, ray, hit) || // right back
+ self.intersect(v3f, v2f, v2b, ray, hit) || // right front
+ self.intersect(v0b, v0f, v3f, ray, hit) || // top left
+ self.intersect(v0b, v3f, v3b, ray, hit) || // top right
+ self.intersect(v1f, v1b, v2b, ray, hit) || // bottom left
+ self.intersect(v1f, v2b, v2f, ray, hit)) { // bottom right
+
+ // calculate the distance between the intersection hit point and camera
+ let d = dsq(hit, ray.origin);
+
+ // we're picking the closest stack in the mesh from the camera
+ if (intersection === null || d < intersection.distance) {
+ intersection = {
+ // each mesh stack is composed of 12 vertices, so there's information
+ // about a node once in 12 * 3 = 36 iterations (to avoid duplication)
+ index: i / 36,
+ distance: d
+ };
+ }
+ }
+ }
+
+ self.postMessage(intersection);
+ close();
+};
+
+/**
+ * Utility function for finding intersections between a ray and a triangle.
+ */
+self.intersect = (function() {
+
+ // creates a new instance of a vector
+ function create() {
+ return new Float32Array(3);
+ }
+
+ // performs a vector addition
+ function add(aVec, aVec2, aDest) {
+ aDest[0] = aVec[0] + aVec2[0];
+ aDest[1] = aVec[1] + aVec2[1];
+ aDest[2] = aVec[2] + aVec2[2];
+ return aDest;
+ }
+
+ // performs a vector subtraction
+ function subtract(aVec, aVec2, aDest) {
+ aDest[0] = aVec[0] - aVec2[0];
+ aDest[1] = aVec[1] - aVec2[1];
+ aDest[2] = aVec[2] - aVec2[2];
+ return aDest;
+ }
+
+ // performs a vector scaling
+ function scale(aVec, aVal, aDest) {
+ aDest[0] = aVec[0] * aVal;
+ aDest[1] = aVec[1] * aVal;
+ aDest[2] = aVec[2] * aVal;
+ return aDest;
+ }
+
+ // generates the cross product of two vectors
+ function cross(aVec, aVec2, aDest) {
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+ let x2 = aVec2[0];
+ let y2 = aVec2[1];
+ let z2 = aVec2[2];
+
+ aDest[0] = y * z2 - z * y2;
+ aDest[1] = z * x2 - x * z2;
+ aDest[2] = x * y2 - y * x2;
+ return aDest;
+ }
+
+ // calculates the dot product of two vectors
+ function dot(aVec, aVec2) {
+ return aVec[0] * aVec2[0] + aVec[1] * aVec2[1] + aVec[2] * aVec2[2];
+ }
+
+ let edge1 = create();
+ let edge2 = create();
+ let pvec = create();
+ let tvec = create();
+ let qvec = create();
+ let lvec = create();
+
+ // checks for ray-triangle intersections using the Fast Minimum-Storage
+ // (simplified) algorithm by Tomas Moller and Ben Trumbore
+ return function intersect(aVert0, aVert1, aVert2, aRay, aDest) {
+ let dir = aRay.direction;
+ let orig = aRay.origin;
+
+ // find vectors for two edges sharing vert0
+ subtract(aVert1, aVert0, edge1);
+ subtract(aVert2, aVert0, edge2);
+
+ // begin calculating determinant - also used to calculate the U parameter
+ cross(dir, edge2, pvec);
+
+ // if determinant is near zero, ray lines in plane of triangle
+ let inv_det = 1 / dot(edge1, pvec);
+
+ // calculate distance from vert0 to ray origin
+ subtract(orig, aVert0, tvec);
+
+ // calculate U parameter and test bounds
+ let u = dot(tvec, pvec) * inv_det;
+ if (u < 0 || u > 1) {
+ return false;
+ }
+
+ // prepare to test V parameter
+ cross(tvec, edge1, qvec);
+
+ // calculate V parameter and test bounds
+ let v = dot(dir, qvec) * inv_det;
+ if (v < 0 || u + v > 1) {
+ return false;
+ }
+
+ // calculate T, ray intersects triangle
+ let t = dot(edge2, qvec) * inv_det;
+
+ scale(dir, t, lvec);
+ add(orig, lvec, aDest);
+ return true;
+ };
+}());
diff --git a/browser/devtools/tilt/moz.build b/browser/devtools/tilt/moz.build
new file mode 100644
index 000000000..5abe8b3be
--- /dev/null
+++ b/browser/devtools/tilt/moz.build
@@ -0,0 +1,8 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+TEST_DIRS += ['test']
+
diff --git a/browser/devtools/tilt/test/Makefile.in b/browser/devtools/tilt/test/Makefile.in
new file mode 100644
index 000000000..cf3e6d351
--- /dev/null
+++ b/browser/devtools/tilt/test/Makefile.in
@@ -0,0 +1,63 @@
+# 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/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_BROWSER_FILES = \
+ head.js \
+ browser_tilt_01_lazy_getter.js \
+ browser_tilt_02_notifications-seq.js \
+ browser_tilt_02_notifications.js \
+ browser_tilt_02_notifications-tabs.js \
+ browser_tilt_03_tab_switch.js \
+ browser_tilt_04_initialization.js \
+ browser_tilt_05_destruction-esc.js \
+ browser_tilt_05_destruction-url.js \
+ browser_tilt_05_destruction.js \
+ browser_tilt_arcball-reset-typeahead.js \
+ browser_tilt_arcball-reset.js \
+ browser_tilt_arcball.js \
+ browser_tilt_controller.js \
+ browser_tilt_gl01.js \
+ browser_tilt_gl02.js \
+ browser_tilt_gl03.js \
+ browser_tilt_gl04.js \
+ browser_tilt_gl05.js \
+ browser_tilt_gl06.js \
+ browser_tilt_gl07.js \
+ browser_tilt_gl08.js \
+ browser_tilt_math01.js \
+ browser_tilt_math02.js \
+ browser_tilt_math03.js \
+ browser_tilt_math04.js \
+ browser_tilt_math05.js \
+ browser_tilt_math06.js \
+ browser_tilt_math07.js \
+ browser_tilt_picking.js \
+ browser_tilt_picking_inspector.js \
+ browser_tilt_picking_delete.js \
+ browser_tilt_picking_highlight01-offs.js \
+ browser_tilt_picking_highlight01.js \
+ browser_tilt_picking_highlight02.js \
+ browser_tilt_picking_highlight03.js \
+ browser_tilt_picking_miv.js \
+ browser_tilt_utils01.js \
+ browser_tilt_utils02.js \
+ browser_tilt_utils03.js \
+ browser_tilt_utils04.js \
+ browser_tilt_utils05.js \
+ browser_tilt_utils06.js \
+ browser_tilt_utils07.js \
+ browser_tilt_utils08.js \
+ browser_tilt_visualizer.js \
+ browser_tilt_zoom.js \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/devtools/tilt/test/browser_tilt_01_lazy_getter.js b/browser/devtools/tilt/test/browser_tilt_01_lazy_getter.js
new file mode 100644
index 000000000..de77ccb90
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_01_lazy_getter.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ ok(Tilt,
+ "The Tilt object wasn't got correctly via defineLazyGetter.");
+ is(Tilt.chromeWindow, window,
+ "The top-level window wasn't saved correctly");
+ ok(Tilt.visualizers,
+ "The holder object for all the instances of the visualizer doesn't exist.")
+ ok(Tilt.NOTIFICATIONS,
+ "The notifications constants weren't referenced correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_02_notifications-seq.js b/browser/devtools/tilt/test/browser_tilt_02_notifications-seq.js
new file mode 100644
index 000000000..18a71338f
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_02_notifications-seq.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tabEvents = "";
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping notifications test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping notifications test because WebGL isn't supported.");
+ return;
+ }
+
+ requestLongerTimeout(10);
+ waitForExplicitFinish();
+
+ createTab(function() {
+ Services.obs.addObserver(finalize, DESTROYED, false);
+ Services.obs.addObserver(obs_STARTUP, STARTUP, false);
+ Services.obs.addObserver(obs_INITIALIZING, INITIALIZING, false);
+ Services.obs.addObserver(obs_INITIALIZED, INITIALIZED, false);
+ Services.obs.addObserver(obs_DESTROYING, DESTROYING, false);
+ Services.obs.addObserver(obs_BEFORE_DESTROYED, BEFORE_DESTROYED, false);
+ Services.obs.addObserver(obs_DESTROYED, DESTROYED, false);
+
+ info("Starting up the Tilt notifications test.");
+ createTilt({}, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function obs_STARTUP(win) {
+ info("Handling the STARTUP notification.");
+ is(win, gBrowser.selectedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "STARTUP;";
+}
+
+function obs_INITIALIZING(win) {
+ info("Handling the INITIALIZING notification.");
+ is(win, gBrowser.selectedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "INITIALIZING;";
+}
+
+function obs_INITIALIZED(win) {
+ info("Handling the INITIALIZED notification.");
+ is(win, gBrowser.selectedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "INITIALIZED;";
+
+ Tilt.destroy(Tilt.currentWindowId, true);
+}
+
+function obs_DESTROYING(win) {
+ info("Handling the DESTROYING( notification.");
+ is(win, gBrowser.selectedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "DESTROYING;";
+}
+
+function obs_BEFORE_DESTROYED(win) {
+ info("Handling the BEFORE_DESTROYED notification.");
+ is(win, gBrowser.selectedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "BEFORE_DESTROYED;";
+}
+
+function obs_DESTROYED(win) {
+ info("Handling the DESTROYED notification.");
+ is(win, gBrowser.selectedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "DESTROYED;";
+}
+
+function finalize(win) {
+ if (!tabEvents) {
+ return;
+ }
+
+ is(win, gBrowser.selectedBrowser.contentWindow, "Saw the correct window");
+ is(tabEvents, "STARTUP;INITIALIZING;INITIALIZED;DESTROYING;BEFORE_DESTROYED;DESTROYED;",
+ "The notifications weren't fired in the correct order.");
+
+ cleanup();
+}
+
+function cleanup() {
+ info("Cleaning up the notifications test.");
+
+ Services.obs.removeObserver(finalize, DESTROYED);
+ Services.obs.removeObserver(obs_INITIALIZING, INITIALIZING);
+ Services.obs.removeObserver(obs_INITIALIZED, INITIALIZED);
+ Services.obs.removeObserver(obs_DESTROYING, DESTROYING);
+ Services.obs.removeObserver(obs_BEFORE_DESTROYED, BEFORE_DESTROYED);
+ Services.obs.removeObserver(obs_DESTROYED, DESTROYED);
+
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_02_notifications-tabs.js b/browser/devtools/tilt/test/browser_tilt_02_notifications-tabs.js
new file mode 100644
index 000000000..435af263c
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_02_notifications-tabs.js
@@ -0,0 +1,173 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tab0, tab1, tab2;
+let testStep = -1;
+
+let expected = [];
+function expect(notification, win) {
+ expected.push({ notification: notification, window: win });
+}
+
+function notification(win, topic) {
+ if (expected.length == 0) {
+ is(topic, null, "Shouldn't see a notification");
+ return;
+ }
+
+ let { notification, window } = expected.shift();
+ is(topic, notification, "Saw the expected notification");
+ is(win, window, "Saw the expected window");
+}
+
+function after(notification, callback) {
+ function observer() {
+ Services.obs.removeObserver(observer, notification);
+ executeSoon(callback);
+ }
+ Services.obs.addObserver(observer, notification, false);
+}
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping tab switch test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping tab switch test because WebGL isn't supported.");
+ return;
+ }
+
+ Services.obs.addObserver(notification, STARTUP, false);
+ Services.obs.addObserver(notification, INITIALIZING, false);
+ Services.obs.addObserver(notification, INITIALIZED, false);
+ Services.obs.addObserver(notification, DESTROYING, false);
+ Services.obs.addObserver(notification, BEFORE_DESTROYED, false);
+ Services.obs.addObserver(notification, DESTROYED, false);
+ Services.obs.addObserver(notification, SHOWN, false);
+ Services.obs.addObserver(notification, HIDDEN, false);
+
+ waitForExplicitFinish();
+
+ tab0 = gBrowser.selectedTab;
+ nextStep();
+}
+
+function createTab2() {
+}
+
+let testSteps = [
+ function step0() {
+ tab1 = createTab(function() {
+ expect(STARTUP, tab1.linkedBrowser.contentWindow);
+ expect(INITIALIZING, tab1.linkedBrowser.contentWindow);
+ expect(INITIALIZED, tab1.linkedBrowser.contentWindow);
+ after(INITIALIZED, nextStep);
+
+ createTilt({}, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+ },
+ function step1() {
+ expect(HIDDEN, tab1.linkedBrowser.contentWindow);
+
+ tab2 = createTab(function() {
+ expect(STARTUP, tab2.linkedBrowser.contentWindow);
+ expect(INITIALIZING, tab2.linkedBrowser.contentWindow);
+ expect(INITIALIZED, tab2.linkedBrowser.contentWindow);
+ after(INITIALIZED, nextStep);
+
+ createTilt({}, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+ },
+ function step2() {
+ expect(HIDDEN, tab2.linkedBrowser.contentWindow);
+ after(HIDDEN, nextStep);
+
+ gBrowser.selectedTab = tab0;
+ },
+ function step3() {
+ expect(SHOWN, tab2.linkedBrowser.contentWindow);
+ after(SHOWN, nextStep);
+
+ gBrowser.selectedTab = tab2;
+ },
+ function step4() {
+ expect(HIDDEN, tab2.linkedBrowser.contentWindow);
+ expect(SHOWN, tab1.linkedBrowser.contentWindow);
+ after(SHOWN, nextStep);
+
+ gBrowser.selectedTab = tab1;
+ },
+ function step5() {
+ expect(HIDDEN, tab1.linkedBrowser.contentWindow);
+ expect(SHOWN, tab2.linkedBrowser.contentWindow);
+ after(SHOWN, nextStep);
+
+ gBrowser.selectedTab = tab2;
+ },
+ function step6() {
+ expect(DESTROYING, tab2.linkedBrowser.contentWindow);
+ expect(BEFORE_DESTROYED, tab2.linkedBrowser.contentWindow);
+ expect(DESTROYED, tab2.linkedBrowser.contentWindow);
+ after(DESTROYED, nextStep);
+
+ Tilt.destroy(Tilt.currentWindowId, true);
+ },
+ function step7() {
+ expect(SHOWN, tab1.linkedBrowser.contentWindow);
+
+ gBrowser.removeCurrentTab();
+ tab2 = null;
+
+ expect(DESTROYING, tab1.linkedBrowser.contentWindow);
+ expect(HIDDEN, tab1.linkedBrowser.contentWindow);
+ expect(BEFORE_DESTROYED, tab1.linkedBrowser.contentWindow);
+ expect(DESTROYED, tab1.linkedBrowser.contentWindow);
+ after(DESTROYED, nextStep);
+
+ gBrowser.removeCurrentTab();
+ tab1 = null;
+ },
+ function step8_cleanup() {
+ is(gBrowser.selectedTab, tab0, "Should be back to the first tab");
+
+ cleanup();
+ }
+];
+
+function cleanup() {
+ if (tab1) {
+ gBrowser.removeTab(tab1);
+ tab1 = null;
+ }
+ if (tab2) {
+ gBrowser.removeTab(tab2);
+ tab2 = null;
+ }
+
+ Services.obs.removeObserver(notification, STARTUP);
+ Services.obs.removeObserver(notification, INITIALIZING);
+ Services.obs.removeObserver(notification, INITIALIZED);
+ Services.obs.removeObserver(notification, DESTROYING);
+ Services.obs.removeObserver(notification, BEFORE_DESTROYED);
+ Services.obs.removeObserver(notification, DESTROYED);
+ Services.obs.removeObserver(notification, SHOWN);
+ Services.obs.removeObserver(notification, HIDDEN);
+
+ finish();
+}
+
+function nextStep() {
+ let step = testSteps.shift();
+ info("Executing " + step.name);
+ step();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_02_notifications.js b/browser/devtools/tilt/test/browser_tilt_02_notifications.js
new file mode 100644
index 000000000..fe42001f1
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_02_notifications.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tab0, tab1;
+let testStep = -1;
+let tabEvents = "";
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping notifications test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping notifications test because WebGL isn't supported.");
+ return;
+ }
+
+ requestLongerTimeout(10);
+ waitForExplicitFinish();
+
+ gBrowser.tabContainer.addEventListener("TabSelect", tabSelect, false);
+ createNewTab();
+}
+
+function createNewTab() {
+ tab0 = gBrowser.selectedTab;
+
+ tab1 = createTab(function() {
+ Services.obs.addObserver(finalize, DESTROYED, false);
+ Services.obs.addObserver(tab_STARTUP, STARTUP, false);
+ Services.obs.addObserver(tab_INITIALIZING, INITIALIZING, false);
+ Services.obs.addObserver(tab_DESTROYING, DESTROYING, false);
+ Services.obs.addObserver(tab_SHOWN, SHOWN, false);
+ Services.obs.addObserver(tab_HIDDEN, HIDDEN, false);
+
+ info("Starting up the Tilt notifications test.");
+ createTilt({
+ onTiltOpen: function()
+ {
+ testStep = 0;
+ tabSelect();
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function tab_STARTUP(win) {
+ info("Handling the STARTUP notification.");
+ is(win, tab1.linkedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "STARTUP;";
+}
+
+function tab_INITIALIZING(win) {
+ info("Handling the INITIALIZING notification.");
+ is(win, tab1.linkedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "INITIALIZING;";
+}
+
+function tab_DESTROYING(win) {
+ info("Handling the DESTROYING notification.");
+ is(win, tab1.linkedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "DESTROYING;";
+}
+
+function tab_SHOWN(win) {
+ info("Handling the SHOWN notification.");
+ is(win, tab1.linkedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "SHOWN;";
+}
+
+function tab_HIDDEN(win) {
+ info("Handling the HIDDEN notification.");
+ is(win, tab1.linkedBrowser.contentWindow, "Saw the correct window");
+ tabEvents += "HIDDEN;";
+}
+
+let testSteps = [
+ function step0() {
+ info("Selecting tab0.");
+ gBrowser.selectedTab = tab0;
+ },
+ function step1() {
+ info("Selecting tab1.");
+ gBrowser.selectedTab = tab1;
+ },
+ function step2() {
+ info("Killing it.");
+ Tilt.destroy(Tilt.currentWindowId, true);
+ }
+];
+
+function finalize(win) {
+ if (!tabEvents) {
+ return;
+ }
+
+ is(win, tab1.linkedBrowser.contentWindow, "Saw the correct window");
+
+ is(tabEvents, "STARTUP;INITIALIZING;HIDDEN;SHOWN;DESTROYING;",
+ "The notifications weren't fired in the correct order.");
+
+ cleanup();
+}
+
+function cleanup() {
+ info("Cleaning up the notifications test.");
+
+ tab0 = null;
+ tab1 = null;
+
+ Services.obs.removeObserver(finalize, DESTROYED);
+ Services.obs.removeObserver(tab_INITIALIZING, INITIALIZING);
+ Services.obs.removeObserver(tab_DESTROYING, DESTROYING);
+ Services.obs.removeObserver(tab_SHOWN, SHOWN);
+ Services.obs.removeObserver(tab_HIDDEN, HIDDEN);
+
+ gBrowser.tabContainer.removeEventListener("TabSelect", tabSelect);
+ gBrowser.removeCurrentTab();
+ finish();
+}
+
+function tabSelect() {
+ if (testStep !== -1) {
+ executeSoon(testSteps[testStep]);
+ testStep++;
+ }
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_03_tab_switch.js b/browser/devtools/tilt/test/browser_tilt_03_tab_switch.js
new file mode 100644
index 000000000..0ea5c886f
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_03_tab_switch.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tab0, tab1, tab2;
+let testStep = -1;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping tab switch test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping tab switch test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ gBrowser.tabContainer.addEventListener("TabSelect", tabSelect, false);
+ createTab1();
+}
+
+function createTab1() {
+ tab0 = gBrowser.selectedTab;
+
+ tab1 = createTab(function() {
+ createTilt({
+ onTiltOpen: function()
+ {
+ createTab2();
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function createTab2() {
+ tab2 = createTab(function() {
+
+ createTilt({
+ onTiltOpen: function()
+ {
+ testStep = 0;
+ tabSelect();
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+let testSteps = [
+ function step0() {
+ gBrowser.selectedTab = tab1;
+ },
+ function step1() {
+ gBrowser.selectedTab = tab0;
+ },
+ function step2() {
+ gBrowser.selectedTab = tab1;
+ },
+ function step3() {
+ gBrowser.selectedTab = tab2;
+ },
+ function step4() {
+ Tilt.destroy(Tilt.currentWindowId);
+ gBrowser.removeCurrentTab();
+ tab2 = null;
+ },
+ function step5() {
+ Tilt.destroy(Tilt.currentWindowId);
+ gBrowser.removeCurrentTab();
+ tab1 = null;
+ },
+ function step6_cleanup() {
+ cleanup();
+ }
+];
+
+function cleanup() {
+ gBrowser.tabContainer.removeEventListener("TabSelect", tabSelect, false);
+
+ if (tab1) {
+ gBrowser.removeTab(tab1);
+ tab1 = null;
+ }
+ if (tab2) {
+ gBrowser.removeTab(tab2);
+ tab2 = null;
+ }
+
+ finish();
+}
+
+function tabSelect() {
+ if (testStep !== -1) {
+ executeSoon(testSteps[testStep]);
+ testStep++;
+ }
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_04_initialization.js b/browser/devtools/tilt/test/browser_tilt_04_initialization.js
new file mode 100644
index 000000000..314fb22e6
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_04_initialization.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping initialization test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping initialization test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ let id = TiltUtils.getWindowId(gBrowser.selectedBrowser.contentWindow);
+
+ is(id, Tilt.currentWindowId,
+ "The unique window identifiers should match for the same window.");
+
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ is(document.activeElement, instance.presenter.canvas,
+ "The visualizer canvas should be focused on initialization.");
+
+ ok(Tilt.visualizers[id] instanceof TiltVisualizer,
+ "A new instance of the visualizer wasn't created properly.");
+ ok(Tilt.visualizers[id].isInitialized(),
+ "The new instance of the visualizer wasn't initialized properly.");
+ },
+ onTiltClose: function()
+ {
+ is(document.activeElement, gBrowser.selectedBrowser,
+ "The focus wasn't correctly given back to the selectedBrowser.");
+
+ is(Tilt.visualizers[id], null,
+ "The current instance of the visualizer wasn't destroyed properly.");
+ },
+ onEnd: function()
+ {
+ cleanup();
+ }
+ }, true, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function cleanup() {
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_05_destruction-esc.js b/browser/devtools/tilt/test/browser_tilt_05_destruction-esc.js
new file mode 100644
index 000000000..503f79254
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_05_destruction-esc.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tiltOpened = false;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping destruction test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping destruction test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function()
+ {
+ tiltOpened = true;
+
+ Services.obs.addObserver(finalize, DESTROYED, false);
+ EventUtils.sendKey("ESCAPE");
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function finalize() {
+ let id = TiltUtils.getWindowId(gBrowser.selectedBrowser.contentWindow);
+
+ is(Tilt.visualizers[id], null,
+ "The current instance of the visualizer wasn't destroyed properly.");
+
+ cleanup();
+}
+
+function cleanup() {
+ if (tiltOpened) { Services.obs.removeObserver(finalize, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_05_destruction-url.js b/browser/devtools/tilt/test/browser_tilt_05_destruction-url.js
new file mode 100644
index 000000000..61d428218
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_05_destruction-url.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tiltOpened = false;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping destruction test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping destruction test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function()
+ {
+ tiltOpened = true;
+
+ Services.obs.addObserver(finalize, DESTROYED, false);
+ window.content.location = "about:mozilla";
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function finalize() {
+ let id = TiltUtils.getWindowId(gBrowser.selectedBrowser.contentWindow);
+
+ is(Tilt.visualizers[id], null,
+ "The current instance of the visualizer wasn't destroyed properly.");
+
+ cleanup();
+}
+
+function cleanup() {
+ if (tiltOpened) { Services.obs.removeObserver(finalize, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_05_destruction.js b/browser/devtools/tilt/test/browser_tilt_05_destruction.js
new file mode 100644
index 000000000..a083fa1bc
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_05_destruction.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tiltOpened = false;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping destruction test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping destruction test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function()
+ {
+ tiltOpened = true;
+
+ Services.obs.addObserver(finalize, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function finalize() {
+ let id = TiltUtils.getWindowId(gBrowser.selectedBrowser.contentWindow);
+
+ is(Tilt.visualizers[id], null,
+ "The current instance of the visualizer wasn't destroyed properly.");
+
+ cleanup();
+}
+
+function cleanup() {
+ if (tiltOpened) { Services.obs.removeObserver(finalize, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js b/browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js
new file mode 100644
index 000000000..366bfa323
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tiltOpened = false;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping part of the arcball test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping part of the arcball test because WebGL isn't supported.");
+ return;
+ }
+
+ requestLongerTimeout(10);
+ waitForExplicitFinish();
+ Services.prefs.setBoolPref("accessibility.typeaheadfind", true);
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ tiltOpened = true;
+
+ performTest(instance.presenter.canvas,
+ instance.controller.arcball, function() {
+
+ info("Killing arcball reset test.");
+
+ Services.prefs.setBoolPref("accessibility.typeaheadfind", false);
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function performTest(canvas, arcball, callback) {
+ is(document.activeElement, canvas,
+ "The visualizer canvas should be focused when performing this test.");
+
+
+ info("Starting arcball reset test.");
+
+ // start translating and rotating sometime at random
+
+ window.setTimeout(function() {
+ info("Synthesizing key down events.");
+
+ EventUtils.synthesizeKey("VK_S", { type: "keydown" }); // add a little
+ EventUtils.synthesizeKey("VK_RIGHT", { type: "keydown" }); // diversity
+
+ // wait for some arcball translations and rotations to happen
+
+ window.setTimeout(function() {
+ info("Synthesizing key up events.");
+
+ EventUtils.synthesizeKey("VK_S", { type: "keyup" });
+ EventUtils.synthesizeKey("VK_RIGHT", { type: "keyup" });
+
+ // ok, transformations finished, we can now try to reset the model view
+
+ window.setTimeout(function() {
+ info("Synthesizing arcball reset key press.");
+
+ arcball._onResetStart = function() {
+ info("Starting arcball reset animation.");
+ };
+
+ arcball._onResetStep = function() {
+ info("\nlastRot: " + quat4.str(arcball._lastRot) +
+ "\ndeltaRot: " + quat4.str(arcball._deltaRot) +
+ "\ncurrentRot: " + quat4.str(arcball._currentRot) +
+ "\nlastTrans: " + vec3.str(arcball._lastTrans) +
+ "\ndeltaTrans: " + vec3.str(arcball._deltaTrans) +
+ "\ncurrentTrans: " + vec3.str(arcball._currentTrans) +
+ "\nadditionalRot: " + vec3.str(arcball._additionalRot) +
+ "\nadditionalTrans: " + vec3.str(arcball._additionalTrans) +
+ "\nzoomAmount: " + arcball._zoomAmount);
+ };
+
+ arcball._onResetFinish = function() {
+ ok(isApproxVec(arcball._lastRot, [0, 0, 0, 1]),
+ "The arcball _lastRot field wasn't reset correctly.");
+ ok(isApproxVec(arcball._deltaRot, [0, 0, 0, 1]),
+ "The arcball _deltaRot field wasn't reset correctly.");
+ ok(isApproxVec(arcball._currentRot, [0, 0, 0, 1]),
+ "The arcball _currentRot field wasn't reset correctly.");
+
+ ok(isApproxVec(arcball._lastTrans, [0, 0, 0]),
+ "The arcball _lastTrans field wasn't reset correctly.");
+ ok(isApproxVec(arcball._deltaTrans, [0, 0, 0]),
+ "The arcball _deltaTrans field wasn't reset correctly.");
+ ok(isApproxVec(arcball._currentTrans, [0, 0, 0]),
+ "The arcball _currentTrans field wasn't reset correctly.");
+
+ ok(isApproxVec(arcball._additionalRot, [0, 0, 0]),
+ "The arcball _additionalRot field wasn't reset correctly.");
+ ok(isApproxVec(arcball._additionalTrans, [0, 0, 0]),
+ "The arcball _additionalTrans field wasn't reset correctly.");
+
+ ok(isApproxVec([arcball._zoomAmount], [0]),
+ "The arcball _zoomAmount field wasn't reset correctly.");
+
+ executeSoon(function() {
+ info("Finishing arcball reset test.");
+ callback();
+ });
+ };
+
+ EventUtils.synthesizeKey("VK_R", { type: "keydown" });
+
+ }, Math.random() * 1000); // leave enough time for transforms to happen
+ }, Math.random() * 1000);
+ }, Math.random() * 1000);
+}
+
+function cleanup() {
+ info("Cleaning up arcball reset test.");
+
+ if (tiltOpened) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_arcball-reset.js b/browser/devtools/tilt/test/browser_tilt_arcball-reset.js
new file mode 100644
index 000000000..72e11236e
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_arcball-reset.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tiltOpened = false;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping part of the arcball test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping part of the arcball test because WebGL isn't supported.");
+ return;
+ }
+
+ requestLongerTimeout(10);
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ tiltOpened = true;
+
+ performTest(instance.presenter.canvas,
+ instance.controller.arcball, function() {
+
+ info("Killing arcball reset test.");
+
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function performTest(canvas, arcball, callback) {
+ is(document.activeElement, canvas,
+ "The visualizer canvas should be focused when performing this test.");
+
+
+ info("Starting arcball reset test.");
+
+ // start translating and rotating sometime at random
+
+ window.setTimeout(function() {
+ info("Synthesizing key down events.");
+
+ EventUtils.synthesizeKey("VK_W", { type: "keydown" });
+ EventUtils.synthesizeKey("VK_LEFT", { type: "keydown" });
+
+ // wait for some arcball translations and rotations to happen
+
+ window.setTimeout(function() {
+ info("Synthesizing key up events.");
+
+ EventUtils.synthesizeKey("VK_W", { type: "keyup" });
+ EventUtils.synthesizeKey("VK_LEFT", { type: "keyup" });
+
+ // ok, transformations finished, we can now try to reset the model view
+
+ window.setTimeout(function() {
+ info("Synthesizing arcball reset key press.");
+
+ arcball._onResetStart = function() {
+ info("Starting arcball reset animation.");
+ };
+
+ arcball._onResetStep = function() {
+ info("\nlastRot: " + quat4.str(arcball._lastRot) +
+ "\ndeltaRot: " + quat4.str(arcball._deltaRot) +
+ "\ncurrentRot: " + quat4.str(arcball._currentRot) +
+ "\nlastTrans: " + vec3.str(arcball._lastTrans) +
+ "\ndeltaTrans: " + vec3.str(arcball._deltaTrans) +
+ "\ncurrentTrans: " + vec3.str(arcball._currentTrans) +
+ "\nadditionalRot: " + vec3.str(arcball._additionalRot) +
+ "\nadditionalTrans: " + vec3.str(arcball._additionalTrans) +
+ "\nzoomAmount: " + arcball._zoomAmount);
+ };
+
+ arcball._onResetFinish = function() {
+ ok(isApproxVec(arcball._lastRot, [0, 0, 0, 1]),
+ "The arcball _lastRot field wasn't reset correctly.");
+ ok(isApproxVec(arcball._deltaRot, [0, 0, 0, 1]),
+ "The arcball _deltaRot field wasn't reset correctly.");
+ ok(isApproxVec(arcball._currentRot, [0, 0, 0, 1]),
+ "The arcball _currentRot field wasn't reset correctly.");
+
+ ok(isApproxVec(arcball._lastTrans, [0, 0, 0]),
+ "The arcball _lastTrans field wasn't reset correctly.");
+ ok(isApproxVec(arcball._deltaTrans, [0, 0, 0]),
+ "The arcball _deltaTrans field wasn't reset correctly.");
+ ok(isApproxVec(arcball._currentTrans, [0, 0, 0]),
+ "The arcball _currentTrans field wasn't reset correctly.");
+
+ ok(isApproxVec(arcball._additionalRot, [0, 0, 0]),
+ "The arcball _additionalRot field wasn't reset correctly.");
+ ok(isApproxVec(arcball._additionalTrans, [0, 0, 0]),
+ "The arcball _additionalTrans field wasn't reset correctly.");
+
+ ok(isApproxVec([arcball._zoomAmount], [0]),
+ "The arcball _zoomAmount field wasn't reset correctly.");
+
+ executeSoon(function() {
+ info("Finishing arcball reset test.");
+ callback();
+ });
+ };
+
+ EventUtils.synthesizeKey("VK_R", { type: "keydown" });
+
+ }, Math.random() * 1000); // leave enough time for transforms to happen
+ }, Math.random() * 1000);
+ }, Math.random() * 1000);
+}
+
+function cleanup() {
+ info("Cleaning up arcball reset test.");
+
+ if (tiltOpened) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_arcball.js b/browser/devtools/tilt/test/browser_tilt_arcball.js
new file mode 100644
index 000000000..3d1078e1b
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_arcball.js
@@ -0,0 +1,496 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function cloneUpdate(update) {
+ return {
+ rotation: quat4.create(update.rotation),
+ translation: vec3.create(update.translation)
+ };
+}
+
+function isExpectedUpdate(update1, update2) {
+ if (update1.length !== update2.length) {
+ return false;
+ }
+ for (let i = 0, len = update1.length; i < len; i++) {
+ if (!isApproxVec(update1[i].rotation, update2[i].rotation) ||
+ !isApproxVec(update1[i].translation, update2[i].translation)) {
+ info("isExpectedUpdate expected " + JSON.stringify(update1), ", got " +
+ JSON.stringify(update2) + " instead.");
+ return false;
+ }
+ }
+ return true;
+}
+
+function test() {
+ let arcball1 = new TiltVisualizer.Arcball(window, 123, 456);
+
+ is(arcball1.width, 123,
+ "The first arcball width wasn't set correctly.");
+ is(arcball1.height, 456,
+ "The first arcball height wasn't set correctly.");
+ is(arcball1.radius, 123,
+ "The first arcball radius wasn't implicitly set correctly.");
+
+
+ let arcball2 = new TiltVisualizer.Arcball(window, 987, 654);
+
+ is(arcball2.width, 987,
+ "The second arcball width wasn't set correctly.");
+ is(arcball2.height, 654,
+ "The second arcball height wasn't set correctly.");
+ is(arcball2.radius, 654,
+ "The second arcball radius wasn't implicitly set correctly.");
+
+
+ let arcball3 = new TiltVisualizer.Arcball(window, 512, 512);
+
+ let sphereVec = vec3.create();
+ arcball3._pointToSphere(123, 456, 256, 512, 512, sphereVec);
+
+ ok(isApproxVec(sphereVec, [-0.009765625, 0.390625, 0.9204980731010437]),
+ "The _pointToSphere() function didn't map the coordinates correctly.");
+
+ let stack1 = [];
+ let expect1 = [
+ { rotation: [
+ -0.08877250552177429, 0.0242881178855896,
+ -0.04222869873046875, -0.9948599338531494],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.13086390495300293, 0.03413732722401619,
+ -0.06334304809570312, -0.9887855648994446],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.15138940513134003, 0.03854173421859741,
+ -0.07390022277832031, -0.9849540591239929],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.1615273654460907, 0.040619146078825,
+ -0.0791788101196289, -0.9828477501869202],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.16656573116779327, 0.04162723943591118,
+ -0.0818181037902832, -0.9817478656768799],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.16907735168933868, 0.042123712599277496,
+ -0.08313775062561035, -0.9811863303184509],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17033125460147858, 0.042370058596134186,
+ -0.08379757404327393, -0.9809026718139648],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17095772922039032, 0.04249274358153343,
+ -0.08412748575210571, -0.9807600975036621],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17127084732055664, 0.04255397245287895,
+ -0.0842924416065216, -0.9806886315345764],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.171427384018898, 0.042584557086229324,
+ -0.08437491953372955, -0.9806528687477112],
+ translation: [0, 0, 0] }];
+
+ arcball3.mouseDown(10, 10, 1);
+ arcball3.mouseMove(10, 100);
+ for (let i1 = 0; i1 < 10; i1++) {
+ stack1.push(cloneUpdate(arcball3.update()));
+ }
+
+ ok(isExpectedUpdate(stack1, expect1),
+ "Mouse down & move events didn't create the expected transform. results.");
+
+ let stack2 = [];
+ let expect2 = [
+ { rotation: [
+ -0.1684110015630722, 0.04199237748980522,
+ -0.0827873945236206, -0.9813361167907715],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.16936375200748444, 0.04218007251620293,
+ -0.08328840136528015, -0.9811217188835144],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17003019154071808, 0.04231100529432297,
+ -0.08363909274339676, -0.9809709787368774],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17049652338027954, 0.042402446269989014,
+ -0.0838845893740654, -0.9808651208877563],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17082282900810242, 0.042466338723897934,
+ -0.08405643701553345, -0.9807908535003662],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17105120420455933, 0.04251104220747948,
+ -0.08417671173810959, -0.9807388186454773],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17121103405952454, 0.04254228621721268,
+ -0.08426092565059662, -0.9807023406028748],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17132291197776794, 0.042564138770103455,
+ -0.08431987464427948, -0.9806767106056213],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.1714012324810028, 0.04257945716381073,
+ -0.08436112850904465, -0.9806588888168335],
+ translation: [0, 0, 0] },
+ { rotation: [
+ -0.17145603895187378, 0.042590171098709106,
+ -0.08439001441001892, -0.9806463718414307],
+ translation: [0, 0, 0] }];
+
+ arcball3.mouseUp(100, 100);
+ for (let i2 = 0; i2 < 10; i2++) {
+ stack2.push(cloneUpdate(arcball3.update()));
+ }
+
+ ok(isExpectedUpdate(stack2, expect2),
+ "Mouse up events didn't create the expected transformation results.");
+
+ let stack3 = [];
+ let expect3 = [
+ { rotation: [
+ -0.17149439454078674, 0.04259764403104782,
+ -0.08441022783517838, -0.9806375503540039],
+ translation: [0, 0, -1] },
+ { rotation: [
+ -0.17152123153209686, 0.04260288551449776,
+ -0.08442437648773193, -0.980631411075592],
+ translation: [0, 0, -1.899999976158142] },
+ { rotation: [
+ -0.1715400665998459, 0.04260658100247383,
+ -0.08443428575992584, -0.9806271195411682],
+ translation: [0, 0, -2.7100000381469727] },
+ { rotation: [
+ -0.17155319452285767, 0.04260912910103798,
+ -0.08444121479988098, -0.9806240797042847],
+ translation: [0, 0, -3.439000129699707] },
+ { rotation: [
+ -0.17156240344047546, 0.042610932141542435,
+ -0.08444607257843018, -0.9806219935417175],
+ translation: [0, 0, -4.095099925994873] },
+ { rotation: [
+ -0.1715688556432724, 0.042612191289663315,
+ -0.08444946259260178, -0.9806205034255981],
+ translation: [0, 0, -4.685589790344238] },
+ { rotation: [
+ -0.17157337069511414, 0.04261308163404465,
+ -0.0844518393278122, -0.980619490146637],
+ translation: [0, 0, -5.217031002044678] },
+ { rotation: [
+ -0.17157652974128723, 0.0426136814057827,
+ -0.0844535157084465, -0.9806187748908997],
+ translation: [0, 0, -5.6953277587890625] },
+ { rotation: [
+ -0.17157875001430511, 0.04261413961648941,
+ -0.08445467799901962, -0.9806182980537415],
+ translation: [0, 0, -6.125794887542725] },
+ { rotation: [
+ -0.17158031463623047, 0.04261442646384239,
+ -0.08445550501346588, -0.980617880821228],
+ translation: [0, 0, -6.5132155418396] }];
+
+ arcball3.zoom(10);
+ for (let i3 = 0; i3 < 10; i3++) {
+ stack3.push(cloneUpdate(arcball3.update()));
+ }
+
+ ok(isExpectedUpdate(stack3, expect3),
+ "Mouse zoom events didn't create the expected transformation results.");
+
+ let stack4 = [];
+ let expect4 = [
+ { rotation: [
+ -0.17158135771751404, 0.04261462762951851,
+ -0.08445606380701065, -0.9806176424026489],
+ translation: [0, 0, -6.861894130706787] },
+ { rotation: [
+ -0.1715821474790573, 0.04261479899287224,
+ -0.08445646613836288, -0.9806175231933594],
+ translation: [0, 0, -7.1757049560546875] },
+ { rotation: [
+ -0.1715826541185379, 0.0426148846745491,
+ -0.08445674180984497, -0.980617344379425],
+ translation: [0, 0, -7.458134651184082] },
+ { rotation: [
+ -0.17158304154872894, 0.04261497035622597,
+ -0.08445693552494049, -0.9806172847747803],
+ translation: [0, 0, -7.7123212814331055] },
+ { rotation: [
+ -0.17158329486846924, 0.042615000158548355,
+ -0.08445708453655243, -0.9806172251701355],
+ translation: [0, 0, -7.941089153289795] },
+ { rotation: [
+ -0.17158347368240356, 0.04261505603790283,
+ -0.084457166492939, -0.9806172251701355],
+ translation: [0, 0, -8.146980285644531] },
+ { rotation: [
+ -0.1715836226940155, 0.04261508584022522,
+ -0.08445724099874496, -0.9806171655654907],
+ translation: [0, 0, -8.332282066345215] },
+ { rotation: [
+ -0.17158368229866028, 0.04261508584022522,
+ -0.08445728570222855, -0.980617105960846],
+ translation: [0, 0, -8.499053955078125] },
+ { rotation: [
+ -0.17158377170562744, 0.04261511191725731,
+ -0.08445732295513153, -0.980617105960846],
+ translation: [0, 0, -8.649148941040039] },
+ { rotation: [
+ -0.17158380150794983, 0.04261511191725731,
+ -0.08445733785629272, -0.980617105960846],
+ translation: [0, 0, -8.784234046936035] }];
+
+ arcball3.keyDown(arcball3.rotateKeys.left);
+ arcball3.keyDown(arcball3.rotateKeys.right);
+ arcball3.keyDown(arcball3.rotateKeys.up);
+ arcball3.keyDown(arcball3.rotateKeys.down);
+ arcball3.keyDown(arcball3.panKeys.left);
+ arcball3.keyDown(arcball3.panKeys.right);
+ arcball3.keyDown(arcball3.panKeys.up);
+ arcball3.keyDown(arcball3.panKeys.down);
+ for (let i4 = 0; i4 < 10; i4++) {
+ stack4.push(cloneUpdate(arcball3.update()));
+ }
+
+ ok(isExpectedUpdate(stack4, expect4),
+ "Key down events didn't create the expected transformation results.");
+
+ let stack5 = [];
+ let expect5 = [
+ { rotation: [
+ -0.1715838462114334, 0.04261511191725731,
+ -0.08445736765861511, -0.980617105960846],
+ translation: [0, 0, -8.905810356140137] },
+ { rotation: [
+ -0.1715838462114334, 0.04261511191725731,
+ -0.08445736765861511, -0.980617105960846],
+ translation: [0, 0, -9.015229225158691] },
+ { rotation: [
+ -0.1715838462114334, 0.04261511191725731,
+ -0.08445736765861511, -0.980617105960846],
+ translation: [0, 0, -9.113706588745117] },
+ { rotation: [
+ -0.1715838611125946, 0.04261511191725731,
+ -0.0844573825597763, -0.9806170463562012],
+ translation: [0, 0, -9.202336311340332] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -9.282102584838867] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -9.35389232635498] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -9.418502807617188] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -9.476652145385742] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -9.528986930847168] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -9.576087951660156] }];
+
+ arcball3.keyUp(arcball3.rotateKeys.left);
+ arcball3.keyUp(arcball3.rotateKeys.right);
+ arcball3.keyUp(arcball3.rotateKeys.up);
+ arcball3.keyUp(arcball3.rotateKeys.down);
+ arcball3.keyUp(arcball3.panKeys.left);
+ arcball3.keyUp(arcball3.panKeys.right);
+ arcball3.keyUp(arcball3.panKeys.up);
+ arcball3.keyUp(arcball3.panKeys.down);
+ for (let i5 = 0; i5 < 10; i5++) {
+ stack5.push(cloneUpdate(arcball3.update()));
+ }
+
+ ok(isExpectedUpdate(stack5, expect5),
+ "Key up events didn't create the expected transformation results.");
+
+ let stack6 = [];
+ let expect6 = [
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -9.618478775024414] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, -6.156630992889404] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 0.4590320587158203] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 9.913128852844238] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 21.921815872192383] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 36.22963333129883] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 52.60667037963867] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 70.84600067138672] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 90.76139831542969] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 112.18525695800781] }];
+
+ arcball3.keyDown(arcball3.zoomKeys["in"][0]);
+ arcball3.keyDown(arcball3.zoomKeys["in"][1]);
+ arcball3.keyDown(arcball3.zoomKeys["in"][2]);
+ for (let i6 = 0; i6 < 10; i6++) {
+ stack6.push(cloneUpdate(arcball3.update()));
+ }
+ arcball3.keyUp(arcball3.zoomKeys["in"][0]);
+ arcball3.keyUp(arcball3.zoomKeys["in"][1]);
+ arcball3.keyUp(arcball3.zoomKeys["in"][2]);
+
+ ok(isExpectedUpdate(stack6, expect6),
+ "Key zoom in events didn't create the expected transformation results.");
+
+ let stack7 = [];
+ let expect7 = [
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 134.96673583984375] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 151.97006225585938] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 163.77305603027344] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 170.895751953125] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 173.80618286132812] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 172.92556762695312] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 168.6330108642578] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 161.26971435546875] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 151.1427459716797] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 138.52847290039062] }];
+
+ arcball3.keyDown(arcball3.zoomKeys["out"][0]);
+ arcball3.keyDown(arcball3.zoomKeys["out"][1]);
+ for (let i7 = 0; i7 < 10; i7++) {
+ stack7.push(cloneUpdate(arcball3.update()));
+ }
+ arcball3.keyUp(arcball3.zoomKeys["out"][0]);
+ arcball3.keyUp(arcball3.zoomKeys["out"][1]);
+
+ ok(isExpectedUpdate(stack7, expect7),
+ "Key zoom out events didn't create the expected transformation results.");
+
+ let stack8 = [];
+ let expect8 = [
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 123.67562866210938] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 111.30806732177734] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 100.17726135253906] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 90.15953826904297] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 81.14358520507812] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 73.02922821044922] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 65.72630310058594] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 59.15367126464844] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 53.238304138183594] },
+ { rotation: [
+ -0.17158392071723938, 0.0426151417195797,
+ -0.0844573974609375, -0.980617105960846],
+ translation: [0, 0, 47.91447448730469] }];
+
+ arcball3.keyDown(arcball3.zoomKeys["unzoom"]);
+ for (let i8 = 0; i8 < 10; i8++) {
+ stack8.push(cloneUpdate(arcball3.update()));
+ }
+ arcball3.keyUp(arcball3.zoomKeys["unzoom"]);
+
+ ok(isExpectedUpdate(stack8, expect8),
+ "Key zoom reset events didn't create the expected transformation results.");
+
+
+ arcball3.resize(123, 456);
+ is(arcball3.width, 123,
+ "The third arcball width wasn't updated correctly.");
+ is(arcball3.height, 456,
+ "The third arcball height wasn't updated correctly.");
+ is(arcball3.radius, 123,
+ "The third arcball radius wasn't implicitly updated correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_controller.js b/browser/devtools/tilt/test/browser_tilt_controller.js
new file mode 100644
index 000000000..0dbf37aad
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_controller.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping controller test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping controller test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ let canvas = instance.presenter.canvas;
+ let prev_tran = vec3.create([0, 0, 0]);
+ let prev_rot = quat4.create([0, 0, 0, 1]);
+
+ function tran() {
+ return instance.presenter.transforms.translation;
+ }
+
+ function rot() {
+ return instance.presenter.transforms.rotation;
+ }
+
+ function save() {
+ prev_tran = vec3.create(tran());
+ prev_rot = quat4.create(rot());
+ }
+
+ ok(isEqualVec(tran(), prev_tran),
+ "At init, the translation should be zero.");
+ ok(isEqualVec(rot(), prev_rot),
+ "At init, the rotation should be zero.");
+
+
+ function testEventCancel(cancellingEvent) {
+ is(document.activeElement, canvas,
+ "The visualizer canvas should be focused when performing this test.");
+
+ EventUtils.synthesizeKey("VK_A", { type: "keydown" });
+ EventUtils.synthesizeKey("VK_LEFT", { type: "keydown" });
+ instance.controller._update();
+
+ ok(!isEqualVec(tran(), prev_tran),
+ "After a translation key is pressed, the vector should change.");
+ ok(!isEqualVec(rot(), prev_rot),
+ "After a rotation key is pressed, the quaternion should change.");
+
+ save();
+
+
+ cancellingEvent();
+ instance.controller._update();
+
+ ok(!isEqualVec(tran(), prev_tran),
+ "Even if the canvas lost focus, the vector has some inertia.");
+ ok(!isEqualVec(rot(), prev_rot),
+ "Even if the canvas lost focus, the quaternion has some inertia.");
+
+ save();
+
+
+ while (!isEqualVec(tran(), prev_tran) ||
+ !isEqualVec(rot(), prev_rot)) {
+ instance.controller._update();
+ save();
+ }
+
+ ok(isEqualVec(tran(), prev_tran) && isEqualVec(rot(), prev_rot),
+ "After focus lost, the transforms inertia eventually stops.");
+ }
+
+ info("Setting typeaheadfind to true.");
+
+ Services.prefs.setBoolPref("accessibility.typeaheadfind", true);
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("T", { type: "keydown", altKey: 1 });
+ });
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("I", { type: "keydown", ctrlKey: 1 });
+ });
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("L", { type: "keydown", metaKey: 1 });
+ });
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("T", { type: "keydown", shiftKey: 1 });
+ });
+
+ info("Setting typeaheadfind to false.");
+
+ Services.prefs.setBoolPref("accessibility.typeaheadfind", false);
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("T", { type: "keydown", altKey: 1 });
+ });
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("I", { type: "keydown", ctrlKey: 1 });
+ });
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("L", { type: "keydown", metaKey: 1 });
+ });
+ testEventCancel(function() {
+ EventUtils.synthesizeKey("T", { type: "keydown", shiftKey: 1 });
+ });
+
+ info("Testing if loosing focus halts any stacked arcball animations.");
+
+ testEventCancel(function() {
+ gBrowser.selectedBrowser.contentWindow.focus();
+ });
+ },
+ onEnd: function()
+ {
+ cleanup();
+ }
+ }, true, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function cleanup() {
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl01.js b/browser/devtools/tilt/test/browser_tilt_gl01.js
new file mode 100644
index 000000000..7f931d7a3
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl01.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl01 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ ok(renderer,
+ "The TiltGL.Renderer constructor should have initialized a new object.");
+
+ ok(gl instanceof WebGLRenderingContext,
+ "The renderer context wasn't created correctly from the passed canvas.");
+
+
+ let clearColor = gl.getParameter(gl.COLOR_CLEAR_VALUE),
+ clearDepth = gl.getParameter(gl.DEPTH_CLEAR_VALUE);
+
+ is(clearColor[0], 0,
+ "The default red clear color wasn't set correctly at initialization.");
+ is(clearColor[1], 0,
+ "The default green clear color wasn't set correctly at initialization.");
+ is(clearColor[2], 0,
+ "The default blue clear color wasn't set correctly at initialization.");
+ is(clearColor[3], 0,
+ "The default alpha clear color wasn't set correctly at initialization.");
+ is(clearDepth, 1,
+ "The default clear depth wasn't set correctly at initialization.");
+
+ is(renderer.width, canvas.width,
+ "The renderer width wasn't set correctly from the passed canvas.");
+ is(renderer.height, canvas.height,
+ "The renderer height wasn't set correctly from the passed canvas.");
+
+ ok(renderer.mvMatrix,
+ "The model view matrix wasn't initialized properly.");
+ ok(renderer.projMatrix,
+ "The model view matrix wasn't initialized properly.");
+
+ ok(isApproxVec(renderer._fillColor, [1, 1, 1, 1]),
+ "The default fill color wasn't set correctly.");
+ ok(isApproxVec(renderer._strokeColor, [0, 0, 0, 1]),
+ "The default stroke color wasn't set correctly.");
+ is(renderer._strokeWeightValue, 1,
+ "The default stroke weight wasn't set correctly.");
+
+ ok(renderer._colorShader,
+ "A default color shader should have been created.");
+
+ ok(typeof renderer.Program, "function",
+ "At init, the renderer should have created a Program constructor.");
+ ok(typeof renderer.VertexBuffer, "function",
+ "At init, the renderer should have created a VertexBuffer constructor.");
+ ok(typeof renderer.IndexBuffer, "function",
+ "At init, the renderer should have created a IndexBuffer constructor.");
+ ok(typeof renderer.Texture, "function",
+ "At init, the renderer should have created a Texture constructor.");
+
+ renderer.depthTest(true);
+ is(gl.getParameter(gl.DEPTH_TEST), true,
+ "The depth test wasn't enabled when requested.");
+
+ renderer.depthTest(false);
+ is(gl.getParameter(gl.DEPTH_TEST), false,
+ "The depth test wasn't disabled when requested.");
+
+ renderer.stencilTest(true);
+ is(gl.getParameter(gl.STENCIL_TEST), true,
+ "The stencil test wasn't enabled when requested.");
+
+ renderer.stencilTest(false);
+ is(gl.getParameter(gl.STENCIL_TEST), false,
+ "The stencil test wasn't disabled when requested.");
+
+ renderer.cullFace("front");
+ is(gl.getParameter(gl.CULL_FACE), true,
+ "The cull face wasn't enabled when requested.");
+ is(gl.getParameter(gl.CULL_FACE_MODE), gl.FRONT,
+ "The cull face front mode wasn't set correctly.");
+
+ renderer.cullFace("back");
+ is(gl.getParameter(gl.CULL_FACE), true,
+ "The cull face wasn't enabled when requested.");
+ is(gl.getParameter(gl.CULL_FACE_MODE), gl.BACK,
+ "The cull face back mode wasn't set correctly.");
+
+ renderer.cullFace("both");
+ is(gl.getParameter(gl.CULL_FACE), true,
+ "The cull face wasn't enabled when requested.");
+ is(gl.getParameter(gl.CULL_FACE_MODE), gl.FRONT_AND_BACK,
+ "The cull face back mode wasn't set correctly.");
+
+ renderer.cullFace(false);
+ is(gl.getParameter(gl.CULL_FACE), false,
+ "The cull face wasn't disabled when requested.");
+
+ renderer.frontFace("cw");
+ is(gl.getParameter(gl.FRONT_FACE), gl.CW,
+ "The front face cw mode wasn't set correctly.");
+
+ renderer.frontFace("ccw");
+ is(gl.getParameter(gl.FRONT_FACE), gl.CCW,
+ "The front face ccw mode wasn't set correctly.");
+
+ renderer.blendMode("alpha");
+ is(gl.getParameter(gl.BLEND), true,
+ "The blend mode wasn't enabled when requested.");
+ is(gl.getParameter(gl.BLEND_SRC_ALPHA), gl.SRC_ALPHA,
+ "The soruce blend func wasn't set correctly.");
+ is(gl.getParameter(gl.BLEND_DST_ALPHA), gl.ONE_MINUS_SRC_ALPHA,
+ "The destination blend func wasn't set correctly.");
+
+ renderer.blendMode("add");
+ is(gl.getParameter(gl.BLEND), true,
+ "The blend mode wasn't enabled when requested.");
+ is(gl.getParameter(gl.BLEND_SRC_ALPHA), gl.SRC_ALPHA,
+ "The soruce blend func wasn't set correctly.");
+ is(gl.getParameter(gl.BLEND_DST_ALPHA), gl.ONE,
+ "The destination blend func wasn't set correctly.");
+
+ renderer.blendMode(false);
+ is(gl.getParameter(gl.CULL_FACE), false,
+ "The blend mode wasn't disabled when requested.");
+
+
+ is(gl.getParameter(gl.CURRENT_PROGRAM), null,
+ "No program should be initially set in the WebGL context.");
+
+ renderer.useColorShader(new renderer.VertexBuffer([1, 2, 3], 3));
+
+ ok(gl.getParameter(gl.CURRENT_PROGRAM) instanceof WebGLProgram,
+ "The correct program hasn't been set in the WebGL context.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl02.js b/browser/devtools/tilt/test/browser_tilt_gl02.js
new file mode 100644
index 000000000..a55acdd41
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl02.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl02 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ renderer.fill([1, 0, 0, 1]);
+ ok(isApproxVec(renderer._fillColor, [1, 0, 0, 1]),
+ "The fill color wasn't set correctly.");
+
+ renderer.stroke([0, 1, 0, 1]);
+ ok(isApproxVec(renderer._strokeColor, [0, 1, 0, 1]),
+ "The stroke color wasn't set correctly.");
+
+ renderer.strokeWeight(2);
+ is(renderer._strokeWeightValue, 2,
+ "The stroke weight wasn't set correctly.");
+ is(gl.getParameter(gl.LINE_WIDTH), 2,
+ "The stroke weight wasn't applied correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl03.js b/browser/devtools/tilt/test/browser_tilt_gl03.js
new file mode 100644
index 000000000..491f9279b
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl03.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl03 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ renderer.defaults();
+ is(gl.getParameter(gl.DEPTH_TEST), true,
+ "The depth test wasn't set to the correct default value.");
+ is(gl.getParameter(gl.STENCIL_TEST), false,
+ "The stencil test wasn't set to the correct default value.");
+ is(gl.getParameter(gl.CULL_FACE), false,
+ "The cull face wasn't set to the correct default value.");
+ is(gl.getParameter(gl.FRONT_FACE), gl.CCW,
+ "The front face wasn't set to the correct default value.");
+ is(gl.getParameter(gl.BLEND), true,
+ "The blend mode wasn't set to the correct default value.");
+ is(gl.getParameter(gl.BLEND_SRC_ALPHA), gl.SRC_ALPHA,
+ "The soruce blend func wasn't set to the correct default value.");
+ is(gl.getParameter(gl.BLEND_DST_ALPHA), gl.ONE_MINUS_SRC_ALPHA,
+ "The destination blend func wasn't set to the correct default value.");
+
+
+ ok(isApproxVec(renderer._fillColor, [1, 1, 1, 1]),
+ "The fill color wasn't set to the correct default value.");
+ ok(isApproxVec(renderer._strokeColor, [0, 0, 0, 1]),
+ "The stroke color wasn't set to the correct default value.");
+ is(renderer._strokeWeightValue, 1,
+ "The stroke weight wasn't set to the correct default value.");
+ is(gl.getParameter(gl.LINE_WIDTH), 1,
+ "The stroke weight wasn't applied with the correct default value.");
+
+
+ ok(isApproxVec(renderer.projMatrix, [
+ 1.2071068286895752, 0, 0, 0, 0, -2.4142136573791504, 0, 0, 0, 0,
+ -1.0202020406723022, -1, -181.06602478027344, 181.06602478027344,
+ 148.14492797851562, 181.06602478027344
+ ]), "The default perspective projection matrix wasn't set correctly.");
+
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1
+ ]), "The default model view matrix wasn't set correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl04.js b/browser/devtools/tilt/test/browser_tilt_gl04.js
new file mode 100644
index 000000000..4ccfd472a
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl04.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl04 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ renderer.perspective();
+ ok(isApproxVec(renderer.projMatrix, [
+ 1.2071068286895752, 0, 0, 0, 0, -2.4142136573791504, 0, 0, 0, 0,
+ -1.0202020406723022, -1, -181.06602478027344, 181.06602478027344,
+ 148.14492797851562, 181.06602478027344
+ ]), "The default perspective proj. matrix wasn't calculated correctly.");
+
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1
+ ]), "Changing the perpective matrix should reset the modelview by default.");
+
+
+ renderer.ortho();
+ ok(isApproxVec(renderer.projMatrix, [
+ 0.006666666828095913, 0, 0, 0, 0, -0.013333333656191826, 0, 0, 0, 0, -1,
+ 0, -1, 1, 0, 1
+ ]), "The default ortho proj. matrix wasn't calculated correctly.");
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1
+ ]), "Changing the ortho matrix should reset the modelview by default.");
+
+
+ renderer.projection(mat4.perspective(45, 1, 0.1, 100));
+ ok(isApproxVec(renderer.projMatrix, [
+ 2.4142136573791504, 0, 0, 0, 0, 2.4142136573791504, 0, 0, 0, 0,
+ -1.0020020008087158, -1, 0, 0, -0.20020020008087158, 0
+ ]), "A custom proj. matrix couldn't be set correctly.");
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1
+ ]), "Setting a custom proj. matrix should reset the model view by default.");
+
+
+ renderer.translate(1, 1, 1);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1
+ ]), "The translation transformation wasn't applied correctly.");
+
+ renderer.rotate(0.5, 1, 1, 1);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 0.9183883666992188, 0.317602276802063, -0.23599065840244293, 0,
+ -0.23599065840244293, 0.9183883666992188, 0.317602276802063, 0,
+ 0.317602276802063, -0.23599065840244293, 0.9183883666992188, 0, 1, 1, 1, 1
+ ]), "The rotation transformation wasn't applied correctly.");
+
+ renderer.rotateX(0.5);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 0.9183883666992188, 0.317602276802063, -0.23599065840244293, 0,
+ -0.05483464524149895, 0.6928216814994812, 0.7190210819244385, 0,
+ 0.391862154006958, -0.6474001407623291, 0.6536949872970581, 0, 1, 1, 1, 1
+ ]), "The X rotation transformation wasn't applied correctly.");
+
+ renderer.rotateY(0.5);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 0.6180928945541382, 0.5891023874282837, -0.5204993486404419, 0,
+ -0.05483464524149895, 0.6928216814994812, 0.7190210819244385, 0,
+ 0.7841902375221252, -0.4158804416656494, 0.4605313837528229, 0, 1, 1, 1, 1
+ ]), "The Y rotation transformation wasn't applied correctly.");
+
+ renderer.rotateZ(0.5);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 0.5161384344100952, 0.8491423726081848, -0.11206408590078354, 0,
+ -0.3444514572620392, 0.3255774974822998, 0.8805410265922546, 0,
+ 0.7841902375221252, -0.4158804416656494, 0.4605313837528229, 0, 1, 1, 1, 1
+ ]), "The Z rotation transformation wasn't applied correctly.");
+
+ renderer.scale(2, 2, 2);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1.0322768688201904, 1.6982847452163696, -0.22412817180156708, 0,
+ -0.6889029145240784, 0.6511549949645996, 1.7610820531845093, 0,
+ 1.5683804750442505, -0.8317608833312988, 0.9210627675056458, 0, 1, 1, 1, 1
+ ]), "The Z rotation transformation wasn't applied correctly.");
+
+ renderer.transform(mat4.create());
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1.0322768688201904, 1.6982847452163696, -0.22412817180156708, 0,
+ -0.6889029145240784, 0.6511549949645996, 1.7610820531845093, 0,
+ 1.5683804750442505, -0.8317608833312988, 0.9210627675056458, 0, 1, 1, 1, 1
+ ]), "The identity matrix transformation wasn't applied correctly.");
+
+ renderer.origin(1, 1, 1);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1
+ ]), "The origin wasn't reset to identity correctly.");
+
+ renderer.translate(1, 2);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 0, 1
+ ]), "The second translation transformation wasn't applied correctly.");
+
+ renderer.scale(3, 4);
+ ok(isApproxVec(renderer.mvMatrix, [
+ 3, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1, 0, 1, 2, 0, 1
+ ]), "The second scale transformation wasn't applied correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl05.js b/browser/devtools/tilt/test/browser_tilt_gl05.js
new file mode 100644
index 000000000..ffc4d1bb6
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl05.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl05 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ let mesh = {
+ vertices: new renderer.VertexBuffer([1, 2, 3], 3),
+ indices: new renderer.IndexBuffer([1]),
+ };
+
+ ok(mesh.vertices instanceof TiltGL.VertexBuffer,
+ "The mesh vertices weren't saved at initialization.");
+ ok(mesh.indices instanceof TiltGL.IndexBuffer,
+ "The mesh indices weren't saved at initialization.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl06.js b/browser/devtools/tilt/test/browser_tilt_gl06.js
new file mode 100644
index 000000000..0f900d1b1
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl06.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl06 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ let vb = new renderer.VertexBuffer([1, 2, 3, 4, 5, 6], 3);
+
+ ok(vb instanceof TiltGL.VertexBuffer,
+ "The vertex buffer object wasn't instantiated correctly.");
+ ok(vb._ref,
+ "The vertex buffer gl element wasn't created at initialization.");
+ ok(vb.components,
+ "The vertex buffer components weren't created at initialization.");
+ is(vb.itemSize, 3,
+ "The vertex buffer item size isn't set correctly.");
+ is(vb.numItems, 2,
+ "The vertex buffer number of items weren't calculated correctly.");
+
+
+ let ib = new renderer.IndexBuffer([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
+
+ ok(ib instanceof TiltGL.IndexBuffer,
+ "The index buffer object wasn't instantiated correctly.");
+ ok(ib._ref,
+ "The index buffer gl element wasn't created at initialization.");
+ ok(ib.components,
+ "The index buffer components weren't created at initialization.");
+ is(ib.itemSize, 1,
+ "The index buffer item size isn't set correctly.");
+ is(ib.numItems, 10,
+ "The index buffer number of items weren't calculated correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl07.js b/browser/devtools/tilt/test/browser_tilt_gl07.js
new file mode 100644
index 000000000..671a4fa2c
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl07.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl07 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ let p = new renderer.Program({
+ vs: TiltGL.ColorShader.vs,
+ fs: TiltGL.ColorShader.fs,
+ attributes: ["vertexPosition"],
+ uniforms: ["mvMatrix", "projMatrix", "fill"]
+ });
+
+ ok(p instanceof TiltGL.Program,
+ "The program object wasn't instantiated correctly.");
+
+ ok(p._ref,
+ "The program WebGL object wasn't created properly.");
+ isnot(p._id, -1,
+ "The program id wasn't set properly.");
+ ok(p._attributes,
+ "The program attributes cache wasn't created properly.");
+ ok(p._uniforms,
+ "The program uniforms cache wasn't created properly.");
+
+ is(typeof p._attributes.vertexPosition, "number",
+ "The vertexPosition attribute wasn't cached as it should.");
+ is(typeof p._uniforms.mvMatrix, "object",
+ "The mvMatrix uniform wasn't cached as it should.");
+ is(typeof p._uniforms.projMatrix, "object",
+ "The projMatrix uniform wasn't cached as it should.");
+ is(typeof p._uniforms.fill, "object",
+ "The fill uniform wasn't cached as it should.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_gl08.js b/browser/devtools/tilt/test/browser_tilt_gl08.js
new file mode 100644
index 000000000..10fff4932
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_gl08.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let isWebGLAvailable;
+
+function onWebGLFail() {
+ isWebGLAvailable = false;
+}
+
+function onWebGLSuccess() {
+ isWebGLAvailable = true;
+}
+
+function test() {
+ if (!isWebGLSupported()) {
+ info("Skipping tilt_gl08 because WebGL isn't supported on this hardware.");
+ return;
+ }
+
+ let canvas = createCanvas();
+
+ let renderer = new TiltGL.Renderer(canvas, onWebGLFail, onWebGLSuccess);
+ let gl = renderer.context;
+
+ if (!isWebGLAvailable) {
+ return;
+ }
+
+
+ let t = new renderer.Texture({
+ source: canvas,
+ format: "RGB"
+ });
+
+ ok(t instanceof TiltGL.Texture,
+ "The texture object wasn't instantiated correctly.");
+
+ ok(t._ref,
+ "The texture WebGL object wasn't created properly.");
+ isnot(t._id, -1,
+ "The texture id wasn't set properly.");
+ isnot(t.width, -1,
+ "The texture width wasn't set properly.");
+ isnot(t.height, -1,
+ "The texture height wasn't set properly.");
+ ok(t.loaded,
+ "The texture loaded flag wasn't set to true as it should.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_math01.js b/browser/devtools/tilt/test/browser_tilt_math01.js
new file mode 100644
index 000000000..da9e23285
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_math01.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ ok(isApprox(TiltMath.radians(30), 0.523598775),
+ "The radians() function didn't calculate the value correctly.");
+
+ ok(isApprox(TiltMath.degrees(0.5), 28.64788975),
+ "The degrees() function didn't calculate the value correctly.");
+
+ ok(isApprox(TiltMath.map(0.5, 0, 1, 0, 100), 50),
+ "The map() function didn't calculate the value correctly.");
+
+ is(TiltMath.isPowerOfTwo(32), true,
+ "The isPowerOfTwo() function didn't return the expected value.");
+
+ is(TiltMath.isPowerOfTwo(33), false,
+ "The isPowerOfTwo() function didn't return the expected value.");
+
+ ok(isApprox(TiltMath.nextPowerOfTwo(31), 32),
+ "The nextPowerOfTwo() function didn't calculate the 1st value correctly.");
+
+ ok(isApprox(TiltMath.nextPowerOfTwo(32), 32),
+ "The nextPowerOfTwo() function didn't calculate the 2nd value correctly.");
+
+ ok(isApprox(TiltMath.nextPowerOfTwo(33), 64),
+ "The nextPowerOfTwo() function didn't calculate the 3rd value correctly.");
+
+ ok(isApprox(TiltMath.clamp(5, 1, 3), 3),
+ "The clamp() function didn't calculate the 1st value correctly.");
+
+ ok(isApprox(TiltMath.clamp(5, 3, 1), 3),
+ "The clamp() function didn't calculate the 2nd value correctly.");
+
+ ok(isApprox(TiltMath.saturate(5), 1),
+ "The saturate() function didn't calculate the 1st value correctly.");
+
+ ok(isApprox(TiltMath.saturate(-5), 0),
+ "The saturate() function didn't calculate the 2nd value correctly.");
+
+ ok(isApproxVec(TiltMath.hex2rgba("#f00"), [1, 0, 0, 1]),
+ "The hex2rgba() function didn't calculate the 1st rgba values correctly.");
+
+ ok(isApproxVec(TiltMath.hex2rgba("#f008"), [1, 0, 0, 0.53]),
+ "The hex2rgba() function didn't calculate the 2nd rgba values correctly.");
+
+ ok(isApproxVec(TiltMath.hex2rgba("#ff0000"), [1, 0, 0, 1]),
+ "The hex2rgba() function didn't calculate the 3rd rgba values correctly.");
+
+ ok(isApproxVec(TiltMath.hex2rgba("#ff0000aa"), [1, 0, 0, 0.66]),
+ "The hex2rgba() function didn't calculate the 4th rgba values correctly.");
+
+ ok(isApproxVec(TiltMath.hex2rgba("rgba(255, 0, 0, 0.5)"), [1, 0, 0, 0.5]),
+ "The hex2rgba() function didn't calculate the 5th rgba values correctly.");
+
+ ok(isApproxVec(TiltMath.hex2rgba("rgb(255, 0, 0)"), [1, 0, 0, 1]),
+ "The hex2rgba() function didn't calculate the 6th rgba values correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_math02.js b/browser/devtools/tilt/test/browser_tilt_math02.js
new file mode 100644
index 000000000..dae2708c4
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_math02.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let v1 = vec3.create();
+
+ ok(v1, "Should have created a vector with vec3.create().");
+ is(v1.length, 3, "A vec3 should have 3 elements.");
+
+ ok(isApproxVec(v1, [0, 0, 0]),
+ "When created, a vec3 should have the values default to 0.");
+
+ vec3.set([1, 2, 3], v1);
+ ok(isApproxVec(v1, [1, 2, 3]),
+ "The vec3.set() function didn't set the values correctly.");
+
+ vec3.zero(v1);
+ ok(isApproxVec(v1, [0, 0, 0]),
+ "The vec3.zero() function didn't set the values correctly.");
+
+ let v2 = vec3.create([4, 5, 6]);
+ ok(isApproxVec(v2, [4, 5, 6]),
+ "When cloning arrays, a vec3 should have the values copied.");
+
+ let v3 = vec3.create(v2);
+ ok(isApproxVec(v3, [4, 5, 6]),
+ "When cloning vectors, a vec3 should have the values copied.");
+
+ vec3.add(v2, v3);
+ ok(isApproxVec(v2, [8, 10, 12]),
+ "The vec3.add() function didn't set the x value correctly.");
+
+ vec3.subtract(v2, v3);
+ ok(isApproxVec(v2, [4, 5, 6]),
+ "The vec3.subtract() function didn't set the values correctly.");
+
+ vec3.negate(v2);
+ ok(isApproxVec(v2, [-4, -5, -6]),
+ "The vec3.negate() function didn't set the values correctly.");
+
+ vec3.scale(v2, -2);
+ ok(isApproxVec(v2, [8, 10, 12]),
+ "The vec3.scale() function didn't set the values correctly.");
+
+ vec3.normalize(v1);
+ ok(isApproxVec(v1, [0, 0, 0]),
+ "Normalizing a vector with zero length should return [0, 0, 0].");
+
+ vec3.normalize(v2);
+ ok(isApproxVec(v2, [
+ 0.4558423161506653, 0.5698028802871704, 0.6837634444236755
+ ]), "The vec3.normalize() function didn't set the values correctly.");
+
+ vec3.cross(v2, v3);
+ ok(isApproxVec(v2, [
+ 5.960464477539063e-8, -1.1920928955078125e-7, 5.960464477539063e-8
+ ]), "The vec3.cross() function didn't set the values correctly.");
+
+ vec3.dot(v2, v3);
+ ok(isApproxVec(v2, [
+ 5.960464477539063e-8, -1.1920928955078125e-7, 5.960464477539063e-8
+ ]), "The vec3.dot() function didn't set the values correctly.");
+
+ ok(isApproxVec([vec3.length(v2)], [1.4600096599955427e-7]),
+ "The vec3.length() function didn't calculate the value correctly.");
+
+ vec3.direction(v2, v3);
+ ok(isApproxVec(v2, [
+ -0.4558422863483429, -0.5698028802871704, -0.6837634444236755
+ ]), "The vec3.direction() function didn't set the values correctly.");
+
+ vec3.lerp(v2, v3, 0.5);
+ ok(isApproxVec(v2, [
+ 1.7720788717269897, 2.2150986194610596, 2.65811824798584
+ ]), "The vec3.lerp() function didn't set the values correctly.");
+
+
+ vec3.project([100, 100, 10], [0, 0, 100, 100],
+ mat4.create(), mat4.perspective(45, 1, 0.1, 100), v1);
+ ok(isApproxVec(v1, [-1157.10693359375, 1257.10693359375, 0]),
+ "The vec3.project() function didn't set the values correctly.");
+
+ vec3.unproject([100, 100, 1], [0, 0, 100, 100],
+ mat4.create(), mat4.perspective(45, 1, 0.1, 100), v1);
+ ok(isApproxVec(v1, [
+ 41.420406341552734, -41.420406341552734, -99.99771118164062
+ ]), "The vec3.project() function didn't set the values correctly.");
+
+
+ let ray = vec3.createRay([10, 10, 0], [100, 100, 1], [0, 0, 100, 100],
+ mat4.create(), mat4.perspective(45, 1, 0.1, 100));
+
+ ok(isApproxVec(ray.origin, [
+ -0.03313708305358887, 0.03313708305358887, -0.1000000014901161
+ ]), "The vec3.createRay() function didn't create the position correctly.");
+ ok(isApproxVec(ray.direction, [
+ 0.35788586614428364, -0.35788586614428364, -0.862458934459091
+ ]), "The vec3.createRay() function didn't create the direction correctly.");
+
+
+ is(vec3.str([0, 0, 0]), "[0, 0, 0]",
+ "The vec3.str() function didn't work properly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_math03.js b/browser/devtools/tilt/test/browser_tilt_math03.js
new file mode 100644
index 000000000..9a039ae77
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_math03.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let m1 = mat3.create();
+
+ ok(m1, "Should have created a matrix with mat3.create().");
+ is(m1.length, 9, "A mat3 should have 9 elements.");
+
+ ok(isApproxVec(m1, [1, 0, 0, 0, 1, 0, 0, 0, 1]),
+ "When created, a mat3 should have the values default to identity.");
+
+ mat3.set([1, 2, 3, 4, 5, 6, 7, 8, 9], m1);
+ ok(isApproxVec(m1, [1, 2, 3, 4, 5, 6, 7, 8, 9]),
+ "The mat3.set() function didn't set the values correctly.");
+
+ mat3.transpose(m1);
+ ok(isApproxVec(m1, [1, 4, 7, 2, 5, 8, 3, 6, 9]),
+ "The mat3.transpose() function didn't set the values correctly.");
+
+ mat3.identity(m1);
+ ok(isApproxVec(m1, [1, 0, 0, 0, 1, 0, 0, 0, 1]),
+ "The mat3.identity() function didn't set the values correctly.");
+
+ let m2 = mat3.toMat4(m1);
+ ok(isApproxVec(m2, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
+ "The mat3.toMat4() function didn't set the values correctly.");
+
+
+ is(mat3.str([1, 2, 3, 4, 5, 6, 7, 8, 9]), "[1, 2, 3, 4, 5, 6, 7, 8, 9]",
+ "The mat3.str() function didn't work properly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_math04.js b/browser/devtools/tilt/test/browser_tilt_math04.js
new file mode 100644
index 000000000..587dc45fd
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_math04.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let m1 = mat4.create();
+
+ ok(m1, "Should have created a matrix with mat4.create().");
+ is(m1.length, 16, "A mat4 should have 16 elements.");
+
+ ok(isApproxVec(m1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
+ "When created, a mat4 should have the values default to identity.");
+
+ mat4.set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], m1);
+ ok(isApproxVec(m1, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
+ "The mat4.set() function didn't set the values correctly.");
+
+ mat4.transpose(m1);
+ ok(isApproxVec(m1, [1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16]),
+ "The mat4.transpose() function didn't set the values correctly.");
+
+ mat4.identity(m1);
+ ok(isApproxVec(m1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
+ "The mat4.identity() function didn't set the values correctly.");
+
+ ok(isApprox(mat4.determinant(m1), 1),
+ "The mat4.determinant() function didn't calculate the value correctly.");
+
+ let m2 = mat4.inverse([1, 3, 1, 1, 1, 1, 2, 1, 2, 3, 4, 1, 1, 1, 1, 1]);
+ ok(isApproxVec(m2, [
+ -1, -3, 1, 3, 0.5, 0, 0, -0.5, 0, 1, 0, -1, 0.5, 2, -1, -0.5
+ ]), "The mat4.inverse() function didn't calculate the values correctly.");
+
+ let m3 = mat4.toRotationMat([
+ 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16]);
+ ok(isApproxVec(m3, [
+ 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 0, 0, 0, 1
+ ]), "The mat4.toRotationMat() func. didn't calculate the values correctly.");
+
+ let m4 = mat4.toMat3([
+ 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16]);
+ ok(isApproxVec(m4, [1, 5, 9, 2, 6, 10, 3, 7, 11]),
+ "The mat4.toMat3() function didn't set the values correctly.");
+
+ let m5 = mat4.toInverseMat3([
+ 1, 3, 1, 1, 1, 1, 2, 1, 2, 3, 4, 1, 1, 1, 1, 1]);
+ ok(isApproxVec(m5, [2, 9, -5, 0, -2, 1, -1, -3, 2]),
+ "The mat4.toInverseMat3() function didn't set the values correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_math05.js b/browser/devtools/tilt/test/browser_tilt_math05.js
new file mode 100644
index 000000000..d39695f55
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_math05.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let m1 = mat4.create([
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
+
+ let m2 = mat4.create([
+ 0, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16]);
+
+ mat4.multiply(m1, m2);
+ ok(isApproxVec(m1, [
+ 275, 302, 329, 356, 304, 336, 368, 400,
+ 332, 368, 404, 440, 360, 400, 440, 480
+ ]), "The mat4.multiply() function didn't set the values correctly.");
+
+ let v1 = mat4.multiplyVec3(m1, [1, 2, 3]);
+ ok(isApproxVec(v1, [2239, 2478, 2717]),
+ "The mat4.multiplyVec3() function didn't set the values correctly.");
+
+ let v2 = mat4.multiplyVec4(m1, [1, 2, 3, 0]);
+ ok(isApproxVec(v2, [1879, 2078, 2277, 2476]),
+ "The mat4.multiplyVec4() function didn't set the values correctly.");
+
+ mat4.translate(m1, [1, 2, 3]);
+ ok(isApproxVec(m1, [
+ 275, 302, 329, 356, 304, 336, 368, 400,
+ 332, 368, 404, 440, 2239, 2478, 2717, 2956
+ ]), "The mat4.translate() function didn't set the values correctly.");
+
+ mat4.scale(m1, [1, 2, 3]);
+ ok(isApproxVec(m1, [
+ 275, 302, 329, 356, 608, 672, 736, 800,
+ 996, 1104, 1212, 1320, 2239, 2478, 2717, 2956
+ ]), "The mat4.scale() function didn't set the values correctly.");
+
+ mat4.rotate(m1, 0.5, [1, 1, 1]);
+ ok(isApproxVec(m1, [
+ 210.6123046875, 230.2483367919922, 249.88438415527344, 269.5204162597656,
+ 809.8145751953125, 896.520751953125, 983.2268676757812,
+ 1069.9329833984375, 858.5731201171875, 951.23095703125,
+ 1043.8887939453125, 1136.5465087890625, 2239, 2478, 2717, 2956
+ ]), "The mat4.rotate() function didn't set the values correctly.");
+
+ mat4.rotateX(m1, 0.5);
+ ok(isApproxVec(m1, [
+ 210.6123046875, 230.2483367919922, 249.88438415527344, 269.5204162597656,
+ 1122.301025390625, 1242.8154296875, 1363.3297119140625,
+ 1483.843994140625, 365.2230224609375, 404.96875, 444.71453857421875,
+ 484.460205078125, 2239, 2478, 2717, 2956
+ ]), "The mat4.rotateX() function didn't set the values correctly.");
+
+ mat4.rotateY(m1, 0.5);
+ ok(isApproxVec(m1, [
+ 9.732441902160645, 7.909564018249512, 6.086670875549316,
+ 4.263822555541992, 1122.301025390625, 1242.8154296875, 1363.3297119140625,
+ 1483.843994140625, 421.48626708984375, 465.78045654296875,
+ 510.0746765136719, 554.3687744140625, 2239, 2478, 2717, 2956
+ ]), "The mat4.rotateY() function didn't set the values correctly.");
+
+ mat4.rotateZ(m1, 0.5);
+ ok(isApproxVec(m1, [
+ 546.6007690429688, 602.7787475585938, 658.9566650390625, 715.1345825195312,
+ 980.245849609375, 1086.881103515625, 1193.5162353515625,
+ 1300.1514892578125, 421.48626708984375, 465.78045654296875,
+ 510.0746765136719, 554.3687744140625, 2239, 2478, 2717, 2956
+ ]), "The mat4.rotateZ() function didn't set the values correctly.");
+
+
+ let m3 = mat4.frustum(0, 100, 200, 0, 0.1, 100);
+ ok(isApproxVec(m3, [
+ 0.0020000000949949026, 0, 0, 0, 0, -0.0010000000474974513, 0, 0, 1, -1,
+ -1.0020020008087158, -1, 0, 0, -0.20020020008087158, 0
+ ]), "The mat4.frustum() function didn't compute the values correctly.");
+
+ let m4 = mat4.perspective(45, 1.6, 0.1, 100);
+ ok(isApproxVec(m4, [1.5088834762573242, 0, 0, 0, 0, 2.4142136573791504, 0,
+ 0, 0, 0, -1.0020020008087158, -1, 0, 0, -0.20020020008087158, 0
+ ]), "The mat4.frustum() function didn't compute the values correctly.");
+
+ let m5 = mat4.ortho(0, 100, 200, 0, -1, 1);
+ ok(isApproxVec(m5, [
+ 0.019999999552965164, 0, 0, 0, 0, -0.009999999776482582, 0, 0,
+ 0, 0, -1, 0, -1, 1, 0, 1
+ ]), "The mat4.ortho() function didn't compute the values correctly.");
+
+ let m6 = mat4.lookAt([1, 2, 3], [4, 5, 6], [0, 1, 0]);
+ ok(isApproxVec(m6, [
+ -0.7071067690849304, -0.40824830532073975, -0.5773502588272095, 0, 0,
+ 0.8164966106414795, -0.5773502588272095, 0, 0.7071067690849304,
+ -0.40824830532073975, -0.5773502588272095, 0, -1.4142135381698608, 0,
+ 3.464101552963257, 1
+ ]), "The mat4.lookAt() function didn't compute the values correctly.");
+
+
+ is(mat4.str([
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
+ "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]",
+ "The mat4.str() function didn't work properly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_math06.js b/browser/devtools/tilt/test/browser_tilt_math06.js
new file mode 100644
index 000000000..2ed331eaa
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_math06.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let q1 = quat4.create();
+
+ ok(q1, "Should have created a quaternion with quat4.create().");
+ is(q1.length, 4, "A quat4 should have 4 elements.");
+
+ ok(isApproxVec(q1, [0, 0, 0, 1]),
+ "When created, a vec3 should have the values default to identity.");
+
+ quat4.set([1, 2, 3, 4], q1);
+ ok(isApproxVec(q1, [1, 2, 3, 4]),
+ "The quat4.set() function didn't set the values correctly.");
+
+ quat4.identity(q1);
+ ok(isApproxVec(q1, [0, 0, 0, 1]),
+ "The quat4.identity() function didn't set the values correctly.");
+
+ quat4.set([5, 6, 7, 8], q1);
+ ok(isApproxVec(q1, [5, 6, 7, 8]),
+ "The quat4.set() function didn't set the values correctly.");
+
+ quat4.calculateW(q1);
+ ok(isApproxVec(q1, [5, 6, 7, -10.440306663513184]),
+ "The quat4.calculateW() function didn't compute the values correctly.");
+
+ quat4.inverse(q1);
+ ok(isApproxVec(q1, [-5, -6, -7, -10.440306663513184]),
+ "The quat4.inverse() function didn't compute the values correctly.");
+
+ quat4.normalize(q1);
+ ok(isApproxVec(q1, [
+ -0.33786869049072266, -0.40544241666793823,
+ -0.4730161726474762, -0.7054905295372009
+ ]), "The quat4.normalize() function didn't compute the values correctly.");
+
+ ok(isApprox(quat4.length(q1), 1),
+ "The mat4.length() function didn't calculate the value correctly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_math07.js b/browser/devtools/tilt/test/browser_tilt_math07.js
new file mode 100644
index 000000000..309d3763d
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_math07.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let q1 = quat4.create([1, 2, 3, 4]);
+ let q2 = quat4.create([5, 6, 7, 8]);
+
+ quat4.multiply(q1, q2);
+ ok(isApproxVec(q1, [24, 48, 48, -6]),
+ "The quat4.multiply() function didn't set the values correctly.");
+
+ let v1 = quat4.multiplyVec3(q1, [9, 9, 9]);
+ ok(isApproxVec(v1, [5508, 54756, 59940]),
+ "The quat4.multiplyVec3() function didn't set the values correctly.");
+
+ let m1 = quat4.toMat3(q1);
+ ok(isApproxVec(m1, [
+ -9215, 2880, 1728, 1728, -5759, 4896, 2880, 4320, -5759
+ ]), "The quat4.toMat3() function didn't set the values correctly.");
+
+ let m2 = quat4.toMat4(q1);
+ ok(isApproxVec(m2, [
+ -9215, 2880, 1728, 0, 1728, -5759, 4896, 0,
+ 2880, 4320, -5759, 0, 0, 0, 0, 1
+ ]), "The quat4.toMat4() function didn't set the values correctly.");
+
+ quat4.calculateW(q1);
+ quat4.calculateW(q2);
+ quat4.slerp(q1, q2, 0.5);
+ ok(isApproxVec(q1, [24, 48, 48, -71.99305725097656]),
+ "The quat4.slerp() function didn't set the values correctly.");
+
+ let q3 = quat4.fromAxis([1, 1, 1], 0.5);
+ ok(isApproxVec(q3, [
+ 0.24740396440029144, 0.24740396440029144, 0.24740396440029144,
+ 0.9689124226570129
+ ]), "The quat4.fromAxis() function didn't compute the values correctly.");
+
+ let q4 = quat4.fromEuler(0.5, 0.75, 1.25);
+ ok(isApproxVec(q4, [
+ 0.15310347080230713, 0.39433568716049194,
+ 0.4540249705314636, 0.7841683626174927
+ ]), "The quat4.fromEuler() function didn't compute the values correctly.");
+
+
+ is(quat4.str([1, 2, 3, 4]), "[1, 2, 3, 4]",
+ "The quat4.str() function didn't work properly.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking.js b/browser/devtools/tilt/test/browser_tilt_picking.js
new file mode 100644
index 000000000..c79056a3b
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let pickDone = false;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping picking test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping picking test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ let presenter = instance.presenter;
+ let canvas = presenter.canvas;
+
+ presenter._onSetupMesh = function() {
+ let p = getPickablePoint(presenter);
+
+ presenter.pickNode(p[0], p[1], {
+ onpick: function(data)
+ {
+ ok(data.index > 0,
+ "Simply picking a node didn't work properly.");
+
+ pickDone = true;
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ }
+ });
+ };
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function cleanup() {
+ if (pickDone) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking_delete.js b/browser/devtools/tilt/test/browser_tilt_picking_delete.js
new file mode 100644
index 000000000..c45d44b03
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking_delete.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let nodeDeleted = false;
+let presenter;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping picking delete test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping picking delete test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ presenter = instance.presenter;
+ Services.obs.addObserver(whenNodeRemoved, NODE_REMOVED, false);
+
+ presenter._onSetupMesh = function() {
+ let p = getPickablePoint(presenter);
+
+ presenter.highlightNodeAt(p[0], p[1], {
+ onpick: function()
+ {
+ ok(presenter._currentSelection > 0,
+ "Highlighting a node didn't work properly.");
+ ok(!presenter._highlight.disabled,
+ "After highlighting a node, it should be highlighted. D'oh.");
+
+ nodeDeleted = true;
+ presenter.deleteNode();
+ }
+ });
+ };
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function whenNodeRemoved() {
+ ok(presenter._currentSelection > 0,
+ "Deleting a node shouldn't change the current selection.");
+ ok(presenter._highlight.disabled,
+ "After deleting a node, it shouldn't be highlighted.");
+
+ let nodeIndex = presenter._currentSelection;
+ let vertices = presenter._meshStacks[0].vertices.components;
+
+ for (let i = 0, k = 36 * nodeIndex; i < 36; i++) {
+ is(vertices[i + k], 0,
+ "The stack vertices weren't degenerated properly.");
+ }
+
+ executeSoon(function() {
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+}
+
+function cleanup() {
+ if (nodeDeleted) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking_highlight01-offs.js b/browser/devtools/tilt/test/browser_tilt_picking_highlight01-offs.js
new file mode 100644
index 000000000..abfd4f586
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight01-offs.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let nodeHighlighted = false;
+let presenter;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping highlight test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping highlight test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ presenter = instance.presenter;
+ Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
+
+ presenter._onInitializationFinished = function() {
+ let contentDocument = presenter.contentWindow.document;
+ let div = contentDocument.getElementById("far-far-away");
+
+ nodeHighlighted = true;
+ presenter.highlightNode(div, "moveIntoView");
+ };
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function whenHighlighting() {
+ ok(presenter._currentSelection > 0,
+ "Highlighting a node didn't work properly.");
+ ok(!presenter._highlight.disabled,
+ "After highlighting a node, it should be highlighted. D'oh.");
+ ok(presenter.controller.arcball._resetInProgress,
+ "Highlighting a node that's not already visible should trigger a reset!");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenHighlighting, HIGHLIGHTING);
+ Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
+ presenter.highlightNode(null);
+ });
+}
+
+function whenUnhighlighting() {
+ ok(presenter._currentSelection < 0,
+ "Unhighlighting a should remove the current selection.");
+ ok(presenter._highlight.disabled,
+ "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh.");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenUnhighlighting, UNHIGHLIGHTING);
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+}
+
+function cleanup() {
+ if (nodeHighlighted) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking_highlight01.js b/browser/devtools/tilt/test/browser_tilt_picking_highlight01.js
new file mode 100644
index 000000000..82871270e
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight01.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let nodeHighlighted = false;
+let presenter;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping highlight test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping highlight test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ presenter = instance.presenter;
+ Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
+
+ presenter._onInitializationFinished = function() {
+ let contentDocument = presenter.contentWindow.document;
+ let div = contentDocument.getElementById("first-law");
+
+ nodeHighlighted = true;
+ presenter.highlightNode(div, "moveIntoView");
+ };
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function whenHighlighting() {
+ ok(presenter._currentSelection > 0,
+ "Highlighting a node didn't work properly.");
+ ok(!presenter._highlight.disabled,
+ "After highlighting a node, it should be highlighted. D'oh.");
+ ok(!presenter.controller.arcball._resetInProgress,
+ "Highlighting a node that's already visible shouldn't trigger a reset.");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenHighlighting, HIGHLIGHTING);
+ Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
+ presenter.highlightNode(null);
+ });
+}
+
+function whenUnhighlighting() {
+ ok(presenter._currentSelection < 0,
+ "Unhighlighting a should remove the current selection.");
+ ok(presenter._highlight.disabled,
+ "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh.");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenUnhighlighting, UNHIGHLIGHTING);
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+}
+
+function cleanup() {
+ if (nodeHighlighted) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking_highlight02.js b/browser/devtools/tilt/test/browser_tilt_picking_highlight02.js
new file mode 100644
index 000000000..fc8d0fc51
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight02.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let nodeHighlighted = false;
+let presenter;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping highlight test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping highlight test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ presenter = instance.presenter;
+ Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
+
+ presenter._onInitializationFinished = function() {
+ nodeHighlighted = true;
+ presenter.highlightNodeAt.apply(this, getPickablePoint(presenter));
+ };
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function whenHighlighting() {
+ ok(presenter._currentSelection > 0,
+ "Highlighting a node didn't work properly.");
+ ok(!presenter._highlight.disabled,
+ "After highlighting a node, it should be highlighted. D'oh.");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenHighlighting, HIGHLIGHTING);
+ Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
+ presenter.highlightNode(null);
+ });
+}
+
+function whenUnhighlighting() {
+ ok(presenter._currentSelection < 0,
+ "Unhighlighting a should remove the current selection.");
+ ok(presenter._highlight.disabled,
+ "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh.");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenUnhighlighting, UNHIGHLIGHTING);
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+}
+
+function cleanup() {
+ if (nodeHighlighted) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking_highlight03.js b/browser/devtools/tilt/test/browser_tilt_picking_highlight03.js
new file mode 100644
index 000000000..721189f65
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight03.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let nodeHighlighted = false;
+let presenter;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping highlight test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping highlight test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ presenter = instance.presenter;
+ Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
+
+ presenter._onInitializationFinished = function() {
+ nodeHighlighted = true;
+ presenter.highlightNodeFor(3); // 1 = html, 2 = body, 3 = first div
+ };
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function whenHighlighting() {
+ ok(presenter._currentSelection > 0,
+ "Highlighting a node didn't work properly.");
+ ok(!presenter._highlight.disabled,
+ "After highlighting a node, it should be highlighted. D'oh.");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenHighlighting, HIGHLIGHTING);
+ Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
+ presenter.highlightNodeFor(-1);
+ });
+}
+
+function whenUnhighlighting() {
+ ok(presenter._currentSelection < 0,
+ "Unhighlighting a should remove the current selection.");
+ ok(presenter._highlight.disabled,
+ "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh.");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenUnhighlighting, UNHIGHLIGHTING);
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+}
+
+function cleanup() {
+ if (nodeHighlighted) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking_inspector.js b/browser/devtools/tilt/test/browser_tilt_picking_inspector.js
new file mode 100644
index 000000000..0ec302a07
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking_inspector.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let presenter;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping highlight test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping highlight test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+ let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
+
+ gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+ let contentDocument = toolbox.target.tab.linkedBrowser.contentDocument;
+ let div = contentDocument.getElementById("first-law");
+ toolbox.getCurrentPanel().selection.setNode(div);
+
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ presenter = instance.presenter;
+ whenOpen();
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+ });
+}
+
+function whenOpen() {
+ ok(presenter._currentSelection > 0,
+ "Highlighting a node didn't work properly.");
+ ok(!presenter._highlight.disabled,
+ "After highlighting a node, it should be highlighted. D'oh.");
+ ok(!presenter.controller.arcball._resetInProgress,
+ "Highlighting a node that's already visible shouldn't trigger a reset.");
+
+ executeSoon(function() {
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+}
+
+function cleanup() {
+ Services.obs.removeObserver(cleanup, DESTROYED);
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_picking_miv.js b/browser/devtools/tilt/test/browser_tilt_picking_miv.js
new file mode 100644
index 000000000..64b911a00
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking_miv.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let nodeHighlighted = false;
+let presenter;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping highlight test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping highlight test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ presenter = instance.presenter;
+ Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
+
+ presenter._onInitializationFinished = function() {
+ let contentDocument = presenter.contentWindow.document;
+ let div = contentDocument.getElementById("far-far-away");
+
+ nodeHighlighted = true;
+ presenter.highlightNode(div);
+ };
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function whenHighlighting() {
+ ok(presenter._currentSelection > 0,
+ "Highlighting a node didn't work properly.");
+ ok(!presenter._highlight.disabled,
+ "After highlighting a node, it should be highlighted. D'oh.");
+ ok(!presenter.controller.arcball._resetInProgress,
+ "Highlighting a node that's not already visible shouldn't trigger a reset " +
+ "without this being explicitly requested!");
+
+ EventUtils.sendKey("F");
+ executeSoon(whenBringingIntoView);
+}
+
+function whenBringingIntoView() {
+ ok(presenter._currentSelection > 0,
+ "The node should still be selected.");
+ ok(!presenter._highlight.disabled,
+ "The node should still be highlighted");
+ ok(presenter.controller.arcball._resetInProgress,
+ "Highlighting a node that's not already visible should trigger a reset " +
+ "when this is being explicitly requested!");
+
+ executeSoon(function() {
+ Services.obs.removeObserver(whenHighlighting, HIGHLIGHTING);
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+}
+
+function cleanup() {
+ if (nodeHighlighted) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils01.js b/browser/devtools/tilt/test/browser_tilt_utils01.js
new file mode 100644
index 000000000..7beb6a3a2
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils01.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let prefs = TiltUtils.Preferences;
+ ok(prefs, "The TiltUtils.Preferences wasn't found.");
+
+ prefs.create("test-pref-bool", "boolean", true);
+ prefs.create("test-pref-int", "integer", 42);
+ prefs.create("test-pref-string", "string", "hello world!");
+
+ is(prefs.get("test-pref-bool", "boolean"), true,
+ "The boolean test preference wasn't initially set correctly.");
+ is(prefs.get("test-pref-int", "integer"), 42,
+ "The integer test preference wasn't initially set correctly.");
+ is(prefs.get("test-pref-string", "string"), "hello world!",
+ "The string test preference wasn't initially set correctly.");
+
+
+ prefs.set("test-pref-bool", "boolean", false);
+ prefs.set("test-pref-int", "integer", 24);
+ prefs.set("test-pref-string", "string", "!dlrow olleh");
+
+ is(prefs.get("test-pref-bool", "boolean"), false,
+ "The boolean test preference wasn't changed correctly.");
+ is(prefs.get("test-pref-int", "integer"), 24,
+ "The integer test preference wasn't changed correctly.");
+ is(prefs.get("test-pref-string", "string"), "!dlrow olleh",
+ "The string test preference wasn't changed correctly.");
+
+
+ is(typeof prefs.get("unknown-pref", "boolean"), "undefined",
+ "Inexisted boolean prefs should be handled as undefined.");
+ is(typeof prefs.get("unknown-pref", "integer"), "undefined",
+ "Inexisted integer prefs should be handled as undefined.");
+ is(typeof prefs.get("unknown-pref", "string"), "undefined",
+ "Inexisted string prefs should be handled as undefined.");
+
+
+ is(prefs.get("test-pref-bool", "integer"), null,
+ "The get() boolean function didn't handle incorrect types as it should.");
+ is(prefs.get("test-pref-bool", "string"), null,
+ "The get() boolean function didn't handle incorrect types as it should.");
+ is(prefs.get("test-pref-int", "boolean"), null,
+ "The get() integer function didn't handle incorrect types as it should.");
+ is(prefs.get("test-pref-int", "string"), null,
+ "The get() integer function didn't handle incorrect types as it should.");
+ is(prefs.get("test-pref-string", "boolean"), null,
+ "The get() string function didn't handle incorrect types as it should.");
+ is(prefs.get("test-pref-string", "integer"), null,
+ "The get() string function didn't handle incorrect types as it should.");
+
+
+ is(typeof prefs.get(), "undefined",
+ "The get() function should not work if not enough params are passed.");
+ is(typeof prefs.set(), "undefined",
+ "The set() function should not work if not enough params are passed.");
+ is(typeof prefs.create(), "undefined",
+ "The create() function should not work if not enough params are passed.");
+
+
+ is(prefs.get("test-pref-wrong-type", "wrong-type", 1), null,
+ "The get() function should expect only correct pref types.");
+ is(prefs.set("test-pref-wrong-type", "wrong-type", 1), false,
+ "The set() function should expect only correct pref types.");
+ is(prefs.create("test-pref-wrong-type", "wrong-type", 1), false,
+ "The create() function should expect only correct pref types.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils02.js b/browser/devtools/tilt/test/browser_tilt_utils02.js
new file mode 100644
index 000000000..fcee265c6
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils02.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let l10 = TiltUtils.L10n;
+ ok(l10, "The TiltUtils.L10n wasn't found.");
+
+
+ ok(l10.stringBundle,
+ "The necessary string bundle wasn't found.");
+ is(l10.get(), null,
+ "The get() function shouldn't work if no params are passed.");
+ is(l10.format(), null,
+ "The format() function shouldn't work if no params are passed.");
+
+ is(typeof l10.get("initWebGL.error"), "string",
+ "No valid string was returned from a corect name in the bundle.");
+ is(typeof l10.format("linkProgram.error", ["error"]), "string",
+ "No valid formatted string was returned from a name in the bundle.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils03.js b/browser/devtools/tilt/test/browser_tilt_utils03.js
new file mode 100644
index 000000000..61d256fe1
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils03.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let dom = TiltUtils.DOM;
+
+ is(dom.parentNode, null,
+ "The parent node should not be initially set.");
+
+ dom.parentNode = {};
+ ok(dom.parentNode,
+ "The parent node should now be set.");
+
+ TiltUtils.clearCache();
+ is(dom.parentNode, null,
+ "The parent node should not be set after clearing the cache.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils04.js b/browser/devtools/tilt/test/browser_tilt_utils04.js
new file mode 100644
index 000000000..8574c266e
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils04.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ let dom = TiltUtils.DOM;
+ ok(dom, "The TiltUtils.DOM wasn't found.");
+
+
+ is(dom.initCanvas(), null,
+ "The initCanvas() function shouldn't work if no parent node is set.");
+
+
+ dom.parentNode = gBrowser.parentNode;
+ ok(dom.parentNode,
+ "The necessary parent node wasn't found.");
+
+
+ let canvas = dom.initCanvas(null, {
+ append: true,
+ focusable: true,
+ width: 123,
+ height: 456,
+ id: "tilt-test-canvas"
+ });
+
+ is(canvas.width, 123,
+ "The test canvas doesn't have the correct width set.");
+ is(canvas.height, 456,
+ "The test canvas doesn't have the correct height set.");
+ is(canvas.getAttribute("tabindex"), 1,
+ "The test canvas tab index wasn't set correctly.");
+ is(canvas.getAttribute("id"), "tilt-test-canvas",
+ "The test canvas doesn't have the correct id set.");
+ ok(dom.parentNode.ownerDocument.getElementById(canvas.id),
+ "A canvas should be appended to the parent node if specified.");
+ canvas.parentNode.removeChild(canvas);
+
+ let canvas2 = dom.initCanvas(null, { id: "tilt-test-canvas2" });
+
+ is(canvas2.width, dom.parentNode.clientWidth,
+ "The second test canvas doesn't have the implicit width set.");
+ is(canvas2.height, dom.parentNode.clientHeight,
+ "The second test canvas doesn't have the implicit height set.");
+ is(canvas2.id, "tilt-test-canvas2",
+ "The second test canvas doesn't have the correct id set.");
+ is(dom.parentNode.ownerDocument.getElementById(canvas2.id), null,
+ "A canvas shouldn't be appended to the parent node if not specified.");
+
+
+ dom.parentNode = null;
+ is(dom.parentNode, null,
+ "The necessary parent node shouldn't be found anymore.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils05.js b/browser/devtools/tilt/test/browser_tilt_utils05.js
new file mode 100644
index 000000000..0f09d198e
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils05.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const STACK_THICKNESS = 15;
+
+function init(callback) {
+ let iframe = gBrowser.ownerDocument.createElement("iframe");
+
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad, true);
+ callback(iframe);
+
+ gBrowser.parentNode.removeChild(iframe);
+ finish();
+ }, true);
+
+ iframe.setAttribute("src", ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<head>",
+ "<style>",
+ "</style>",
+ "<script>",
+ "</script>",
+ "</head>",
+ "<body style='margin: 0;'>",
+ "<div style='margin-top: 98px;" +
+ "margin-left: 76px;" +
+ "width: 123px;" +
+ "height: 456px;' id='test-div'>",
+ "<span></span>",
+ "</div>",
+ "</body>",
+ "</html>"
+ ].join(""));
+
+ gBrowser.parentNode.appendChild(iframe);
+}
+
+function test() {
+ waitForExplicitFinish();
+ ok(TiltUtils, "The TiltUtils object doesn't exist.");
+
+ let dom = TiltUtils.DOM;
+ ok(dom, "The TiltUtils.DOM wasn't found.");
+
+ init(function(iframe) {
+ let cwDimensions = dom.getContentWindowDimensions(iframe.contentWindow);
+
+ is(cwDimensions.width - iframe.contentWindow.scrollMaxX,
+ iframe.contentWindow.innerWidth,
+ "The content window width wasn't calculated correctly.");
+ is(cwDimensions.height - iframe.contentWindow.scrollMaxY,
+ iframe.contentWindow.innerHeight,
+ "The content window height wasn't calculated correctly.");
+
+ let nodeCoordinates = LayoutHelpers.getRect(
+ iframe.contentDocument.getElementById("test-div"), iframe.contentWindow);
+
+ let frameOffset = LayoutHelpers.getIframeContentOffset(iframe);
+ let frameRect = iframe.getBoundingClientRect();
+
+ is(nodeCoordinates.top, frameRect.top + frameOffset[0] + 98,
+ "The node coordinates top value wasn't calculated correctly.");
+ is(nodeCoordinates.left, frameRect.left + frameOffset[1] + 76,
+ "The node coordinates left value wasn't calculated correctly.");
+ is(nodeCoordinates.width, 123,
+ "The node coordinates width value wasn't calculated correctly.");
+ is(nodeCoordinates.height, 456,
+ "The node coordinates height value wasn't calculated correctly.");
+
+
+ let store = dom.traverse(iframe.contentWindow);
+
+ let expected = [
+ { name: "html", depth: 0 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "head", depth: 1 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "body", depth: 1 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "style", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "script", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "div", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "span", depth: 3 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ ];
+
+ is(store.nodes.length, expected.length,
+ "The traverse() function didn't walk the correct number of nodes.");
+ is(store.info.length, expected.length,
+ "The traverse() function didn't examine the correct number of nodes.");
+
+ for (let i = 0; i < expected.length; i++) {
+ is(store.info[i].name, expected[i].name,
+ "traversed node " + (i + 1) + " isn't the expected one.");
+ is(store.info[i].coord.depth, expected[i].depth,
+ "traversed node " + (i + 1) + " doesn't have the expected depth.");
+ is(store.info[i].coord.thickness, expected[i].thickness,
+ "traversed node " + (i + 1) + " doesn't have the expected thickness.");
+ }
+ });
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils06.js b/browser/devtools/tilt/test/browser_tilt_utils06.js
new file mode 100644
index 000000000..eee915261
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils06.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let someObject = {
+ a: 1,
+ func: function()
+ {
+ this.b = 2;
+ }
+};
+
+let anotherObject = {
+ _finalize: function()
+ {
+ someObject.c = 3;
+ }
+};
+
+function test() {
+ ok(TiltUtils, "The TiltUtils object doesn't exist.");
+
+ TiltUtils.bindObjectFunc(someObject, "", anotherObject);
+ someObject.func();
+
+ is(someObject.a, 1,
+ "The bindObjectFunc() messed the non-function members of the object.");
+ isnot(someObject.b, 2,
+ "The bindObjectFunc() didn't ignore the old scope correctly.");
+ is(anotherObject.b, 2,
+ "The bindObjectFunc() didn't set the new scope correctly.");
+
+
+ TiltUtils.destroyObject(anotherObject);
+ is(someObject.c, 3,
+ "The finalize function wasn't called when an object was destroyed.");
+
+
+ TiltUtils.destroyObject(someObject);
+ is(typeof someObject.a, "undefined",
+ "Not all members of the destroyed object were deleted.");
+ is(typeof someObject.func, "undefined",
+ "Not all function members of the destroyed object were deleted.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils07.js b/browser/devtools/tilt/test/browser_tilt_utils07.js
new file mode 100644
index 000000000..0c07300a8
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils07.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const STACK_THICKNESS = 15;
+
+function init(callback) {
+ let iframe = gBrowser.ownerDocument.createElement("iframe");
+
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad, true);
+ callback(iframe);
+
+ gBrowser.parentNode.removeChild(iframe);
+ finish();
+ }, true);
+
+ iframe.setAttribute("src", ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body style='margin: 0;'>",
+ "<frameset cols='50%,50%'>",
+ "<frame src='",
+ ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body style='margin: 0;'>",
+ "<div id='test-div' style='width: 123px; height: 456px;'></div>",
+ "</body>",
+ "</html>"
+ ].join(""),
+ "' />",
+ "<frame src='",
+ ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body style='margin: 0;'>",
+ "<span></span>",
+ "</body>",
+ "</html>"
+ ].join(""),
+ "' />",
+ "</frameset>",
+ "<iframe src='",
+ ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body>",
+ "<span></span>",
+ "</body>",
+ "</html>"
+ ].join(""),
+ "'></iframe>",
+ "<frame src='",
+ ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body style='margin: 0;'>",
+ "<span></span>",
+ "</body>",
+ "</html>"
+ ].join(""),
+ "' />",
+ "<frame src='",
+ ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body style='margin: 0;'>",
+ "<iframe src='",
+ ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body>",
+ "<div></div>",
+ "</body>",
+ "</html>"
+ ].join(""),
+ "'></iframe>",
+ "</body>",
+ "</html>"
+ ].join(""),
+ "' />",
+ "</body>",
+ "</html>"
+ ].join(""));
+
+ gBrowser.parentNode.appendChild(iframe);
+}
+
+function test() {
+ waitForExplicitFinish();
+ ok(TiltUtils, "The TiltUtils object doesn't exist.");
+
+ let dom = TiltUtils.DOM;
+ ok(dom, "The TiltUtils.DOM wasn't found.");
+
+ init(function(iframe) {
+ let cwDimensions = dom.getContentWindowDimensions(iframe.contentWindow);
+
+ is(cwDimensions.width - iframe.contentWindow.scrollMaxX,
+ iframe.contentWindow.innerWidth,
+ "The content window width wasn't calculated correctly.");
+ is(cwDimensions.height - iframe.contentWindow.scrollMaxY,
+ iframe.contentWindow.innerHeight,
+ "The content window height wasn't calculated correctly.");
+
+ let nodeCoordinates = LayoutHelpers.getRect(
+ iframe.contentDocument.getElementById("test-div"), iframe.contentWindow);
+
+ let frameOffset = LayoutHelpers.getIframeContentOffset(iframe);
+ let frameRect = iframe.getBoundingClientRect();
+
+ is(nodeCoordinates.top, frameRect.top + frameOffset[0],
+ "The node coordinates top value wasn't calculated correctly.");
+ is(nodeCoordinates.left, frameRect.left + frameOffset[1],
+ "The node coordinates left value wasn't calculated correctly.");
+ is(nodeCoordinates.width, 123,
+ "The node coordinates width value wasn't calculated correctly.");
+ is(nodeCoordinates.height, 456,
+ "The node coordinates height value wasn't calculated correctly.");
+
+
+ let store = dom.traverse(iframe.contentWindow);
+
+ let expected = [
+ { name: "html", depth: 0 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "head", depth: 1 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "body", depth: 1 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "div", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "span", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "iframe", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "span", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "iframe", depth: 2 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "html", depth: 3 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "html", depth: 3 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "head", depth: 4 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "body", depth: 4 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "head", depth: 4 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "body", depth: 4 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "span", depth: 5 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "div", depth: 5 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ ];
+
+ is(store.nodes.length, expected.length,
+ "The traverse() function didn't walk the correct number of nodes.");
+ is(store.info.length, expected.length,
+ "The traverse() function didn't examine the correct number of nodes.");
+
+ for (let i = 0; i < expected.length; i++) {
+ is(store.info[i].name, expected[i].name,
+ "traversed node " + (i + 1) + " isn't the expected one.");
+ is(store.info[i].coord.depth, expected[i].depth,
+ "traversed node " + (i + 1) + " doesn't have the expected depth.");
+ is(store.info[i].coord.thickness, expected[i].thickness,
+ "traversed node " + (i + 1) + " doesn't have the expected thickness.");
+ }
+ });
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_utils08.js b/browser/devtools/tilt/test/browser_tilt_utils08.js
new file mode 100644
index 000000000..797c9e7a7
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_utils08.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const STACK_THICKNESS = 15;
+
+function init(callback) {
+ let iframe = gBrowser.ownerDocument.createElement("iframe");
+
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad, true);
+ callback(iframe);
+
+ gBrowser.parentNode.removeChild(iframe);
+ finish();
+ }, true);
+
+ iframe.setAttribute("src", ["data:text/html,",
+ "<!DOCTYPE html>",
+ "<html>",
+ "<body style='margin: 0;'>",
+ "<div>",
+ "<p>Foo</p>",
+ "<div>",
+ "<span>Bar</span>",
+ "</div>",
+ "<div></div>",
+ "</div>",
+ "</body>",
+ "</html>"
+ ].join(""));
+
+ gBrowser.parentNode.appendChild(iframe);
+}
+
+function nodeCallback(aContentWindow, aNode, aParentPosition) {
+ let coord = TiltUtils.DOM.getNodePosition(aContentWindow, aNode, aParentPosition);
+
+ if (aNode.localName != "div")
+ coord.thickness = 0;
+
+ if (aNode.localName == "span")
+ coord.depth += STACK_THICKNESS;
+
+ return coord;
+}
+
+function test() {
+ waitForExplicitFinish();
+ ok(TiltUtils, "The TiltUtils object doesn't exist.");
+
+ let dom = TiltUtils.DOM;
+ ok(dom, "The TiltUtils.DOM wasn't found.");
+
+ init(function(iframe) {
+ let store = dom.traverse(iframe.contentWindow, {
+ nodeCallback: nodeCallback
+ });
+
+ let expected = [
+ { name: "html", depth: 0 * STACK_THICKNESS, thickness: 0 },
+ { name: "head", depth: 0 * STACK_THICKNESS, thickness: 0 },
+ { name: "body", depth: 0 * STACK_THICKNESS, thickness: 0 },
+ { name: "div", depth: 0 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "p", depth: 1 * STACK_THICKNESS, thickness: 0 },
+ { name: "div", depth: 1 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "div", depth: 1 * STACK_THICKNESS, thickness: STACK_THICKNESS },
+ { name: "span", depth: 3 * STACK_THICKNESS, thickness: 0 },
+ ];
+
+ is(store.nodes.length, expected.length,
+ "The traverse() function didn't walk the correct number of nodes.");
+ is(store.info.length, expected.length,
+ "The traverse() function didn't examine the correct number of nodes.");
+
+ for (let i = 0; i < expected.length; i++) {
+ is(store.info[i].name, expected[i].name,
+ "traversed node " + (i + 1) + " isn't the expected one.");
+ is(store.info[i].coord.depth, expected[i].depth,
+ "traversed node " + (i + 1) + " doesn't have the expected depth.");
+ is(store.info[i].coord.thickness, expected[i].thickness,
+ "traversed node " + (i + 1) + " doesn't have the expected thickness.");
+ }
+ });
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_visualizer.js b/browser/devtools/tilt/test/browser_tilt_visualizer.js
new file mode 100644
index 000000000..bc7c2bc18
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_visualizer.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping notifications test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping visualizer test because WebGL isn't supported.");
+ return;
+ }
+
+ let webGLError = false;
+ let webGLLoad = false;
+
+ let visualizer = new TiltVisualizer({
+ chromeWindow: window,
+ contentWindow: gBrowser.selectedBrowser.contentWindow,
+ parentNode: gBrowser.selectedBrowser.parentNode,
+ notifications: Tilt.NOTIFICATIONS,
+ tab: gBrowser.selectedTab,
+
+ onError: function onWebGLError()
+ {
+ webGLError = true;
+ },
+
+ onLoad: function onWebGLLoad()
+ {
+ webGLLoad = true;
+ }
+ });
+ visualizer.init();
+
+ ok(webGLError ^ webGLLoad,
+ "The WebGL context should either be created or not.");
+
+ if (webGLError) {
+ info("Skipping visualizer test because WebGL couldn't be initialized.");
+ return;
+ }
+
+ ok(visualizer.canvas,
+ "Visualizer constructor should have created a child canvas object.");
+ ok(visualizer.presenter,
+ "Visualizer constructor should have created a child presenter object.");
+ ok(visualizer.controller,
+ "Visualizer constructor should have created a child controller object.");
+ ok(visualizer.isInitialized(),
+ "The visualizer should have been initialized properly.");
+ ok(visualizer.presenter.isInitialized(),
+ "The visualizer presenter should have been initialized properly.");
+ ok(visualizer.controller.isInitialized(),
+ "The visualizer controller should have been initialized properly.");
+
+ testPresenter(visualizer.presenter);
+ testController(visualizer.controller);
+
+ visualizer.removeOverlay();
+ is(visualizer.canvas.parentNode, null,
+ "The visualizer canvas wasn't removed from the parent node.");
+
+ visualizer.cleanup();
+ is(visualizer.presenter, undefined,
+ "The visualizer presenter wasn't destroyed.");
+ is(visualizer.controller, undefined,
+ "The visualizer controller wasn't destroyed.");
+ is(visualizer.canvas, undefined,
+ "The visualizer canvas wasn't destroyed.");
+}
+
+function testPresenter(presenter) {
+ ok(presenter._renderer,
+ "The presenter renderer wasn't initialized properly.");
+ ok(presenter._visualizationProgram,
+ "The presenter visualizationProgram wasn't initialized properly.");
+ ok(presenter._texture,
+ "The presenter texture wasn't initialized properly.");
+ ok(!presenter._meshStacks,
+ "The presenter meshStacks shouldn't be initialized yet.");
+ ok(!presenter._meshWireframe,
+ "The presenter meshWireframe shouldn't be initialized yet.");
+ ok(presenter._traverseData,
+ "The presenter nodesInformation wasn't initialized properly.");
+ ok(presenter._highlight,
+ "The presenter highlight wasn't initialized properly.");
+ ok(presenter._highlight.disabled,
+ "The presenter highlight should be initially disabled.");
+ ok(isApproxVec(presenter._highlight.v0, [0, 0, 0]),
+ "The presenter highlight first vertex should be initially zeroed.");
+ ok(isApproxVec(presenter._highlight.v1, [0, 0, 0]),
+ "The presenter highlight second vertex should be initially zeroed.");
+ ok(isApproxVec(presenter._highlight.v2, [0, 0, 0]),
+ "The presenter highlight third vertex should be initially zeroed.");
+ ok(isApproxVec(presenter._highlight.v3, [0, 0, 0]),
+ "The presenter highlight fourth vertex should be initially zeroed.");
+ ok(presenter.transforms,
+ "The presenter transforms wasn't initialized properly.");
+ is(presenter.transforms.zoom, 1,
+ "The presenter transforms zoom should be initially 1.");
+ ok(isApproxVec(presenter.transforms.offset, [0, 0, 0]),
+ "The presenter transforms offset should be initially zeroed.");
+ ok(isApproxVec(presenter.transforms.translation, [0, 0, 0]),
+ "The presenter transforms translation should be initially zeroed.");
+ ok(isApproxVec(presenter.transforms.rotation, [0, 0, 0, 1]),
+ "The presenter transforms rotation should be initially set to identity.");
+
+ presenter.setTranslation([1, 2, 3]);
+ presenter.setRotation([5, 6, 7, 8]);
+
+ ok(isApproxVec(presenter.transforms.translation, [1, 2, 3]),
+ "The presenter transforms translation wasn't modified as it should");
+ ok(isApproxVec(presenter.transforms.rotation, [5, 6, 7, 8]),
+ "The presenter transforms rotation wasn't modified as it should");
+ ok(presenter._redraw,
+ "The new transforms should have issued a redraw request.");
+}
+
+function testController(controller) {
+ ok(controller.arcball,
+ "The controller arcball wasn't initialized properly.");
+ ok(!controller.coordinates,
+ "The presenter meshWireframe shouldn't be initialized yet.");
+}
diff --git a/browser/devtools/tilt/test/browser_tilt_zoom.js b/browser/devtools/tilt/test/browser_tilt_zoom.js
new file mode 100644
index 000000000..af6ac2c91
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_zoom.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const ZOOM = 2;
+const RESIZE = 50;
+let tiltOpened = false;
+
+function test() {
+ if (!isTiltEnabled()) {
+ info("Skipping controller test because Tilt isn't enabled.");
+ return;
+ }
+ if (!isWebGLSupported()) {
+ info("Skipping controller test because WebGL isn't supported.");
+ return;
+ }
+
+ waitForExplicitFinish();
+
+ createTab(function() {
+ TiltUtils.setDocumentZoom(window, ZOOM);
+
+ createTilt({
+ onTiltOpen: function(instance)
+ {
+ tiltOpened = true;
+
+ ok(isApprox(instance.presenter._getPageZoom(), ZOOM),
+ "The Highlighter zoom doesn't have the expected results.");
+
+ ok(isApprox(instance.presenter.transforms.zoom, ZOOM),
+ "The presenter transforms zoom wasn't initially set correctly.");
+
+ let contentWindow = gBrowser.selectedBrowser.contentWindow;
+ let initialWidth = contentWindow.innerWidth;
+ let initialHeight = contentWindow.innerHeight;
+
+ let renderer = instance.presenter._renderer;
+ let arcball = instance.controller.arcball;
+
+ ok(isApprox(contentWindow.innerWidth * ZOOM, renderer.width, 1),
+ "The renderer width wasn't set correctly before the resize.");
+ ok(isApprox(contentWindow.innerHeight * ZOOM, renderer.height, 1),
+ "The renderer height wasn't set correctly before the resize.");
+
+ ok(isApprox(contentWindow.innerWidth * ZOOM, arcball.width, 1),
+ "The arcball width wasn't set correctly before the resize.");
+ ok(isApprox(contentWindow.innerHeight * ZOOM, arcball.height, 1),
+ "The arcball height wasn't set correctly before the resize.");
+
+
+ window.resizeBy(-RESIZE * ZOOM, -RESIZE * ZOOM);
+
+ executeSoon(function() {
+ ok(isApprox(contentWindow.innerWidth + RESIZE, initialWidth, 1),
+ "The content window width wasn't set correctly after the resize.");
+ ok(isApprox(contentWindow.innerHeight + RESIZE, initialHeight, 1),
+ "The content window height wasn't set correctly after the resize.");
+
+ ok(isApprox(contentWindow.innerWidth * ZOOM, renderer.width, 1),
+ "The renderer width wasn't set correctly after the resize.");
+ ok(isApprox(contentWindow.innerHeight * ZOOM, renderer.height, 1),
+ "The renderer height wasn't set correctly after the resize.");
+
+ ok(isApprox(contentWindow.innerWidth * ZOOM, arcball.width, 1),
+ "The arcball width wasn't set correctly after the resize.");
+ ok(isApprox(contentWindow.innerHeight * ZOOM, arcball.height, 1),
+ "The arcball height wasn't set correctly after the resize.");
+
+
+ window.resizeBy(RESIZE * ZOOM, RESIZE * ZOOM);
+
+
+ Services.obs.addObserver(cleanup, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+ }
+ }, false, function suddenDeath()
+ {
+ info("Tilt could not be initialized properly.");
+ cleanup();
+ });
+ });
+}
+
+function cleanup() {
+ if (tiltOpened) { Services.obs.removeObserver(cleanup, DESTROYED); }
+ gBrowser.removeCurrentTab();
+ finish();
+}
diff --git a/browser/devtools/tilt/test/head.js b/browser/devtools/tilt/test/head.js
new file mode 100644
index 000000000..25482ead6
--- /dev/null
+++ b/browser/devtools/tilt/test/head.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let {devtools} = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+let TiltManager = devtools.require("devtools/tilt/tilt").TiltManager;
+let TiltGL = devtools.require("devtools/tilt/tilt-gl");
+let {EPSILON, TiltMath, vec3, mat3, mat4, quat4} = devtools.require("devtools/tilt/tilt-math");
+let TiltUtils = devtools.require("devtools/tilt/tilt-utils");
+let {TiltVisualizer} = devtools.require("devtools/tilt/tilt-visualizer");
+
+let tempScope = {};
+Components.utils.import("resource:///modules/devtools/LayoutHelpers.jsm", tempScope);
+let LayoutHelpers = tempScope.LayoutHelpers;
+
+
+const DEFAULT_HTML = "data:text/html," +
+ "<DOCTYPE html>" +
+ "<html>" +
+ "<head>" +
+ "<meta charset='utf-8'/>" +
+ "<title>Three Laws</title>" +
+ "</head>" +
+ "<body>" +
+ "<div id='first-law'>" +
+ "A robot may not injure a human being or, through inaction, allow a " +
+ "human being to come to harm." +
+ "</div>" +
+ "<div>" +
+ "A robot must obey the orders given to it by human beings, except " +
+ "where such orders would conflict with the First Law." +
+ "</div>" +
+ "<div>" +
+ "A robot must protect its own existence as long as such protection " +
+ "does not conflict with the First or Second Laws." +
+ "</div>" +
+ "<div id='far-far-away' style='position: absolute; top: 250%;'>" +
+ "I like bacon." +
+ "</div>" +
+ "<body>" +
+ "</html>";
+
+let Tilt = TiltManager.getTiltForBrowser(window);
+
+const STARTUP = Tilt.NOTIFICATIONS.STARTUP;
+const INITIALIZING = Tilt.NOTIFICATIONS.INITIALIZING;
+const INITIALIZED = Tilt.NOTIFICATIONS.INITIALIZED;
+const DESTROYING = Tilt.NOTIFICATIONS.DESTROYING;
+const BEFORE_DESTROYED = Tilt.NOTIFICATIONS.BEFORE_DESTROYED;
+const DESTROYED = Tilt.NOTIFICATIONS.DESTROYED;
+const SHOWN = Tilt.NOTIFICATIONS.SHOWN;
+const HIDDEN = Tilt.NOTIFICATIONS.HIDDEN;
+const HIGHLIGHTING = Tilt.NOTIFICATIONS.HIGHLIGHTING;
+const UNHIGHLIGHTING = Tilt.NOTIFICATIONS.UNHIGHLIGHTING;
+const NODE_REMOVED = Tilt.NOTIFICATIONS.NODE_REMOVED;
+
+const TILT_ENABLED = Services.prefs.getBoolPref("devtools.tilt.enabled");
+
+
+function isTiltEnabled() {
+ info("Apparently, Tilt is" + (TILT_ENABLED ? "" : " not") + " enabled.");
+ return TILT_ENABLED;
+}
+
+function isWebGLSupported() {
+ let supported = !TiltGL.isWebGLForceEnabled() &&
+ TiltGL.isWebGLSupported() &&
+ TiltGL.create3DContext(createCanvas());
+
+ info("Apparently, WebGL is" + (supported ? "" : " not") + " supported.");
+ return supported;
+}
+
+function isApprox(num1, num2, delta) {
+ if (Math.abs(num1 - num2) > (delta || EPSILON)) {
+ info("isApprox expected " + num1 + ", got " + num2 + " instead.");
+ return false;
+ }
+ return true;
+}
+
+function isApproxVec(vec1, vec2, delta) {
+ vec1 = Array.prototype.slice.call(vec1);
+ vec2 = Array.prototype.slice.call(vec2);
+
+ if (vec1.length !== vec2.length) {
+ return false;
+ }
+ for (let i = 0, len = vec1.length; i < len; i++) {
+ if (!isApprox(vec1[i], vec2[i], delta)) {
+ info("isApproxVec expected [" + vec1 + "], got [" + vec2 + "] instead.");
+ return false;
+ }
+ }
+ return true;
+}
+
+function isEqualVec(vec1, vec2) {
+ vec1 = Array.prototype.slice.call(vec1);
+ vec2 = Array.prototype.slice.call(vec2);
+
+ if (vec1.length !== vec2.length) {
+ return false;
+ }
+ for (let i = 0, len = vec1.length; i < len; i++) {
+ if (vec1[i] !== vec2[i]) {
+ info("isEqualVec expected [" + vec1 + "], got [" + vec2 + "] instead.");
+ return false;
+ }
+ }
+ return true;
+}
+
+function createCanvas() {
+ return document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+}
+
+
+function createTab(callback, location) {
+ info("Creating a tab, with callback " + typeof callback +
+ ", and location " + location + ".");
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+
+ gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ callback(tab);
+ }, true);
+
+ gBrowser.selectedBrowser.contentWindow.location = location || DEFAULT_HTML;
+ return tab;
+}
+
+
+function createTilt(callbacks, close, suddenDeath) {
+ info("Creating Tilt, with callbacks {" + Object.keys(callbacks) + "}" +
+ ", autoclose param " + close +
+ ", and sudden death handler " + typeof suddenDeath + ".");
+
+ handleFailure(suddenDeath);
+
+ Services.prefs.setBoolPref("webgl.verbose", true);
+ TiltUtils.Output.suppressAlerts = true;
+
+ info("Attempting to start Tilt.");
+ Services.obs.addObserver(onTiltOpen, INITIALIZING, false);
+ Tilt.toggle();
+
+ function onTiltOpen() {
+ info("Tilt was opened.");
+ Services.obs.removeObserver(onTiltOpen, INITIALIZING);
+
+ executeSoon(function() {
+ if ("function" === typeof callbacks.onTiltOpen) {
+ info("Calling 'onTiltOpen'.");
+ callbacks.onTiltOpen(Tilt.visualizers[Tilt.currentWindowId]);
+ }
+ if (close) {
+ executeSoon(function() {
+ info("Attempting to close Tilt.");
+ Services.obs.addObserver(onTiltClose, DESTROYED, false);
+ Tilt.destroy(Tilt.currentWindowId);
+ });
+ }
+ });
+ }
+
+ function onTiltClose() {
+ info("Tilt was closed.");
+ Services.obs.removeObserver(onTiltClose, DESTROYED);
+
+ executeSoon(function() {
+ if ("function" === typeof callbacks.onTiltClose) {
+ info("Calling 'onTiltClose'.");
+ callbacks.onTiltClose();
+ }
+ if ("function" === typeof callbacks.onEnd) {
+ info("Calling 'onEnd'.");
+ callbacks.onEnd();
+ }
+ });
+ }
+
+ function handleFailure(suddenDeath) {
+ Tilt.failureCallback = function() {
+ info("Tilt FAIL.");
+ Services.obs.removeObserver(onTiltOpen, INITIALIZING);
+
+ info("Now relying on sudden death handler " + typeof suddenDeath + ".");
+ suddenDeath && suddenDeath();
+ }
+ }
+}
+
+function getPickablePoint(presenter) {
+ let vertices = presenter._meshStacks[0].vertices.components;
+
+ let topLeft = vec3.create([vertices[0], vertices[1], vertices[2]]);
+ let bottomRight = vec3.create([vertices[6], vertices[7], vertices[8]]);
+ let center = vec3.lerp(topLeft, bottomRight, 0.5, []);
+
+ let renderer = presenter._renderer;
+ let viewport = [0, 0, renderer.width, renderer.height];
+
+ return vec3.project(center, viewport, renderer.mvMatrix, renderer.projMatrix);
+}
diff --git a/browser/devtools/tilt/test/moz.build b/browser/devtools/tilt/test/moz.build
new file mode 100644
index 000000000..895d11993
--- /dev/null
+++ b/browser/devtools/tilt/test/moz.build
@@ -0,0 +1,6 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
diff --git a/browser/devtools/tilt/tilt-gl.js b/browser/devtools/tilt/tilt-gl.js
new file mode 100644
index 000000000..0f3367fec
--- /dev/null
+++ b/browser/devtools/tilt/tilt-gl.js
@@ -0,0 +1,1595 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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";
+
+const {Cc, Ci, Cu} = require("chrome");
+
+let TiltUtils = require("devtools/tilt/tilt-utils");
+let {TiltMath, mat4} = require("devtools/tilt/tilt-math");
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const WEBGL_CONTEXT_NAME = "experimental-webgl";
+
+
+/**
+ * Module containing thin wrappers around low-level WebGL functions.
+ */
+let TiltGL = {};
+module.exports = TiltGL;
+
+/**
+ * Contains commonly used helper methods used in any 3D application.
+ *
+ * @param {HTMLCanvasElement} aCanvas
+ * the canvas element used for rendering
+ * @param {Function} onError
+ * optional, function called if initialization failed
+ * @param {Function} onLoad
+ * optional, function called if initialization worked
+ */
+TiltGL.Renderer = function TGL_Renderer(aCanvas, onError, onLoad)
+{
+ /**
+ * The WebGL context obtained from the canvas element, used for drawing.
+ */
+ this.context = TiltGL.create3DContext(aCanvas);
+
+ // check if the context was created successfully
+ if (!this.context) {
+ TiltUtils.Output.alert("Firefox", TiltUtils.L10n.get("initTilt.error"));
+ TiltUtils.Output.error(TiltUtils.L10n.get("initWebGL.error"));
+
+ if ("function" === typeof onError) {
+ onError();
+ }
+ return;
+ }
+
+ // set the default clear color and depth buffers
+ this.context.clearColor(0, 0, 0, 0);
+ this.context.clearDepth(1);
+
+ /**
+ * Variables representing the current framebuffer width and height.
+ */
+ this.width = aCanvas.width;
+ this.height = aCanvas.height;
+ this.initialWidth = this.width;
+ this.initialHeight = this.height;
+
+ /**
+ * The current model view matrix.
+ */
+ this.mvMatrix = mat4.identity(mat4.create());
+
+ /**
+ * The current projection matrix.
+ */
+ this.projMatrix = mat4.identity(mat4.create());
+
+ /**
+ * The current fill color applied to any objects which can be filled.
+ * These are rectangles, circles, boxes, 2d or 3d primitives in general.
+ */
+ this._fillColor = [];
+
+ /**
+ * The current stroke color applied to any objects which can be stroked.
+ * This property mostly refers to lines.
+ */
+ this._strokeColor = [];
+
+ /**
+ * Variable representing the current stroke weight.
+ */
+ this._strokeWeightValue = 0;
+
+ /**
+ * A shader useful for drawing vertices with only a color component.
+ */
+ this._colorShader = new TiltGL.Program(this.context, {
+ vs: TiltGL.ColorShader.vs,
+ fs: TiltGL.ColorShader.fs,
+ attributes: ["vertexPosition"],
+ uniforms: ["mvMatrix", "projMatrix", "fill"]
+ });
+
+ // create helper functions to create shaders, meshes, buffers and textures
+ this.Program =
+ TiltGL.Program.bind(TiltGL.Program, this.context);
+ this.VertexBuffer =
+ TiltGL.VertexBuffer.bind(TiltGL.VertexBuffer, this.context);
+ this.IndexBuffer =
+ TiltGL.IndexBuffer.bind(TiltGL.IndexBuffer, this.context);
+ this.Texture =
+ TiltGL.Texture.bind(TiltGL.Texture, this.context);
+
+ // set the default mvp matrices, tint, fill, stroke and other visual props.
+ this.defaults();
+
+ // the renderer was created successfully
+ if ("function" === typeof onLoad) {
+ onLoad();
+ }
+};
+
+TiltGL.Renderer.prototype = {
+
+ /**
+ * Clears the color and depth buffers.
+ */
+ clear: function TGLR_clear()
+ {
+ let gl = this.context;
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+ },
+
+ /**
+ * Sets if depth testing should be enabled or not.
+ * Disabling could be useful when handling transparency (for example).
+ *
+ * @param {Boolean} aEnabledFlag
+ * true if depth testing should be enabled
+ */
+ depthTest: function TGLR_depthTest(aEnabledFlag)
+ {
+ let gl = this.context;
+
+ if (aEnabledFlag) {
+ gl.enable(gl.DEPTH_TEST);
+ } else {
+ gl.disable(gl.DEPTH_TEST);
+ }
+ },
+
+ /**
+ * Sets if stencil testing should be enabled or not.
+ *
+ * @param {Boolean} aEnabledFlag
+ * true if stencil testing should be enabled
+ */
+ stencilTest: function TGLR_stencilTest(aEnabledFlag)
+ {
+ let gl = this.context;
+
+ if (aEnabledFlag) {
+ gl.enable(gl.STENCIL_TEST);
+ } else {
+ gl.disable(gl.STENCIL_TEST);
+ }
+ },
+
+ /**
+ * Sets cull face, either "front", "back" or disabled.
+ *
+ * @param {String} aModeFlag
+ * blending mode, either "front", "back", "both" or falsy
+ */
+ cullFace: function TGLR_cullFace(aModeFlag)
+ {
+ let gl = this.context;
+
+ switch (aModeFlag) {
+ case "front":
+ gl.enable(gl.CULL_FACE);
+ gl.cullFace(gl.FRONT);
+ break;
+ case "back":
+ gl.enable(gl.CULL_FACE);
+ gl.cullFace(gl.BACK);
+ break;
+ case "both":
+ gl.enable(gl.CULL_FACE);
+ gl.cullFace(gl.FRONT_AND_BACK);
+ break;
+ default:
+ gl.disable(gl.CULL_FACE);
+ }
+ },
+
+ /**
+ * Specifies the orientation of front-facing polygons.
+ *
+ * @param {String} aModeFlag
+ * either "cw" or "ccw"
+ */
+ frontFace: function TGLR_frontFace(aModeFlag)
+ {
+ let gl = this.context;
+
+ switch (aModeFlag) {
+ case "cw":
+ gl.frontFace(gl.CW);
+ break;
+ case "ccw":
+ gl.frontFace(gl.CCW);
+ break;
+ }
+ },
+
+ /**
+ * Sets blending, either "alpha" or "add" (additive blending).
+ * Anything else disables blending.
+ *
+ * @param {String} aModeFlag
+ * blending mode, either "alpha", "add" or falsy
+ */
+ blendMode: function TGLR_blendMode(aModeFlag)
+ {
+ let gl = this.context;
+
+ switch (aModeFlag) {
+ case "alpha":
+ gl.enable(gl.BLEND);
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
+ break;
+ case "add":
+ gl.enable(gl.BLEND);
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
+ break;
+ default:
+ gl.disable(gl.BLEND);
+ }
+ },
+
+ /**
+ * Helper function to activate the color shader.
+ *
+ * @param {TiltGL.VertexBuffer} aVerticesBuffer
+ * a buffer of vertices positions
+ * @param {Array} aColor
+ * the color fill to be used as [r, g, b, a] with 0..1 range
+ * @param {Array} aMvMatrix
+ * the model view matrix
+ * @param {Array} aProjMatrix
+ * the projection matrix
+ */
+ useColorShader: function TGLR_useColorShader(
+ aVerticesBuffer, aColor, aMvMatrix, aProjMatrix)
+ {
+ let program = this._colorShader;
+
+ // use this program
+ program.use();
+
+ // bind the attributes and uniforms as necessary
+ program.bindVertexBuffer("vertexPosition", aVerticesBuffer);
+ program.bindUniformMatrix("mvMatrix", aMvMatrix || this.mvMatrix);
+ program.bindUniformMatrix("projMatrix", aProjMatrix || this.projMatrix);
+ program.bindUniformVec4("fill", aColor || this._fillColor);
+ },
+
+ /**
+ * Draws bound vertex buffers using the specified parameters.
+ *
+ * @param {Number} aDrawMode
+ * WebGL enum, like TRIANGLES
+ * @param {Number} aCount
+ * the number of indices to be rendered
+ */
+ drawVertices: function TGLR_drawVertices(aDrawMode, aCount)
+ {
+ this.context.drawArrays(aDrawMode, 0, aCount);
+ },
+
+ /**
+ * Draws bound vertex buffers using the specified parameters.
+ * This function also makes use of an index buffer.
+ *
+ * @param {Number} aDrawMode
+ * WebGL enum, like TRIANGLES
+ * @param {TiltGL.IndexBuffer} aIndicesBuffer
+ * indices for the vertices buffer
+ */
+ drawIndexedVertices: function TGLR_drawIndexedVertices(
+ aDrawMode, aIndicesBuffer)
+ {
+ let gl = this.context;
+
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, aIndicesBuffer._ref);
+ gl.drawElements(aDrawMode, aIndicesBuffer.numItems, gl.UNSIGNED_SHORT, 0);
+ },
+
+ /**
+ * Sets the current fill color.
+ *
+ * @param {Array} aColor
+ * the color fill to be used as [r, g, b, a] with 0..1 range
+ * @param {Number} aMultiplyAlpha
+ * optional, scalar to multiply the alpha element with
+ */
+ fill: function TGLR_fill(aColor, aMultiplyAlpha)
+ {
+ let fill = this._fillColor;
+
+ fill[0] = aColor[0];
+ fill[1] = aColor[1];
+ fill[2] = aColor[2];
+ fill[3] = aColor[3] * (aMultiplyAlpha || 1);
+ },
+
+ /**
+ * Sets the current stroke color.
+ *
+ * @param {Array} aColor
+ * the color stroke to be used as [r, g, b, a] with 0..1 range
+ * @param {Number} aMultiplyAlpha
+ * optional, scalar to multiply the alpha element with
+ */
+ stroke: function TGLR_stroke(aColor, aMultiplyAlpha)
+ {
+ let stroke = this._strokeColor;
+
+ stroke[0] = aColor[0];
+ stroke[1] = aColor[1];
+ stroke[2] = aColor[2];
+ stroke[3] = aColor[3] * (aMultiplyAlpha || 1);
+ },
+
+ /**
+ * Sets the current stroke weight (line width).
+ *
+ * @param {Number} aWeight
+ * the stroke weight
+ */
+ strokeWeight: function TGLR_strokeWeight(aWeight)
+ {
+ if (this._strokeWeightValue !== aWeight) {
+ this._strokeWeightValue = aWeight;
+ this.context.lineWidth(aWeight);
+ }
+ },
+
+ /**
+ * Sets a default perspective projection, with the near frustum rectangle
+ * mapped to the canvas width and height bounds.
+ */
+ perspective: function TGLR_perspective()
+ {
+ let fov = 45;
+ let w = this.width;
+ let h = this.height;
+ let x = w / 2;
+ let y = h / 2;
+ let z = y / Math.tan(TiltMath.radians(fov) / 2);
+ let aspect = w / h;
+ let znear = z / 10;
+ let zfar = z * 10;
+
+ mat4.perspective(fov, aspect, znear, zfar, this.projMatrix, -1);
+ mat4.translate(this.projMatrix, [-x, -y, -z]);
+ mat4.identity(this.mvMatrix);
+ },
+
+ /**
+ * Sets a default orthographic projection (recommended for 2d rendering).
+ */
+ ortho: function TGLR_ortho()
+ {
+ mat4.ortho(0, this.width, this.height, 0, -1, 1, this.projMatrix);
+ mat4.identity(this.mvMatrix);
+ },
+
+ /**
+ * Sets a custom projection matrix.
+ * @param {Array} matrix: the custom projection matrix to be used
+ */
+ projection: function TGLR_projection(aMatrix)
+ {
+ mat4.set(aMatrix, this.projMatrix);
+ mat4.identity(this.mvMatrix);
+ },
+
+ /**
+ * Resets the model view matrix to identity.
+ * This is a default matrix with no rotation, no scaling, at (0, 0, 0);
+ */
+ origin: function TGLR_origin()
+ {
+ mat4.identity(this.mvMatrix);
+ },
+
+ /**
+ * Transforms the model view matrix with a new matrix.
+ * Useful for creating custom transformations.
+ *
+ * @param {Array} matrix: the matrix to be multiply the model view with
+ */
+ transform: function TGLR_transform(aMatrix)
+ {
+ mat4.multiply(this.mvMatrix, aMatrix);
+ },
+
+ /**
+ * Translates the model view by the x, y and z coordinates.
+ *
+ * @param {Number} x
+ * the x amount of translation
+ * @param {Number} y
+ * the y amount of translation
+ * @param {Number} z
+ * optional, the z amount of translation
+ */
+ translate: function TGLR_translate(x, y, z)
+ {
+ mat4.translate(this.mvMatrix, [x, y, z || 0]);
+ },
+
+ /**
+ * Rotates the model view by a specified angle on the x, y and z axis.
+ *
+ * @param {Number} angle
+ * the angle expressed in radians
+ * @param {Number} x
+ * the x axis of the rotation
+ * @param {Number} y
+ * the y axis of the rotation
+ * @param {Number} z
+ * the z axis of the rotation
+ */
+ rotate: function TGLR_rotate(angle, x, y, z)
+ {
+ mat4.rotate(this.mvMatrix, angle, [x, y, z]);
+ },
+
+ /**
+ * Rotates the model view by a specified angle on the x axis.
+ *
+ * @param {Number} aAngle
+ * the angle expressed in radians
+ */
+ rotateX: function TGLR_rotateX(aAngle)
+ {
+ mat4.rotateX(this.mvMatrix, aAngle);
+ },
+
+ /**
+ * Rotates the model view by a specified angle on the y axis.
+ *
+ * @param {Number} aAngle
+ * the angle expressed in radians
+ */
+ rotateY: function TGLR_rotateY(aAngle)
+ {
+ mat4.rotateY(this.mvMatrix, aAngle);
+ },
+
+ /**
+ * Rotates the model view by a specified angle on the z axis.
+ *
+ * @param {Number} aAngle
+ * the angle expressed in radians
+ */
+ rotateZ: function TGLR_rotateZ(aAngle)
+ {
+ mat4.rotateZ(this.mvMatrix, aAngle);
+ },
+
+ /**
+ * Scales the model view by the x, y and z coordinates.
+ *
+ * @param {Number} x
+ * the x amount of scaling
+ * @param {Number} y
+ * the y amount of scaling
+ * @param {Number} z
+ * optional, the z amount of scaling
+ */
+ scale: function TGLR_scale(x, y, z)
+ {
+ mat4.scale(this.mvMatrix, [x, y, z || 1]);
+ },
+
+ /**
+ * Performs a custom interpolation between two matrices.
+ * The result is saved in the first operand.
+ *
+ * @param {Array} aMat
+ * the first matrix
+ * @param {Array} aMat2
+ * the second matrix
+ * @param {Number} aLerp
+ * interpolation amount between the two inputs
+ * @param {Number} aDamping
+ * optional, scalar adjusting the interpolation amortization
+ * @param {Number} aBalance
+ * optional, scalar adjusting the interpolation shift ammount
+ */
+ lerp: function TGLR_lerp(aMat, aMat2, aLerp, aDamping, aBalance)
+ {
+ if (aLerp < 0 || aLerp > 1) {
+ return;
+ }
+
+ // calculate the interpolation factor based on the damping and step
+ let f = Math.pow(1 - Math.pow(aLerp, aDamping || 1), 1 / aBalance || 1);
+
+ // interpolate each element from the two matrices
+ for (let i = 0, len = this.projMatrix.length; i < len; i++) {
+ aMat[i] = aMat[i] + f * (aMat2[i] - aMat[i]);
+ }
+ },
+
+ /**
+ * Resets the drawing style to default.
+ */
+ defaults: function TGLR_defaults()
+ {
+ this.depthTest(true);
+ this.stencilTest(false);
+ this.cullFace(false);
+ this.frontFace("ccw");
+ this.blendMode("alpha");
+ this.fill([1, 1, 1, 1]);
+ this.stroke([0, 0, 0, 1]);
+ this.strokeWeight(1);
+ this.perspective();
+ this.origin();
+ },
+
+ /**
+ * Draws a quad composed of four vertices.
+ * Vertices must be in clockwise order, or else drawing will be distorted.
+ * Do not abuse this function, it is quite slow.
+ *
+ * @param {Array} aV0
+ * the [x, y, z] position of the first triangle point
+ * @param {Array} aV1
+ * the [x, y, z] position of the second triangle point
+ * @param {Array} aV2
+ * the [x, y, z] position of the third triangle point
+ * @param {Array} aV3
+ * the [x, y, z] position of the fourth triangle point
+ */
+ quad: function TGLR_quad(aV0, aV1, aV2, aV3)
+ {
+ let gl = this.context;
+ let fill = this._fillColor;
+ let stroke = this._strokeColor;
+ let vert = new TiltGL.VertexBuffer(gl, [aV0[0], aV0[1], aV0[2] || 0,
+ aV1[0], aV1[1], aV1[2] || 0,
+ aV2[0], aV2[1], aV2[2] || 0,
+ aV3[0], aV3[1], aV3[2] || 0], 3);
+
+ // use the necessary shader and draw the vertices
+ this.useColorShader(vert, fill);
+ this.drawVertices(gl.TRIANGLE_FAN, vert.numItems);
+
+ this.useColorShader(vert, stroke);
+ this.drawVertices(gl.LINE_LOOP, vert.numItems);
+
+ TiltUtils.destroyObject(vert);
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ finalize: function TGLR_finalize()
+ {
+ if (this.context) {
+ TiltUtils.destroyObject(this._colorShader);
+ }
+ }
+};
+
+/**
+ * Creates a vertex buffer containing an array of elements.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {Array} aElementsArray
+ * an array of numbers (floats)
+ * @param {Number} aItemSize
+ * how many items create a block
+ * @param {Number} aNumItems
+ * optional, how many items to use from the array
+ */
+TiltGL.VertexBuffer = function TGL_VertexBuffer(
+ aContext, aElementsArray, aItemSize, aNumItems)
+{
+ /**
+ * The parent WebGL context.
+ */
+ this._context = aContext;
+
+ /**
+ * The array buffer.
+ */
+ this._ref = null;
+
+ /**
+ * Array of number components contained in the buffer.
+ */
+ this.components = null;
+
+ /**
+ * Variables defining the internal structure of the buffer.
+ */
+ this.itemSize = 0;
+ this.numItems = 0;
+
+ // if the array is specified in the constructor, initialize directly
+ if (aElementsArray) {
+ this.initBuffer(aElementsArray, aItemSize, aNumItems);
+ }
+};
+
+TiltGL.VertexBuffer.prototype = {
+
+ /**
+ * Initializes buffer data to be used for drawing, using an array of floats.
+ * The "aNumItems" param can be specified to use only a portion of the array.
+ *
+ * @param {Array} aElementsArray
+ * an array of floats
+ * @param {Number} aItemSize
+ * how many items create a block
+ * @param {Number} aNumItems
+ * optional, how many items to use from the array
+ */
+ initBuffer: function TGLVB_initBuffer(aElementsArray, aItemSize, aNumItems)
+ {
+ let gl = this._context;
+
+ // the aNumItems parameter is optional, we can compute it if not specified
+ aNumItems = aNumItems || aElementsArray.length / aItemSize;
+
+ // create the Float32Array using the elements array
+ this.components = new Float32Array(aElementsArray);
+
+ // create an array buffer and bind the elements as a Float32Array
+ this._ref = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._ref);
+ gl.bufferData(gl.ARRAY_BUFFER, this.components, gl.STATIC_DRAW);
+
+ // remember some properties, useful when binding the buffer to a shader
+ this.itemSize = aItemSize;
+ this.numItems = aNumItems;
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ finalize: function TGLVB_finalize()
+ {
+ if (this._context) {
+ this._context.deleteBuffer(this._ref);
+ }
+ }
+};
+
+/**
+ * Creates an index buffer containing an array of indices.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {Array} aElementsArray
+ * an array of unsigned integers
+ * @param {Number} aNumItems
+ * optional, how many items to use from the array
+ */
+TiltGL.IndexBuffer = function TGL_IndexBuffer(
+ aContext, aElementsArray, aNumItems)
+{
+ /**
+ * The parent WebGL context.
+ */
+ this._context = aContext;
+
+ /**
+ * The element array buffer.
+ */
+ this._ref = null;
+
+ /**
+ * Array of number components contained in the buffer.
+ */
+ this.components = null;
+
+ /**
+ * Variables defining the internal structure of the buffer.
+ */
+ this.itemSize = 0;
+ this.numItems = 0;
+
+ // if the array is specified in the constructor, initialize directly
+ if (aElementsArray) {
+ this.initBuffer(aElementsArray, aNumItems);
+ }
+};
+
+TiltGL.IndexBuffer.prototype = {
+
+ /**
+ * Initializes a buffer of vertex indices, using an array of unsigned ints.
+ * The item size will automatically default to 1, and the "numItems" will be
+ * equal to the number of items in the array if not specified.
+ *
+ * @param {Array} aElementsArray
+ * an array of numbers (unsigned integers)
+ * @param {Number} aNumItems
+ * optional, how many items to use from the array
+ */
+ initBuffer: function TGLIB_initBuffer(aElementsArray, aNumItems)
+ {
+ let gl = this._context;
+
+ // the aNumItems parameter is optional, we can compute it if not specified
+ aNumItems = aNumItems || aElementsArray.length;
+
+ // create the Uint16Array using the elements array
+ this.components = new Uint16Array(aElementsArray);
+
+ // create an array buffer and bind the elements as a Uint16Array
+ this._ref = gl.createBuffer();
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._ref);
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.components, gl.STATIC_DRAW);
+
+ // remember some properties, useful when binding the buffer to a shader
+ this.itemSize = 1;
+ this.numItems = aNumItems;
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ finalize: function TGLIB_finalize()
+ {
+ if (this._context) {
+ this._context.deleteBuffer(this._ref);
+ }
+ }
+};
+
+/**
+ * A program is composed of a vertex and a fragment shader.
+ *
+ * @param {Object} aProperties
+ * optional, an object containing the following properties:
+ * {String} vs: the vertex shader source code
+ * {String} fs: the fragment shader source code
+ * {Array} attributes: an array of attributes as strings
+ * {Array} uniforms: an array of uniforms as strings
+ */
+TiltGL.Program = function(aContext, aProperties)
+{
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ /**
+ * The parent WebGL context.
+ */
+ this._context = aContext;
+
+ /**
+ * A reference to the actual GLSL program.
+ */
+ this._ref = null;
+
+ /**
+ * Each program has an unique id assigned.
+ */
+ this._id = -1;
+
+ /**
+ * Two arrays: an attributes array, containing all the cached attributes
+ * and a uniforms array, containing all the cached uniforms.
+ */
+ this._attributes = null;
+ this._uniforms = null;
+
+ // if the sources are specified in the constructor, initialize directly
+ if (aProperties.vs && aProperties.fs) {
+ this.initProgram(aProperties);
+ }
+};
+
+TiltGL.Program.prototype = {
+
+ /**
+ * Initializes a shader program, using specified source code as strings.
+ *
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {String} vs: the vertex shader source code
+ * {String} fs: the fragment shader source code
+ * {Array} attributes: an array of attributes as strings
+ * {Array} uniforms: an array of uniforms as strings
+ */
+ initProgram: function TGLP_initProgram(aProperties)
+ {
+ this._ref = TiltGL.ProgramUtils.create(this._context, aProperties);
+
+ // cache for faster access
+ this._id = this._ref.id;
+ this._attributes = this._ref.attributes;
+ this._uniforms = this._ref.uniforms;
+
+ // cleanup
+ delete this._ref.id;
+ delete this._ref.attributes;
+ delete this._ref.uniforms;
+ },
+
+ /**
+ * Uses the shader program as current one for the WebGL context; it also
+ * enables vertex attributes necessary to enable when using this program.
+ * This method also does some useful caching, as the function "useProgram"
+ * could take quite a lot of time.
+ */
+ use: function TGLP_use()
+ {
+ let id = this._id;
+ let utils = TiltGL.ProgramUtils;
+
+ // check if the program wasn't already active
+ if (utils._activeProgram !== id) {
+ utils._activeProgram = id;
+
+ // use the the program if it wasn't already set
+ this._context.useProgram(this._ref);
+ this.cleanupVertexAttrib();
+
+ // enable any necessary vertex attributes using the cache
+ for each (let attribute in this._attributes) {
+ this._context.enableVertexAttribArray(attribute);
+ utils._enabledAttributes.push(attribute);
+ }
+ }
+ },
+
+ /**
+ * Disables all currently enabled vertex attribute arrays.
+ */
+ cleanupVertexAttrib: function TGLP_cleanupVertexAttrib()
+ {
+ let utils = TiltGL.ProgramUtils;
+
+ for each (let attribute in utils._enabledAttributes) {
+ this._context.disableVertexAttribArray(attribute);
+ }
+ utils._enabledAttributes = [];
+ },
+
+ /**
+ * Binds a vertex buffer as an array buffer for a specific shader attribute.
+ *
+ * @param {String} aAtribute
+ * the attribute name obtained from the shader
+ * @param {Float32Array} aBuffer
+ * the buffer to be bound
+ */
+ bindVertexBuffer: function TGLP_bindVertexBuffer(aAtribute, aBuffer)
+ {
+ // get the cached attribute value from the shader
+ let gl = this._context;
+ let attr = this._attributes[aAtribute];
+ let size = aBuffer.itemSize;
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, aBuffer._ref);
+ gl.vertexAttribPointer(attr, size, gl.FLOAT, false, 0, 0);
+ },
+
+ /**
+ * Binds a uniform matrix to the current shader.
+ *
+ * @param {String} aUniform
+ * the uniform name to bind the variable to
+ * @param {Float32Array} m
+ * the matrix to be bound
+ */
+ bindUniformMatrix: function TGLP_bindUniformMatrix(aUniform, m)
+ {
+ this._context.uniformMatrix4fv(this._uniforms[aUniform], false, m);
+ },
+
+ /**
+ * Binds a uniform vector of 4 elements to the current shader.
+ *
+ * @param {String} aUniform
+ * the uniform name to bind the variable to
+ * @param {Float32Array} v
+ * the vector to be bound
+ */
+ bindUniformVec4: function TGLP_bindUniformVec4(aUniform, v)
+ {
+ this._context.uniform4fv(this._uniforms[aUniform], v);
+ },
+
+ /**
+ * Binds a simple float element to the current shader.
+ *
+ * @param {String} aUniform
+ * the uniform name to bind the variable to
+ * @param {Number} v
+ * the variable to be bound
+ */
+ bindUniformFloat: function TGLP_bindUniformFloat(aUniform, f)
+ {
+ this._context.uniform1f(this._uniforms[aUniform], f);
+ },
+
+ /**
+ * Binds a uniform texture for a sampler to the current shader.
+ *
+ * @param {String} aSampler
+ * the sampler name to bind the texture to
+ * @param {TiltGL.Texture} aTexture
+ * the texture to be bound
+ */
+ bindTexture: function TGLP_bindTexture(aSampler, aTexture)
+ {
+ let gl = this._context;
+
+ gl.activeTexture(gl.TEXTURE0);
+ gl.bindTexture(gl.TEXTURE_2D, aTexture._ref);
+ gl.uniform1i(this._uniforms[aSampler], 0);
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ finalize: function TGLP_finalize()
+ {
+ if (this._context) {
+ this._context.useProgram(null);
+ this._context.deleteProgram(this._ref);
+ }
+ }
+};
+
+/**
+ * Utility functions for handling GLSL shaders and programs.
+ */
+TiltGL.ProgramUtils = {
+
+ /**
+ * Initializes a shader program, using specified source code as strings,
+ * returning the newly created shader program, by compiling and linking the
+ * vertex and fragment shader.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {String} vs: the vertex shader source code
+ * {String} fs: the fragment shader source code
+ * {Array} attributes: an array of attributes as strings
+ * {Array} uniforms: an array of uniforms as strings
+ */
+ create: function TGLPU_create(aContext, aProperties)
+ {
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ // compile the two shaders
+ let vertShader = this.compile(aContext, aProperties.vs, "vertex");
+ let fragShader = this.compile(aContext, aProperties.fs, "fragment");
+ let program = this.link(aContext, vertShader, fragShader);
+
+ aContext.deleteShader(vertShader);
+ aContext.deleteShader(fragShader);
+
+ return this.cache(aContext, aProperties, program);
+ },
+
+ /**
+ * Compiles a shader source of a specific type, either vertex or fragment.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {String} aShaderSource
+ * the source code for the shader
+ * @param {String} aShaderType
+ * the shader type ("vertex" or "fragment")
+ *
+ * @return {WebGLShader} the compiled shader
+ */
+ compile: function TGLPU_compile(aContext, aShaderSource, aShaderType)
+ {
+ let gl = aContext, shader, status;
+
+ // make sure the shader source is valid
+ if ("string" !== typeof aShaderSource || aShaderSource.length < 1) {
+ TiltUtils.Output.error(
+ TiltUtils.L10n.get("compileShader.source.error"));
+ return null;
+ }
+
+ // also make sure the necessary shader mime type is valid
+ if (aShaderType === "vertex") {
+ shader = gl.createShader(gl.VERTEX_SHADER);
+ } else if (aShaderType === "fragment") {
+ shader = gl.createShader(gl.FRAGMENT_SHADER);
+ } else {
+ TiltUtils.Output.error(
+ TiltUtils.L10n.format("compileShader.type.error", [aShaderSource]));
+ return null;
+ }
+
+ // set the shader source and compile it
+ gl.shaderSource(shader, aShaderSource);
+ gl.compileShader(shader);
+
+ // remember the shader source (useful for debugging and caching)
+ shader.src = aShaderSource;
+
+ // verify the compile status; if something went wrong, log the error
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+ status = gl.getShaderInfoLog(shader);
+
+ TiltUtils.Output.error(
+ TiltUtils.L10n.format("compileShader.compile.error", [status]));
+ return null;
+ }
+
+ // return the newly compiled shader from the specified source
+ return shader;
+ },
+
+ /**
+ * Links two compiled vertex or fragment shaders together to form a program.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {WebGLShader} aVertShader
+ * the compiled vertex shader
+ * @param {WebGLShader} aFragShader
+ * the compiled fragment shader
+ *
+ * @return {WebGLProgram} the newly created and linked shader program
+ */
+ link: function TGLPU_link(aContext, aVertShader, aFragShader)
+ {
+ let gl = aContext, program, status;
+
+ // create a program and attach the compiled vertex and fragment shaders
+ program = gl.createProgram();
+
+ // attach the vertex and fragment shaders to the program
+ gl.attachShader(program, aVertShader);
+ gl.attachShader(program, aFragShader);
+ gl.linkProgram(program);
+
+ // verify the link status; if something went wrong, log the error
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+ status = gl.getProgramInfoLog(program);
+
+ TiltUtils.Output.error(
+ TiltUtils.L10n.format("linkProgram.error", [status]));
+ return null;
+ }
+
+ // generate an id for the program
+ program.id = this._count++;
+
+ return program;
+ },
+
+ /**
+ * Caches shader attributes and uniforms as properties for a program object.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {Array} attributes: optional, an array of attributes as strings
+ * {Array} uniforms: optional, an array of uniforms as strings
+ * @param {WebGLProgram} aProgram
+ * the shader program used for caching
+ *
+ * @return {WebGLProgram} the same program
+ */
+ cache: function TGLPU_cache(aContext, aProperties, aProgram)
+ {
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ // make sure the attributes and uniforms cache objects are created
+ aProgram.attributes = {};
+ aProgram.uniforms = {};
+
+ Object.defineProperty(aProgram.attributes, "length",
+ { value: 0, writable: true, enumerable: false, configurable: true });
+
+ Object.defineProperty(aProgram.uniforms, "length",
+ { value: 0, writable: true, enumerable: false, configurable: true });
+
+
+ let attr = aProperties.attributes;
+ let unif = aProperties.uniforms;
+
+ if (attr) {
+ for (let i = 0, len = attr.length; i < len; i++) {
+ // try to get a shader attribute from the program
+ let param = attr[i];
+ let loc = aContext.getAttribLocation(aProgram, param);
+
+ if ("number" === typeof loc && loc > -1) {
+ // if we get an attribute location, store it
+ // bind the new parameter only if it was not already defined
+ if (aProgram.attributes[param] === undefined) {
+ aProgram.attributes[param] = loc;
+ aProgram.attributes.length++;
+ }
+ }
+ }
+ }
+
+ if (unif) {
+ for (let i = 0, len = unif.length; i < len; i++) {
+ // try to get a shader uniform from the program
+ let param = unif[i];
+ let loc = aContext.getUniformLocation(aProgram, param);
+
+ if ("object" === typeof loc && loc) {
+ // if we get a uniform object, store it
+ // bind the new parameter only if it was not already defined
+ if (aProgram.uniforms[param] === undefined) {
+ aProgram.uniforms[param] = loc;
+ aProgram.uniforms.length++;
+ }
+ }
+ }
+ }
+
+ return aProgram;
+ },
+
+ /**
+ * The total number of programs created.
+ */
+ _count: 0,
+
+ /**
+ * Represents the current active shader, identified by an id.
+ */
+ _activeProgram: -1,
+
+ /**
+ * Represents the current enabled attributes.
+ */
+ _enabledAttributes: []
+};
+
+/**
+ * This constructor creates a texture from an Image.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {Object} aProperties
+ * optional, an object containing the following properties:
+ * {Image} source: the source image for the texture
+ * {String} format: the format of the texture ("RGB" or "RGBA")
+ * {String} fill: optional, color to fill the transparent bits
+ * {String} stroke: optional, color to draw an outline
+ * {Number} strokeWeight: optional, the width of the outline
+ * {String} minFilter: either "nearest" or "linear"
+ * {String} magFilter: either "nearest" or "linear"
+ * {String} wrapS: either "repeat" or "clamp"
+ * {String} wrapT: either "repeat" or "clamp"
+ * {Boolean} mipmap: true if should generate mipmap
+ */
+TiltGL.Texture = function(aContext, aProperties)
+{
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ /**
+ * The parent WebGL context.
+ */
+ this._context = aContext;
+
+ /**
+ * A reference to the WebGL texture object.
+ */
+ this._ref = null;
+
+ /**
+ * Each texture has an unique id assigned.
+ */
+ this._id = -1;
+
+ /**
+ * Variables specifying the width and height of the texture.
+ * If these values are less than 0, the texture hasn't loaded yet.
+ */
+ this.width = -1;
+ this.height = -1;
+
+ /**
+ * Specifies if the texture has loaded or not.
+ */
+ this.loaded = false;
+
+ // if the image is specified in the constructor, initialize directly
+ if ("object" === typeof aProperties.source) {
+ this.initTexture(aProperties);
+ } else {
+ TiltUtils.Output.error(
+ TiltUtils.L10n.get("initTexture.source.error"));
+ }
+};
+
+TiltGL.Texture.prototype = {
+
+ /**
+ * Initializes a texture from a pre-existing image or canvas.
+ *
+ * @param {Image} aImage
+ * the source image or canvas
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {Image} source: the source image for the texture
+ * {String} format: the format of the texture ("RGB" or "RGBA")
+ * {String} fill: optional, color to fill the transparent bits
+ * {String} stroke: optional, color to draw an outline
+ * {Number} strokeWeight: optional, the width of the outline
+ * {String} minFilter: either "nearest" or "linear"
+ * {String} magFilter: either "nearest" or "linear"
+ * {String} wrapS: either "repeat" or "clamp"
+ * {String} wrapT: either "repeat" or "clamp"
+ * {Boolean} mipmap: true if should generate mipmap
+ */
+ initTexture: function TGLT_initTexture(aProperties)
+ {
+ this._ref = TiltGL.TextureUtils.create(this._context, aProperties);
+
+ // cache for faster access
+ this._id = this._ref.id;
+ this.width = this._ref.width;
+ this.height = this._ref.height;
+ this.loaded = true;
+
+ // cleanup
+ delete this._ref.id;
+ delete this._ref.width;
+ delete this._ref.height;
+ delete this.onload;
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ finalize: function TGLT_finalize()
+ {
+ if (this._context) {
+ this._context.deleteTexture(this._ref);
+ }
+ }
+};
+
+/**
+ * Utility functions for creating and manipulating textures.
+ */
+TiltGL.TextureUtils = {
+
+ /**
+ * Initializes a texture from a pre-existing image or canvas.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {Image} aImage
+ * the source image or canvas
+ * @param {Object} aProperties
+ * an object containing some of the following properties:
+ * {Image} source: the source image for the texture
+ * {String} format: the format of the texture ("RGB" or "RGBA")
+ * {String} fill: optional, color to fill the transparent bits
+ * {String} stroke: optional, color to draw an outline
+ * {Number} strokeWeight: optional, the width of the outline
+ * {String} minFilter: either "nearest" or "linear"
+ * {String} magFilter: either "nearest" or "linear"
+ * {String} wrapS: either "repeat" or "clamp"
+ * {String} wrapT: either "repeat" or "clamp"
+ * {Boolean} mipmap: true if should generate mipmap
+ *
+ * @return {WebGLTexture} the created texture
+ */
+ create: function TGLTU_create(aContext, aProperties)
+ {
+ // make sure the properties argument is an object
+ aProperties = aProperties || {};
+
+ if (!aProperties.source) {
+ return null;
+ }
+
+ let gl = aContext;
+ let width = aProperties.source.width;
+ let height = aProperties.source.height;
+ let format = gl[aProperties.format || "RGB"];
+
+ // make sure the image is power of two before binding to a texture
+ let source = this.resizeImageToPowerOfTwo(aProperties);
+
+ // first, create the texture to hold the image data
+ let texture = gl.createTexture();
+
+ // attach the image data to the newly create texture
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, format, format, gl.UNSIGNED_BYTE, source);
+ this.setTextureParams(gl, aProperties);
+
+ // do some cleanup
+ gl.bindTexture(gl.TEXTURE_2D, null);
+
+ // remember the width and the height
+ texture.width = width;
+ texture.height = height;
+
+ // generate an id for the texture
+ texture.id = this._count++;
+
+ return texture;
+ },
+
+ /**
+ * Sets texture parameters for the current texture binding.
+ * Optionally, you can also (re)set the current texture binding manually.
+ *
+ * @param {Object} aContext
+ * a WebGL context
+ * @param {Object} aProperties
+ * an object containing the texture properties
+ */
+ setTextureParams: function TGLTU_setTextureParams(aContext, aProperties)
+ {
+ // make sure the properties argument is an object
+ aProperties = aProperties || {};
+
+ let gl = aContext;
+ let minFilter = gl.TEXTURE_MIN_FILTER;
+ let magFilter = gl.TEXTURE_MAG_FILTER;
+ let wrapS = gl.TEXTURE_WRAP_S;
+ let wrapT = gl.TEXTURE_WRAP_T;
+
+ // bind a new texture if necessary
+ if (aProperties.texture) {
+ gl.bindTexture(gl.TEXTURE_2D, aProperties.texture.ref);
+ }
+
+ // set the minification filter
+ if ("nearest" === aProperties.minFilter) {
+ gl.texParameteri(gl.TEXTURE_2D, minFilter, gl.NEAREST);
+ } else if ("linear" === aProperties.minFilter && aProperties.mipmap) {
+ gl.texParameteri(gl.TEXTURE_2D, minFilter, gl.LINEAR_MIPMAP_LINEAR);
+ } else {
+ gl.texParameteri(gl.TEXTURE_2D, minFilter, gl.LINEAR);
+ }
+
+ // set the magnification filter
+ if ("nearest" === aProperties.magFilter) {
+ gl.texParameteri(gl.TEXTURE_2D, magFilter, gl.NEAREST);
+ } else {
+ gl.texParameteri(gl.TEXTURE_2D, magFilter, gl.LINEAR);
+ }
+
+ // set the wrapping on the x-axis for the texture
+ if ("repeat" === aProperties.wrapS) {
+ gl.texParameteri(gl.TEXTURE_2D, wrapS, gl.REPEAT);
+ } else {
+ gl.texParameteri(gl.TEXTURE_2D, wrapS, gl.CLAMP_TO_EDGE);
+ }
+
+ // set the wrapping on the y-axis for the texture
+ if ("repeat" === aProperties.wrapT) {
+ gl.texParameteri(gl.TEXTURE_2D, wrapT, gl.REPEAT);
+ } else {
+ gl.texParameteri(gl.TEXTURE_2D, wrapT, gl.CLAMP_TO_EDGE);
+ }
+
+ // generate mipmap if necessary
+ if (aProperties.mipmap) {
+ gl.generateMipmap(gl.TEXTURE_2D);
+ }
+ },
+
+ /**
+ * This shim renders a content window to a canvas element, but clamps the
+ * maximum width and height of the canvas to the WebGL MAX_TEXTURE_SIZE.
+ *
+ * @param {Window} aContentWindow
+ * the content window to get a texture from
+ * @param {Number} aMaxImageSize
+ * the maximum image size to be used
+ *
+ * @return {Image} the new content window image
+ */
+ createContentImage: function TGLTU_createContentImage(
+ aContentWindow, aMaxImageSize)
+ {
+ // calculate the total width and height of the content page
+ let size = TiltUtils.DOM.getContentWindowDimensions(aContentWindow);
+
+ // use a custom canvas element and a 2d context to draw the window
+ let canvas = TiltUtils.DOM.initCanvas(null);
+ canvas.width = TiltMath.clamp(size.width, 0, aMaxImageSize);
+ canvas.height = TiltMath.clamp(size.height, 0, aMaxImageSize);
+
+ // use the 2d context.drawWindow() magic
+ let ctx = canvas.getContext("2d");
+ ctx.drawWindow(aContentWindow, 0, 0, canvas.width, canvas.height, "#fff");
+
+ return canvas;
+ },
+
+ /**
+ * Scales an image's width and height to next power of two.
+ * If the image already has power of two sizes, it is immediately returned,
+ * otherwise, a new image is created.
+ *
+ * @param {Image} aImage
+ * the image to be scaled
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {Image} source: the source image to resize
+ * {Boolean} resize: true to resize the image if it has npot dimensions
+ * {String} fill: optional, color to fill the transparent bits
+ * {String} stroke: optional, color to draw an image outline
+ * {Number} strokeWeight: optional, the width of the outline
+ *
+ * @return {Image} the resized image
+ */
+ resizeImageToPowerOfTwo: function TGLTU_resizeImageToPowerOfTwo(aProperties)
+ {
+ // make sure the properties argument is an object
+ aProperties = aProperties || {};
+
+ if (!aProperties.source) {
+ return null;
+ }
+
+ let isPowerOfTwoWidth = TiltMath.isPowerOfTwo(aProperties.source.width);
+ let isPowerOfTwoHeight = TiltMath.isPowerOfTwo(aProperties.source.height);
+
+ // first check if the image is not already power of two
+ if (!aProperties.resize || (isPowerOfTwoWidth && isPowerOfTwoHeight)) {
+ return aProperties.source;
+ }
+
+ // calculate the power of two dimensions for the npot image
+ let width = TiltMath.nextPowerOfTwo(aProperties.source.width);
+ let height = TiltMath.nextPowerOfTwo(aProperties.source.height);
+
+ // create a canvas, then we will use a 2d context to scale the image
+ let canvas = TiltUtils.DOM.initCanvas(null, {
+ width: width,
+ height: height
+ });
+
+ let ctx = canvas.getContext("2d");
+
+ // optional fill (useful when handling transparent images)
+ if (aProperties.fill) {
+ ctx.fillStyle = aProperties.fill;
+ ctx.fillRect(0, 0, width, height);
+ }
+
+ // draw the image with power of two dimensions
+ ctx.drawImage(aProperties.source, 0, 0, width, height);
+
+ // optional stroke (useful when creating textures for edges)
+ if (aProperties.stroke) {
+ ctx.strokeStyle = aProperties.stroke;
+ ctx.lineWidth = aProperties.strokeWeight;
+ ctx.strokeRect(0, 0, width, height);
+ }
+
+ return canvas;
+ },
+
+ /**
+ * The total number of textures created.
+ */
+ _count: 0
+};
+
+/**
+ * A color shader. The only useful thing it does is set the gl_FragColor.
+ *
+ * @param {Attribute} vertexPosition: the vertex position
+ * @param {Uniform} mvMatrix: the model view matrix
+ * @param {Uniform} projMatrix: the projection matrix
+ * @param {Uniform} color: the color to set the gl_FragColor to
+ */
+TiltGL.ColorShader = {
+
+ /**
+ * Vertex shader.
+ */
+ vs: [
+ "attribute vec3 vertexPosition;",
+
+ "uniform mat4 mvMatrix;",
+ "uniform mat4 projMatrix;",
+
+ "void main() {",
+ " gl_Position = projMatrix * mvMatrix * vec4(vertexPosition, 1.0);",
+ "}"
+ ].join("\n"),
+
+ /**
+ * Fragment shader.
+ */
+ fs: [
+ "#ifdef GL_ES",
+ "precision lowp float;",
+ "#endif",
+
+ "uniform vec4 fill;",
+
+ "void main() {",
+ " gl_FragColor = fill;",
+ "}"
+ ].join("\n")
+};
+
+TiltGL.isWebGLForceEnabled = function TGL_isWebGLForceEnabled()
+{
+ return Services.prefs.getBoolPref("webgl.force-enabled");
+};
+
+/**
+ * Tests if the WebGL OpenGL or Angle renderer is available using the
+ * GfxInfo service.
+ *
+ * @return {Boolean} true if WebGL is available
+ */
+TiltGL.isWebGLSupported = function TGL_isWebGLSupported()
+{
+ let supported = false;
+
+ try {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ let angle = gfxInfo.FEATURE_WEBGL_ANGLE;
+ let opengl = gfxInfo.FEATURE_WEBGL_OPENGL;
+
+ // if either the Angle or OpenGL renderers are available, WebGL should work
+ supported = gfxInfo.getFeatureStatus(angle) === gfxInfo.FEATURE_NO_INFO ||
+ gfxInfo.getFeatureStatus(opengl) === gfxInfo.FEATURE_NO_INFO;
+ } catch(e) {
+ if (e && e.message) { TiltUtils.Output.error(e.message); }
+ return false;
+ }
+ return supported;
+};
+
+/**
+ * Helper function to create a 3D context.
+ *
+ * @param {HTMLCanvasElement} aCanvas
+ * the canvas to get the WebGL context from
+ * @param {Object} aFlags
+ * optional, flags used for initialization
+ *
+ * @return {Object} the WebGL context, or null if anything failed
+ */
+TiltGL.create3DContext = function TGL_create3DContext(aCanvas, aFlags)
+{
+ TiltGL.clearCache();
+
+ // try to get a valid context from an existing canvas
+ let context = null;
+
+ try {
+ context = aCanvas.getContext(WEBGL_CONTEXT_NAME, aFlags);
+ } catch(e) {
+ if (e && e.message) { TiltUtils.Output.error(e.message); }
+ return null;
+ }
+ return context;
+};
+
+/**
+ * Clears the cache and sets all the variables to default.
+ */
+TiltGL.clearCache = function TGL_clearCache()
+{
+ TiltGL.ProgramUtils._activeProgram = -1;
+ TiltGL.ProgramUtils._enabledAttributes = [];
+};
diff --git a/browser/devtools/tilt/tilt-math.js b/browser/devtools/tilt/tilt-math.js
new file mode 100644
index 000000000..6b2d2e101
--- /dev/null
+++ b/browser/devtools/tilt/tilt-math.js
@@ -0,0 +1,2322 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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";
+
+const {Cu} = require("chrome");
+
+let TiltUtils = require("devtools/tilt/tilt-utils");
+
+/**
+ * Module containing high performance matrix and vector operations for WebGL.
+ * Inspired by glMatrix, version 0.9.6, (c) 2011 Brandon Jones.
+ */
+
+let EPSILON = 0.01;
+exports.EPSILON = EPSILON;
+
+const PI_OVER_180 = Math.PI / 180;
+const INV_PI_OVER_180 = 180 / Math.PI;
+const FIFTEEN_OVER_225 = 15 / 225;
+const ONE_OVER_255 = 1 / 255;
+
+/**
+ * vec3 - 3 Dimensional Vector.
+ */
+let vec3 = {
+
+ /**
+ * Creates a new instance of a vec3 using the Float32Array type.
+ * Any array containing at least 3 numeric elements can serve as a vec3.
+ *
+ * @param {Array} aVec
+ * optional, vec3 containing values to initialize with
+ *
+ * @return {Array} a new instance of a vec3
+ */
+ create: function V3_create(aVec)
+ {
+ let dest = new Float32Array(3);
+
+ if (aVec) {
+ vec3.set(aVec, dest);
+ } else {
+ vec3.zero(dest);
+ }
+ return dest;
+ },
+
+ /**
+ * Copies the values of one vec3 to another.
+ *
+ * @param {Array} aVec
+ * vec3 containing values to copy
+ * @param {Array} aDest
+ * vec3 receiving copied values
+ *
+ * @return {Array} the destination vec3 receiving copied values
+ */
+ set: function V3_set(aVec, aDest)
+ {
+ aDest[0] = aVec[0];
+ aDest[1] = aVec[1];
+ aDest[2] = aVec[2] || 0;
+ return aDest;
+ },
+
+ /**
+ * Sets a vec3 to an zero vector.
+ *
+ * @param {Array} aDest
+ * vec3 to set
+ *
+ * @return {Array} the same vector
+ */
+ zero: function V3_zero(aDest)
+ {
+ aDest[0] = 0;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ return aDest;
+ },
+
+ /**
+ * Performs a vector addition.
+ *
+ * @param {Array} aVec
+ * vec3, first operand
+ * @param {Array} aVec2
+ * vec3, second operand
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ add: function V3_add(aVec, aVec2, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ aDest[0] = aVec[0] + aVec2[0];
+ aDest[1] = aVec[1] + aVec2[1];
+ aDest[2] = aVec[2] + aVec2[2];
+ return aDest;
+ },
+
+ /**
+ * Performs a vector subtraction.
+ *
+ * @param {Array} aVec
+ * vec3, first operand
+ * @param {Array} aVec2
+ * vec3, second operand
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ subtract: function V3_subtract(aVec, aVec2, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ aDest[0] = aVec[0] - aVec2[0];
+ aDest[1] = aVec[1] - aVec2[1];
+ aDest[2] = aVec[2] - aVec2[2];
+ return aDest;
+ },
+
+ /**
+ * Negates the components of a vec3.
+ *
+ * @param {Array} aVec
+ * vec3 to negate
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ negate: function V3_negate(aVec, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ aDest[0] = -aVec[0];
+ aDest[1] = -aVec[1];
+ aDest[2] = -aVec[2];
+ return aDest;
+ },
+
+ /**
+ * Multiplies the components of a vec3 by a scalar value.
+ *
+ * @param {Array} aVec
+ * vec3 to scale
+ * @param {Number} aVal
+ * numeric value to scale by
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ scale: function V3_scale(aVec, aVal, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ aDest[0] = aVec[0] * aVal;
+ aDest[1] = aVec[1] * aVal;
+ aDest[2] = aVec[2] * aVal;
+ return aDest;
+ },
+
+ /**
+ * Generates a unit vector of the same direction as the provided vec3.
+ * If vector length is 0, returns [0, 0, 0].
+ *
+ * @param {Array} aVec
+ * vec3 to normalize
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ normalize: function V3_normalize(aVec, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+ let len = Math.sqrt(x * x + y * y + z * z);
+
+ if (Math.abs(len) < EPSILON) {
+ aDest[0] = 0;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ return aDest;
+ }
+
+ len = 1 / len;
+ aDest[0] = x * len;
+ aDest[1] = y * len;
+ aDest[2] = z * len;
+ return aDest;
+ },
+
+ /**
+ * Generates the cross product of two vectors.
+ *
+ * @param {Array} aVec
+ * vec3, first operand
+ * @param {Array} aVec2
+ * vec3, second operand
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ cross: function V3_cross(aVec, aVec2, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+ let x2 = aVec2[0];
+ let y2 = aVec2[1];
+ let z2 = aVec2[2];
+
+ aDest[0] = y * z2 - z * y2;
+ aDest[1] = z * x2 - x * z2;
+ aDest[2] = x * y2 - y * x2;
+ return aDest;
+ },
+
+ /**
+ * Caclulate the dot product of two vectors.
+ *
+ * @param {Array} aVec
+ * vec3, first operand
+ * @param {Array} aVec2
+ * vec3, second operand
+ *
+ * @return {Array} dot product of the first and second operand
+ */
+ dot: function V3_dot(aVec, aVec2)
+ {
+ return aVec[0] * aVec2[0] + aVec[1] * aVec2[1] + aVec[2] * aVec2[2];
+ },
+
+ /**
+ * Caclulate the length of a vec3.
+ *
+ * @param {Array} aVec
+ * vec3 to calculate length of
+ *
+ * @return {Array} length of the vec3
+ */
+ length: function V3_length(aVec)
+ {
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+
+ return Math.sqrt(x * x + y * y + z * z);
+ },
+
+ /**
+ * Generates a unit vector pointing from one vector to another.
+ *
+ * @param {Array} aVec
+ * origin vec3
+ * @param {Array} aVec2
+ * vec3 to point to
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ direction: function V3_direction(aVec, aVec2, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ let x = aVec[0] - aVec2[0];
+ let y = aVec[1] - aVec2[1];
+ let z = aVec[2] - aVec2[2];
+ let len = Math.sqrt(x * x + y * y + z * z);
+
+ if (Math.abs(len) < EPSILON) {
+ aDest[0] = 0;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ return aDest;
+ }
+
+ len = 1 / len;
+ aDest[0] = x * len;
+ aDest[1] = y * len;
+ aDest[2] = z * len;
+ return aDest;
+ },
+
+ /**
+ * Performs a linear interpolation between two vec3.
+ *
+ * @param {Array} aVec
+ * first vector
+ * @param {Array} aVec2
+ * second vector
+ * @param {Number} aLerp
+ * interpolation amount between the two inputs
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, first operand otherwise
+ */
+ lerp: function V3_lerp(aVec, aVec2, aLerp, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ aDest[0] = aVec[0] + aLerp * (aVec2[0] - aVec[0]);
+ aDest[1] = aVec[1] + aLerp * (aVec2[1] - aVec[1]);
+ aDest[2] = aVec[2] + aLerp * (aVec2[2] - aVec[2]);
+ return aDest;
+ },
+
+ /**
+ * Projects a 3D point on a 2D screen plane.
+ *
+ * @param {Array} aP
+ * the [x, y, z] coordinates of the point to project
+ * @param {Array} aViewport
+ * the viewport [x, y, width, height] coordinates
+ * @param {Array} aMvMatrix
+ * the model view matrix
+ * @param {Array} aProjMatrix
+ * the projection matrix
+ * @param {Array} aDest
+ * optional parameter, the array to write the values to
+ *
+ * @return {Array} the projected coordinates
+ */
+ project: function V3_project(aP, aViewport, aMvMatrix, aProjMatrix, aDest)
+ {
+ /*jshint undef: false */
+
+ let mvpMatrix = new Float32Array(16);
+ let coordinates = new Float32Array(4);
+
+ // compute the perspective * model view matrix
+ mat4.multiply(aProjMatrix, aMvMatrix, mvpMatrix);
+
+ // now transform that vector into homogenous coordinates
+ coordinates[0] = aP[0];
+ coordinates[1] = aP[1];
+ coordinates[2] = aP[2];
+ coordinates[3] = 1;
+ mat4.multiplyVec4(mvpMatrix, coordinates);
+
+ // transform the homogenous coordinates into screen space
+ coordinates[0] /= coordinates[3];
+ coordinates[0] *= aViewport[2] * 0.5;
+ coordinates[0] += aViewport[2] * 0.5;
+ coordinates[1] /= coordinates[3];
+ coordinates[1] *= -aViewport[3] * 0.5;
+ coordinates[1] += aViewport[3] * 0.5;
+ coordinates[2] = 0;
+
+ if (!aDest) {
+ vec3.set(coordinates, aP);
+ } else {
+ vec3.set(coordinates, aDest);
+ }
+ return coordinates;
+ },
+
+ /**
+ * Unprojects a 2D point to 3D space.
+ *
+ * @param {Array} aP
+ * the [x, y, z] coordinates of the point to unproject;
+ * the z value should range between 0 and 1, as clipping plane
+ * @param {Array} aViewport
+ * the viewport [x, y, width, height] coordinates
+ * @param {Array} aMvMatrix
+ * the model view matrix
+ * @param {Array} aProjMatrix
+ * the projection matrix
+ * @param {Array} aDest
+ * optional parameter, the array to write the values to
+ *
+ * @return {Array} the unprojected coordinates
+ */
+ unproject: function V3_unproject(
+ aP, aViewport, aMvMatrix, aProjMatrix, aDest)
+ {
+ /*jshint undef: false */
+
+ let mvpMatrix = new Float32Array(16);
+ let coordinates = new Float32Array(4);
+
+ // compute the inverse of the perspective * model view matrix
+ mat4.multiply(aProjMatrix, aMvMatrix, mvpMatrix);
+ mat4.inverse(mvpMatrix);
+
+ // transformation of normalized coordinates (-1 to 1)
+ coordinates[0] = +((aP[0] - aViewport[0]) / aViewport[2] * 2 - 1);
+ coordinates[1] = -((aP[1] - aViewport[1]) / aViewport[3] * 2 - 1);
+ coordinates[2] = 2 * aP[2] - 1;
+ coordinates[3] = 1;
+
+ // now transform that vector into space coordinates
+ mat4.multiplyVec4(mvpMatrix, coordinates);
+
+ // invert to normalize x, y, and z values
+ coordinates[3] = 1 / coordinates[3];
+ coordinates[0] *= coordinates[3];
+ coordinates[1] *= coordinates[3];
+ coordinates[2] *= coordinates[3];
+
+ if (!aDest) {
+ vec3.set(coordinates, aP);
+ } else {
+ vec3.set(coordinates, aDest);
+ }
+ return coordinates;
+ },
+
+ /**
+ * Create a ray between two points using the current model view & projection
+ * matrices. This is useful when creating a ray destined for 3D picking.
+ *
+ * @param {Array} aP0
+ * the [x, y, z] coordinates of the first point
+ * @param {Array} aP1
+ * the [x, y, z] coordinates of the second point
+ * @param {Array} aViewport
+ * the viewport [x, y, width, height] coordinates
+ * @param {Array} aMvMatrix
+ * the model view matrix
+ * @param {Array} aProjMatrix
+ * the projection matrix
+ *
+ * @return {Object} a ray object containing the direction vector between
+ * the two unprojected points, the position and the lookAt
+ */
+ createRay: function V3_createRay(aP0, aP1, aViewport, aMvMatrix, aProjMatrix)
+ {
+ // unproject the two points
+ vec3.unproject(aP0, aViewport, aMvMatrix, aProjMatrix, aP0);
+ vec3.unproject(aP1, aViewport, aMvMatrix, aProjMatrix, aP1);
+
+ return {
+ origin: aP0,
+ direction: vec3.normalize(vec3.subtract(aP1, aP0))
+ };
+ },
+
+ /**
+ * Returns a string representation of a vector.
+ *
+ * @param {Array} aVec
+ * vec3 to represent as a string
+ *
+ * @return {String} representation of the vector
+ */
+ str: function V3_str(aVec)
+ {
+ return '[' + aVec[0] + ", " + aVec[1] + ", " + aVec[2] + ']';
+ }
+};
+
+exports.vec3 = vec3;
+
+/**
+ * mat3 - 3x3 Matrix.
+ */
+let mat3 = {
+
+ /**
+ * Creates a new instance of a mat3 using the Float32Array array type.
+ * Any array containing at least 9 numeric elements can serve as a mat3.
+ *
+ * @param {Array} aMat
+ * optional, mat3 containing values to initialize with
+ *
+ * @return {Array} a new instance of a mat3
+ */
+ create: function M3_create(aMat)
+ {
+ let dest = new Float32Array(9);
+
+ if (aMat) {
+ mat3.set(aMat, dest);
+ } else {
+ mat3.identity(dest);
+ }
+ return dest;
+ },
+
+ /**
+ * Copies the values of one mat3 to another.
+ *
+ * @param {Array} aMat
+ * mat3 containing values to copy
+ * @param {Array} aDest
+ * mat3 receiving copied values
+ *
+ * @return {Array} the destination mat3 receiving copied values
+ */
+ set: function M3_set(aMat, aDest)
+ {
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[1];
+ aDest[2] = aMat[2];
+ aDest[3] = aMat[3];
+ aDest[4] = aMat[4];
+ aDest[5] = aMat[5];
+ aDest[6] = aMat[6];
+ aDest[7] = aMat[7];
+ aDest[8] = aMat[8];
+ return aDest;
+ },
+
+ /**
+ * Sets a mat3 to an identity matrix.
+ *
+ * @param {Array} aDest
+ * mat3 to set
+ *
+ * @return {Array} the same matrix
+ */
+ identity: function M3_identity(aDest)
+ {
+ aDest[0] = 1;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ aDest[3] = 0;
+ aDest[4] = 1;
+ aDest[5] = 0;
+ aDest[6] = 0;
+ aDest[7] = 0;
+ aDest[8] = 1;
+ return aDest;
+ },
+
+ /**
+ * Transposes a mat3 (flips the values over the diagonal).
+ *
+ * @param {Array} aMat
+ * mat3 to transpose
+ * @param {Array} aDest
+ * optional, mat3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat3 if specified, first operand otherwise
+ */
+ transpose: function M3_transpose(aMat, aDest)
+ {
+ if (!aDest || aMat === aDest) {
+ let a01 = aMat[1];
+ let a02 = aMat[2];
+ let a12 = aMat[5];
+
+ aMat[1] = aMat[3];
+ aMat[2] = aMat[6];
+ aMat[3] = a01;
+ aMat[5] = aMat[7];
+ aMat[6] = a02;
+ aMat[7] = a12;
+ return aMat;
+ }
+
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[3];
+ aDest[2] = aMat[6];
+ aDest[3] = aMat[1];
+ aDest[4] = aMat[4];
+ aDest[5] = aMat[7];
+ aDest[6] = aMat[2];
+ aDest[7] = aMat[5];
+ aDest[8] = aMat[8];
+ return aDest;
+ },
+
+ /**
+ * Copies the elements of a mat3 into the upper 3x3 elements of a mat4.
+ *
+ * @param {Array} aMat
+ * mat3 containing values to copy
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat3 if specified, first operand otherwise
+ */
+ toMat4: function M3_toMat4(aMat, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(16);
+ }
+
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[1];
+ aDest[2] = aMat[2];
+ aDest[3] = 0;
+ aDest[4] = aMat[3];
+ aDest[5] = aMat[4];
+ aDest[6] = aMat[5];
+ aDest[7] = 0;
+ aDest[8] = aMat[6];
+ aDest[9] = aMat[7];
+ aDest[10] = aMat[8];
+ aDest[11] = 0;
+ aDest[12] = 0;
+ aDest[13] = 0;
+ aDest[14] = 0;
+ aDest[15] = 1;
+ return aDest;
+ },
+
+ /**
+ * Returns a string representation of a 3x3 matrix.
+ *
+ * @param {Array} aMat
+ * mat3 to represent as a string
+ *
+ * @return {String} representation of the matrix
+ */
+ str: function M3_str(aMat)
+ {
+ return "[" + aMat[0] + ", " + aMat[1] + ", " + aMat[2] +
+ ", " + aMat[3] + ", " + aMat[4] + ", " + aMat[5] +
+ ", " + aMat[6] + ", " + aMat[7] + ", " + aMat[8] + "]";
+ }
+};
+
+exports.mat3 = mat3;
+
+/**
+ * mat4 - 4x4 Matrix.
+ */
+let mat4 = {
+
+ /**
+ * Creates a new instance of a mat4 using the default Float32Array type.
+ * Any array containing at least 16 numeric elements can serve as a mat4.
+ *
+ * @param {Array} aMat
+ * optional, mat4 containing values to initialize with
+ *
+ * @return {Array} a new instance of a mat4
+ */
+ create: function M4_create(aMat)
+ {
+ let dest = new Float32Array(16);
+
+ if (aMat) {
+ mat4.set(aMat, dest);
+ } else {
+ mat4.identity(dest);
+ }
+ return dest;
+ },
+
+ /**
+ * Copies the values of one mat4 to another
+ *
+ * @param {Array} aMat
+ * mat4 containing values to copy
+ * @param {Array} aDest
+ * mat4 receiving copied values
+ *
+ * @return {Array} the destination mat4 receiving copied values
+ */
+ set: function M4_set(aMat, aDest)
+ {
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[1];
+ aDest[2] = aMat[2];
+ aDest[3] = aMat[3];
+ aDest[4] = aMat[4];
+ aDest[5] = aMat[5];
+ aDest[6] = aMat[6];
+ aDest[7] = aMat[7];
+ aDest[8] = aMat[8];
+ aDest[9] = aMat[9];
+ aDest[10] = aMat[10];
+ aDest[11] = aMat[11];
+ aDest[12] = aMat[12];
+ aDest[13] = aMat[13];
+ aDest[14] = aMat[14];
+ aDest[15] = aMat[15];
+ return aDest;
+ },
+
+ /**
+ * Sets a mat4 to an identity matrix.
+ *
+ * @param {Array} aDest
+ * mat4 to set
+ *
+ * @return {Array} the same matrix
+ */
+ identity: function M4_identity(aDest)
+ {
+ aDest[0] = 1;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ aDest[3] = 0;
+ aDest[4] = 0;
+ aDest[5] = 1;
+ aDest[6] = 0;
+ aDest[7] = 0;
+ aDest[8] = 0;
+ aDest[9] = 0;
+ aDest[10] = 1;
+ aDest[11] = 0;
+ aDest[12] = 0;
+ aDest[13] = 0;
+ aDest[14] = 0;
+ aDest[15] = 1;
+ return aDest;
+ },
+
+ /**
+ * Transposes a mat4 (flips the values over the diagonal).
+ *
+ * @param {Array} aMat
+ * mat4 to transpose
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ transpose: function M4_transpose(aMat, aDest)
+ {
+ if (!aDest || aMat === aDest) {
+ let a01 = aMat[1];
+ let a02 = aMat[2];
+ let a03 = aMat[3];
+ let a12 = aMat[6];
+ let a13 = aMat[7];
+ let a23 = aMat[11];
+
+ aMat[1] = aMat[4];
+ aMat[2] = aMat[8];
+ aMat[3] = aMat[12];
+ aMat[4] = a01;
+ aMat[6] = aMat[9];
+ aMat[7] = aMat[13];
+ aMat[8] = a02;
+ aMat[9] = a12;
+ aMat[11] = aMat[14];
+ aMat[12] = a03;
+ aMat[13] = a13;
+ aMat[14] = a23;
+ return aMat;
+ }
+
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[4];
+ aDest[2] = aMat[8];
+ aDest[3] = aMat[12];
+ aDest[4] = aMat[1];
+ aDest[5] = aMat[5];
+ aDest[6] = aMat[9];
+ aDest[7] = aMat[13];
+ aDest[8] = aMat[2];
+ aDest[9] = aMat[6];
+ aDest[10] = aMat[10];
+ aDest[11] = aMat[14];
+ aDest[12] = aMat[3];
+ aDest[13] = aMat[7];
+ aDest[14] = aMat[11];
+ aDest[15] = aMat[15];
+ return aDest;
+ },
+
+ /**
+ * Calculate the determinant of a mat4.
+ *
+ * @param {Array} aMat
+ * mat4 to calculate determinant of
+ *
+ * @return {Number} determinant of the matrix
+ */
+ determinant: function M4_determinant(mat)
+ {
+ let a00 = mat[0], a01 = mat[1], a02 = mat[2], a03 = mat[3];
+ let a10 = mat[4], a11 = mat[5], a12 = mat[6], a13 = mat[7];
+ let a20 = mat[8], a21 = mat[9], a22 = mat[10], a23 = mat[11];
+ let a30 = mat[12], a31 = mat[13], a32 = mat[14], a33 = mat[15];
+
+ return a30 * a21 * a12 * a03 - a20 * a31 * a12 * a03 -
+ a30 * a11 * a22 * a03 + a10 * a31 * a22 * a03 +
+ a20 * a11 * a32 * a03 - a10 * a21 * a32 * a03 -
+ a30 * a21 * a02 * a13 + a20 * a31 * a02 * a13 +
+ a30 * a01 * a22 * a13 - a00 * a31 * a22 * a13 -
+ a20 * a01 * a32 * a13 + a00 * a21 * a32 * a13 +
+ a30 * a11 * a02 * a23 - a10 * a31 * a02 * a23 -
+ a30 * a01 * a12 * a23 + a00 * a31 * a12 * a23 +
+ a10 * a01 * a32 * a23 - a00 * a11 * a32 * a23 -
+ a20 * a11 * a02 * a33 + a10 * a21 * a02 * a33 +
+ a20 * a01 * a12 * a33 - a00 * a21 * a12 * a33 -
+ a10 * a01 * a22 * a33 + a00 * a11 * a22 * a33;
+ },
+
+ /**
+ * Calculate the inverse of a mat4.
+ *
+ * @param {Array} aMat
+ * mat4 to calculate inverse of
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ inverse: function M4_inverse(aMat, aDest)
+ {
+ if (!aDest) {
+ aDest = aMat;
+ }
+
+ let a00 = aMat[0], a01 = aMat[1], a02 = aMat[2], a03 = aMat[3];
+ let a10 = aMat[4], a11 = aMat[5], a12 = aMat[6], a13 = aMat[7];
+ let a20 = aMat[8], a21 = aMat[9], a22 = aMat[10], a23 = aMat[11];
+ let a30 = aMat[12], a31 = aMat[13], a32 = aMat[14], a33 = aMat[15];
+
+ let b00 = a00 * a11 - a01 * a10;
+ let b01 = a00 * a12 - a02 * a10;
+ let b02 = a00 * a13 - a03 * a10;
+ let b03 = a01 * a12 - a02 * a11;
+ let b04 = a01 * a13 - a03 * a11;
+ let b05 = a02 * a13 - a03 * a12;
+ let b06 = a20 * a31 - a21 * a30;
+ let b07 = a20 * a32 - a22 * a30;
+ let b08 = a20 * a33 - a23 * a30;
+ let b09 = a21 * a32 - a22 * a31;
+ let b10 = a21 * a33 - a23 * a31;
+ let b11 = a22 * a33 - a23 * a32;
+ let id = 1 / ((b00 * b11 - b01 * b10 + b02 * b09 +
+ b03 * b08 - b04 * b07 + b05 * b06) || EPSILON);
+
+ aDest[0] = ( a11 * b11 - a12 * b10 + a13 * b09) * id;
+ aDest[1] = (-a01 * b11 + a02 * b10 - a03 * b09) * id;
+ aDest[2] = ( a31 * b05 - a32 * b04 + a33 * b03) * id;
+ aDest[3] = (-a21 * b05 + a22 * b04 - a23 * b03) * id;
+ aDest[4] = (-a10 * b11 + a12 * b08 - a13 * b07) * id;
+ aDest[5] = ( a00 * b11 - a02 * b08 + a03 * b07) * id;
+ aDest[6] = (-a30 * b05 + a32 * b02 - a33 * b01) * id;
+ aDest[7] = ( a20 * b05 - a22 * b02 + a23 * b01) * id;
+ aDest[8] = ( a10 * b10 - a11 * b08 + a13 * b06) * id;
+ aDest[9] = (-a00 * b10 + a01 * b08 - a03 * b06) * id;
+ aDest[10] = ( a30 * b04 - a31 * b02 + a33 * b00) * id;
+ aDest[11] = (-a20 * b04 + a21 * b02 - a23 * b00) * id;
+ aDest[12] = (-a10 * b09 + a11 * b07 - a12 * b06) * id;
+ aDest[13] = ( a00 * b09 - a01 * b07 + a02 * b06) * id;
+ aDest[14] = (-a30 * b03 + a31 * b01 - a32 * b00) * id;
+ aDest[15] = ( a20 * b03 - a21 * b01 + a22 * b00) * id;
+ return aDest;
+ },
+
+ /**
+ * Copies the upper 3x3 elements of a mat4 into another mat4.
+ *
+ * @param {Array} aMat
+ * mat4 containing values to copy
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ toRotationMat: function M4_toRotationMat(aMat, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(16);
+ }
+
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[1];
+ aDest[2] = aMat[2];
+ aDest[3] = aMat[3];
+ aDest[4] = aMat[4];
+ aDest[5] = aMat[5];
+ aDest[6] = aMat[6];
+ aDest[7] = aMat[7];
+ aDest[8] = aMat[8];
+ aDest[9] = aMat[9];
+ aDest[10] = aMat[10];
+ aDest[11] = aMat[11];
+ aDest[12] = 0;
+ aDest[13] = 0;
+ aDest[14] = 0;
+ aDest[15] = 1;
+ return aDest;
+ },
+
+ /**
+ * Copies the upper 3x3 elements of a mat4 into a mat3.
+ *
+ * @param {Array} aMat
+ * mat4 containing values to copy
+ * @param {Array} aDest
+ * optional, mat3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat3 if specified, first operand otherwise
+ */
+ toMat3: function M4_toMat3(aMat, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(9);
+ }
+
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[1];
+ aDest[2] = aMat[2];
+ aDest[3] = aMat[4];
+ aDest[4] = aMat[5];
+ aDest[5] = aMat[6];
+ aDest[6] = aMat[8];
+ aDest[7] = aMat[9];
+ aDest[8] = aMat[10];
+ return aDest;
+ },
+
+ /**
+ * Calculate the inverse of the upper 3x3 elements of a mat4 and copies
+ * the result into a mat3. The resulting matrix is useful for calculating
+ * transformed normals.
+ *
+ * @param {Array} aMat
+ * mat4 containing values to invert and copy
+ * @param {Array} aDest
+ * optional, mat3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat3 if specified, first operand otherwise
+ */
+ toInverseMat3: function M4_toInverseMat3(aMat, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(9);
+ }
+
+ let a00 = aMat[0], a01 = aMat[1], a02 = aMat[2];
+ let a10 = aMat[4], a11 = aMat[5], a12 = aMat[6];
+ let a20 = aMat[8], a21 = aMat[9], a22 = aMat[10];
+
+ let b01 = a22 * a11 - a12 * a21;
+ let b11 = -a22 * a10 + a12 * a20;
+ let b21 = a21 * a10 - a11 * a20;
+ let id = 1 / ((a00 * b01 + a01 * b11 + a02 * b21) || EPSILON);
+
+ aDest[0] = b01 * id;
+ aDest[1] = (-a22 * a01 + a02 * a21) * id;
+ aDest[2] = ( a12 * a01 - a02 * a11) * id;
+ aDest[3] = b11 * id;
+ aDest[4] = ( a22 * a00 - a02 * a20) * id;
+ aDest[5] = (-a12 * a00 + a02 * a10) * id;
+ aDest[6] = b21 * id;
+ aDest[7] = (-a21 * a00 + a01 * a20) * id;
+ aDest[8] = ( a11 * a00 - a01 * a10) * id;
+ return aDest;
+ },
+
+ /**
+ * Performs a matrix multiplication.
+ *
+ * @param {Array} aMat
+ * first operand
+ * @param {Array} aMat2
+ * second operand
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ multiply: function M4_multiply(aMat, aMat2, aDest)
+ {
+ if (!aDest) {
+ aDest = aMat;
+ }
+
+ let a00 = aMat[0], a01 = aMat[1], a02 = aMat[2], a03 = aMat[3];
+ let a10 = aMat[4], a11 = aMat[5], a12 = aMat[6], a13 = aMat[7];
+ let a20 = aMat[8], a21 = aMat[9], a22 = aMat[10], a23 = aMat[11];
+ let a30 = aMat[12], a31 = aMat[13], a32 = aMat[14], a33 = aMat[15];
+
+ let b00 = aMat2[0], b01 = aMat2[1], b02 = aMat2[2], b03 = aMat2[3];
+ let b10 = aMat2[4], b11 = aMat2[5], b12 = aMat2[6], b13 = aMat2[7];
+ let b20 = aMat2[8], b21 = aMat2[9], b22 = aMat2[10], b23 = aMat2[11];
+ let b30 = aMat2[12], b31 = aMat2[13], b32 = aMat2[14], b33 = aMat2[15];
+
+ aDest[0] = b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30;
+ aDest[1] = b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31;
+ aDest[2] = b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32;
+ aDest[3] = b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33;
+ aDest[4] = b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30;
+ aDest[5] = b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31;
+ aDest[6] = b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32;
+ aDest[7] = b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33;
+ aDest[8] = b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30;
+ aDest[9] = b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31;
+ aDest[10] = b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32;
+ aDest[11] = b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33;
+ aDest[12] = b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30;
+ aDest[13] = b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31;
+ aDest[14] = b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32;
+ aDest[15] = b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33;
+ return aDest;
+ },
+
+ /**
+ * Transforms a vec3 with the given matrix.
+ * 4th vector component is implicitly 1.
+ *
+ * @param {Array} aMat
+ * mat4 to transform the vector with
+ * @param {Array} aVec
+ * vec3 to transform
+ * @param {Array} aDest
+ * optional, vec3 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, aVec operand otherwise
+ */
+ multiplyVec3: function M4_multiplyVec3(aMat, aVec, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+
+ aDest[0] = aMat[0] * x + aMat[4] * y + aMat[8] * z + aMat[12];
+ aDest[1] = aMat[1] * x + aMat[5] * y + aMat[9] * z + aMat[13];
+ aDest[2] = aMat[2] * x + aMat[6] * y + aMat[10] * z + aMat[14];
+ return aDest;
+ },
+
+ /**
+ * Transforms a vec4 with the given matrix.
+ *
+ * @param {Array} aMat
+ * mat4 to transform the vector with
+ * @param {Array} aVec
+ * vec4 to transform
+ * @param {Array} aDest
+ * optional, vec4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec4 if specified, vec4 operand otherwise
+ */
+ multiplyVec4: function M4_multiplyVec4(aMat, aVec, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+ let w = aVec[3];
+
+ aDest[0] = aMat[0] * x + aMat[4] * y + aMat[8] * z + aMat[12] * w;
+ aDest[1] = aMat[1] * x + aMat[5] * y + aMat[9] * z + aMat[13] * w;
+ aDest[2] = aMat[2] * x + aMat[6] * y + aMat[10] * z + aMat[14] * w;
+ aDest[3] = aMat[3] * x + aMat[7] * y + aMat[11] * z + aMat[15] * w;
+ return aDest;
+ },
+
+ /**
+ * Translates a matrix by the given vector.
+ *
+ * @param {Array} aMat
+ * mat4 to translate
+ * @param {Array} aVec
+ * vec3 specifying the translation
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ translate: function M4_translate(aMat, aVec, aDest)
+ {
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+
+ if (!aDest || aMat === aDest) {
+ aMat[12] = aMat[0] * x + aMat[4] * y + aMat[8] * z + aMat[12];
+ aMat[13] = aMat[1] * x + aMat[5] * y + aMat[9] * z + aMat[13];
+ aMat[14] = aMat[2] * x + aMat[6] * y + aMat[10] * z + aMat[14];
+ aMat[15] = aMat[3] * x + aMat[7] * y + aMat[11] * z + aMat[15];
+ return aMat;
+ }
+
+ let a00 = aMat[0], a01 = aMat[1], a02 = aMat[2], a03 = aMat[3];
+ let a10 = aMat[4], a11 = aMat[5], a12 = aMat[6], a13 = aMat[7];
+ let a20 = aMat[8], a21 = aMat[9], a22 = aMat[10], a23 = aMat[11];
+
+ aDest[0] = a00;
+ aDest[1] = a01;
+ aDest[2] = a02;
+ aDest[3] = a03;
+ aDest[4] = a10;
+ aDest[5] = a11;
+ aDest[6] = a12;
+ aDest[7] = a13;
+ aDest[8] = a20;
+ aDest[9] = a21;
+ aDest[10] = a22;
+ aDest[11] = a23;
+ aDest[12] = a00 * x + a10 * y + a20 * z + aMat[12];
+ aDest[13] = a01 * x + a11 * y + a21 * z + aMat[13];
+ aDest[14] = a02 * x + a12 * y + a22 * z + aMat[14];
+ aDest[15] = a03 * x + a13 * y + a23 * z + aMat[15];
+ return aDest;
+ },
+
+ /**
+ * Scales a matrix by the given vector.
+ *
+ * @param {Array} aMat
+ * mat4 to translate
+ * @param {Array} aVec
+ * vec3 specifying the scale on each axis
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ scale: function M4_scale(aMat, aVec, aDest)
+ {
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+
+ if (!aDest || aMat === aDest) {
+ aMat[0] *= x;
+ aMat[1] *= x;
+ aMat[2] *= x;
+ aMat[3] *= x;
+ aMat[4] *= y;
+ aMat[5] *= y;
+ aMat[6] *= y;
+ aMat[7] *= y;
+ aMat[8] *= z;
+ aMat[9] *= z;
+ aMat[10] *= z;
+ aMat[11] *= z;
+ return aMat;
+ }
+
+ aDest[0] = aMat[0] * x;
+ aDest[1] = aMat[1] * x;
+ aDest[2] = aMat[2] * x;
+ aDest[3] = aMat[3] * x;
+ aDest[4] = aMat[4] * y;
+ aDest[5] = aMat[5] * y;
+ aDest[6] = aMat[6] * y;
+ aDest[7] = aMat[7] * y;
+ aDest[8] = aMat[8] * z;
+ aDest[9] = aMat[9] * z;
+ aDest[10] = aMat[10] * z;
+ aDest[11] = aMat[11] * z;
+ aDest[12] = aMat[12];
+ aDest[13] = aMat[13];
+ aDest[14] = aMat[14];
+ aDest[15] = aMat[15];
+ return aDest;
+ },
+
+ /**
+ * Rotates a matrix by the given angle around the specified axis.
+ * If rotating around a primary axis (x, y, z) one of the specialized
+ * rotation functions should be used instead for performance,
+ *
+ * @param {Array} aMat
+ * mat4 to rotate
+ * @param {Number} aAngle
+ * the angle (in radians) to rotate
+ * @param {Array} aAxis
+ * vec3 representing the axis to rotate around
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ rotate: function M4_rotate(aMat, aAngle, aAxis, aDest)
+ {
+ let x = aAxis[0];
+ let y = aAxis[1];
+ let z = aAxis[2];
+ let len = 1 / (Math.sqrt(x * x + y * y + z * z) || EPSILON);
+
+ x *= len;
+ y *= len;
+ z *= len;
+
+ let s = Math.sin(aAngle);
+ let c = Math.cos(aAngle);
+ let t = 1 - c;
+
+ let a00 = aMat[0], a01 = aMat[1], a02 = aMat[2], a03 = aMat[3];
+ let a10 = aMat[4], a11 = aMat[5], a12 = aMat[6], a13 = aMat[7];
+ let a20 = aMat[8], a21 = aMat[9], a22 = aMat[10], a23 = aMat[11];
+
+ let b00 = x * x * t + c, b01 = y * x * t + z * s, b02 = z * x * t - y * s;
+ let b10 = x * y * t - z * s, b11 = y * y * t + c, b12 = z * y * t + x * s;
+ let b20 = x * z * t + y * s, b21 = y * z * t - x * s, b22 = z * z * t + c;
+
+ if (!aDest) {
+ aDest = aMat;
+ } else if (aMat !== aDest) {
+ aDest[12] = aMat[12];
+ aDest[13] = aMat[13];
+ aDest[14] = aMat[14];
+ aDest[15] = aMat[15];
+ }
+
+ aDest[0] = a00 * b00 + a10 * b01 + a20 * b02;
+ aDest[1] = a01 * b00 + a11 * b01 + a21 * b02;
+ aDest[2] = a02 * b00 + a12 * b01 + a22 * b02;
+ aDest[3] = a03 * b00 + a13 * b01 + a23 * b02;
+ aDest[4] = a00 * b10 + a10 * b11 + a20 * b12;
+ aDest[5] = a01 * b10 + a11 * b11 + a21 * b12;
+ aDest[6] = a02 * b10 + a12 * b11 + a22 * b12;
+ aDest[7] = a03 * b10 + a13 * b11 + a23 * b12;
+ aDest[8] = a00 * b20 + a10 * b21 + a20 * b22;
+ aDest[9] = a01 * b20 + a11 * b21 + a21 * b22;
+ aDest[10] = a02 * b20 + a12 * b21 + a22 * b22;
+ aDest[11] = a03 * b20 + a13 * b21 + a23 * b22;
+ return aDest;
+ },
+
+ /**
+ * Rotates a matrix by the given angle around the X axis.
+ *
+ * @param {Array} aMat
+ * mat4 to rotate
+ * @param {Number} aAngle
+ * the angle (in radians) to rotate
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ rotateX: function M4_rotateX(aMat, aAngle, aDest)
+ {
+ let s = Math.sin(aAngle);
+ let c = Math.cos(aAngle);
+
+ let a10 = aMat[4], a11 = aMat[5], a12 = aMat[6], a13 = aMat[7];
+ let a20 = aMat[8], a21 = aMat[9], a22 = aMat[10], a23 = aMat[11];
+
+ if (!aDest) {
+ aDest = aMat;
+ } else if (aMat !== aDest) {
+ aDest[0] = aMat[0];
+ aDest[1] = aMat[1];
+ aDest[2] = aMat[2];
+ aDest[3] = aMat[3];
+ aDest[12] = aMat[12];
+ aDest[13] = aMat[13];
+ aDest[14] = aMat[14];
+ aDest[15] = aMat[15];
+ }
+
+ aDest[4] = a10 * c + a20 * s;
+ aDest[5] = a11 * c + a21 * s;
+ aDest[6] = a12 * c + a22 * s;
+ aDest[7] = a13 * c + a23 * s;
+ aDest[8] = a10 * -s + a20 * c;
+ aDest[9] = a11 * -s + a21 * c;
+ aDest[10] = a12 * -s + a22 * c;
+ aDest[11] = a13 * -s + a23 * c;
+ return aDest;
+ },
+
+ /**
+ * Rotates a matrix by the given angle around the Y axix.
+ *
+ * @param {Array} aMat
+ * mat4 to rotate
+ * @param {Number} aAngle
+ * the angle (in radians) to rotate
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ rotateY: function M4_rotateY(aMat, aAngle, aDest)
+ {
+ let s = Math.sin(aAngle);
+ let c = Math.cos(aAngle);
+
+ let a00 = aMat[0], a01 = aMat[1], a02 = aMat[2], a03 = aMat[3];
+ let a20 = aMat[8], a21 = aMat[9], a22 = aMat[10], a23 = aMat[11];
+
+ if (!aDest) {
+ aDest = aMat;
+ } else if (aMat !== aDest) {
+ aDest[4] = aMat[4];
+ aDest[5] = aMat[5];
+ aDest[6] = aMat[6];
+ aDest[7] = aMat[7];
+ aDest[12] = aMat[12];
+ aDest[13] = aMat[13];
+ aDest[14] = aMat[14];
+ aDest[15] = aMat[15];
+ }
+
+ aDest[0] = a00 * c + a20 * -s;
+ aDest[1] = a01 * c + a21 * -s;
+ aDest[2] = a02 * c + a22 * -s;
+ aDest[3] = a03 * c + a23 * -s;
+ aDest[8] = a00 * s + a20 * c;
+ aDest[9] = a01 * s + a21 * c;
+ aDest[10] = a02 * s + a22 * c;
+ aDest[11] = a03 * s + a23 * c;
+ return aDest;
+ },
+
+ /**
+ * Rotates a matrix by the given angle around the Z axix.
+ *
+ * @param {Array} aMat
+ * mat4 to rotate
+ * @param {Number} aAngle
+ * the angle (in radians) to rotate
+ * @param {Array} aDest
+ * optional, mat4 receiving operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ rotateZ: function M4_rotateZ(aMat, aAngle, aDest)
+ {
+ let s = Math.sin(aAngle);
+ let c = Math.cos(aAngle);
+
+ let a00 = aMat[0], a01 = aMat[1], a02 = aMat[2], a03 = aMat[3];
+ let a10 = aMat[4], a11 = aMat[5], a12 = aMat[6], a13 = aMat[7];
+
+ if (!aDest) {
+ aDest = aMat;
+ } else if (aMat !== aDest) {
+ aDest[8] = aMat[8];
+ aDest[9] = aMat[9];
+ aDest[10] = aMat[10];
+ aDest[11] = aMat[11];
+ aDest[12] = aMat[12];
+ aDest[13] = aMat[13];
+ aDest[14] = aMat[14];
+ aDest[15] = aMat[15];
+ }
+
+ aDest[0] = a00 * c + a10 * s;
+ aDest[1] = a01 * c + a11 * s;
+ aDest[2] = a02 * c + a12 * s;
+ aDest[3] = a03 * c + a13 * s;
+ aDest[4] = a00 * -s + a10 * c;
+ aDest[5] = a01 * -s + a11 * c;
+ aDest[6] = a02 * -s + a12 * c;
+ aDest[7] = a03 * -s + a13 * c;
+ return aDest;
+ },
+
+ /**
+ * Generates a frustum matrix with the given bounds.
+ *
+ * @param {Number} aLeft
+ * scalar, left bound of the frustum
+ * @param {Number} aRight
+ * scalar, right bound of the frustum
+ * @param {Number} aBottom
+ * scalar, bottom bound of the frustum
+ * @param {Number} aTop
+ * scalar, top bound of the frustum
+ * @param {Number} aNear
+ * scalar, near bound of the frustum
+ * @param {Number} aFar
+ * scalar, far bound of the frustum
+ * @param {Array} aDest
+ * optional, mat4 frustum matrix will be written into
+ * if not specified result is written to a new mat4
+ *
+ * @return {Array} the destination mat4 if specified, a new mat4 otherwise
+ */
+ frustum: function M4_frustum(
+ aLeft, aRight, aBottom, aTop, aNear, aFar, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(16);
+ }
+
+ let rl = (aRight - aLeft);
+ let tb = (aTop - aBottom);
+ let fn = (aFar - aNear);
+
+ aDest[0] = (aNear * 2) / rl;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ aDest[3] = 0;
+ aDest[4] = 0;
+ aDest[5] = (aNear * 2) / tb;
+ aDest[6] = 0;
+ aDest[7] = 0;
+ aDest[8] = (aRight + aLeft) / rl;
+ aDest[9] = (aTop + aBottom) / tb;
+ aDest[10] = -(aFar + aNear) / fn;
+ aDest[11] = -1;
+ aDest[12] = 0;
+ aDest[13] = 0;
+ aDest[14] = -(aFar * aNear * 2) / fn;
+ aDest[15] = 0;
+ return aDest;
+ },
+
+ /**
+ * Generates a perspective projection matrix with the given bounds.
+ *
+ * @param {Number} aFovy
+ * scalar, vertical field of view (degrees)
+ * @param {Number} aAspect
+ * scalar, aspect ratio (typically viewport width/height)
+ * @param {Number} aNear
+ * scalar, near bound of the frustum
+ * @param {Number} aFar
+ * scalar, far bound of the frustum
+ * @param {Array} aDest
+ * optional, mat4 frustum matrix will be written into
+ * if not specified result is written to a new mat4
+ *
+ * @return {Array} the destination mat4 if specified, a new mat4 otherwise
+ */
+ perspective: function M4_perspective(
+ aFovy, aAspect, aNear, aFar, aDest, aFlip)
+ {
+ let upper = aNear * Math.tan(aFovy * 0.00872664626); // PI * 180 / 2
+ let right = upper * aAspect;
+ let top = upper * (aFlip || 1);
+
+ return mat4.frustum(-right, right, -top, top, aNear, aFar, aDest);
+ },
+
+ /**
+ * Generates a orthogonal projection matrix with the given bounds.
+ *
+ * @param {Number} aLeft
+ * scalar, left bound of the frustum
+ * @param {Number} aRight
+ * scalar, right bound of the frustum
+ * @param {Number} aBottom
+ * scalar, bottom bound of the frustum
+ * @param {Number} aTop
+ * scalar, top bound of the frustum
+ * @param {Number} aNear
+ * scalar, near bound of the frustum
+ * @param {Number} aFar
+ * scalar, far bound of the frustum
+ * @param {Array} aDest
+ * optional, mat4 frustum matrix will be written into
+ * if not specified result is written to a new mat4
+ *
+ * @return {Array} the destination mat4 if specified, a new mat4 otherwise
+ */
+ ortho: function M4_ortho(aLeft, aRight, aBottom, aTop, aNear, aFar, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(16);
+ }
+
+ let rl = (aRight - aLeft);
+ let tb = (aTop - aBottom);
+ let fn = (aFar - aNear);
+
+ aDest[0] = 2 / rl;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ aDest[3] = 0;
+ aDest[4] = 0;
+ aDest[5] = 2 / tb;
+ aDest[6] = 0;
+ aDest[7] = 0;
+ aDest[8] = 0;
+ aDest[9] = 0;
+ aDest[10] = -2 / fn;
+ aDest[11] = 0;
+ aDest[12] = -(aLeft + aRight) / rl;
+ aDest[13] = -(aTop + aBottom) / tb;
+ aDest[14] = -(aFar + aNear) / fn;
+ aDest[15] = 1;
+ return aDest;
+ },
+
+ /**
+ * Generates a look-at matrix with the given eye position, focal point, and
+ * up axis.
+ *
+ * @param {Array} aEye
+ * vec3, position of the viewer
+ * @param {Array} aCenter
+ * vec3, point the viewer is looking at
+ * @param {Array} aUp
+ * vec3 pointing up
+ * @param {Array} aDest
+ * optional, mat4 frustum matrix will be written into
+ * if not specified result is written to a new mat4
+ *
+ * @return {Array} the destination mat4 if specified, a new mat4 otherwise
+ */
+ lookAt: function M4_lookAt(aEye, aCenter, aUp, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(16);
+ }
+
+ let eyex = aEye[0];
+ let eyey = aEye[1];
+ let eyez = aEye[2];
+ let upx = aUp[0];
+ let upy = aUp[1];
+ let upz = aUp[2];
+ let centerx = aCenter[0];
+ let centery = aCenter[1];
+ let centerz = aCenter[2];
+
+ let z0 = eyex - aCenter[0];
+ let z1 = eyey - aCenter[1];
+ let z2 = eyez - aCenter[2];
+ let len = 1 / (Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2) || EPSILON);
+
+ z0 *= len;
+ z1 *= len;
+ z2 *= len;
+
+ let x0 = upy * z2 - upz * z1;
+ let x1 = upz * z0 - upx * z2;
+ let x2 = upx * z1 - upy * z0;
+ len = 1 / (Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2) || EPSILON);
+
+ x0 *= len;
+ x1 *= len;
+ x2 *= len;
+
+ let y0 = z1 * x2 - z2 * x1;
+ let y1 = z2 * x0 - z0 * x2;
+ let y2 = z0 * x1 - z1 * x0;
+ len = 1 / (Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2) || EPSILON);
+
+ y0 *= len;
+ y1 *= len;
+ y2 *= len;
+
+ aDest[0] = x0;
+ aDest[1] = y0;
+ aDest[2] = z0;
+ aDest[3] = 0;
+ aDest[4] = x1;
+ aDest[5] = y1;
+ aDest[6] = z1;
+ aDest[7] = 0;
+ aDest[8] = x2;
+ aDest[9] = y2;
+ aDest[10] = z2;
+ aDest[11] = 0;
+ aDest[12] = -(x0 * eyex + x1 * eyey + x2 * eyez);
+ aDest[13] = -(y0 * eyex + y1 * eyey + y2 * eyez);
+ aDest[14] = -(z0 * eyex + z1 * eyey + z2 * eyez);
+ aDest[15] = 1;
+
+ return aDest;
+ },
+
+ /**
+ * Returns a string representation of a 4x4 matrix.
+ *
+ * @param {Array} aMat
+ * mat4 to represent as a string
+ *
+ * @return {String} representation of the matrix
+ */
+ str: function M4_str(mat)
+ {
+ return "[" + mat[0] + ", " + mat[1] + ", " + mat[2] + ", " + mat[3] +
+ ", "+ mat[4] + ", " + mat[5] + ", " + mat[6] + ", " + mat[7] +
+ ", "+ mat[8] + ", " + mat[9] + ", " + mat[10] + ", " + mat[11] +
+ ", "+ mat[12] + ", " + mat[13] + ", " + mat[14] + ", " + mat[15] +
+ "]";
+ }
+};
+
+exports.mat4 = mat4;
+
+/**
+ * quat4 - Quaternion.
+ */
+let quat4 = {
+
+ /**
+ * Creates a new instance of a quat4 using the default Float32Array type.
+ * Any array containing at least 4 numeric elements can serve as a quat4.
+ *
+ * @param {Array} aQuat
+ * optional, quat4 containing values to initialize with
+ *
+ * @return {Array} a new instance of a quat4
+ */
+ create: function Q4_create(aQuat)
+ {
+ let dest = new Float32Array(4);
+
+ if (aQuat) {
+ quat4.set(aQuat, dest);
+ } else {
+ quat4.identity(dest);
+ }
+ return dest;
+ },
+
+ /**
+ * Copies the values of one quat4 to another.
+ *
+ * @param {Array} aQuat
+ * quat4 containing values to copy
+ * @param {Array} aDest
+ * quat4 receiving copied values
+ *
+ * @return {Array} the destination quat4 receiving copied values
+ */
+ set: function Q4_set(aQuat, aDest)
+ {
+ aDest[0] = aQuat[0];
+ aDest[1] = aQuat[1];
+ aDest[2] = aQuat[2];
+ aDest[3] = aQuat[3];
+ return aDest;
+ },
+
+ /**
+ * Sets a quat4 to an identity quaternion.
+ *
+ * @param {Array} aDest
+ * quat4 to set
+ *
+ * @return {Array} the same quaternion
+ */
+ identity: function Q4_identity(aDest)
+ {
+ aDest[0] = 0;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ aDest[3] = 1;
+ return aDest;
+ },
+
+ /**
+ * Calculate the W component of a quat4 from the X, Y, and Z components.
+ * Assumes that quaternion is 1 unit in length.
+ * Any existing W component will be ignored.
+ *
+ * @param {Array} aQuat
+ * quat4 to calculate W component of
+ * @param {Array} aDest
+ * optional, quat4 receiving calculated values
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination quat if specified, first operand otherwise
+ */
+ calculateW: function Q4_calculateW(aQuat, aDest)
+ {
+ if (!aDest) {
+ aDest = aQuat;
+ }
+
+ let x = aQuat[0];
+ let y = aQuat[1];
+ let z = aQuat[2];
+
+ aDest[0] = x;
+ aDest[1] = y;
+ aDest[2] = z;
+ aDest[3] = -Math.sqrt(Math.abs(1 - x * x - y * y - z * z));
+ return aDest;
+ },
+
+ /**
+ * Calculate the inverse of a quat4.
+ *
+ * @param {Array} aQuat
+ * quat4 to calculate the inverse of
+ * @param {Array} aDest
+ * optional, quat4 receiving the inverse values
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination quat if specified, first operand otherwise
+ */
+ inverse: function Q4_inverse(aQuat, aDest)
+ {
+ if (!aDest) {
+ aDest = aQuat;
+ }
+
+ aQuat[0] = -aQuat[0];
+ aQuat[1] = -aQuat[1];
+ aQuat[2] = -aQuat[2];
+ return aQuat;
+ },
+
+ /**
+ * Generates a unit quaternion of the same direction as the provided quat4.
+ * If quaternion length is 0, returns [0, 0, 0, 0].
+ *
+ * @param {Array} aQuat
+ * quat4 to normalize
+ * @param {Array} aDest
+ * optional, quat4 receiving the operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination quat if specified, first operand otherwise
+ */
+ normalize: function Q4_normalize(aQuat, aDest)
+ {
+ if (!aDest) {
+ aDest = aQuat;
+ }
+
+ let x = aQuat[0];
+ let y = aQuat[1];
+ let z = aQuat[2];
+ let w = aQuat[3];
+ let len = Math.sqrt(x * x + y * y + z * z + w * w);
+
+ if (Math.abs(len) < EPSILON) {
+ aDest[0] = 0;
+ aDest[1] = 0;
+ aDest[2] = 0;
+ aDest[3] = 0;
+ return aDest;
+ }
+
+ len = 1 / len;
+ aDest[0] = x * len;
+ aDest[1] = y * len;
+ aDest[2] = z * len;
+ aDest[3] = w * len;
+ return aDest;
+ },
+
+ /**
+ * Calculate the length of a quat4.
+ *
+ * @param {Array} aQuat
+ * quat4 to calculate the length of
+ *
+ * @return {Number} length of the quaternion
+ */
+ length: function Q4_length(aQuat)
+ {
+ let x = aQuat[0];
+ let y = aQuat[1];
+ let z = aQuat[2];
+ let w = aQuat[3];
+
+ return Math.sqrt(x * x + y * y + z * z + w * w);
+ },
+
+ /**
+ * Performs a quaternion multiplication.
+ *
+ * @param {Array} aQuat
+ * first operand
+ * @param {Array} aQuat2
+ * second operand
+ * @param {Array} aDest
+ * optional, quat4 receiving the operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination quat if specified, first operand otherwise
+ */
+ multiply: function Q4_multiply(aQuat, aQuat2, aDest)
+ {
+ if (!aDest) {
+ aDest = aQuat;
+ }
+
+ let qax = aQuat[0];
+ let qay = aQuat[1];
+ let qaz = aQuat[2];
+ let qaw = aQuat[3];
+ let qbx = aQuat2[0];
+ let qby = aQuat2[1];
+ let qbz = aQuat2[2];
+ let qbw = aQuat2[3];
+
+ aDest[0] = qax * qbw + qaw * qbx + qay * qbz - qaz * qby;
+ aDest[1] = qay * qbw + qaw * qby + qaz * qbx - qax * qbz;
+ aDest[2] = qaz * qbw + qaw * qbz + qax * qby - qay * qbx;
+ aDest[3] = qaw * qbw - qax * qbx - qay * qby - qaz * qbz;
+ return aDest;
+ },
+
+ /**
+ * Transforms a vec3 with the given quaternion.
+ *
+ * @param {Array} aQuat
+ * quat4 to transform the vector with
+ * @param {Array} aVec
+ * vec3 to transform
+ * @param {Array} aDest
+ * optional, vec3 receiving the operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination vec3 if specified, aVec operand otherwise
+ */
+ multiplyVec3: function Q4_multiplyVec3(aQuat, aVec, aDest)
+ {
+ if (!aDest) {
+ aDest = aVec;
+ }
+
+ let x = aVec[0];
+ let y = aVec[1];
+ let z = aVec[2];
+
+ let qx = aQuat[0];
+ let qy = aQuat[1];
+ let qz = aQuat[2];
+ let qw = aQuat[3];
+
+ let ix = qw * x + qy * z - qz * y;
+ let iy = qw * y + qz * x - qx * z;
+ let iz = qw * z + qx * y - qy * x;
+ let iw = -qx * x - qy * y - qz * z;
+
+ aDest[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy;
+ aDest[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz;
+ aDest[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx;
+ return aDest;
+ },
+
+ /**
+ * Performs a spherical linear interpolation between two quat4.
+ *
+ * @param {Array} aQuat
+ * first quaternion
+ * @param {Array} aQuat2
+ * second quaternion
+ * @param {Number} aSlerp
+ * interpolation amount between the two inputs
+ * @param {Array} aDest
+ * optional, quat4 receiving the operation result
+ * if not specified result is written to the first operand
+ *
+ * @return {Array} the destination quat if specified, first operand otherwise
+ */
+ slerp: function Q4_slerp(aQuat, aQuat2, aSlerp, aDest)
+ {
+ if (!aDest) {
+ aDest = aQuat;
+ }
+
+ let cosHalfTheta = aQuat[0] * aQuat2[0] +
+ aQuat[1] * aQuat2[1] +
+ aQuat[2] * aQuat2[2] +
+ aQuat[3] * aQuat2[3];
+
+ if (Math.abs(cosHalfTheta) >= 1) {
+ aDest[0] = aQuat[0];
+ aDest[1] = aQuat[1];
+ aDest[2] = aQuat[2];
+ aDest[3] = aQuat[3];
+ return aDest;
+ }
+
+ let halfTheta = Math.acos(cosHalfTheta);
+ let sinHalfTheta = Math.sqrt(1 - cosHalfTheta * cosHalfTheta);
+
+ if (Math.abs(sinHalfTheta) < EPSILON) {
+ aDest[0] = (aQuat[0] * 0.5 + aQuat2[0] * 0.5);
+ aDest[1] = (aQuat[1] * 0.5 + aQuat2[1] * 0.5);
+ aDest[2] = (aQuat[2] * 0.5 + aQuat2[2] * 0.5);
+ aDest[3] = (aQuat[3] * 0.5 + aQuat2[3] * 0.5);
+ return aDest;
+ }
+
+ let ratioA = Math.sin((1 - aSlerp) * halfTheta) / sinHalfTheta;
+ let ratioB = Math.sin(aSlerp * halfTheta) / sinHalfTheta;
+
+ aDest[0] = (aQuat[0] * ratioA + aQuat2[0] * ratioB);
+ aDest[1] = (aQuat[1] * ratioA + aQuat2[1] * ratioB);
+ aDest[2] = (aQuat[2] * ratioA + aQuat2[2] * ratioB);
+ aDest[3] = (aQuat[3] * ratioA + aQuat2[3] * ratioB);
+ return aDest;
+ },
+
+ /**
+ * Calculates a 3x3 matrix from the given quat4.
+ *
+ * @param {Array} aQuat
+ * quat4 to create matrix from
+ * @param {Array} aDest
+ * optional, mat3 receiving the initialization result
+ * if not specified, a new matrix is created
+ *
+ * @return {Array} the destination mat3 if specified, first operand otherwise
+ */
+ toMat3: function Q4_toMat3(aQuat, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(9);
+ }
+
+ let x = aQuat[0];
+ let y = aQuat[1];
+ let z = aQuat[2];
+ let w = aQuat[3];
+
+ let x2 = x + x;
+ let y2 = y + y;
+ let z2 = z + z;
+ let xx = x * x2;
+ let xy = x * y2;
+ let xz = x * z2;
+ let yy = y * y2;
+ let yz = y * z2;
+ let zz = z * z2;
+ let wx = w * x2;
+ let wy = w * y2;
+ let wz = w * z2;
+
+ aDest[0] = 1 - (yy + zz);
+ aDest[1] = xy - wz;
+ aDest[2] = xz + wy;
+ aDest[3] = xy + wz;
+ aDest[4] = 1 - (xx + zz);
+ aDest[5] = yz - wx;
+ aDest[6] = xz - wy;
+ aDest[7] = yz + wx;
+ aDest[8] = 1 - (xx + yy);
+ return aDest;
+ },
+
+ /**
+ * Calculates a 4x4 matrix from the given quat4.
+ *
+ * @param {Array} aQuat
+ * quat4 to create matrix from
+ * @param {Array} aDest
+ * optional, mat4 receiving the initialization result
+ * if not specified, a new matrix is created
+ *
+ * @return {Array} the destination mat4 if specified, first operand otherwise
+ */
+ toMat4: function Q4_toMat4(aQuat, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(16);
+ }
+
+ let x = aQuat[0];
+ let y = aQuat[1];
+ let z = aQuat[2];
+ let w = aQuat[3];
+
+ let x2 = x + x;
+ let y2 = y + y;
+ let z2 = z + z;
+ let xx = x * x2;
+ let xy = x * y2;
+ let xz = x * z2;
+ let yy = y * y2;
+ let yz = y * z2;
+ let zz = z * z2;
+ let wx = w * x2;
+ let wy = w * y2;
+ let wz = w * z2;
+
+ aDest[0] = 1 - (yy + zz);
+ aDest[1] = xy - wz;
+ aDest[2] = xz + wy;
+ aDest[3] = 0;
+ aDest[4] = xy + wz;
+ aDest[5] = 1 - (xx + zz);
+ aDest[6] = yz - wx;
+ aDest[7] = 0;
+ aDest[8] = xz - wy;
+ aDest[9] = yz + wx;
+ aDest[10] = 1 - (xx + yy);
+ aDest[11] = 0;
+ aDest[12] = 0;
+ aDest[13] = 0;
+ aDest[14] = 0;
+ aDest[15] = 1;
+ return aDest;
+ },
+
+ /**
+ * Creates a rotation quaternion from axis-angle.
+ * This function expects that the axis is a normalized vector.
+ *
+ * @param {Array} aAxis
+ * an array of elements representing the [x, y, z] axis
+ * @param {Number} aAngle
+ * the angle of rotation
+ * @param {Array} aDest
+ * optional, quat4 receiving the initialization result
+ * if not specified, a new quaternion is created
+ *
+ * @return {Array} the quaternion as [x, y, z, w]
+ */
+ fromAxis: function Q4_fromAxis(aAxis, aAngle, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(4);
+ }
+
+ let ang = aAngle * 0.5;
+ let sin = Math.sin(ang);
+ let cos = Math.cos(ang);
+
+ aDest[0] = aAxis[0] * sin;
+ aDest[1] = aAxis[1] * sin;
+ aDest[2] = aAxis[2] * sin;
+ aDest[3] = cos;
+ return aDest;
+ },
+
+ /**
+ * Creates a rotation quaternion from Euler angles.
+ *
+ * @param {Number} aYaw
+ * the yaw angle of rotation
+ * @param {Number} aPitch
+ * the pitch angle of rotation
+ * @param {Number} aRoll
+ * the roll angle of rotation
+ * @param {Array} aDest
+ * optional, quat4 receiving the initialization result
+ * if not specified, a new quaternion is created
+ *
+ * @return {Array} the quaternion as [x, y, z, w]
+ */
+ fromEuler: function Q4_fromEuler(aYaw, aPitch, aRoll, aDest)
+ {
+ if (!aDest) {
+ aDest = new Float32Array(4);
+ }
+
+ let x = aPitch * 0.5;
+ let y = aYaw * 0.5;
+ let z = aRoll * 0.5;
+
+ let sinr = Math.sin(x);
+ let sinp = Math.sin(y);
+ let siny = Math.sin(z);
+ let cosr = Math.cos(x);
+ let cosp = Math.cos(y);
+ let cosy = Math.cos(z);
+
+ aDest[0] = sinr * cosp * cosy - cosr * sinp * siny;
+ aDest[1] = cosr * sinp * cosy + sinr * cosp * siny;
+ aDest[2] = cosr * cosp * siny - sinr * sinp * cosy;
+ aDest[3] = cosr * cosp * cosy + sinr * sinp * siny;
+ return aDest;
+ },
+
+ /**
+ * Returns a string representation of a quaternion.
+ *
+ * @param {Array} aQuat
+ * quat4 to represent as a string
+ *
+ * @return {String} representation of the quaternion
+ */
+ str: function Q4_str(aQuat) {
+ return "[" + aQuat[0] + ", " +
+ aQuat[1] + ", " +
+ aQuat[2] + ", " +
+ aQuat[3] + "]";
+ }
+};
+
+exports.quat4 = quat4;
+
+/**
+ * Various algebraic math functions required by the engine.
+ */
+let TiltMath = {
+
+ /**
+ * Helper function, converts degrees to radians.
+ *
+ * @param {Number} aDegrees
+ * the degrees to be converted to radians
+ *
+ * @return {Number} the degrees converted to radians
+ */
+ radians: function TM_radians(aDegrees)
+ {
+ return aDegrees * PI_OVER_180;
+ },
+
+ /**
+ * Helper function, converts radians to degrees.
+ *
+ * @param {Number} aRadians
+ * the radians to be converted to degrees
+ *
+ * @return {Number} the radians converted to degrees
+ */
+ degrees: function TM_degrees(aRadians)
+ {
+ return aRadians * INV_PI_OVER_180;
+ },
+
+ /**
+ * Re-maps a number from one range to another.
+ *
+ * @param {Number} aValue
+ * the number to map
+ * @param {Number} aLow1
+ * the normal lower bound of the number
+ * @param {Number} aHigh1
+ * the normal upper bound of the number
+ * @param {Number} aLow2
+ * the new lower bound of the number
+ * @param {Number} aHigh2
+ * the new upper bound of the number
+ *
+ * @return {Number} the remapped number
+ */
+ map: function TM_map(aValue, aLow1, aHigh1, aLow2, aHigh2)
+ {
+ return aLow2 + (aHigh2 - aLow2) * ((aValue - aLow1) / (aHigh1 - aLow1));
+ },
+
+ /**
+ * Returns if number is power of two.
+ *
+ * @param {Number} aNumber
+ * the number to be verified
+ *
+ * @return {Boolean} true if x is power of two
+ */
+ isPowerOfTwo: function TM_isPowerOfTwo(aNumber)
+ {
+ return !(aNumber & (aNumber - 1));
+ },
+
+ /**
+ * Returns the next closest power of two greater than a number.
+ *
+ * @param {Number} aNumber
+ * the number to be converted
+ *
+ * @return {Number} the next closest power of two for x
+ */
+ nextPowerOfTwo: function TM_nextPowerOfTwo(aNumber)
+ {
+ --aNumber;
+
+ for (let i = 1; i < 32; i <<= 1) {
+ aNumber = aNumber | aNumber >> i;
+ }
+ return aNumber + 1;
+ },
+
+ /**
+ * A convenient way of limiting values to a set boundary.
+ *
+ * @param {Number} aValue
+ * the number to be limited
+ * @param {Number} aMin
+ * the minimum allowed value for the number
+ * @param {Number} aMax
+ * the maximum allowed value for the number
+ */
+ clamp: function TM_clamp(aValue, aMin, aMax)
+ {
+ return Math.max(aMin, Math.min(aMax, aValue));
+ },
+
+ /**
+ * Convenient way to clamp a value to 0..1
+ *
+ * @param {Number} aValue
+ * the number to be limited
+ */
+ saturate: function TM_saturate(aValue)
+ {
+ return Math.max(0, Math.min(1, aValue));
+ },
+
+ /**
+ * Converts a hex color to rgba.
+ * If the passed param is invalid, it will be converted to [0, 0, 0, 1];
+ *
+ * @param {String} aColor
+ * color expressed in hex, or using rgb() or rgba()
+ *
+ * @return {Array} with 4 color 0..1 components: [red, green, blue, alpha]
+ */
+ hex2rgba: (function()
+ {
+ let cache = {};
+
+ return function TM_hex2rgba(aColor) {
+ let hex = aColor.charAt(0) === "#" ? aColor.substring(1) : aColor;
+
+ // check the cache to see if this color wasn't converted already
+ if (cache[hex] !== undefined) {
+ return cache[hex];
+ }
+
+ // e.g. "f00"
+ if (hex.length === 3) {
+ let r = parseInt(hex.substring(0, 1), 16) * FIFTEEN_OVER_225;
+ let g = parseInt(hex.substring(1, 2), 16) * FIFTEEN_OVER_225;
+ let b = parseInt(hex.substring(2, 3), 16) * FIFTEEN_OVER_225;
+
+ return (cache[hex] = [r, g, b, 1]);
+ }
+ // e.g. "f008"
+ if (hex.length === 4) {
+ let r = parseInt(hex.substring(0, 1), 16) * FIFTEEN_OVER_225;
+ let g = parseInt(hex.substring(1, 2), 16) * FIFTEEN_OVER_225;
+ let b = parseInt(hex.substring(2, 3), 16) * FIFTEEN_OVER_225;
+ let a = parseInt(hex.substring(3, 4), 16) * FIFTEEN_OVER_225;
+
+ return (cache[hex] = [r, g, b, a]);
+ }
+ // e.g. "ff0000"
+ if (hex.length === 6) {
+ let r = parseInt(hex.substring(0, 2), 16) * ONE_OVER_255;
+ let g = parseInt(hex.substring(2, 4), 16) * ONE_OVER_255;
+ let b = parseInt(hex.substring(4, 6), 16) * ONE_OVER_255;
+ let a = 1;
+
+ return (cache[hex] = [r, g, b, a]);
+ }
+ // e.g "ff0000aa"
+ if (hex.length === 8) {
+ let r = parseInt(hex.substring(0, 2), 16) * ONE_OVER_255;
+ let g = parseInt(hex.substring(2, 4), 16) * ONE_OVER_255;
+ let b = parseInt(hex.substring(4, 6), 16) * ONE_OVER_255;
+ let a = parseInt(hex.substring(6, 8), 16) * ONE_OVER_255;
+
+ return (cache[hex] = [r, g, b, a]);
+ }
+ // e.g. "rgba(255, 0, 0, 0.5)"
+ if (hex.match("^rgba")) {
+ let rgba = hex.substring(5, hex.length - 1).split(",");
+ rgba[0] *= ONE_OVER_255;
+ rgba[1] *= ONE_OVER_255;
+ rgba[2] *= ONE_OVER_255;
+ // in CSS, the alpha component of rgba() is already in the range 0..1
+
+ return (cache[hex] = rgba);
+ }
+ // e.g. "rgb(255, 0, 0)"
+ if (hex.match("^rgb")) {
+ let rgba = hex.substring(4, hex.length - 1).split(",");
+ rgba[0] *= ONE_OVER_255;
+ rgba[1] *= ONE_OVER_255;
+ rgba[2] *= ONE_OVER_255;
+ rgba[3] = 1;
+
+ return (cache[hex] = rgba);
+ }
+
+ // your argument is invalid
+ return (cache[hex] = [0, 0, 0, 1]);
+ };
+ }())
+};
+
+exports.TiltMath = TiltMath;
+
+// bind the owner object to the necessary functions
+TiltUtils.bindObjectFunc(vec3);
+TiltUtils.bindObjectFunc(mat3);
+TiltUtils.bindObjectFunc(mat4);
+TiltUtils.bindObjectFunc(quat4);
+TiltUtils.bindObjectFunc(TiltMath);
diff --git a/browser/devtools/tilt/tilt-utils.js b/browser/devtools/tilt/tilt-utils.js
new file mode 100644
index 000000000..1309f24f4
--- /dev/null
+++ b/browser/devtools/tilt/tilt-utils.js
@@ -0,0 +1,612 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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";
+
+const {Cc, Ci, Cu} = require("chrome");
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+
+const STACK_THICKNESS = 15;
+
+/**
+ * Module containing various helper functions used throughout Tilt.
+ */
+this.TiltUtils = {};
+module.exports = this.TiltUtils;
+
+/**
+ * Various console/prompt output functions required by the engine.
+ */
+TiltUtils.Output = {
+
+ /**
+ * Logs a message to the console.
+ *
+ * @param {String} aMessage
+ * the message to be logged
+ */
+ log: function TUO_log(aMessage)
+ {
+ if (this.suppressLogs) {
+ return;
+ }
+ // get the console service
+ let consoleService = Cc["@mozilla.org/consoleservice;1"]
+ .getService(Ci.nsIConsoleService);
+
+ // log the message
+ consoleService.logStringMessage(aMessage);
+ },
+
+ /**
+ * Logs an error to the console.
+ *
+ * @param {String} aMessage
+ * the message to be logged
+ * @param {Object} aProperties
+ * and object containing script error initialization details
+ */
+ error: function TUO_error(aMessage, aProperties)
+ {
+ if (this.suppressErrors) {
+ return;
+ }
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ // get the console service
+ let consoleService = Cc["@mozilla.org/consoleservice;1"]
+ .getService(Ci.nsIConsoleService);
+
+ // get the script error service
+ let scriptError = Cc["@mozilla.org/scripterror;1"]
+ .createInstance(Ci.nsIScriptError);
+
+ // initialize a script error
+ scriptError.init(aMessage,
+ aProperties.sourceName || "",
+ aProperties.sourceLine || "",
+ aProperties.lineNumber || 0,
+ aProperties.columnNumber || 0,
+ aProperties.flags || 0,
+ aProperties.category || "");
+
+ // log the error
+ consoleService.logMessage(scriptError);
+ },
+
+ /**
+ * Shows a modal alert message popup.
+ *
+ * @param {String} aTitle
+ * the title of the popup
+ * @param {String} aMessage
+ * the message to be logged
+ */
+ alert: function TUO_alert(aTitle, aMessage)
+ {
+ if (this.suppressAlerts) {
+ return;
+ }
+ if (!aMessage) {
+ aMessage = aTitle;
+ aTitle = "";
+ }
+
+ // get the prompt service
+ let prompt = Cc["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Ci.nsIPromptService);
+
+ // show the alert message
+ prompt.alert(null, aTitle, aMessage);
+ }
+};
+
+/**
+ * Helper functions for managing preferences.
+ */
+TiltUtils.Preferences = {
+
+ /**
+ * Gets a custom Tilt preference.
+ * If the preference does not exist, undefined is returned. If it does exist,
+ * but the type is not correctly specified, null is returned.
+ *
+ * @param {String} aPref
+ * the preference name
+ * @param {String} aType
+ * either "boolean", "string" or "integer"
+ *
+ * @return {Boolean | String | Number} the requested preference
+ */
+ get: function TUP_get(aPref, aType)
+ {
+ if (!aPref || !aType) {
+ return;
+ }
+
+ try {
+ let prefs = this._branch;
+
+ switch(aType) {
+ case "boolean":
+ return prefs.getBoolPref(aPref);
+ case "string":
+ return prefs.getCharPref(aPref);
+ case "integer":
+ return prefs.getIntPref(aPref);
+ }
+ return null;
+
+ } catch(e) {
+ // handle any unexpected exceptions
+ TiltUtils.Output.error(e.message);
+ return undefined;
+ }
+ },
+
+ /**
+ * Sets a custom Tilt preference.
+ * If the preference already exists, it is overwritten.
+ *
+ * @param {String} aPref
+ * the preference name
+ * @param {String} aType
+ * either "boolean", "string" or "integer"
+ * @param {String} aValue
+ * a new preference value
+ *
+ * @return {Boolean} true if the preference was set successfully
+ */
+ set: function TUP_set(aPref, aType, aValue)
+ {
+ if (!aPref || !aType || aValue === undefined || aValue === null) {
+ return;
+ }
+
+ try {
+ let prefs = this._branch;
+
+ switch(aType) {
+ case "boolean":
+ return prefs.setBoolPref(aPref, aValue);
+ case "string":
+ return prefs.setCharPref(aPref, aValue);
+ case "integer":
+ return prefs.setIntPref(aPref, aValue);
+ }
+ } catch(e) {
+ // handle any unexpected exceptions
+ TiltUtils.Output.error(e.message);
+ }
+ return false;
+ },
+
+ /**
+ * Creates a custom Tilt preference.
+ * If the preference already exists, it is left unchanged.
+ *
+ * @param {String} aPref
+ * the preference name
+ * @param {String} aType
+ * either "boolean", "string" or "integer"
+ * @param {String} aValue
+ * the initial preference value
+ *
+ * @return {Boolean} true if the preference was initialized successfully
+ */
+ create: function TUP_create(aPref, aType, aValue)
+ {
+ if (!aPref || !aType || aValue === undefined || aValue === null) {
+ return;
+ }
+
+ try {
+ let prefs = this._branch;
+
+ if (!prefs.prefHasUserValue(aPref)) {
+ switch(aType) {
+ case "boolean":
+ return prefs.setBoolPref(aPref, aValue);
+ case "string":
+ return prefs.setCharPref(aPref, aValue);
+ case "integer":
+ return prefs.setIntPref(aPref, aValue);
+ }
+ }
+ } catch(e) {
+ // handle any unexpected exceptions
+ TiltUtils.Output.error(e.message);
+ }
+ return false;
+ },
+
+ /**
+ * The preferences branch for this extension.
+ */
+ _branch: (function(aBranch) {
+ return Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService)
+ .getBranch(aBranch);
+
+ }("devtools.tilt."))
+};
+
+/**
+ * Easy way to access the string bundle.
+ */
+TiltUtils.L10n = {
+
+ /**
+ * The string bundle element.
+ */
+ stringBundle: null,
+
+ /**
+ * Returns a string in the string bundle.
+ * If the string bundle is not found, null is returned.
+ *
+ * @param {String} aName
+ * the string name in the bundle
+ *
+ * @return {String} the equivalent string from the bundle
+ */
+ get: function TUL_get(aName)
+ {
+ // check to see if the parent string bundle document element is valid
+ if (!this.stringBundle || !aName) {
+ return null;
+ }
+ return this.stringBundle.GetStringFromName(aName);
+ },
+
+ /**
+ * Returns a formatted string using the string bundle.
+ * If the string bundle is not found, null is returned.
+ *
+ * @param {String} aName
+ * the string name in the bundle
+ * @param {Array} aArgs
+ * an array of arguments for the formatted string
+ *
+ * @return {String} the equivalent formatted string from the bundle
+ */
+ format: function TUL_format(aName, aArgs)
+ {
+ // check to see if the parent string bundle document element is valid
+ if (!this.stringBundle || !aName || !aArgs) {
+ return null;
+ }
+ return this.stringBundle.formatStringFromName(aName, aArgs, aArgs.length);
+ }
+};
+
+/**
+ * Utilities for accessing and manipulating a document.
+ */
+TiltUtils.DOM = {
+
+ /**
+ * Current parent node object used when creating canvas elements.
+ */
+ parentNode: null,
+
+ /**
+ * Helper method, allowing to easily create and manage a canvas element.
+ * If the width and height params are falsy, they default to the parent node
+ * client width and height.
+ *
+ * @param {Document} aParentNode
+ * the parent node used to create the canvas
+ * if not specified, it will be reused from the cache
+ * @param {Object} aProperties
+ * optional, object containing some of the following props:
+ * {Boolean} focusable
+ * optional, true to make the canvas focusable
+ * {Boolean} append
+ * optional, true to append the canvas to the parent node
+ * {Number} width
+ * optional, specifies the width of the canvas
+ * {Number} height
+ * optional, specifies the height of the canvas
+ * {String} id
+ * optional, id for the created canvas element
+ *
+ * @return {HTMLCanvasElement} the newly created canvas element
+ */
+ initCanvas: function TUD_initCanvas(aParentNode, aProperties)
+ {
+ // check to see if the parent node element is valid
+ if (!(aParentNode = aParentNode || this.parentNode)) {
+ return null;
+ }
+
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ // cache this parent node so that it can be reused
+ this.parentNode = aParentNode;
+
+ // create the canvas element
+ let canvas = aParentNode.ownerDocument.
+ createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+
+ let width = aProperties.width || aParentNode.clientWidth;
+ let height = aProperties.height || aParentNode.clientHeight;
+ let id = aProperties.id || null;
+
+ canvas.setAttribute("style", "min-width: 1px; min-height: 1px;");
+ canvas.setAttribute("width", width);
+ canvas.setAttribute("height", height);
+ canvas.setAttribute("id", id);
+
+ // the canvas is unfocusable by default, we may require otherwise
+ if (aProperties.focusable) {
+ canvas.setAttribute("tabindex", "1");
+ canvas.style.outline = "none";
+ }
+
+ // append the canvas element to the current parent node, if specified
+ if (aProperties.append) {
+ aParentNode.appendChild(canvas);
+ }
+
+ return canvas;
+ },
+
+ /**
+ * Gets the full webpage dimensions (width and height).
+ *
+ * @param {Window} aContentWindow
+ * the content window holding the document
+ *
+ * @return {Object} an object containing the width and height coords
+ */
+ getContentWindowDimensions: function TUD_getContentWindowDimensions(
+ aContentWindow)
+ {
+ return {
+ width: aContentWindow.innerWidth + aContentWindow.scrollMaxX,
+ height: aContentWindow.innerHeight + aContentWindow.scrollMaxY
+ };
+ },
+
+ /**
+ * Calculates the position and depth to display a node, this can be overriden
+ * to change the visualization.
+ *
+ * @param {Window} aContentWindow
+ * the window content holding the document
+ * @param {Node} aNode
+ * the node to get the position for
+ * @param {Object} aParentPosition
+ * the position of the parent node, as returned by this
+ * function
+ *
+ * @return {Object} an object describing the node's position in 3D space
+ * containing the following properties:
+ * {Number} top
+ * distance along the x axis
+ * {Number} left
+ * distance along the y axis
+ * {Number} depth
+ * distance along the z axis
+ * {Number} width
+ * width of the node
+ * {Number} height
+ * height of the node
+ * {Number} thickness
+ * thickness of the node
+ */
+ getNodePosition: function TUD_getNodePosition(aContentWindow, aNode,
+ aParentPosition) {
+ // get the x, y, width and height coordinates of the node
+ let coord = LayoutHelpers.getRect(aNode, aContentWindow);
+ if (!coord) {
+ return null;
+ }
+
+ coord.depth = aParentPosition ? (aParentPosition.depth + aParentPosition.thickness) : 0;
+ coord.thickness = STACK_THICKNESS;
+
+ return coord;
+ },
+
+ /**
+ * Traverses a document object model & calculates useful info for each node.
+ *
+ * @param {Window} aContentWindow
+ * the window content holding the document
+ * @param {Object} aProperties
+ * optional, an object containing the following properties:
+ * {Function} nodeCallback
+ * a function to call instead of TiltUtils.DOM.getNodePosition
+ * to get the position and depth to display nodes
+ * {Object} invisibleElements
+ * elements which should be ignored
+ * {Number} minSize
+ * the minimum dimensions needed for a node to be traversed
+ * {Number} maxX
+ * the maximum left position of an element
+ * {Number} maxY
+ * the maximum top position of an element
+ *
+ * @return {Array} list containing nodes positions and local names
+ */
+ traverse: function TUD_traverse(aContentWindow, aProperties)
+ {
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ let aInvisibleElements = aProperties.invisibleElements || {};
+ let aMinSize = aProperties.minSize || -1;
+ let aMaxX = aProperties.maxX || Number.MAX_VALUE;
+ let aMaxY = aProperties.maxY || Number.MAX_VALUE;
+
+ let nodeCallback = aProperties.nodeCallback || this.getNodePosition.bind(this);
+
+ let nodes = aContentWindow.document.childNodes;
+ let store = { info: [], nodes: [] };
+ let depth = 0;
+
+ let queue = [
+ { parentPosition: null, nodes: aContentWindow.document.childNodes }
+ ]
+
+ while (queue.length) {
+ let { nodes, parentPosition } = queue.shift();
+
+ for (let node of nodes) {
+ // skip some nodes to avoid visualization meshes that are too bloated
+ let name = node.localName;
+ if (!name || aInvisibleElements[name]) {
+ continue;
+ }
+
+ let coord = nodeCallback(aContentWindow, node, parentPosition);
+ if (!coord) {
+ continue;
+ }
+
+ // the maximum size slices the traversal where needed
+ if (coord.left > aMaxX || coord.top > aMaxY) {
+ continue;
+ }
+
+ // use this node only if it actually has visible dimensions
+ if (coord.width > aMinSize && coord.height > aMinSize) {
+
+ // save the necessary details into a list to be returned later
+ store.info.push({ coord: coord, name: name });
+ store.nodes.push(node);
+ }
+
+ let childNodes = (name === "iframe" || name === "frame") ? node.contentDocument.childNodes : node.childNodes;
+ if (childNodes.length > 0)
+ queue.push({ parentPosition: coord, nodes: childNodes });
+ }
+ }
+
+ return store;
+ }
+};
+
+/**
+ * Binds a new owner object to the child functions.
+ * If the new parent is not specified, it will default to the passed scope.
+ *
+ * @param {Object} aScope
+ * the object from which all functions will be rebound
+ * @param {String} aRegex
+ * a regular expression to identify certain functions
+ * @param {Object} aParent
+ * the new parent for the object's functions
+ */
+TiltUtils.bindObjectFunc = function TU_bindObjectFunc(aScope, aRegex, aParent)
+{
+ if (!aScope) {
+ return;
+ }
+
+ for (let i in aScope) {
+ try {
+ if ("function" === typeof aScope[i] && (aRegex ? i.match(aRegex) : 1)) {
+ aScope[i] = aScope[i].bind(aParent || aScope);
+ }
+ } catch(e) {
+ TiltUtils.Output.error(e);
+ }
+ }
+};
+
+/**
+ * Destroys an object and deletes all members.
+ *
+ * @param {Object} aScope
+ * the object from which all children will be destroyed
+ */
+TiltUtils.destroyObject = function TU_destroyObject(aScope)
+{
+ if (!aScope) {
+ return;
+ }
+
+ // objects in Tilt usually use a function to handle internal destruction
+ if ("function" === typeof aScope._finalize) {
+ aScope._finalize();
+ }
+ for (let i in aScope) {
+ if (aScope.hasOwnProperty(i)) {
+ delete aScope[i];
+ }
+ }
+};
+
+/**
+ * Retrieve the unique ID of a window object.
+ *
+ * @param {Window} aWindow
+ * the window to get the ID from
+ *
+ * @return {Number} the window ID
+ */
+TiltUtils.getWindowId = function TU_getWindowId(aWindow)
+{
+ if (!aWindow) {
+ return;
+ }
+
+ return aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .currentInnerWindowID;
+};
+
+/**
+ * Sets the markup document viewer zoom for the currently selected browser.
+ *
+ * @param {Window} aChromeWindow
+ * the top-level browser window
+ *
+ * @param {Number} the zoom ammount
+ */
+TiltUtils.setDocumentZoom = function TU_setDocumentZoom(aChromeWindow, aZoom) {
+ aChromeWindow.gBrowser.selectedBrowser.markupDocumentViewer.fullZoom = aZoom;
+};
+
+/**
+ * Performs a garbage collection.
+ *
+ * @param {Window} aChromeWindow
+ * the top-level browser window
+ */
+TiltUtils.gc = function TU_gc(aChromeWindow)
+{
+ aChromeWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .garbageCollect();
+};
+
+/**
+ * Clears the cache and sets all the variables to null.
+ */
+TiltUtils.clearCache = function TU_clearCache()
+{
+ TiltUtils.DOM.parentNode = null;
+};
+
+// bind the owner object to the necessary functions
+TiltUtils.bindObjectFunc(TiltUtils.Output);
+TiltUtils.bindObjectFunc(TiltUtils.Preferences);
+TiltUtils.bindObjectFunc(TiltUtils.L10n);
+TiltUtils.bindObjectFunc(TiltUtils.DOM);
+
+// set the necessary string bundle
+XPCOMUtils.defineLazyGetter(TiltUtils.L10n, "stringBundle", function() {
+ return Services.strings.createBundle(
+ "chrome://browser/locale/devtools/tilt.properties");
+});
diff --git a/browser/devtools/tilt/tilt-visualizer-style.js b/browser/devtools/tilt/tilt-visualizer-style.js
new file mode 100644
index 000000000..7078a02dd
--- /dev/null
+++ b/browser/devtools/tilt/tilt-visualizer-style.js
@@ -0,0 +1,46 @@
+/* -*- Mode: javascript, tab-width: 2, indent-tabs-mode: nil, c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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";
+
+let {TiltMath} = require("devtools/tilt/tilt-math");
+let rgba = TiltMath.hex2rgba;
+
+/**
+ * Various colors and style settings used throughout Tilt.
+ */
+module.exports = {
+ canvas: {
+ background: "linear-gradient(#454545 0%, #000 100%)",
+ },
+
+ nodes: {
+ highlight: {
+ defaultFill: rgba("#555"),
+ defaultStroke: rgba("#000"),
+ defaultStrokeWeight: 1
+ },
+
+ html: rgba("#8880"),
+ body: rgba("#fff0"),
+ h1: rgba("#e667af"),
+ h2: rgba("#c667af"),
+ h3: rgba("#a667af"),
+ h4: rgba("#8667af"),
+ h5: rgba("#8647af"),
+ h6: rgba("#8627af"),
+ div: rgba("#5dc8cd"),
+ span: rgba("#67e46f"),
+ table: rgba("#ff0700"),
+ tr: rgba("#ff4540"),
+ td: rgba("#ff7673"),
+ ul: rgba("#4671d5"),
+ li: rgba("#6c8cd5"),
+ p: rgba("#aaa"),
+ a: rgba("#123eab"),
+ img: rgba("#ffb473"),
+ iframe: rgba("#85004b")
+ }
+};
diff --git a/browser/devtools/tilt/tilt-visualizer.js b/browser/devtools/tilt/tilt-visualizer.js
new file mode 100644
index 000000000..0fa1bee40
--- /dev/null
+++ b/browser/devtools/tilt/tilt-visualizer.js
@@ -0,0 +1,2260 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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";
+
+const {Cu, Ci, ChromeWorker} = require("chrome");
+
+let TiltGL = require("devtools/tilt/tilt-gl");
+let TiltUtils = require("devtools/tilt/tilt-utils");
+let TiltVisualizerStyle = require("devtools/tilt/tilt-visualizer-style");
+let {EPSILON, TiltMath, vec3, mat4, quat4} = require("devtools/tilt/tilt-math");
+let {TargetFactory} = require("devtools/framework/target");
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
+
+const ELEMENT_MIN_SIZE = 4;
+const INVISIBLE_ELEMENTS = {
+ "head": true,
+ "base": true,
+ "basefont": true,
+ "isindex": true,
+ "link": true,
+ "meta": true,
+ "option": true,
+ "script": true,
+ "style": true,
+ "title": true
+};
+
+// a node is represented in the visualization mesh as a rectangular stack
+// of 5 quads composed of 12 vertices; we draw these as triangles using an
+// index buffer of 12 unsigned int elements, obviously one for each vertex;
+// if a webpage has enough nodes to overflow the index buffer elements size,
+// weird things may happen; thus, when necessary, we'll split into groups
+const MAX_GROUP_NODES = Math.pow(2, Uint16Array.BYTES_PER_ELEMENT * 8) / 12 - 1;
+
+const WIREFRAME_COLOR = [0, 0, 0, 0.25];
+const INTRO_TRANSITION_DURATION = 1000;
+const OUTRO_TRANSITION_DURATION = 800;
+const INITIAL_Z_TRANSLATION = 400;
+const MOVE_INTO_VIEW_ACCURACY = 50;
+
+const MOUSE_CLICK_THRESHOLD = 10;
+const MOUSE_INTRO_DELAY = 200;
+const ARCBALL_SENSITIVITY = 0.5;
+const ARCBALL_ROTATION_STEP = 0.15;
+const ARCBALL_TRANSLATION_STEP = 35;
+const ARCBALL_ZOOM_STEP = 0.1;
+const ARCBALL_ZOOM_MIN = -3000;
+const ARCBALL_ZOOM_MAX = 500;
+const ARCBALL_RESET_SPHERICAL_FACTOR = 0.1;
+const ARCBALL_RESET_LINEAR_FACTOR = 0.01;
+
+const TILT_CRAFTER = "resource:///modules/devtools/tilt/TiltWorkerCrafter.js";
+const TILT_PICKER = "resource:///modules/devtools/tilt/TiltWorkerPicker.js";
+
+
+/**
+ * Initializes the visualization presenter and controller.
+ *
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {Window} chromeWindow: a reference to the top level window
+ * {Window} contentWindow: the content window holding the visualized doc
+ * {Element} parentNode: the parent node to hold the visualization
+ * {Object} notifications: necessary notifications for Tilt
+ * {Function} onError: optional, function called if initialization failed
+ * {Function} onLoad: optional, function called if initialization worked
+ */
+function TiltVisualizer(aProperties)
+{
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ /**
+ * Save a reference to the top-level window.
+ */
+ this.chromeWindow = aProperties.chromeWindow;
+ this.tab = aProperties.tab;
+
+ /**
+ * The canvas element used for rendering the visualization.
+ */
+ this.canvas = TiltUtils.DOM.initCanvas(aProperties.parentNode, {
+ focusable: true,
+ append: true
+ });
+
+ /**
+ * Visualization logic and drawing loop.
+ */
+ this.presenter = new TiltVisualizer.Presenter(this.canvas,
+ aProperties.chromeWindow,
+ aProperties.contentWindow,
+ aProperties.notifications,
+ aProperties.onError || null,
+ aProperties.onLoad || null);
+
+ /**
+ * Visualization mouse and keyboard controller.
+ */
+ this.controller = new TiltVisualizer.Controller(this.canvas, this.presenter);
+}
+
+exports.TiltVisualizer = TiltVisualizer;
+
+TiltVisualizer.prototype = {
+
+ /**
+ * Initializes the visualizer.
+ */
+ init: function TV_init()
+ {
+ this.presenter.init();
+ this.bindToInspector(this.tab);
+ },
+
+ /**
+ * Checks if this object was initialized properly.
+ *
+ * @return {Boolean} true if the object was initialized properly
+ */
+ isInitialized: function TV_isInitialized()
+ {
+ return this.presenter && this.presenter.isInitialized() &&
+ this.controller && this.controller.isInitialized();
+ },
+
+ /**
+ * Removes the overlay canvas used for rendering the visualization.
+ */
+ removeOverlay: function TV_removeOverlay()
+ {
+ if (this.canvas && this.canvas.parentNode) {
+ this.canvas.parentNode.removeChild(this.canvas);
+ }
+ },
+
+ /**
+ * Explicitly cleans up this visualizer and sets everything to null.
+ */
+ cleanup: function TV_cleanup()
+ {
+ this.unbindInspector();
+
+ if (this.controller) {
+ TiltUtils.destroyObject(this.controller);
+ }
+ if (this.presenter) {
+ TiltUtils.destroyObject(this.presenter);
+ }
+
+ let chromeWindow = this.chromeWindow;
+
+ TiltUtils.destroyObject(this);
+ TiltUtils.clearCache();
+ TiltUtils.gc(chromeWindow);
+ },
+
+ /**
+ * Listen to the inspector activity.
+ */
+ bindToInspector: function TV_bindToInspector(aTab)
+ {
+ this._browserTab = aTab;
+
+ this.onNewNodeFromInspector = this.onNewNodeFromInspector.bind(this);
+ this.onNewNodeFromTilt = this.onNewNodeFromTilt.bind(this);
+ this.onInspectorReady = this.onInspectorReady.bind(this);
+ this.onToolboxDestroyed = this.onToolboxDestroyed.bind(this);
+
+ gDevTools.on("inspector-ready", this.onInspectorReady);
+ gDevTools.on("toolbox-destroyed", this.onToolboxDestroyed);
+
+ Services.obs.addObserver(this.onNewNodeFromTilt,
+ this.presenter.NOTIFICATIONS.HIGHLIGHTING,
+ false);
+ Services.obs.addObserver(this.onNewNodeFromTilt,
+ this.presenter.NOTIFICATIONS.UNHIGHLIGHTING,
+ false);
+
+ let target = TargetFactory.forTab(aTab);
+ let toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ let panel = toolbox.getPanel("inspector");
+ if (panel) {
+ this.inspector = panel;
+ this.inspector.selection.on("new-node", this.onNewNodeFromInspector);
+ this.onNewNodeFromInspector();
+ }
+ }
+ },
+
+ /**
+ * Unregister inspector event listeners.
+ */
+ unbindInspector: function TV_unbindInspector()
+ {
+ this._browserTab = null;
+
+ if (this.inspector) {
+ if (this.inspector.selection) {
+ this.inspector.selection.off("new-node", this.onNewNodeFromInspector);
+ }
+ this.inspector = null;
+ }
+
+ gDevTools.off("inspector-ready", this.onInspectorReady);
+ gDevTools.off("toolbox-destroyed", this.onToolboxDestroyed);
+
+ Services.obs.removeObserver(this.onNewNodeFromTilt,
+ this.presenter.NOTIFICATIONS.HIGHLIGHTING);
+ Services.obs.removeObserver(this.onNewNodeFromTilt,
+ this.presenter.NOTIFICATIONS.UNHIGHLIGHTING);
+ },
+
+ /**
+ * When a new inspector is started.
+ */
+ onInspectorReady: function TV_onInspectorReady(event, toolbox, panel)
+ {
+ if (toolbox.target.tab === this._browserTab) {
+ this.inspector = panel;
+ this.inspector.selection.on("new-node", this.onNewNodeFromInspector);
+ this.onNewNodeFromTilt();
+ }
+ },
+
+ /**
+ * When the toolbox, therefor the inspector, is closed.
+ */
+ onToolboxDestroyed: function TV_onToolboxDestroyed(event, tab)
+ {
+ if (tab === this._browserTab &&
+ this.inspector) {
+ if (this.inspector.selection) {
+ this.inspector.selection.off("new-node", this.onNewNodeFromInspector);
+ }
+ this.inspector = null;
+ }
+ },
+
+ /**
+ * When a new node is selected in the inspector.
+ */
+ onNewNodeFromInspector: function TV_onNewNodeFromInspector()
+ {
+ if (this.inspector &&
+ this.inspector.selection.reason != "tilt") {
+ let selection = this.inspector.selection;
+ let canHighlightNode = selection.isNode() &&
+ selection.isConnected() &&
+ selection.isElementNode();
+ if (canHighlightNode) {
+ this.presenter.highlightNode(selection.node);
+ } else {
+ this.presenter.highlightNodeFor(-1);
+ }
+ }
+ },
+
+ /**
+ * When a new node is selected in Tilt.
+ */
+ onNewNodeFromTilt: function TV_onNewNodeFromTilt()
+ {
+ if (!this.inspector) {
+ return;
+ }
+ let nodeIndex = this.presenter._currentSelection;
+ if (nodeIndex < 0) {
+ this.inspector.selection.setNode(null, "tilt");
+ }
+ let node = this.presenter._traverseData.nodes[nodeIndex];
+ this.inspector.selection.setNode(node, "tilt");
+ },
+};
+
+/**
+ * This object manages the visualization logic and drawing loop.
+ *
+ * @param {HTMLCanvasElement} aCanvas
+ * the canvas element used for rendering
+ * @param {Window} aChromeWindow
+ * a reference to the top-level window
+ * @param {Window} aContentWindow
+ * the content window holding the document to be visualized
+ * @param {Object} aNotifications
+ * necessary notifications for Tilt
+ * @param {Function} onError
+ * function called if initialization failed
+ * @param {Function} onLoad
+ * function called if initialization worked
+ */
+TiltVisualizer.Presenter = function TV_Presenter(
+ aCanvas, aChromeWindow, aContentWindow, aNotifications, onError, onLoad)
+{
+ /**
+ * A canvas overlay used for drawing the visualization.
+ */
+ this.canvas = aCanvas;
+
+ /**
+ * Save a reference to the top-level window, to access Tilt.
+ */
+ this.chromeWindow = aChromeWindow;
+
+ /**
+ * The content window generating the visualization
+ */
+ this.contentWindow = aContentWindow;
+
+ /**
+ * Shortcut for accessing notifications strings.
+ */
+ this.NOTIFICATIONS = aNotifications;
+
+ /**
+ * Use the default node callback function
+ */
+ this.nodeCallback = null;
+
+ /**
+ * Create the renderer, containing useful functions for easy drawing.
+ */
+ this._renderer = new TiltGL.Renderer(aCanvas, onError, onLoad);
+
+ /**
+ * A custom shader used for drawing the visualization mesh.
+ */
+ this._visualizationProgram = null;
+
+ /**
+ * The combined mesh representing the document visualization.
+ */
+ this._texture = null;
+ this._meshData = null;
+ this._meshStacks = null;
+ this._meshWireframe = null;
+ this._traverseData = null;
+
+ /**
+ * A highlight quad drawn over a stacked dom node.
+ */
+ this._highlight = {
+ disabled: true,
+ v0: vec3.create(),
+ v1: vec3.create(),
+ v2: vec3.create(),
+ v3: vec3.create()
+ };
+
+ /**
+ * Scene transformations, exposing offset, translation and rotation.
+ * Modified by events in the controller through delegate functions.
+ */
+ this.transforms = {
+ zoom: 1,
+ offset: vec3.create(), // mesh offset, aligned to the viewport center
+ translation: vec3.create(), // scene translation, on the [x, y, z] axis
+ rotation: quat4.create() // scene rotation, expressed as a quaternion
+ };
+
+ /**
+ * Variables holding information about the initial and current node selected.
+ */
+ this._currentSelection = -1; // the selected node index
+ this._initialMeshConfiguration = false; // true if the 3D mesh was configured
+
+ /**
+ * Variable specifying if the scene should be redrawn.
+ * This should happen usually when the visualization is translated/rotated.
+ */
+ this._redraw = true;
+
+ /**
+ * Total time passed since the rendering started.
+ * If the rendering is paused, this property won't get updated.
+ */
+ this._time = 0;
+
+ /**
+ * Frame delta time (the ammount of time passed for each frame).
+ * This is used to smoothly interpolate animation transfroms.
+ */
+ this._delta = 0;
+ this._prevFrameTime = 0;
+ this._currFrameTime = 0;
+};
+
+TiltVisualizer.Presenter.prototype = {
+
+ /**
+ * Initializes the presenter and starts the animation loop
+ */
+ init: function TVP_init()
+ {
+ this._setup();
+ this._loop();
+ },
+
+ /**
+ * The initialization logic.
+ */
+ _setup: function TVP__setup()
+ {
+ let renderer = this._renderer;
+
+ // if the renderer was destroyed, don't continue setup
+ if (!renderer || !renderer.context) {
+ return;
+ }
+
+ // create the visualization shaders and program to draw the stacks mesh
+ this._visualizationProgram = new renderer.Program({
+ vs: TiltVisualizer.MeshShader.vs,
+ fs: TiltVisualizer.MeshShader.fs,
+ attributes: ["vertexPosition", "vertexTexCoord", "vertexColor"],
+ uniforms: ["mvMatrix", "projMatrix", "sampler"]
+ });
+
+ // get the document zoom to properly scale the visualization
+ this.transforms.zoom = this._getPageZoom();
+
+ // bind the owner object to the necessary functions
+ TiltUtils.bindObjectFunc(this, "^_on");
+ TiltUtils.bindObjectFunc(this, "_loop");
+
+ this._setupTexture();
+ this._setupMeshData();
+ this._setupEventListeners();
+ this.canvas.focus();
+ },
+
+ /**
+ * Get page zoom factor.
+ * @return {Number}
+ */
+ _getPageZoom: function TVP__getPageZoom() {
+ return this.contentWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .fullZoom;
+ },
+
+ /**
+ * The animation logic.
+ */
+ _loop: function TVP__loop()
+ {
+ let renderer = this._renderer;
+
+ // if the renderer was destroyed, don't continue rendering
+ if (!renderer || !renderer.context) {
+ return;
+ }
+
+ // prepare for the next frame of the animation loop
+ this.chromeWindow.mozRequestAnimationFrame(this._loop);
+
+ // only redraw if we really have to
+ if (this._redraw) {
+ this._redraw = false;
+ this._drawVisualization();
+ }
+
+ // update the current presenter transfroms from the controller
+ if ("function" === typeof this._controllerUpdate) {
+ this._controllerUpdate(this._time, this._delta);
+ }
+
+ this._handleFrameDelta();
+ this._handleKeyframeNotifications();
+ },
+
+ /**
+ * Calculates the current frame delta time.
+ */
+ _handleFrameDelta: function TVP__handleFrameDelta()
+ {
+ this._prevFrameTime = this._currFrameTime;
+ this._currFrameTime = this.chromeWindow.mozAnimationStartTime;
+ this._delta = this._currFrameTime - this._prevFrameTime;
+ },
+
+ /**
+ * Draws the visualization mesh and highlight quad.
+ */
+ _drawVisualization: function TVP__drawVisualization()
+ {
+ let renderer = this._renderer;
+ let transforms = this.transforms;
+ let w = renderer.width;
+ let h = renderer.height;
+ let ih = renderer.initialHeight;
+
+ // if the mesh wasn't created yet, don't continue rendering
+ if (!this._meshStacks || !this._meshWireframe) {
+ return;
+ }
+
+ // clear the context to an opaque black background
+ renderer.clear();
+ renderer.perspective();
+
+ // apply a transition transformation using an ortho and perspective matrix
+ let ortho = mat4.ortho(0, w, h, 0, -1000, 1000);
+
+ if (!this._isExecutingDestruction) {
+ let f = this._time / INTRO_TRANSITION_DURATION;
+ renderer.lerp(renderer.projMatrix, ortho, f, 8);
+ } else {
+ let f = this._time / OUTRO_TRANSITION_DURATION;
+ renderer.lerp(renderer.projMatrix, ortho, 1 - f, 8);
+ }
+
+ // apply the preliminary transformations to the model view
+ renderer.translate(w * 0.5, ih * 0.5, -INITIAL_Z_TRANSLATION);
+
+ // calculate the camera matrix using the rotation and translation
+ renderer.translate(transforms.translation[0], 0,
+ transforms.translation[2]);
+
+ renderer.transform(quat4.toMat4(transforms.rotation));
+
+ // offset the visualization mesh to center
+ renderer.translate(transforms.offset[0],
+ transforms.offset[1] + transforms.translation[1], 0);
+
+ renderer.scale(transforms.zoom, transforms.zoom);
+
+ // draw the visualization mesh
+ renderer.strokeWeight(2);
+ renderer.depthTest(true);
+ this._drawMeshStacks();
+ this._drawMeshWireframe();
+ this._drawHighlight();
+
+ // make sure the initial transition is drawn until finished
+ if (this._time < INTRO_TRANSITION_DURATION ||
+ this._time < OUTRO_TRANSITION_DURATION) {
+ this._redraw = true;
+ }
+ this._time += this._delta;
+ },
+
+ /**
+ * Draws the meshStacks object.
+ */
+ _drawMeshStacks: function TVP__drawMeshStacks()
+ {
+ let renderer = this._renderer;
+ let mesh = this._meshStacks;
+
+ let visualizationProgram = this._visualizationProgram;
+ let texture = this._texture;
+ let mvMatrix = renderer.mvMatrix;
+ let projMatrix = renderer.projMatrix;
+
+ // use the necessary shader
+ visualizationProgram.use();
+
+ for (let i = 0, len = mesh.length; i < len; i++) {
+ let group = mesh[i];
+
+ // bind the attributes and uniforms as necessary
+ visualizationProgram.bindVertexBuffer("vertexPosition", group.vertices);
+ visualizationProgram.bindVertexBuffer("vertexTexCoord", group.texCoord);
+ visualizationProgram.bindVertexBuffer("vertexColor", group.color);
+
+ visualizationProgram.bindUniformMatrix("mvMatrix", mvMatrix);
+ visualizationProgram.bindUniformMatrix("projMatrix", projMatrix);
+ visualizationProgram.bindTexture("sampler", texture);
+
+ // draw the vertices as TRIANGLES indexed elements
+ renderer.drawIndexedVertices(renderer.context.TRIANGLES, group.indices);
+ }
+
+ // save the current model view and projection matrices
+ mesh.mvMatrix = mat4.create(mvMatrix);
+ mesh.projMatrix = mat4.create(projMatrix);
+ },
+
+ /**
+ * Draws the meshWireframe object.
+ */
+ _drawMeshWireframe: function TVP__drawMeshWireframe()
+ {
+ let renderer = this._renderer;
+ let mesh = this._meshWireframe;
+
+ for (let i = 0, len = mesh.length; i < len; i++) {
+ let group = mesh[i];
+
+ // use the necessary shader
+ renderer.useColorShader(group.vertices, WIREFRAME_COLOR);
+
+ // draw the vertices as LINES indexed elements
+ renderer.drawIndexedVertices(renderer.context.LINES, group.indices);
+ }
+ },
+
+ /**
+ * Draws a highlighted quad around a currently selected node.
+ */
+ _drawHighlight: function TVP__drawHighlight()
+ {
+ // check if there's anything to highlight (i.e any node is selected)
+ if (!this._highlight.disabled) {
+
+ // set the corresponding state to draw the highlight quad
+ let renderer = this._renderer;
+ let highlight = this._highlight;
+
+ renderer.depthTest(false);
+ renderer.fill(highlight.fill, 0.5);
+ renderer.stroke(highlight.stroke);
+ renderer.strokeWeight(highlight.strokeWeight);
+ renderer.quad(highlight.v0, highlight.v1, highlight.v2, highlight.v3);
+ }
+ },
+
+ /**
+ * Creates or refreshes the texture applied to the visualization mesh.
+ */
+ _setupTexture: function TVP__setupTexture()
+ {
+ let renderer = this._renderer;
+
+ // destroy any previously created texture
+ TiltUtils.destroyObject(this._texture); this._texture = null;
+
+ // if the renderer was destroyed, don't continue setup
+ if (!renderer || !renderer.context) {
+ return;
+ }
+
+ // get the maximum texture size
+ this._maxTextureSize =
+ renderer.context.getParameter(renderer.context.MAX_TEXTURE_SIZE);
+
+ // use a simple shim to get the image representation of the document
+ // this will be removed once the MOZ_window_region_texture bug #653656
+ // is finished; currently just converting the document image to a texture
+ // applied to the mesh
+ this._texture = new renderer.Texture({
+ source: TiltGL.TextureUtils.createContentImage(this.contentWindow,
+ this._maxTextureSize),
+ format: "RGB"
+ });
+
+ if ("function" === typeof this._onSetupTexture) {
+ this._onSetupTexture();
+ this._onSetupTexture = null;
+ }
+ },
+
+ /**
+ * Create the combined mesh representing the document visualization by
+ * traversing the document & adding a stack for each node that is drawable.
+ *
+ * @param {Object} aMeshData
+ * object containing the necessary mesh verts, texcoord etc.
+ */
+ _setupMesh: function TVP__setupMesh(aMeshData)
+ {
+ let renderer = this._renderer;
+
+ // destroy any previously created mesh
+ TiltUtils.destroyObject(this._meshStacks); this._meshStacks = [];
+ TiltUtils.destroyObject(this._meshWireframe); this._meshWireframe = [];
+
+ // if the renderer was destroyed, don't continue setup
+ if (!renderer || !renderer.context) {
+ return;
+ }
+
+ // save the mesh data for future use
+ this._meshData = aMeshData;
+
+ // create a sub-mesh for each group in the mesh data
+ for (let i = 0, len = aMeshData.groups.length; i < len; i++) {
+ let group = aMeshData.groups[i];
+
+ // create the visualization mesh using the vertices, texture coordinates
+ // and indices computed when traversing the document object model
+ this._meshStacks.push({
+ vertices: new renderer.VertexBuffer(group.vertices, 3),
+ texCoord: new renderer.VertexBuffer(group.texCoord, 2),
+ color: new renderer.VertexBuffer(group.color, 3),
+ indices: new renderer.IndexBuffer(group.stacksIndices)
+ });
+
+ // additionally, create a wireframe representation to make the
+ // visualization a bit more pretty
+ this._meshWireframe.push({
+ vertices: this._meshStacks[i].vertices,
+ indices: new renderer.IndexBuffer(group.wireframeIndices)
+ });
+ }
+
+ // configure the required mesh transformations and background only once
+ if (!this._initialMeshConfiguration) {
+ this._initialMeshConfiguration = true;
+
+ // set the necessary mesh offsets
+ this.transforms.offset[0] = -renderer.width * 0.5;
+ this.transforms.offset[1] = -renderer.height * 0.5;
+
+ // make sure the canvas is opaque now that the initialization is finished
+ this.canvas.style.background = TiltVisualizerStyle.canvas.background;
+
+ this._drawVisualization();
+ this._redraw = true;
+ }
+
+ if ("function" === typeof this._onSetupMesh) {
+ this._onSetupMesh();
+ this._onSetupMesh = null;
+ }
+ },
+
+ /**
+ * Computes the mesh vertices, texture coordinates etc. by groups of nodes.
+ */
+ _setupMeshData: function TVP__setupMeshData()
+ {
+ let renderer = this._renderer;
+
+ // if the renderer was destroyed, don't continue setup
+ if (!renderer || !renderer.context) {
+ return;
+ }
+
+ // traverse the document and get the depths, coordinates and local names
+ this._traverseData = TiltUtils.DOM.traverse(this.contentWindow, {
+ nodeCallback: this.nodeCallback,
+ invisibleElements: INVISIBLE_ELEMENTS,
+ minSize: ELEMENT_MIN_SIZE,
+ maxX: this._texture.width,
+ maxY: this._texture.height
+ });
+
+ let worker = new ChromeWorker(TILT_CRAFTER);
+
+ worker.addEventListener("message", function TVP_onMessage(event) {
+ this._setupMesh(event.data);
+ }.bind(this), false);
+
+ // calculate necessary information regarding vertices, texture coordinates
+ // etc. in a separate thread, as this process may take a while
+ worker.postMessage({
+ maxGroupNodes: MAX_GROUP_NODES,
+ style: TiltVisualizerStyle.nodes,
+ texWidth: this._texture.width,
+ texHeight: this._texture.height,
+ nodesInfo: this._traverseData.info
+ });
+ },
+
+ /**
+ * Sets up event listeners necessary for the presenter.
+ */
+ _setupEventListeners: function TVP__setupEventListeners()
+ {
+ this.contentWindow.addEventListener("resize", this._onResize, false);
+ },
+
+ /**
+ * Called when the content window of the current browser is resized.
+ */
+ _onResize: function TVP_onResize(e)
+ {
+ let zoom = this._getPageZoom();
+ let width = e.target.innerWidth * zoom;
+ let height = e.target.innerHeight * zoom;
+
+ // handle aspect ratio changes to update the projection matrix
+ this._renderer.width = width;
+ this._renderer.height = height;
+
+ this._redraw = true;
+ },
+
+ /**
+ * Highlights a specific node.
+ *
+ * @param {Element} aNode
+ * the html node to be highlighted
+ * @param {String} aFlags
+ * flags specifying highlighting options
+ */
+ highlightNode: function TVP_highlightNode(aNode, aFlags)
+ {
+ this.highlightNodeFor(this._traverseData.nodes.indexOf(aNode), aFlags);
+ },
+
+ /**
+ * Picks a stacked dom node at the x and y screen coordinates and highlights
+ * the selected node in the mesh.
+ *
+ * @param {Number} x
+ * the current horizontal coordinate of the mouse
+ * @param {Number} y
+ * the current vertical coordinate of the mouse
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {Function} onpick: function to be called after picking succeeded
+ * {Function} onfail: function to be called after picking failed
+ */
+ highlightNodeAt: function TVP_highlightNodeAt(x, y, aProperties)
+ {
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ // try to pick a mesh node using the current x, y coordinates
+ this.pickNode(x, y, {
+
+ /**
+ * Mesh picking failed (nothing was found for the picked point).
+ */
+ onfail: function TVP_onHighlightFail()
+ {
+ this.highlightNodeFor(-1);
+
+ if ("function" === typeof aProperties.onfail) {
+ aProperties.onfail();
+ }
+ }.bind(this),
+
+ /**
+ * Mesh picking succeeded.
+ *
+ * @param {Object} aIntersection
+ * object containing the intersection details
+ */
+ onpick: function TVP_onHighlightPick(aIntersection)
+ {
+ this.highlightNodeFor(aIntersection.index);
+
+ if ("function" === typeof aProperties.onpick) {
+ aProperties.onpick();
+ }
+ }.bind(this)
+ });
+ },
+
+ /**
+ * Sets the corresponding highlight coordinates and color based on the
+ * information supplied.
+ *
+ * @param {Number} aNodeIndex
+ * the index of the node in the this._traverseData array
+ * @param {String} aFlags
+ * flags specifying highlighting options
+ */
+ highlightNodeFor: function TVP_highlightNodeFor(aNodeIndex, aFlags)
+ {
+ this._redraw = true;
+
+ // if the node was already selected, don't do anything
+ if (this._currentSelection === aNodeIndex) {
+ return;
+ }
+
+ // if an invalid or nonexisted node is specified, disable the highlight
+ if (aNodeIndex < 0) {
+ this._currentSelection = -1;
+ this._highlight.disabled = true;
+
+ Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.UNHIGHLIGHTING, null);
+ return;
+ }
+
+ let highlight = this._highlight;
+ let info = this._traverseData.info[aNodeIndex];
+ let style = TiltVisualizerStyle.nodes;
+
+ highlight.disabled = false;
+ highlight.fill = style[info.name] || style.highlight.defaultFill;
+ highlight.stroke = style.highlight.defaultStroke;
+ highlight.strokeWeight = style.highlight.defaultStrokeWeight;
+
+ let x = info.coord.left;
+ let y = info.coord.top;
+ let w = info.coord.width;
+ let h = info.coord.height;
+ let z = info.coord.depth + info.coord.thickness;
+
+ vec3.set([x, y, z], highlight.v0);
+ vec3.set([x + w, y, z], highlight.v1);
+ vec3.set([x + w, y + h, z], highlight.v2);
+ vec3.set([x, y + h, z], highlight.v3);
+
+ this._currentSelection = aNodeIndex;
+
+ // if something is highlighted, make sure it's inside the current viewport;
+ // the point which should be moved into view is considered the center [x, y]
+ // position along the top edge of the currently selected node
+
+ if (aFlags && aFlags.indexOf("moveIntoView") !== -1)
+ {
+ this.controller.arcball.moveIntoView(vec3.lerp(
+ vec3.scale(this._highlight.v0, this.transforms.zoom, []),
+ vec3.scale(this._highlight.v1, this.transforms.zoom, []), 0.5));
+ }
+
+ Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.HIGHLIGHTING, null);
+ },
+
+ /**
+ * Deletes a node from the visualization mesh.
+ *
+ * @param {Number} aNodeIndex
+ * the index of the node in the this._traverseData array;
+ * if not specified, it will default to the current selection
+ */
+ deleteNode: function TVP_deleteNode(aNodeIndex)
+ {
+ // we probably don't want to delete the html or body node.. just sayin'
+ if ((aNodeIndex = aNodeIndex || this._currentSelection) < 1) {
+ return;
+ }
+
+ let renderer = this._renderer;
+
+ let groupIndex = parseInt(aNodeIndex / MAX_GROUP_NODES);
+ let nodeIndex = parseInt((aNodeIndex + (groupIndex ? 1 : 0)) % MAX_GROUP_NODES);
+ let group = this._meshStacks[groupIndex];
+ let vertices = group.vertices.components;
+
+ for (let i = 0, k = 36 * nodeIndex; i < 36; i++) {
+ vertices[i + k] = 0;
+ }
+
+ group.vertices = new renderer.VertexBuffer(vertices, 3);
+ this._highlight.disabled = true;
+ this._redraw = true;
+
+ Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.NODE_REMOVED, null);
+ },
+
+ /**
+ * Picks a stacked dom node at the x and y screen coordinates and issues
+ * a callback function with the found intersection.
+ *
+ * @param {Number} x
+ * the current horizontal coordinate of the mouse
+ * @param {Number} y
+ * the current vertical coordinate of the mouse
+ * @param {Object} aProperties
+ * an object containing the following properties:
+ * {Function} onpick: function to be called at intersection
+ * {Function} onfail: function to be called if no intersections
+ */
+ pickNode: function TVP_pickNode(x, y, aProperties)
+ {
+ // make sure the properties parameter is a valid object
+ aProperties = aProperties || {};
+
+ // if the mesh wasn't created yet, don't continue picking
+ if (!this._meshStacks || !this._meshWireframe) {
+ return;
+ }
+
+ let worker = new ChromeWorker(TILT_PICKER);
+
+ worker.addEventListener("message", function TVP_onMessage(event) {
+ if (event.data) {
+ if ("function" === typeof aProperties.onpick) {
+ aProperties.onpick(event.data);
+ }
+ } else {
+ if ("function" === typeof aProperties.onfail) {
+ aProperties.onfail();
+ }
+ }
+ }, false);
+
+ let zoom = this._getPageZoom();
+ let width = this._renderer.width * zoom;
+ let height = this._renderer.height * zoom;
+ x *= zoom;
+ y *= zoom;
+
+ // create a ray following the mouse direction from the near clipping plane
+ // to the far clipping plane, to check for intersections with the mesh,
+ // and do all the heavy lifting in a separate thread
+ worker.postMessage({
+ vertices: this._meshData.allVertices,
+
+ // create the ray destined for 3D picking
+ ray: vec3.createRay([x, y, 0], [x, y, 1], [0, 0, width, height],
+ this._meshStacks.mvMatrix,
+ this._meshStacks.projMatrix)
+ });
+ },
+
+ /**
+ * Delegate translation method, used by the controller.
+ *
+ * @param {Array} aTranslation
+ * the new translation on the [x, y, z] axis
+ */
+ setTranslation: function TVP_setTranslation(aTranslation)
+ {
+ let x = aTranslation[0];
+ let y = aTranslation[1];
+ let z = aTranslation[2];
+ let transforms = this.transforms;
+
+ // only update the translation if it's not already set
+ if (transforms.translation[0] !== x ||
+ transforms.translation[1] !== y ||
+ transforms.translation[2] !== z) {
+
+ vec3.set(aTranslation, transforms.translation);
+ this._redraw = true;
+ }
+ },
+
+ /**
+ * Delegate rotation method, used by the controller.
+ *
+ * @param {Array} aQuaternion
+ * the rotation quaternion, as [x, y, z, w]
+ */
+ setRotation: function TVP_setRotation(aQuaternion)
+ {
+ let x = aQuaternion[0];
+ let y = aQuaternion[1];
+ let z = aQuaternion[2];
+ let w = aQuaternion[3];
+ let transforms = this.transforms;
+
+ // only update the rotation if it's not already set
+ if (transforms.rotation[0] !== x ||
+ transforms.rotation[1] !== y ||
+ transforms.rotation[2] !== z ||
+ transforms.rotation[3] !== w) {
+
+ quat4.set(aQuaternion, transforms.rotation);
+ this._redraw = true;
+ }
+ },
+
+ /**
+ * Handles notifications at specific frame counts.
+ */
+ _handleKeyframeNotifications: function TV__handleKeyframeNotifications()
+ {
+ if (!TiltVisualizer.Prefs.introTransition && !this._isExecutingDestruction) {
+ this._time = INTRO_TRANSITION_DURATION;
+ }
+ if (!TiltVisualizer.Prefs.outroTransition && this._isExecutingDestruction) {
+ this._time = OUTRO_TRANSITION_DURATION;
+ }
+
+ if (this._time >= INTRO_TRANSITION_DURATION &&
+ !this._isInitializationFinished &&
+ !this._isExecutingDestruction) {
+
+ this._isInitializationFinished = true;
+ Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.INITIALIZED, null);
+
+ if ("function" === typeof this._onInitializationFinished) {
+ this._onInitializationFinished();
+ }
+ }
+
+ if (this._time >= OUTRO_TRANSITION_DURATION &&
+ !this._isDestructionFinished &&
+ this._isExecutingDestruction) {
+
+ this._isDestructionFinished = true;
+ Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.BEFORE_DESTROYED, null);
+
+ if ("function" === typeof this._onDestructionFinished) {
+ this._onDestructionFinished();
+ }
+ }
+ },
+
+ /**
+ * Starts executing the destruction sequence and issues a callback function
+ * when finished.
+ *
+ * @param {Function} aCallback
+ * the destruction finished callback
+ */
+ executeDestruction: function TV_executeDestruction(aCallback)
+ {
+ if (!this._isExecutingDestruction) {
+ this._isExecutingDestruction = true;
+ this._onDestructionFinished = aCallback;
+
+ // if we execute the destruction after the initialization finishes,
+ // proceed normally; otherwise, skip everything and immediately issue
+ // the callback
+
+ if (this._time > OUTRO_TRANSITION_DURATION) {
+ this._time = 0;
+ this._redraw = true;
+ } else {
+ aCallback();
+ }
+ }
+ },
+
+ /**
+ * Checks if this object was initialized properly.
+ *
+ * @return {Boolean} true if the object was initialized properly
+ */
+ isInitialized: function TVP_isInitialized()
+ {
+ return this._renderer && this._renderer.context;
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ _finalize: function TVP__finalize()
+ {
+ TiltUtils.destroyObject(this._visualizationProgram);
+ TiltUtils.destroyObject(this._texture);
+
+ if (this._meshStacks) {
+ this._meshStacks.forEach(function(group) {
+ TiltUtils.destroyObject(group.vertices);
+ TiltUtils.destroyObject(group.texCoord);
+ TiltUtils.destroyObject(group.color);
+ TiltUtils.destroyObject(group.indices);
+ });
+ }
+ if (this._meshWireframe) {
+ this._meshWireframe.forEach(function(group) {
+ TiltUtils.destroyObject(group.indices);
+ });
+ }
+
+ TiltUtils.destroyObject(this._renderer);
+
+ // Closing the tab would result in contentWindow being a dead object,
+ // so operations like removing event listeners won't work anymore.
+ if (this.contentWindow == this.chromeWindow.content) {
+ this.contentWindow.removeEventListener("resize", this._onResize, false);
+ }
+ }
+};
+
+/**
+ * A mouse and keyboard controller implementation.
+ *
+ * @param {HTMLCanvasElement} aCanvas
+ * the visualization canvas element
+ * @param {TiltVisualizer.Presenter} aPresenter
+ * the presenter instance to control
+ */
+TiltVisualizer.Controller = function TV_Controller(aCanvas, aPresenter)
+{
+ /**
+ * A canvas overlay on which mouse and keyboard event listeners are attached.
+ */
+ this.canvas = aCanvas;
+
+ /**
+ * Save a reference to the presenter to modify its model-view transforms.
+ */
+ this.presenter = aPresenter;
+ this.presenter.controller = this;
+
+ /**
+ * The initial controller dimensions and offset, in pixels.
+ */
+ this._zoom = aPresenter.transforms.zoom;
+ this._left = (aPresenter.contentWindow.pageXOffset || 0) * this._zoom;
+ this._top = (aPresenter.contentWindow.pageYOffset || 0) * this._zoom;
+ this._width = aCanvas.width;
+ this._height = aCanvas.height;
+
+ /**
+ * Arcball used to control the visualization using the mouse.
+ */
+ this.arcball = new TiltVisualizer.Arcball(
+ this.presenter.chromeWindow, this._width, this._height, 0,
+ [
+ this._width + this._left < aPresenter._maxTextureSize ? -this._left : 0,
+ this._height + this._top < aPresenter._maxTextureSize ? -this._top : 0
+ ]);
+
+ /**
+ * Object containing the rotation quaternion and the translation amount.
+ */
+ this._coordinates = null;
+
+ // bind the owner object to the necessary functions
+ TiltUtils.bindObjectFunc(this, "_update");
+ TiltUtils.bindObjectFunc(this, "^_on");
+
+ // add the necessary event listeners
+ this.addEventListeners();
+
+ // attach this controller's update function to the presenter ondraw event
+ this.presenter._controllerUpdate = this._update;
+};
+
+TiltVisualizer.Controller.prototype = {
+
+ /**
+ * Adds events listeners required by this controller.
+ */
+ addEventListeners: function TVC_addEventListeners()
+ {
+ let canvas = this.canvas;
+ let presenter = this.presenter;
+
+ // bind commonly used mouse and keyboard events with the controller
+ canvas.addEventListener("mousedown", this._onMouseDown, false);
+ canvas.addEventListener("mouseup", this._onMouseUp, false);
+ canvas.addEventListener("mousemove", this._onMouseMove, false);
+ canvas.addEventListener("mouseover", this._onMouseOver, false);
+ canvas.addEventListener("mouseout", this._onMouseOut, false);
+ canvas.addEventListener("MozMousePixelScroll", this._onMozScroll, false);
+ canvas.addEventListener("keydown", this._onKeyDown, false);
+ canvas.addEventListener("keyup", this._onKeyUp, false);
+ canvas.addEventListener("keypress", this._onKeyPress, true);
+ canvas.addEventListener("blur", this._onBlur, false);
+
+ // handle resize events to change the arcball dimensions
+ presenter.contentWindow.addEventListener("resize", this._onResize, false);
+ },
+
+ /**
+ * Removes all added events listeners required by this controller.
+ */
+ removeEventListeners: function TVC_removeEventListeners()
+ {
+ let canvas = this.canvas;
+ let presenter = this.presenter;
+
+ canvas.removeEventListener("mousedown", this._onMouseDown, false);
+ canvas.removeEventListener("mouseup", this._onMouseUp, false);
+ canvas.removeEventListener("mousemove", this._onMouseMove, false);
+ canvas.removeEventListener("mouseover", this._onMouseOver, false);
+ canvas.removeEventListener("mouseout", this._onMouseOut, false);
+ canvas.removeEventListener("MozMousePixelScroll", this._onMozScroll, false);
+ canvas.removeEventListener("keydown", this._onKeyDown, false);
+ canvas.removeEventListener("keyup", this._onKeyUp, false);
+ canvas.removeEventListener("keypress", this._onKeyPress, true);
+ canvas.removeEventListener("blur", this._onBlur, false);
+
+ // Closing the tab would result in contentWindow being a dead object,
+ // so operations like removing event listeners won't work anymore.
+ if (presenter.contentWindow == presenter.chromeWindow.content) {
+ presenter.contentWindow.removeEventListener("resize", this._onResize, false);
+ }
+ },
+
+ /**
+ * Function called each frame, updating the visualization camera transforms.
+ *
+ * @param {Number} aTime
+ * total time passed since rendering started
+ * @param {Number} aDelta
+ * the current animation frame delta
+ */
+ _update: function TVC__update(aTime, aDelta)
+ {
+ this._time = aTime;
+ this._coordinates = this.arcball.update(aDelta);
+
+ this.presenter.setRotation(this._coordinates.rotation);
+ this.presenter.setTranslation(this._coordinates.translation);
+ },
+
+ /**
+ * Called once after every time a mouse button is pressed.
+ */
+ _onMouseDown: function TVC__onMouseDown(e)
+ {
+ e.target.focus();
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this._time < MOUSE_INTRO_DELAY) {
+ return;
+ }
+
+ // calculate x and y coordinates using using the client and target offset
+ let button = e.which;
+ this._downX = e.clientX - e.target.offsetLeft;
+ this._downY = e.clientY - e.target.offsetTop;
+
+ this.arcball.mouseDown(this._downX, this._downY, button);
+ },
+
+ /**
+ * Called every time a mouse button is released.
+ */
+ _onMouseUp: function TVC__onMouseUp(e)
+ {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this._time < MOUSE_INTRO_DELAY) {
+ return;
+ }
+
+ // calculate x and y coordinates using using the client and target offset
+ let button = e.which;
+ let upX = e.clientX - e.target.offsetLeft;
+ let upY = e.clientY - e.target.offsetTop;
+
+ // a click in Tilt is issued only when the mouse pointer stays in
+ // relatively the same position
+ if (Math.abs(this._downX - upX) < MOUSE_CLICK_THRESHOLD &&
+ Math.abs(this._downY - upY) < MOUSE_CLICK_THRESHOLD) {
+
+ this.presenter.highlightNodeAt(upX, upY);
+ }
+
+ this.arcball.mouseUp(upX, upY, button);
+ },
+
+ /**
+ * Called every time the mouse moves.
+ */
+ _onMouseMove: function TVC__onMouseMove(e)
+ {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this._time < MOUSE_INTRO_DELAY) {
+ return;
+ }
+
+ // calculate x and y coordinates using using the client and target offset
+ let moveX = e.clientX - e.target.offsetLeft;
+ let moveY = e.clientY - e.target.offsetTop;
+
+ this.arcball.mouseMove(moveX, moveY);
+ },
+
+ /**
+ * Called when the mouse leaves the visualization bounds.
+ */
+ _onMouseOver: function TVC__onMouseOver(e)
+ {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.arcball.mouseOver();
+ },
+
+ /**
+ * Called when the mouse leaves the visualization bounds.
+ */
+ _onMouseOut: function TVC__onMouseOut(e)
+ {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.arcball.mouseOut();
+ },
+
+ /**
+ * Called when the mouse wheel is used.
+ */
+ _onMozScroll: function TVC__onMozScroll(e)
+ {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.arcball.zoom(e.detail);
+ },
+
+ /**
+ * Called when a key is pressed.
+ */
+ _onKeyDown: function TVC__onKeyDown(e)
+ {
+ let code = e.keyCode || e.which;
+
+ if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.arcball.keyDown(code);
+ } else {
+ this.arcball.cancelKeyEvents();
+ }
+ },
+
+ /**
+ * Called when a key is released.
+ */
+ _onKeyUp: function TVC__onKeyUp(e)
+ {
+ let code = e.keyCode || e.which;
+
+ if (code === e.DOM_VK_X) {
+ this.presenter.deleteNode();
+ }
+ if (code === e.DOM_VK_F) {
+ let highlight = this.presenter._highlight;
+ let zoom = this.presenter.transforms.zoom;
+
+ this.arcball.moveIntoView(vec3.lerp(
+ vec3.scale(highlight.v0, zoom, []),
+ vec3.scale(highlight.v1, zoom, []), 0.5));
+ }
+ if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.arcball.keyUp(code);
+ }
+ },
+
+ /**
+ * Called when a key is pressed.
+ */
+ _onKeyPress: function TVC__onKeyPress(e)
+ {
+ if (e.keyCode === e.DOM_VK_ESCAPE) {
+ let {TiltManager} = require("devtools/tilt/tilt");
+ let tilt =
+ TiltManager.getTiltForBrowser(this.presenter.chromeWindow);
+ e.preventDefault();
+ e.stopPropagation();
+ tilt.destroy(tilt.currentWindowId, true);
+ }
+ },
+
+ /**
+ * Called when the canvas looses focus.
+ */
+ _onBlur: function TVC__onBlur(e) {
+ this.arcball.cancelKeyEvents();
+ },
+
+ /**
+ * Called when the content window of the current browser is resized.
+ */
+ _onResize: function TVC__onResize(e)
+ {
+ let zoom = this.presenter._getPageZoom();
+ let width = e.target.innerWidth * zoom;
+ let height = e.target.innerHeight * zoom;
+
+ this.arcball.resize(width, height);
+ },
+
+ /**
+ * Checks if this object was initialized properly.
+ *
+ * @return {Boolean} true if the object was initialized properly
+ */
+ isInitialized: function TVC_isInitialized()
+ {
+ return this.arcball ? true : false;
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ _finalize: function TVC__finalize()
+ {
+ TiltUtils.destroyObject(this.arcball);
+ TiltUtils.destroyObject(this._coordinates);
+
+ this.removeEventListeners();
+ this.presenter.controller = null;
+ this.presenter._controllerUpdate = null;
+ }
+};
+
+/**
+ * This is a general purpose 3D rotation controller described by Ken Shoemake
+ * in the Graphics Interface ’92 Proceedings. It features good behavior
+ * easy implementation, cheap execution.
+ *
+ * @param {Window} aChromeWindow
+ * a reference to the top-level window
+ * @param {Number} aWidth
+ * the width of canvas
+ * @param {Number} aHeight
+ * the height of canvas
+ * @param {Number} aRadius
+ * optional, the radius of the arcball
+ * @param {Array} aInitialTrans
+ * optional, initial vector translation
+ * @param {Array} aInitialRot
+ * optional, initial quaternion rotation
+ */
+TiltVisualizer.Arcball = function TV_Arcball(
+ aChromeWindow, aWidth, aHeight, aRadius, aInitialTrans, aInitialRot)
+{
+ /**
+ * Save a reference to the top-level window to set/remove intervals.
+ */
+ this.chromeWindow = aChromeWindow;
+
+ /**
+ * Values retaining the current horizontal and vertical mouse coordinates.
+ */
+ this._mousePress = vec3.create();
+ this._mouseRelease = vec3.create();
+ this._mouseMove = vec3.create();
+ this._mouseLerp = vec3.create();
+ this._mouseButton = -1;
+
+ /**
+ * Object retaining the current pressed key codes.
+ */
+ this._keyCode = {};
+
+ /**
+ * The vectors representing the mouse coordinates mapped on the arcball
+ * and their perpendicular converted from (x, y) to (x, y, z) at specific
+ * events like mousePressed and mouseDragged.
+ */
+ this._startVec = vec3.create();
+ this._endVec = vec3.create();
+ this._pVec = vec3.create();
+
+ /**
+ * The corresponding rotation quaternions.
+ */
+ this._lastRot = quat4.create();
+ this._deltaRot = quat4.create();
+ this._currentRot = quat4.create(aInitialRot);
+
+ /**
+ * The current camera translation coordinates.
+ */
+ this._lastTrans = vec3.create();
+ this._deltaTrans = vec3.create();
+ this._currentTrans = vec3.create(aInitialTrans);
+ this._zoomAmount = 0;
+
+ /**
+ * Additional rotation and translation vectors.
+ */
+ this._additionalRot = vec3.create();
+ this._additionalTrans = vec3.create();
+ this._deltaAdditionalRot = quat4.create();
+ this._deltaAdditionalTrans = vec3.create();
+
+ // load the keys controlling the arcball
+ this._loadKeys();
+
+ // set the current dimensions of the arcball
+ this.resize(aWidth, aHeight, aRadius);
+};
+
+TiltVisualizer.Arcball.prototype = {
+
+ /**
+ * Call this function whenever you need the updated rotation quaternion
+ * and the zoom amount. These values will be returned as "rotation" and
+ * "translation" properties inside an object.
+ *
+ * @param {Number} aDelta
+ * the current animation frame delta
+ *
+ * @return {Object} the rotation quaternion and the translation amount
+ */
+ update: function TVA_update(aDelta)
+ {
+ let mousePress = this._mousePress;
+ let mouseRelease = this._mouseRelease;
+ let mouseMove = this._mouseMove;
+ let mouseLerp = this._mouseLerp;
+ let mouseButton = this._mouseButton;
+
+ // smoothly update the mouse coordinates
+ mouseLerp[0] += (mouseMove[0] - mouseLerp[0]) * ARCBALL_SENSITIVITY;
+ mouseLerp[1] += (mouseMove[1] - mouseLerp[1]) * ARCBALL_SENSITIVITY;
+
+ // cache the interpolated mouse coordinates
+ let x = mouseLerp[0];
+ let y = mouseLerp[1];
+
+ // the smoothed arcball rotation may not be finished when the mouse is
+ // pressed again, so cancel the rotation if other events occur or the
+ // animation finishes
+ if (mouseButton === 3 || x === mouseRelease[0] && y === mouseRelease[1]) {
+ this._rotating = false;
+ }
+
+ let startVec = this._startVec;
+ let endVec = this._endVec;
+ let pVec = this._pVec;
+
+ let lastRot = this._lastRot;
+ let deltaRot = this._deltaRot;
+ let currentRot = this._currentRot;
+
+ // left mouse button handles rotation
+ if (mouseButton === 1 || this._rotating) {
+ // the rotation doesn't stop immediately after the left mouse button is
+ // released, so add a flag to smoothly continue it until it ends
+ this._rotating = true;
+
+ // find the sphere coordinates of the mouse positions
+ this._pointToSphere(x, y, this.width, this.height, this.radius, endVec);
+
+ // compute the vector perpendicular to the start & end vectors
+ vec3.cross(startVec, endVec, pVec);
+
+ // if the begin and end vectors don't coincide
+ if (vec3.length(pVec) > 0) {
+ deltaRot[0] = pVec[0];
+ deltaRot[1] = pVec[1];
+ deltaRot[2] = pVec[2];
+
+ // in the quaternion values, w is cosine (theta / 2),
+ // where theta is the rotation angle
+ deltaRot[3] = -vec3.dot(startVec, endVec);
+ } else {
+ // return an identity rotation quaternion
+ deltaRot[0] = 0;
+ deltaRot[1] = 0;
+ deltaRot[2] = 0;
+ deltaRot[3] = 1;
+ }
+
+ // calculate the current rotation based on the mouse click events
+ quat4.multiply(lastRot, deltaRot, currentRot);
+ } else {
+ // save the current quaternion to stack rotations
+ quat4.set(currentRot, lastRot);
+ }
+
+ let lastTrans = this._lastTrans;
+ let deltaTrans = this._deltaTrans;
+ let currentTrans = this._currentTrans;
+
+ // right mouse button handles panning
+ if (mouseButton === 3) {
+ // calculate a delta translation between the new and old mouse position
+ // and save it to the current translation
+ deltaTrans[0] = mouseMove[0] - mousePress[0];
+ deltaTrans[1] = mouseMove[1] - mousePress[1];
+
+ currentTrans[0] = lastTrans[0] + deltaTrans[0];
+ currentTrans[1] = lastTrans[1] + deltaTrans[1];
+ } else {
+ // save the current panning to stack translations
+ lastTrans[0] = currentTrans[0];
+ lastTrans[1] = currentTrans[1];
+ }
+
+ let zoomAmount = this._zoomAmount;
+ let keyCode = this._keyCode;
+
+ // mouse wheel handles zooming
+ deltaTrans[2] = (zoomAmount - currentTrans[2]) * ARCBALL_ZOOM_STEP;
+ currentTrans[2] += deltaTrans[2];
+
+ let additionalRot = this._additionalRot;
+ let additionalTrans = this._additionalTrans;
+ let deltaAdditionalRot = this._deltaAdditionalRot;
+ let deltaAdditionalTrans = this._deltaAdditionalTrans;
+
+ let rotateKeys = this.rotateKeys;
+ let panKeys = this.panKeys;
+ let zoomKeys = this.zoomKeys;
+ let resetKey = this.resetKey;
+
+ // handle additional rotation and translation by the keyboard
+ if (keyCode[rotateKeys.left]) {
+ additionalRot[0] -= ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
+ }
+ if (keyCode[rotateKeys.right]) {
+ additionalRot[0] += ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
+ }
+ if (keyCode[rotateKeys.up]) {
+ additionalRot[1] += ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
+ }
+ if (keyCode[rotateKeys.down]) {
+ additionalRot[1] -= ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
+ }
+ if (keyCode[panKeys.left]) {
+ additionalTrans[0] -= ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
+ }
+ if (keyCode[panKeys.right]) {
+ additionalTrans[0] += ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
+ }
+ if (keyCode[panKeys.up]) {
+ additionalTrans[1] -= ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
+ }
+ if (keyCode[panKeys.down]) {
+ additionalTrans[1] += ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
+ }
+ if (keyCode[zoomKeys["in"][0]] ||
+ keyCode[zoomKeys["in"][1]] ||
+ keyCode[zoomKeys["in"][2]]) {
+ this.zoom(-ARCBALL_TRANSLATION_STEP);
+ }
+ if (keyCode[zoomKeys["out"][0]] ||
+ keyCode[zoomKeys["out"][1]]) {
+ this.zoom(ARCBALL_TRANSLATION_STEP);
+ }
+ if (keyCode[zoomKeys["unzoom"]]) {
+ this._zoomAmount = 0;
+ }
+ if (keyCode[resetKey]) {
+ this.reset();
+ }
+
+ // update the delta key rotations and translations
+ deltaAdditionalRot[0] +=
+ (additionalRot[0] - deltaAdditionalRot[0]) * ARCBALL_SENSITIVITY;
+ deltaAdditionalRot[1] +=
+ (additionalRot[1] - deltaAdditionalRot[1]) * ARCBALL_SENSITIVITY;
+ deltaAdditionalRot[2] +=
+ (additionalRot[2] - deltaAdditionalRot[2]) * ARCBALL_SENSITIVITY;
+
+ deltaAdditionalTrans[0] +=
+ (additionalTrans[0] - deltaAdditionalTrans[0]) * ARCBALL_SENSITIVITY;
+ deltaAdditionalTrans[1] +=
+ (additionalTrans[1] - deltaAdditionalTrans[1]) * ARCBALL_SENSITIVITY;
+
+ // create an additional rotation based on the key events
+ quat4.fromEuler(
+ deltaAdditionalRot[0],
+ deltaAdditionalRot[1],
+ deltaAdditionalRot[2], deltaRot);
+
+ // create an additional translation based on the key events
+ vec3.set([deltaAdditionalTrans[0], deltaAdditionalTrans[1], 0], deltaTrans);
+
+ // handle the reset animation steps if necessary
+ if (this._resetInProgress) {
+ this._nextResetStep(aDelta || 1);
+ }
+
+ // return the current rotation and translation
+ return {
+ rotation: quat4.multiply(deltaRot, currentRot),
+ translation: vec3.add(deltaTrans, currentTrans)
+ };
+ },
+
+ /**
+ * Function handling the mouseDown event.
+ * Call this when the mouse was pressed.
+ *
+ * @param {Number} x
+ * the current horizontal coordinate of the mouse
+ * @param {Number} y
+ * the current vertical coordinate of the mouse
+ * @param {Number} aButton
+ * which mouse button was pressed
+ */
+ mouseDown: function TVA_mouseDown(x, y, aButton)
+ {
+ // save the mouse down state and prepare for rotations or translations
+ this._mousePress[0] = x;
+ this._mousePress[1] = y;
+ this._mouseButton = aButton;
+ this._cancelReset();
+ this._save();
+
+ // find the sphere coordinates of the mouse positions
+ this._pointToSphere(
+ x, y, this.width, this.height, this.radius, this._startVec);
+
+ quat4.set(this._currentRot, this._lastRot);
+ },
+
+ /**
+ * Function handling the mouseUp event.
+ * Call this when a mouse button was released.
+ *
+ * @param {Number} x
+ * the current horizontal coordinate of the mouse
+ * @param {Number} y
+ * the current vertical coordinate of the mouse
+ */
+ mouseUp: function TVA_mouseUp(x, y)
+ {
+ // save the mouse up state and prepare for rotations or translations
+ this._mouseRelease[0] = x;
+ this._mouseRelease[1] = y;
+ this._mouseButton = -1;
+ },
+
+ /**
+ * Function handling the mouseMove event.
+ * Call this when the mouse was moved.
+ *
+ * @param {Number} x
+ * the current horizontal coordinate of the mouse
+ * @param {Number} y
+ * the current vertical coordinate of the mouse
+ */
+ mouseMove: function TVA_mouseMove(x, y)
+ {
+ // save the mouse move state and prepare for rotations or translations
+ // only if the mouse is pressed
+ if (this._mouseButton !== -1) {
+ this._mouseMove[0] = x;
+ this._mouseMove[1] = y;
+ }
+ },
+
+ /**
+ * Function handling the mouseOver event.
+ * Call this when the mouse enters the context bounds.
+ */
+ mouseOver: function TVA_mouseOver()
+ {
+ // if the mouse just entered the parent bounds, stop the animation
+ this._mouseButton = -1;
+ },
+
+ /**
+ * Function handling the mouseOut event.
+ * Call this when the mouse leaves the context bounds.
+ */
+ mouseOut: function TVA_mouseOut()
+ {
+ // if the mouse leaves the parent bounds, stop the animation
+ this._mouseButton = -1;
+ },
+
+ /**
+ * Function handling the arcball zoom amount.
+ * Call this, for example, when the mouse wheel was scrolled or zoom keys
+ * were pressed.
+ *
+ * @param {Number} aZoom
+ * the zoom direction and speed
+ */
+ zoom: function TVA_zoom(aZoom)
+ {
+ this._cancelReset();
+ this._zoomAmount = TiltMath.clamp(this._zoomAmount - aZoom,
+ ARCBALL_ZOOM_MIN, ARCBALL_ZOOM_MAX);
+ },
+
+ /**
+ * Function handling the keyDown event.
+ * Call this when a key was pressed.
+ *
+ * @param {Number} aCode
+ * the code corresponding to the key pressed
+ */
+ keyDown: function TVA_keyDown(aCode)
+ {
+ this._cancelReset();
+ this._keyCode[aCode] = true;
+ },
+
+ /**
+ * Function handling the keyUp event.
+ * Call this when a key was released.
+ *
+ * @param {Number} aCode
+ * the code corresponding to the key released
+ */
+ keyUp: function TVA_keyUp(aCode)
+ {
+ this._keyCode[aCode] = false;
+ },
+
+ /**
+ * Maps the 2d coordinates of the mouse location to a 3d point on a sphere.
+ *
+ * @param {Number} x
+ * the current horizontal coordinate of the mouse
+ * @param {Number} y
+ * the current vertical coordinate of the mouse
+ * @param {Number} aWidth
+ * the width of canvas
+ * @param {Number} aHeight
+ * the height of canvas
+ * @param {Number} aRadius
+ * optional, the radius of the arcball
+ * @param {Array} aSphereVec
+ * a 3d vector to store the sphere coordinates
+ */
+ _pointToSphere: function TVA__pointToSphere(
+ x, y, aWidth, aHeight, aRadius, aSphereVec)
+ {
+ // adjust point coords and scale down to range of [-1..1]
+ x = (x - aWidth * 0.5) / aRadius;
+ y = (y - aHeight * 0.5) / aRadius;
+
+ // compute the square length of the vector to the point from the center
+ let normal = 0;
+ let sqlength = x * x + y * y;
+
+ // if the point is mapped outside of the sphere
+ if (sqlength > 1) {
+ // calculate the normalization factor
+ normal = 1 / Math.sqrt(sqlength);
+
+ // set the normalized vector (a point on the sphere)
+ aSphereVec[0] = x * normal;
+ aSphereVec[1] = y * normal;
+ aSphereVec[2] = 0;
+ } else {
+ // set the vector to a point mapped inside the sphere
+ aSphereVec[0] = x;
+ aSphereVec[1] = y;
+ aSphereVec[2] = Math.sqrt(1 - sqlength);
+ }
+ },
+
+ /**
+ * Cancels all pending transformations caused by key events.
+ */
+ cancelKeyEvents: function TVA_cancelKeyEvents()
+ {
+ this._keyCode = {};
+ },
+
+ /**
+ * Cancels all pending transformations caused by mouse events.
+ */
+ cancelMouseEvents: function TVA_cancelMouseEvents()
+ {
+ this._rotating = false;
+ this._mouseButton = -1;
+ },
+
+ /**
+ * Incremental translation method.
+ *
+ * @param {Array} aTranslation
+ * the translation ammount on the [x, y] axis
+ */
+ translate: function TVP_translate(aTranslation)
+ {
+ this._additionalTrans[0] += aTranslation[0];
+ this._additionalTrans[1] += aTranslation[1];
+ },
+
+ /**
+ * Incremental rotation method.
+ *
+ * @param {Array} aRotation
+ * the rotation ammount along the [x, y, z] axis
+ */
+ rotate: function TVP_rotate(aRotation)
+ {
+ // explicitly rotate along y, x, z values because they're eulerian angles
+ this._additionalRot[0] += TiltMath.radians(aRotation[1]);
+ this._additionalRot[1] += TiltMath.radians(aRotation[0]);
+ this._additionalRot[2] += TiltMath.radians(aRotation[2]);
+ },
+
+ /**
+ * Moves a target point into view only if it's outside the currently visible
+ * area bounds (in which case it also resets any additional transforms).
+ *
+ * @param {Arary} aPoint
+ * the [x, y] point which should be brought into view
+ */
+ moveIntoView: function TVA_moveIntoView(aPoint) {
+ let visiblePointX = -(this._currentTrans[0] + this._additionalTrans[0]);
+ let visiblePointY = -(this._currentTrans[1] + this._additionalTrans[1]);
+
+ if (aPoint[1] - visiblePointY - MOVE_INTO_VIEW_ACCURACY > this.height ||
+ aPoint[1] - visiblePointY + MOVE_INTO_VIEW_ACCURACY < 0 ||
+ aPoint[0] - visiblePointX > this.width ||
+ aPoint[0] - visiblePointX < 0) {
+ this.reset([0, -aPoint[1]]);
+ }
+ },
+
+ /**
+ * Resize this implementation to use different bounds.
+ * This function is automatically called when the arcball is created.
+ *
+ * @param {Number} newWidth
+ * the new width of canvas
+ * @param {Number} newHeight
+ * the new height of canvas
+ * @param {Number} newRadius
+ * optional, the new radius of the arcball
+ */
+ resize: function TVA_resize(newWidth, newHeight, newRadius)
+ {
+ if (!newWidth || !newHeight) {
+ return;
+ }
+
+ // set the new width, height and radius dimensions
+ this.width = newWidth;
+ this.height = newHeight;
+ this.radius = newRadius ? newRadius : Math.min(newWidth, newHeight);
+ this._save();
+ },
+
+ /**
+ * Starts an animation resetting the arcball transformations to identity.
+ *
+ * @param {Array} aFinalTranslation
+ * optional, final vector translation
+ * @param {Array} aFinalRotation
+ * optional, final quaternion rotation
+ */
+ reset: function TVA_reset(aFinalTranslation, aFinalRotation)
+ {
+ if ("function" === typeof this._onResetStart) {
+ this._onResetStart();
+ this._onResetStart = null;
+ }
+
+ this.cancelMouseEvents();
+ this.cancelKeyEvents();
+ this._cancelReset();
+
+ this._save();
+ this._resetFinalTranslation = vec3.create(aFinalTranslation);
+ this._resetFinalRotation = quat4.create(aFinalRotation);
+ this._resetInProgress = true;
+ },
+
+ /**
+ * Cancels the current arcball reset animation if there is one.
+ */
+ _cancelReset: function TVA__cancelReset()
+ {
+ if (this._resetInProgress) {
+ this._resetInProgress = false;
+ this._save();
+
+ if ("function" === typeof this._onResetFinish) {
+ this._onResetFinish();
+ this._onResetFinish = null;
+ this._onResetStep = null;
+ }
+ }
+ },
+
+ /**
+ * Executes the next step in the arcball reset animation.
+ *
+ * @param {Number} aDelta
+ * the current animation frame delta
+ */
+ _nextResetStep: function TVA__nextResetStep(aDelta)
+ {
+ // a very large animation frame delta (in case of seriously low framerate)
+ // would cause all the interpolations to become highly unstable
+ aDelta = TiltMath.clamp(aDelta, 1, 100);
+
+ let fNearZero = EPSILON * EPSILON;
+ let fInterpLin = ARCBALL_RESET_LINEAR_FACTOR * aDelta;
+ let fInterpSph = ARCBALL_RESET_SPHERICAL_FACTOR;
+ let fTran = this._resetFinalTranslation;
+ let fRot = this._resetFinalRotation;
+
+ let t = vec3.create(fTran);
+ let r = quat4.multiply(quat4.inverse(quat4.create(this._currentRot)), fRot);
+
+ // reset the rotation quaternion and translation vector
+ vec3.lerp(this._currentTrans, t, fInterpLin);
+ quat4.slerp(this._currentRot, r, fInterpSph);
+
+ // also reset any additional transforms by the keyboard or mouse
+ vec3.scale(this._additionalTrans, fInterpLin);
+ vec3.scale(this._additionalRot, fInterpLin);
+ this._zoomAmount *= fInterpLin;
+
+ // clear the loop if the all values are very close to zero
+ if (vec3.length(vec3.subtract(this._lastRot, fRot, [])) < fNearZero &&
+ vec3.length(vec3.subtract(this._deltaRot, fRot, [])) < fNearZero &&
+ vec3.length(vec3.subtract(this._currentRot, fRot, [])) < fNearZero &&
+ vec3.length(vec3.subtract(this._lastTrans, fTran, [])) < fNearZero &&
+ vec3.length(vec3.subtract(this._deltaTrans, fTran, [])) < fNearZero &&
+ vec3.length(vec3.subtract(this._currentTrans, fTran, [])) < fNearZero &&
+ vec3.length(this._additionalRot) < fNearZero &&
+ vec3.length(this._additionalTrans) < fNearZero) {
+
+ this._cancelReset();
+ }
+
+ if ("function" === typeof this._onResetStep) {
+ this._onResetStep();
+ }
+ },
+
+ /**
+ * Loads the keys to control this arcball.
+ */
+ _loadKeys: function TVA__loadKeys()
+ {
+ this.rotateKeys = {
+ "up": Ci.nsIDOMKeyEvent["DOM_VK_W"],
+ "down": Ci.nsIDOMKeyEvent["DOM_VK_S"],
+ "left": Ci.nsIDOMKeyEvent["DOM_VK_A"],
+ "right": Ci.nsIDOMKeyEvent["DOM_VK_D"],
+ };
+ this.panKeys = {
+ "up": Ci.nsIDOMKeyEvent["DOM_VK_UP"],
+ "down": Ci.nsIDOMKeyEvent["DOM_VK_DOWN"],
+ "left": Ci.nsIDOMKeyEvent["DOM_VK_LEFT"],
+ "right": Ci.nsIDOMKeyEvent["DOM_VK_RIGHT"],
+ };
+ this.zoomKeys = {
+ "in": [
+ Ci.nsIDOMKeyEvent["DOM_VK_I"],
+ Ci.nsIDOMKeyEvent["DOM_VK_ADD"],
+ Ci.nsIDOMKeyEvent["DOM_VK_EQUALS"],
+ ],
+ "out": [
+ Ci.nsIDOMKeyEvent["DOM_VK_O"],
+ Ci.nsIDOMKeyEvent["DOM_VK_SUBTRACT"],
+ ],
+ "unzoom": Ci.nsIDOMKeyEvent["DOM_VK_0"]
+ };
+ this.resetKey = Ci.nsIDOMKeyEvent["DOM_VK_R"];
+ },
+
+ /**
+ * Saves the current arcball state, typically after resize or mouse events.
+ */
+ _save: function TVA__save()
+ {
+ if (this._mousePress) {
+ let x = this._mousePress[0];
+ let y = this._mousePress[1];
+
+ this._mouseMove[0] = x;
+ this._mouseMove[1] = y;
+ this._mouseRelease[0] = x;
+ this._mouseRelease[1] = y;
+ this._mouseLerp[0] = x;
+ this._mouseLerp[1] = y;
+ }
+ },
+
+ /**
+ * Function called when this object is destroyed.
+ */
+ _finalize: function TVA__finalize()
+ {
+ this._cancelReset();
+ }
+};
+
+/**
+ * Tilt configuration preferences.
+ */
+TiltVisualizer.Prefs = {
+
+ /**
+ * Specifies if Tilt is enabled or not.
+ */
+ get enabled()
+ {
+ return this._enabled;
+ },
+
+ set enabled(value)
+ {
+ TiltUtils.Preferences.set("enabled", "boolean", value);
+ this._enabled = value;
+ },
+
+ get introTransition()
+ {
+ return this._introTransition;
+ },
+
+ set introTransition(value)
+ {
+ TiltUtils.Preferences.set("intro_transition", "boolean", value);
+ this._introTransition = value;
+ },
+
+ get outroTransition()
+ {
+ return this._outroTransition;
+ },
+
+ set outroTransition(value)
+ {
+ TiltUtils.Preferences.set("outro_transition", "boolean", value);
+ this._outroTransition = value;
+ },
+
+ /**
+ * Loads the preferences.
+ */
+ load: function TVC_load()
+ {
+ let prefs = TiltVisualizer.Prefs;
+ let get = TiltUtils.Preferences.get;
+
+ prefs._enabled = get("enabled", "boolean");
+ prefs._introTransition = get("intro_transition", "boolean");
+ prefs._outroTransition = get("outro_transition", "boolean");
+ }
+};
+
+/**
+ * A custom visualization shader.
+ *
+ * @param {Attribute} vertexPosition: the vertex position
+ * @param {Attribute} vertexTexCoord: texture coordinates used by the sampler
+ * @param {Attribute} vertexColor: specific [r, g, b] color for each vertex
+ * @param {Uniform} mvMatrix: the model view matrix
+ * @param {Uniform} projMatrix: the projection matrix
+ * @param {Uniform} sampler: the texture sampler to fetch the pixels from
+ */
+TiltVisualizer.MeshShader = {
+
+ /**
+ * Vertex shader.
+ */
+ vs: [
+ "attribute vec3 vertexPosition;",
+ "attribute vec2 vertexTexCoord;",
+ "attribute vec3 vertexColor;",
+
+ "uniform mat4 mvMatrix;",
+ "uniform mat4 projMatrix;",
+
+ "varying vec2 texCoord;",
+ "varying vec3 color;",
+
+ "void main() {",
+ " gl_Position = projMatrix * mvMatrix * vec4(vertexPosition, 1.0);",
+ " texCoord = vertexTexCoord;",
+ " color = vertexColor;",
+ "}"
+ ].join("\n"),
+
+ /**
+ * Fragment shader.
+ */
+ fs: [
+ "#ifdef GL_ES",
+ "precision lowp float;",
+ "#endif",
+
+ "uniform sampler2D sampler;",
+
+ "varying vec2 texCoord;",
+ "varying vec3 color;",
+
+ "void main() {",
+ " if (texCoord.x < 0.0) {",
+ " gl_FragColor = vec4(color, 1.0);",
+ " } else {",
+ " gl_FragColor = vec4(texture2D(sampler, texCoord).rgb, 1.0);",
+ " }",
+ "}"
+ ].join("\n")
+};
diff --git a/browser/devtools/tilt/tilt.js b/browser/devtools/tilt/tilt.js
new file mode 100644
index 000000000..bd41f0432
--- /dev/null
+++ b/browser/devtools/tilt/tilt.js
@@ -0,0 +1,263 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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";
+
+const {Cu} = require("chrome");
+
+let {TiltVisualizer} = require("devtools/tilt/tilt-visualizer");
+let TiltGL = require("devtools/tilt/tilt-gl");
+let TiltUtils = require("devtools/tilt/tilt-utils");
+let EventEmitter = require("devtools/shared/event-emitter");
+let Telemetry = require("devtools/shared/telemetry");
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Tilt notifications dispatched through the nsIObserverService.
+const TILT_NOTIFICATIONS = {
+ // Called early in the startup of a new tilt instance
+ STARTUP: "tilt-startup",
+
+ // Fires when Tilt starts the initialization.
+ INITIALIZING: "tilt-initializing",
+
+ // Fires immediately after initialization is complete.
+ // (when the canvas overlay is visible and the 3D mesh is completely created)
+ INITIALIZED: "tilt-initialized",
+
+ // Fires immediately before the destruction is started.
+ DESTROYING: "tilt-destroying",
+
+ // Fires immediately before the destruction is finished.
+ // (just before the canvas overlay is removed from its parent node)
+ BEFORE_DESTROYED: "tilt-before-destroyed",
+
+ // Fires when Tilt is completely destroyed.
+ DESTROYED: "tilt-destroyed",
+
+ // Fires when Tilt is shown (after a tab-switch).
+ SHOWN: "tilt-shown",
+
+ // Fires when Tilt is hidden (after a tab-switch).
+ HIDDEN: "tilt-hidden",
+
+ // Fires once Tilt highlights an element in the page.
+ HIGHLIGHTING: "tilt-highlighting",
+
+ // Fires once Tilt stops highlighting any element.
+ UNHIGHLIGHTING: "tilt-unhighlighting",
+
+ // Fires when a node is removed from the 3D mesh.
+ NODE_REMOVED: "tilt-node-removed"
+};
+
+let TiltManager = {
+ _instances: new WeakMap(),
+ getTiltForBrowser: function(aChromeWindow)
+ {
+ if (this._instances.has(aChromeWindow)) {
+ return this._instances.get(aChromeWindow);
+ } else {
+ let tilt = new Tilt(aChromeWindow);
+ this._instances.set(aChromeWindow, tilt);
+ return tilt;
+ }
+ },
+}
+
+exports.TiltManager = TiltManager;
+
+/**
+ * Object managing instances of the visualizer.
+ *
+ * @param {Window} aWindow
+ * the chrome window used by each visualizer instance
+ */
+function Tilt(aWindow)
+{
+ /**
+ * Save a reference to the top-level window.
+ */
+ this.chromeWindow = aWindow;
+
+ /**
+ * All the instances of TiltVisualizer.
+ */
+ this.visualizers = {};
+
+ /**
+ * Shortcut for accessing notifications strings.
+ */
+ this.NOTIFICATIONS = TILT_NOTIFICATIONS;
+
+ EventEmitter.decorate(this);
+
+ this.setup();
+
+ this._telemetry = new Telemetry();
+}
+
+Tilt.prototype = {
+
+ /**
+ * Initializes a visualizer for the current tab or closes it if already open.
+ */
+ toggle: function T_toggle()
+ {
+ let contentWindow = this.chromeWindow.gBrowser.selectedBrowser.contentWindow;
+ let id = this.currentWindowId;
+ let self = this;
+
+ contentWindow.addEventListener("beforeunload", function onUnload() {
+ contentWindow.removeEventListener("beforeunload", onUnload, false);
+ self.destroy(id, true);
+ }, false);
+
+ // if the visualizer for the current tab is already open, destroy it now
+ if (this.visualizers[id]) {
+ this.destroy(id, true);
+ this._telemetry.toolClosed("tilt");
+ return;
+ } else {
+ this._telemetry.toolOpened("tilt");
+ }
+
+ // create a visualizer instance for the current tab
+ this.visualizers[id] = new TiltVisualizer({
+ chromeWindow: this.chromeWindow,
+ contentWindow: contentWindow,
+ parentNode: this.chromeWindow.gBrowser.selectedBrowser.parentNode,
+ notifications: this.NOTIFICATIONS,
+ tab: this.chromeWindow.gBrowser.selectedTab
+ });
+
+ Services.obs.notifyObservers(contentWindow, TILT_NOTIFICATIONS.STARTUP, null);
+ this.visualizers[id].init();
+
+ // make sure the visualizer object was initialized properly
+ if (!this.visualizers[id].isInitialized()) {
+ this.destroy(id);
+ this.failureCallback && this.failureCallback();
+ return;
+ }
+
+ this.lastInstanceId = id;
+ this.emit("change", this.chromeWindow.gBrowser.selectedTab);
+ Services.obs.notifyObservers(contentWindow, TILT_NOTIFICATIONS.INITIALIZING, null);
+ },
+
+ /**
+ * Starts destroying a specific instance of the visualizer.
+ *
+ * @param {String} aId
+ * the identifier of the instance in the visualizers array
+ * @param {Boolean} aAnimateFlag
+ * optional, set to true to display a destruction transition
+ */
+ destroy: function T_destroy(aId, aAnimateFlag)
+ {
+ // if the visualizer is destroyed or destroying, don't do anything
+ if (!this.visualizers[aId] || this._isDestroying) {
+ return;
+ }
+ this._isDestroying = true;
+
+ let controller = this.visualizers[aId].controller;
+ let presenter = this.visualizers[aId].presenter;
+
+ let content = presenter.contentWindow;
+ let pageXOffset = content.pageXOffset * presenter.transforms.zoom;
+ let pageYOffset = content.pageYOffset * presenter.transforms.zoom;
+ TiltUtils.setDocumentZoom(this.chromeWindow, presenter.transforms.zoom);
+
+ // if we're not doing any outro animation, just finish destruction directly
+ if (!aAnimateFlag) {
+ this._finish(aId);
+ return;
+ }
+
+ // otherwise, trigger the outro animation and notify necessary observers
+ Services.obs.notifyObservers(content, TILT_NOTIFICATIONS.DESTROYING, null);
+
+ controller.removeEventListeners();
+ controller.arcball.reset([-pageXOffset, -pageYOffset]);
+ presenter.executeDestruction(this._finish.bind(this, aId));
+ },
+
+ /**
+ * Finishes detroying a specific instance of the visualizer.
+ *
+ * @param {String} aId
+ * the identifier of the instance in the visualizers array
+ */
+ _finish: function T__finish(aId)
+ {
+ let contentWindow = this.visualizers[aId].presenter.contentWindow;
+ this.visualizers[aId].removeOverlay();
+ this.visualizers[aId].cleanup();
+ this.visualizers[aId] = null;
+
+ this._isDestroying = false;
+ this.chromeWindow.gBrowser.selectedBrowser.focus();
+ this.emit("change", this.chromeWindow.gBrowser.selectedTab);
+ Services.obs.notifyObservers(contentWindow, TILT_NOTIFICATIONS.DESTROYED, null);
+ },
+
+ /**
+ * Handles the event fired when a tab is selected.
+ */
+ _onTabSelect: function T__onTabSelect()
+ {
+ if (this.visualizers[this.lastInstanceId]) {
+ let contentWindow = this.visualizers[this.lastInstanceId].presenter.contentWindow;
+ Services.obs.notifyObservers(contentWindow, TILT_NOTIFICATIONS.HIDDEN, null);
+ }
+
+ if (this.currentInstance) {
+ let contentWindow = this.currentInstance.presenter.contentWindow;
+ Services.obs.notifyObservers(contentWindow, TILT_NOTIFICATIONS.SHOWN, null);
+ }
+
+ this.lastInstanceId = this.currentWindowId;
+ },
+
+ /**
+ * Add the browser event listeners to handle state changes.
+ */
+ setup: function T_setup()
+ {
+ // load the preferences from the devtools.tilt branch
+ TiltVisualizer.Prefs.load();
+
+ this.chromeWindow.gBrowser.tabContainer.addEventListener(
+ "TabSelect", this._onTabSelect.bind(this), false);
+ },
+
+ /**
+ * Returns true if this tool is enabled.
+ */
+ get enabled()
+ {
+ return (TiltVisualizer.Prefs.enabled &&
+ (TiltGL.isWebGLForceEnabled() || TiltGL.isWebGLSupported()));
+ },
+
+ /**
+ * Gets the ID of the current window object to identify the visualizer.
+ */
+ get currentWindowId()
+ {
+ return TiltUtils.getWindowId(
+ this.chromeWindow.gBrowser.selectedBrowser.contentWindow);
+ },
+
+ /**
+ * Gets the visualizer instance for the current tab.
+ */
+ get currentInstance()
+ {
+ return this.visualizers[this.currentWindowId];
+ },
+};