diff options
author | wolfbeast <mcwerewolf@gmail.com> | 2014-05-21 11:38:25 +0200 |
---|---|---|
committer | wolfbeast <mcwerewolf@gmail.com> | 2014-05-21 11:38:25 +0200 |
commit | d25ba7d760b017b038e5aa6c0a605b4a330eb68d (patch) | |
tree | 16ec27edc7d5f83986f16236d3a36a2682a0f37e /browser/devtools/tilt | |
parent | a942906574671868daf122284a9c4689e6924f74 (diff) | |
download | palemoon-gre-d25ba7d760b017b038e5aa6c0a605b4a330eb68d.tar.gz |
Recommit working copy to repo with proper line endings.
Diffstat (limited to 'browser/devtools/tilt')
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]; + }, +}; |