From ea415071b48d1c3060c6b455536e54e951aa90fe Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 31 Aug 2015 22:54:23 -0700 Subject: [PATCH] OmniTool first pass --- .../{toys/magBalls => libraries}/constants.js | 0 .../magBalls => libraries}/highlighter.js | 6 + examples/libraries/omniTool.js | 300 ++++++++++++++++++ .../libraries/omniTool/models/modelBase.js | 19 ++ examples/libraries/omniTool/models/wand.js | 120 +++++++ examples/libraries/omniTool/modules/test.js | 9 + .../{toys/magBalls => libraries}/utils.js | 8 +- examples/toys/magBalls/magBallsMain.js | 26 -- 8 files changed, 461 insertions(+), 27 deletions(-) rename examples/{toys/magBalls => libraries}/constants.js (100%) rename examples/{toys/magBalls => libraries}/highlighter.js (92%) create mode 100644 examples/libraries/omniTool.js create mode 100644 examples/libraries/omniTool/models/modelBase.js create mode 100644 examples/libraries/omniTool/models/wand.js create mode 100644 examples/libraries/omniTool/modules/test.js rename examples/{toys/magBalls => libraries}/utils.js (93%) delete mode 100644 examples/toys/magBalls/magBallsMain.js diff --git a/examples/toys/magBalls/constants.js b/examples/libraries/constants.js similarity index 100% rename from examples/toys/magBalls/constants.js rename to examples/libraries/constants.js diff --git a/examples/toys/magBalls/highlighter.js b/examples/libraries/highlighter.js similarity index 92% rename from examples/toys/magBalls/highlighter.js rename to examples/libraries/highlighter.js index 149d9ec5b7..b3550b6c8a 100644 --- a/examples/toys/magBalls/highlighter.js +++ b/examples/libraries/highlighter.js @@ -53,6 +53,12 @@ Highlighter.prototype.setSize = function(newSize) { }); } +Highlighter.prototype.setRotation = function(newRotation) { + Overlays.editOverlay(this.highlightCube, { + rotation: newRotation + }); +} + Highlighter.prototype.updateHighlight = function() { if (this.hightlighted) { var properties = Entities.getEntityProperties(this.hightlighted); diff --git a/examples/libraries/omniTool.js b/examples/libraries/omniTool.js new file mode 100644 index 0000000000..643b789a0e --- /dev/null +++ b/examples/libraries/omniTool.js @@ -0,0 +1,300 @@ +// +// Created by Bradley Austin Davis on 2015/09/01 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include("constants.js"); +Script.include("utils.js"); +Script.include("highlighter.js"); +Script.include("omniTool/models/modelBase.js"); +Script.include("omniTool/models/wand.js"); + +OmniToolModules = {}; +OmniToolModuleType = null; + +OmniTool = function(side) { + this.OMNI_KEY = "OmniTool"; + this.MAX_FRAMERATE = 30; + this.UPDATE_INTERVAL = 1.0 / this.MAX_FRAMERATE + this.SIDE = side; + this.PALM = 2 * side; + this.ACTION = findAction(side ? "ACTION2" : "ACTION1"); + this.ALT_ACTION = findAction(side ? "ACTION1" : "ACTION2"); + + this.highlighter = new Highlighter(); + this.ignoreEntities = {}; + this.nearestOmniEntity = { + id: null, + inside: false, + position: null, + distance: Infinity, + radius: 0, + omniProperties: {}, + boundingBox: null, + }; + + this.activeOmniEntityId = null; + this.lastUpdateInterval = 0; + this.tipLength = 0.4; + this.active = false; + this.module = null; + this.moduleEntityId = null; + this.lastScanPosition = ZERO_VECTOR; + this.model = new Wand(); + this.model.setLength(this.tipLength); + + // Connect to desired events + var _this = this; + Controller.actionEvent.connect(function(action, state) { + _this.onActionEvent(action, state); + }); + + Script.update.connect(function(deltaTime) { + _this.lastUpdateInterval += deltaTime; + if (_this.lastUpdateInterval >= _this.UPDATE_INTERVAL) { + _this.onUpdate(_this.lastUpdateInterval); + _this.lastUpdateInterval = 0; + } + }); + + Script.scriptEnding.connect(function() { + _this.onCleanup(); + }); +} + +OmniTool.prototype.onActionEvent = function(action, state) { + // FIXME figure out the issues when only one spatial controller is active + // logDebug("Action: " + action + " " + state); + + if (this.module && this.module.onActionEvent) { + this.module.onActionEvent(action, state); + } + + if (action == this.ACTION) { + if (state) { + this.onClick(); + } else { + this.onRelease(); + } + } + + // FIXME Does not work + //// with only one controller active (listed as 2 here because 'tip' + 'palm') + //// then treat the alt action button as the action button +} + +OmniTool.prototype.getOmniToolData = function(entityId) { + return getEntityCustomData(this.OMNI_KEY, entityId, null); +} + +OmniTool.prototype.setOmniToolData = function(entityId, data) { + setEntityCustomData(this.OMNI_KEY, entityId, data); +} + +OmniTool.prototype.updateOmniToolData = function(entityId, data) { + var currentData = this.getOmniToolData(entityId) || {}; + for (var key in data) { + currentData[key] = data[key]; + } + setEntityCustomData(this.OMNI_KEY, entityId, currentData); +} + +OmniTool.prototype.setActive = function(active) { + if (active === this.active) { + return; + } + logDebug("omnitool changing active state: " + active); + this.active = active; + this.model.setVisible(this.active); + + if (this.module && this.module.onActiveChanged) { + this.module.onActiveChanged(this.side); + } +} + + +OmniTool.prototype.onUpdate = function(deltaTime) { + // FIXME this returns data if either the left or right controller is not on the base + this.position = Controller.getSpatialControlPosition(this.PALM); + // When on the base, hydras report a position of 0 + this.setActive(Vec3.length(this.position) > 0.001); + + var rawRotation = Controller.getSpatialControlRawRotation(this.PALM); + this.rotation = Quat.multiply(MyAvatar.orientation, rawRotation); + + this.model.setTransform({ + rotation: this.rotation, + position: this.position, + }); + + this.scan(); + + if (this.module && this.module.onUpdate) { + this.module.onUpdate(deltaTime); + } +} + +OmniTool.prototype.onClick = function() { + // First check to see if the user is switching to a new omni module + if (this.nearestOmniEntity.inside && this.nearestOmniEntity.omniProperties.script) { + this.activateNewOmniModule(); + return; + } + + // Next check if there is an active module and if so propagate the click + // FIXME how to I switch to a new module? + if (this.module && this.module.onClick) { + this.module.onClick(); + return; + } +} + +OmniTool.prototype.onRelease = function() { + // FIXME how to I switch to a new module? + if (this.module && this.module.onRelease) { + this.module.onRelease(); + return; + } + logDebug("Base omnitool does nothing on release"); +} + +// FIXME resturn a structure of all nearby entities to distances +OmniTool.prototype.findNearestOmniEntity = function(maxDistance, selector) { + if (!maxDistance) { + maxDistance = 2.0; + } + var resultDistance = Infinity; + var resultId = null; + var resultProperties = null; + var resultOmniData = null; + var ids = Entities.findEntities(this.model.tipPosition, maxDistance); + for (var i in ids) { + var entityId = ids[i]; + if (this.ignoreEntities[entityId]) { + continue; + } + var omniData = this.getOmniToolData(entityId); + if (!omniData) { + // FIXME find a place to flush this information + this.ignoreEntities[entityId] = true; + continue; + } + + // Let searchers query specifically + if (selector && !selector(entityId, omniData)) { + continue; + } + + var properties = Entities.getEntityProperties(entityId); + var distance = Vec3.distance(this.model.tipPosition, properties.position); + if (distance < resultDistance) { + resultDistance = distance; + resultId = entityId; + } + } + + return resultId; +} + +OmniTool.prototype.onEnterNearestOmniEntity = function() { + this.nearestOmniEntity.inside = true; + this.highlighter.highlight(this.nearestOmniEntity.id); + logDebug("On enter omniEntity " + this.nearestOmniEntity.id); +} + +OmniTool.prototype.onLeaveNearestOmniEntity = function() { + this.nearestOmniEntity.inside = false; + this.highlighter.highlight(null); + logDebug("On leave omniEntity " + this.nearestOmniEntity.id); +} + +OmniTool.prototype.setNearestOmniEntity = function(entityId) { + if (entityId && entityId !== this.nearestOmniEntity.id) { + if (this.nearestOmniEntity.id && this.nearestOmniEntity.inside) { + this.onLeaveNearestOmniEntity(); + } + this.nearestOmniEntity.id = entityId; + this.nearestOmniEntity.omniProperties = this.getOmniToolData(entityId); + var properties = Entities.getEntityProperties(entityId); + this.nearestOmniEntity.position = properties.position; + // FIXME use a real bounding box, not a sphere + var bbox = properties.boundingBox; + this.nearestOmniEntity.radius = Vec3.length(Vec3.subtract(bbox.center, bbox.brn)); + this.highlighter.setRotation(properties.rotation); + this.highlighter.setSize(Vec3.multiply(1.05, bbox.dimensions)); + } +} + +OmniTool.prototype.scan = function() { + var scanDistance = Vec3.distance(this.model.tipPosition, this.lastScanPosition); + + if (scanDistance < 0.005) { + return; + } + + this.lastScanPosition = this.model.tipPosition; + + this.setNearestOmniEntity(this.findNearestOmniEntity()); + if (this.nearestOmniEntity.id) { + var distance = Vec3.distance(this.model.tipPosition, this.nearestOmniEntity.position); + // track distance on a half centimeter basis + if (Math.abs(this.nearestOmniEntity.distance - distance) > 0.005) { + this.nearestOmniEntity.distance = distance; + if (!this.nearestOmniEntity.inside && distance < this.nearestOmniEntity.radius) { + this.onEnterNearestOmniEntity(); + } + + if (this.nearestOmniEntity.inside && distance > this.nearestOmniEntity.radius + 0.01) { + this.onLeaveNearestOmniEntity(); + } + } + } +} + +OmniTool.prototype.unloadModule = function() { + if (this.module && this.module.onUnload) { + this.module.onUnload(); + } + this.module = null; + this.moduleEntityId = null; +} + +OmniTool.prototype.activateNewOmniModule = function() { + // Support the ability for scripts to just run without replacing the current module + var script = this.nearestOmniEntity.omniProperties.script; + if (script.indexOf("/") < 0) { + script = "omniTool/modules/" + script; + } + + // Reset the tool type + OmniToolModuleType = null; + logDebug("Including script path: " + script); + try { + Script.include(script); + } catch(err) { + logWarn("Failed to include script: " + script + "\n" + err); + return; + } + + // If we're building a new module, unload the old one + if (OmniToolModuleType) { + logDebug("New OmniToolModule: " + OmniToolModuleType); + this.unloadModule(); + + try { + this.module = new OmniToolModules[OmniToolModuleType](this, this.nearestOmniEntity.id); + this.moduleEntityId = this.nearestOmniEntity.id; + if (this.module.onLoad) { + this.module.onLoad(); + } + } catch(err) { + logWarn("Failed to instantiate new module: " + err); + } + } +} + +// FIXME find a good way to sync the two omni tools +OMNI_TOOLS = [ new OmniTool(0), new OmniTool(1) ]; diff --git a/examples/libraries/omniTool/models/modelBase.js b/examples/libraries/omniTool/models/modelBase.js new file mode 100644 index 0000000000..7697856d3f --- /dev/null +++ b/examples/libraries/omniTool/models/modelBase.js @@ -0,0 +1,19 @@ + +ModelBase = function() { + this.length = 0.2; +} + +ModelBase.prototype.setVisible = function(visible) { + this.visible = visible; +} + +ModelBase.prototype.setLength = function(length) { + this.length = length; +} + +ModelBase.prototype.setTransform = function(transform) { + this.rotation = transform.rotation; + this.position = transform.position; + this.tipVector = Vec3.multiplyQbyV(this.rotation, { x: 0, y: this.length, z: 0 }); + this.tipPosition = Vec3.sum(this.position, this.tipVector); +} diff --git a/examples/libraries/omniTool/models/wand.js b/examples/libraries/omniTool/models/wand.js new file mode 100644 index 0000000000..8f0fe92b53 --- /dev/null +++ b/examples/libraries/omniTool/models/wand.js @@ -0,0 +1,120 @@ + +Wand = function() { + // Max updates fps + this.MAX_FRAMERATE = 30 + this.UPDATE_INTERVAL = 1.0 / this.MAX_FRAMERATE + this.DEFAULT_TIP_COLORS = [ { + red: 128, + green: 128, + blue: 128, + }, { + red: 64, + green: 64, + blue: 64, + }]; + this.POINTER_ROTATION = Quat.fromPitchYawRollDegrees(45, 0, 45); + + // FIXME does this need to be a member of this? + this.lastUpdateInterval = 0; + + this.pointers = [ + Overlays.addOverlay("cube", { + position: ZERO_VECTOR, + color: this.DEFAULT_TIP_COLORS[0], + alpha: 1.0, + solid: true, + visible: false, + }), + Overlays.addOverlay("cube", { + position: ZERO_VECTOR, + color: this.DEFAULT_TIP_COLORS[1], + alpha: 1.0, + solid: true, + visible: false, + }) + ]; + + this.wand = Overlays.addOverlay("cube", { + position: ZERO_VECTOR, + color: COLORS.WHITE, + dimensions: { x: 0.01, y: 0.01, z: 0.2 }, + alpha: 1.0, + solid: true, + visible: false + }); + + var _this = this; + Script.scriptEnding.connect(function() { + Overlays.deleteOverlay(_this.pointers[0]); + Overlays.deleteOverlay(_this.pointers[1]); + Overlays.deleteOverlay(_this.wand); + }); + + Script.update.connect(function(deltaTime) { + _this.lastUpdateInterval += deltaTime; + if (_this.lastUpdateInterval >= _this.UPDATE_INTERVAL) { + _this.onUpdate(_this.lastUpdateInterval); + _this.lastUpdateInterval = 0; + } + }); +} + +Wand.prototype = Object.create( ModelBase.prototype ); + +Wand.prototype.setVisible = function(visible) { + ModelBase.prototype.setVisible.call(this, visible); + Overlays.editOverlay(this.pointers[0], { + visible: this.visible + }); + Overlays.editOverlay(this.pointers[1], { + visible: this.visible + }); + Overlays.editOverlay(this.wand, { + visible: this.visible + }); +} + +Wand.prototype.setTransform = function(transform) { + ModelBase.prototype.setTransform.call(this, transform); + + var wandPosition = Vec3.sum(this.position, Vec3.multiply(0.5, this.tipVector)); + Overlays.editOverlay(this.pointers[0], { + position: this.tipPosition, + rotation: this.rotation, + visible: true, + }); + Overlays.editOverlay(this.pointers[1], { + position: this.tipPosition, + rotation: Quat.multiply(this.POINTER_ROTATION, this.rotation), + visible: true, + }); + Overlays.editOverlay(this.wand, { + dimensions: { x: 0.01, y: this.length * 0.9, z: 0.01 }, + position: wandPosition, + rotation: this.rotation, + visible: true, + }); +} + +Wand.prototype.setTipColors = function(color1, color2) { + Overlays.editOverlay(this.pointers[0], { + color: color1 || this.DEFAULT_TIP_COLORS[0], + }); + Overlays.editOverlay(this.pointers[1], { + color: color2 || this.DEFAULT_TIP_COLORS[1], + }); +} + +Wand.prototype.onUpdate = function(deltaTime) { + if (this.visible) { + var time = new Date().getTime() / 250; + var scale1 = Math.abs(Math.sin(time)); + var scale2 = Math.abs(Math.cos(time)); + Overlays.editOverlay(this.pointers[0], { + scale: scale1 * 0.01, + }); + Overlays.editOverlay(this.pointers[1], { + scale: scale2 * 0.01, + }); + } +} diff --git a/examples/libraries/omniTool/modules/test.js b/examples/libraries/omniTool/modules/test.js new file mode 100644 index 0000000000..1ca806affa --- /dev/null +++ b/examples/libraries/omniTool/modules/test.js @@ -0,0 +1,9 @@ + +OmniToolModules.Test = function() { +} + +OmniToolModules.Test.prototype.onClick = function() { + logDebug("Test module onClick"); +} + +OmniToolModuleType = "Test" \ No newline at end of file diff --git a/examples/toys/magBalls/utils.js b/examples/libraries/utils.js similarity index 93% rename from examples/toys/magBalls/utils.js rename to examples/libraries/utils.js index ea1446f858..b15ea6c313 100644 --- a/examples/toys/magBalls/utils.js +++ b/examples/libraries/utils.js @@ -53,11 +53,17 @@ getEntityUserData = function(id) { var results = null; var properties = Entities.getEntityProperties(id); if (properties.userData) { - results = JSON.parse(properties.userData); + try { + results = JSON.parse(properties.userData); + } catch(err) { + logDebug(err); + logDebug(properties.userData); + } } return results ? results : {}; } + // Non-destructively modify the user data of an entity. setEntityCustomData = function(customKey, id, data) { var userData = getEntityUserData(id); diff --git a/examples/toys/magBalls/magBallsMain.js b/examples/toys/magBalls/magBallsMain.js deleted file mode 100644 index 4eb98fab26..0000000000 --- a/examples/toys/magBalls/magBallsMain.js +++ /dev/null @@ -1,26 +0,0 @@ -// -// Created by Bradley Austin Davis on 2015/08/25 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -Script.include("../../libraries/htmlColors.js"); -Script.include("constants.js"); -Script.include("utils.js"); -Script.include("magBalls.js"); - -Script.include("ballController.js"); - -var magBalls = new MagBalls(); - -// Clear any previous balls -// magBalls.clear(); - -MenuController = function(side) { - HandController.call(this, side); -} - -// FIXME resolve some of the issues with dual controllers before allowing both controllers active -var handControllers = [new BallController(LEFT_CONTROLLER, magBalls)]; //, new HandController(RIGHT) ];