define(["lib/jquery", "lib/underscore", "common/network/dispatcher", "common/util/cap_utils", "common/util/log", "common/util/check", "ARCore/embed"], function($, _, Dispatcher, CapUtils, Log, check, Embed) { "use strict"; var logger = Log("Coordinate"); var HD = 0; var DEPTH = 1; var PROJECTOR = 2; var WORLD = 3; var WINDOW = 4; var systemStrings = {}; systemStrings[HD] = "hd"; systemStrings[DEPTH] = "depth"; systemStrings[PROJECTOR] = "projector"; systemStrings[WORLD] = "world"; systemStrings[WINDOW] = "window"; var validSystem = check.Match.Where(function(system) { if(systemStrings[system] === undefined) { throw new check.Match.Error(system + " is not a valid system."); } return true; }); var getWindowOffset = function(callback) { if(Embed.parentDispatcher) { Embed.parentDispatcher.callRPC("lens:getWindowOffset", {}, function(r) { callback(r.top, r.left); }); } else { // we're top-level callback(0, 0); } }; Embed._addPostInitCallback(function(embed) { embed.dispatcher.registerAsyncRPC("lens:getWindowOffset", function(data, callback) { getWindowOffset(function(top, left) { var offset = $(embed.iframe).offset(); callback({ top: top + offset.top, left: left + offset.left }); }); }); }); var projectorToWindow = function(coord, callback) { getWindowOffset(function(top, left) { callback({ x: coord.x - left, y: coord.y - top, z: coord.z }); }); }; var windowToProjector = function(coord, callback) { getWindowOffset(function(top, left) { callback({ x: coord.x + left, y: coord.y + top, z: coord.z }); }); }; /** * Creates a new Coordinate * * @param {System} system The coordinate system, such as {@link Lens.Coordinate.PROJECTOR} * @param {Number} x X coordinate * @param {Number} y Y coordinate * @param {Number} [z] Z coordinate * * * @class Immutable class representing a coordinate in a particular frame of * reference. * * @property {System} system The coordinate system, such as {@link Lens.Coordinate.PROJECTOR}. * @property {Number} x X coordinate. * @property {Number} y Y coordinate. * @property {Number} z Z coordinate, or `undefined`. * * @memberOf Lens */ var Coordinate = function(system, x, y, z) { /** @alias Lens.Coordinate.prototype */ var self = {}; check(system, validSystem); check(x, Number); check(y, Number); check(z, check.Match.Optional(Number)); self.x = x; self.y = y; self.z = z; self.system = system; /** * Transforms this Coordinate to a Coordinate in a different system. * * @param {String} system The system to transform to, such as * {@link Lens.Coordinate.WORLD} * @param {Function} [callback] A function to call with the transformed * coordinate. * * * @return {Promise} A jQuery promise. You can attach callbacks using * this promise or use jQuery's tools to do * parallel transformations. If you passed a callback, * that callback will automatically be registered as * a completion handler for this promise. */ self.transform = function(system, callback) { check(system, validSystem); check(callback, check.Match.Optional(Function)); // asynchronously make sure TransformsModule is enabled, then call // the coord_transform RPC, then resolve the deferred with the // new coordinate. var promise = $.Deferred(function(deferred) { var submitTransformRequest = function(coord, from, to, callback) { if(from === to) { callback(coord); return; } // construct parameters to be sent to Bridge var params = { from: systemStrings[from], to: systemStrings[to], x: x, y: y }; if (z !== undefined) { params.z = z; } CapUtils.ensure("TransformsModule", logger, function(){ Dispatcher._bridge.callRPC("coord_transform", params, function(result) { callback(result); }); }); }; var returnResult = function(coord) { if(system === WINDOW) { // convert projector -> window projectorToWindow(coord, function(windowCoord) { deferred.resolve(Coordinate(system, windowCoord.x, windowCoord.y, windowCoord.z)); }); } else { deferred.resolve(Coordinate(system, coord.x, coord.y, coord.z)); } }; // if we want to convert to window, we first convert to // projector and then go projector -> window in // returnResult() var to = system; if(to === WINDOW) { to = PROJECTOR; } var from = self.system; if(from === WINDOW) { from = PROJECTOR; // convert to projector coordinates windowToProjector(self, function(projectorCoord) { submitTransformRequest(projectorCoord, from, to, returnResult); }); } else { submitTransformRequest(self, from, to, returnResult); } }).promise(); if(callback) { promise.done(callback); } return promise; }; Object.freeze(self); return self; }; _.extend(Coordinate, /** @lends Lens.Coordinate */ { /** * Returns the coordinates for the bounding box of an element. Does * not take into account CSS transforms. Throws an error if given * a CSS selector that does not match any elements, or a jQuery object * with no elements. * * @param {Element | String | jQuery} el * A DOM element, CSS selector, or jQuery object. This element must * be rendered to the screen; it can't have been hidden with * `display: none` in CSS or with jQuery's `.hide()`. * * @return {Array} A 2-element array whose first element is the top left * corner of the element and whose second element is the * bottom right corner. */ forElement: function(el) { check(el, check.Match.Elementish); var o, offsets, leftX, topY, rightX, bottomY; o = $(el); offsets = o.offset(); if(!offsets) { throw "Could not calculate position of element: " + el; } leftX = offsets.left; topY = offsets.top; rightX = leftX + o.width(); bottomY = topY + o.height(); return [Coordinate(WINDOW, leftX, topY), Coordinate(WINDOW, rightX, bottomY)]; }, /** * Returns the offset between window coordinates and projector * coordinates. * * @param {Function} callback * Callback to invoke with the offset. Gets the top offset as the * first argument and the left offset as the second argument. * * @memberof Lens.Coordinate */ getWindowOffset: getWindowOffset, /** * Coordinate frame of the HD camera. Units are pixels. * @memberof Lens.Coordinate */ HD: HD, /** * Coordinate frame of the depth camera. Units are pixels. * @memberof Lens.Coordinate */ DEPTH: DEPTH, /** * Coordinate frame of the projector/page. Units are pixels. * @memberof Lens.Coordinate */ PROJECTOR: PROJECTOR, /** * Coordinate frame of the DOM window. For top-level apps, this is the * same as the projector coordinate frame. For embedded Lens apps, this * can be used to translate coordinates in the frame of the parent app * (projector coordinates) to the frame of the child app (window * coordinates). * @memberof Lens.Coordinate */ WINDOW: WINDOW, /** * Coordinate frame of the depth camera. Units are millimeters. * @memberof Lens.Coordinate */ WORLD: WORLD }); // in dev mode, all coordinates are in the same frame of reference // (because the camera is just a screenshot) Lens._onDev(function() { Dispatcher._bridge.registerRPC("coord_transform", function(data) { return { x: data.x, y: data.y }; }); }); Lens._addMember(Coordinate, "Coordinate"); return Coordinate; });