"use strict"; // starDomains.js // // A teleportable domain constellation // Each star represents a domain placename. The star size will grow up to three times the size for crowded places. // Stars that point to the same domain will be clustered together. Can you see your constellation up there? // // Created by Thijs Wenker on 04/22/2018. // Copyright 2018 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 // (function() { var md5 = Script.require('./node_modules/blueimp-md5/js/md5.min.js'); var EDIT_SETTING = "io.highfidelity.isEditing"; // replace placeNames with their color values var DEBUG_COLOR = false; var HALF = 0.5; var HEX_NUMBER_BASE = 16; var HASH_FULL_LENGTH = 32; var HASH_HALF_LENGTH = HASH_FULL_LENGTH * HALF; var UUID_STRIP_REGEX = /[-{}]/g; var USER_STORIES_API_URL = Account.metaverseServerURL + '/api/v1/user_stories'; var COLOR_HEX_LENGTH = 10; var RED_START = 1; var RED_END = RED_START + COLOR_HEX_LENGTH; var GREEN_START = RED_END; var GREEN_END = GREEN_START + COLOR_HEX_LENGTH; var BLUE_START = GREEN_END; var BLUE_END = BLUE_START + COLOR_HEX_LENGTH; var SECONDS_PER_MILLISECOND = 1000; var UPDATE_TIMEOUT = 30 * SECONDS_PER_MILLISECOND; // update every 30 seconds var MAXIMUM_STARS = 100; var PROTOCOL = { RC65: "jWMeM1PU6wCJjCiLLERaWQ%3D%3D", RC66: "Fah%2FlDA1xHOxUYlVAWsiFQ%3D%3D", CURRENT: encodeURIComponent(Window.protocolSignature()) }; var DOMAIN_RESTRICTIONS = 'open,hifi'; var ENCODED_PROTOCOL = PROTOCOL.CURRENT; var STAR_SPRITE_PATH = Script.resolvePath('Star-sprite-sm.png'); var PROJECTION_MODE = { STELLAR: 1, PLANAR: 2 }; var SKY_ANGLE = 140; var STAR_DISTANCE = 2000; var STAR_SIZE = 0.5; var MAX_CONCURRENCY_GROWTH = 10; // stop growing at 10 users var MAX_ADDED_STAR_SIZE = STAR_SIZE * 2; var MAX_CLUSTER_WIDTH = 1; var MIN_COLOR_VALUE = 144; var MAX_COLOR_VALUE = 255; var SKIP_OWN_DOMAIN = true; var SELECTED_PROJECTION_MODE = PROJECTION_MODE.PLANAR; var triggerMapping = null; var request = Script.require('./modules/request.js').request; var controllerUtils = Script.require('./modules/controllerUtils.js'); function percentageToColor(percentage) { return ((MAX_COLOR_VALUE - MIN_COLOR_VALUE) * percentage) + MIN_COLOR_VALUE; } function stripUuid(uuid) { if (Uuid.fromString(uuid) === null) { return null; } return uuid.replace(UUID_STRIP_REGEX, ''); } function getHashPartToPercentage(hash, start, end) { var maxDigits = end - start; var hexValue = hash.substr(start, maxDigits); var value = parseInt(hexValue, HEX_NUMBER_BASE); var maxHexValue = Array(maxDigits + 1).join('f'); var maxValue = parseInt(maxHexValue, HEX_NUMBER_BASE); return value / maxValue; } function convertHashToUV(hash) { return { u: getHashPartToPercentage(hash, 0, HASH_HALF_LENGTH), v: getHashPartToPercentage(hash, HASH_HALF_LENGTH, HASH_FULL_LENGTH) }; } function convertHashToColor(hash) { return { red: percentageToColor(getHashPartToPercentage(hash, RED_START, RED_END)), green: percentageToColor(getHashPartToPercentage(hash, GREEN_START, GREEN_END)), blue: percentageToColor(getHashPartToPercentage(hash, BLUE_START, BLUE_END)) }; } function getStarLocationPosition(starOverlayManager, domainIDHash, placeNameHash) { var uvPosition = convertHashToUV(domainIDHash); var clusterUVPosition = convertHashToUV(placeNameHash); if (SELECTED_PROJECTION_MODE === PROJECTION_MODE.STELLAR) { var starPitchAngle = (uvPosition.u - HALF) * SKY_ANGLE; var starRollAngle = (uvPosition.v - HALF) * SKY_ANGLE; var localStarDirection = Quat.fromPitchYawRollDegrees(starPitchAngle, 0, starRollAngle); return Vec3.multiplyQbyV(localStarDirection, {x: 0, y: STAR_DISTANCE, z: 0}); } if (SELECTED_PROJECTION_MODE === PROJECTION_MODE.PLANAR) { var dimensions = starOverlayManager.getProperties.call(starOverlayManager, ['dimensions']).dimensions; return { x: ((dimensions.x - MAX_CLUSTER_WIDTH) * (uvPosition.u - HALF)) + ((clusterUVPosition.u - HALF) * MAX_CLUSTER_WIDTH), y: HALF * dimensions.y, z: (dimensions.z - MAX_CLUSTER_WIDTH) * (uvPosition.v - HALF) + ((clusterUVPosition.v - HALF) * MAX_CLUSTER_WIDTH) }; } return Vec3.ZERO; } var getStarHash = function(domainID, placeName) { return md5(domainID + placeName); }; var StarOverlay = (function() { var _getScale = function(userConcurrency) { var calculatedUsers = Math.min(userConcurrency, MAX_CONCURRENCY_GROWTH); var addedUserSize = (calculatedUsers / MAX_CONCURRENCY_GROWTH) * MAX_ADDED_STAR_SIZE; return STAR_SIZE + addedUserSize; }; function StarOverlay(starOverlayManager, userStory) { var strippedUuid = stripUuid(userStory.domain_id); if (strippedUuid === null) { throw "Failed to strip domain_id for StarOverlay creation"; } var domainIDHash = md5(strippedUuid); var placeNameHash = md5(userStory.place_name); this._starOverlayManager = starOverlayManager; this.userStory = userStory; this.localPosition = getStarLocationPosition(starOverlayManager, domainIDHash, placeNameHash); var parentProperties = starOverlayManager.getProperties.call(starOverlayManager, ['position', 'rotation']); this.position = Vec3.sum(parentProperties.position, Vec3.multiplyQbyV(parentProperties.rotation, this.localPosition)); this.id = Overlays.addOverlay("image3d", { url: STAR_SPRITE_PATH, position: this.position, size: 1, scale: _getScale(userStory.details.concurrency), color: convertHashToColor(placeNameHash), alpha: 1, solid: true, isFacingAvatar: true, drawInFront: false, emissive: true }); // Let the star manager know that this star has been updated successfully to prevent removal. this._hasBeenUpdated = true; } StarOverlay.prototype = { _starOverlayManager: null, _hasBeenUpdated: null, id: null, userStory: null, localPosition: null, hasBeenUpdated: function() { return this._hasBeenUpdated; }, prepareUpdate: function() { this._hasBeenUpdated = false; }, update: function(userStory) { this.userStory = userStory; Overlays.editOverlay(this.id, { scale: _getScale(userStory.details.concurrency) }); this._hasBeenUpdated = true; }, cleanUp: function() { Overlays.deleteOverlay(this.id); } }; return StarOverlay; })(); var StarOverlayManager = (function() { function StarOverlayManager(entityID) { this._parentEntityID = entityID; this._starOverlays = {}; this._starOverlayIDsToStarHash = {}; this._parentPropertiesCache = {}; } StarOverlayManager.prototype = { _parentEntityID: null, _starOverlays: null, _starOverlayIDsToStarHash: null, _isInTeleportMode: false, _placeNameOverlay: null, _teleportButtonOverlay: null, _selectedLocation: null, _parentPropertiesCache: null, getParentID: function() { return this._parentEntityID; }, getProperties: function(properties) { var propertiesToFetch = []; properties.forEach(function(property) { if (!(property in this._parentPropertiesCache)) { propertiesToFetch.push(property); } }, this); if (propertiesToFetch.length > 0) { var fetchedProperties = Entities.getEntityProperties(this._parentEntityID, propertiesToFetch); propertiesToFetch.forEach(function(property) { if (!(property in fetchedProperties)) { console.error('Property ' + property + ' could not be retrieved from starDomains parent entity.'); return; } this._parentPropertiesCache[property] = fetchedProperties[property]; }, this); } var returnedProperties = {}; properties.forEach(function(property) { if (!(property in this._parentPropertiesCache)) { console.error('Property ' + property + ' could not be retrieved from starDomains properties cache.'); return; } returnedProperties[property] = this._parentPropertiesCache[property]; }, this); return returnedProperties; }, cancelTeleportMode: function() { if (this._isInTeleportMode) { Overlays.deleteOverlay(this._placeNameOverlay); Overlays.deleteOverlay(this._teleportButtonOverlay); this._isInTeleportMode = false; } }, setupTeleportMode: function(starHash) { if (this._isInTeleportMode) { this.cancelTeleportMode(); } var starOverlay = this._starOverlays[starHash]; var userStory = starOverlay.userStory; this._selectedLocation = 'hifi://' + userStory.place_name + userStory.path; var debugColor = Overlays.getProperty(starOverlay.id, 'color'); var text = userStory.place_name; if (DEBUG_COLOR) { text = [debugColor.red, debugColor.green, debugColor.blue].map(function(colorValue) { return Math.floor(colorValue); }).join(','); } this._placeNameOverlay = Overlays.addOverlay("text3d", { text: text, dimensions: { x: 4, y: 1 }, parentID: starOverlay.id, localPosition: {x: 0, y: 0.5, z: 0}, color: { red: 255, green: 255, blue: 255 }, alpha: 0.9, lineHeight: 1, backgroundAlpha: 0, ignoreRayIntersection: true, isFacingAvatar: true, drawInFront: true }); this._teleportButtonOverlay = Overlays.addOverlay("text3d", { text: "GO THERE", dimensions: { x: 2.8, y: 0.7 }, parentID: starOverlay.id, localPosition: {x: 0, y: -0.5, z: 0}, color: { red: 0, green: 180, blue: 239 }, alpha: 1, lineHeight: 0.5, backgroundAlpha: 0.2, isFacingAvatar: true, drawInFront: true }); this._isInTeleportMode = true; }, handlePickRay: function(pickRay) { if (this._isInTeleportMode) { var buttonRayResult = Overlays.findRayIntersection(pickRay, true, [this._teleportButtonOverlay]); if (buttonRayResult.intersects) { location = this._selectedLocation; return; } } var starRayResult = Overlays.findRayIntersection(pickRay, true, Object.keys(this._starOverlayIDsToStarHash)); if (!starRayResult.intersects) { this.cancelTeleportMode(); return; } this.setupTeleportMode(this._starOverlayIDsToStarHash[starRayResult.overlayID]); }, /** * Prepare the star update * empties properties cache and prepares the stars overlays for update */ prepareUpdate: function() { // empty the properties cache before update this._parentPropertiesCache = {}; Object.keys(this._starOverlays).forEach(function(starHash) { var starOverlay = this._starOverlays[starHash]; starOverlay.prepareUpdate.call(starOverlay); }, this); }, /** * Finalize the star update * removes stars that have not been received in the update. */ finalizeUpdate: function() { // refresh overlays translations map this._starOverlayIDsToStarHash = {}; Object.keys(this._starOverlays).forEach(function(starHash) { var starOverlay = this._starOverlays[starHash]; if (!starOverlay.hasBeenUpdated.call(starOverlay)) { // remove starOverlays that have not been addressed in this update starOverlay.cleanUp.call(starOverlay); delete this._starOverlays[starHash]; return; } this._starOverlayIDsToStarHash[starOverlay.id] = starHash; }, this); }, updateStars: function() { if (this._isInTeleportMode) { // don't update the stars while in teleport mode return; } var starOverlayManager = this; request(USER_STORIES_API_URL + '?now=' + (new Date()).toISOString() + '&include_actions=concurrency&restriction=' + DOMAIN_RESTRICTIONS + '&require_online=true' + '&protocol=' + ENCODED_PROTOCOL + '&page=1&per_page=' + MAXIMUM_STARS, function (error, data) { starOverlayManager.prepareUpdate.call(starOverlayManager); data.user_stories.forEach(function(userStory) { if (SKIP_OWN_DOMAIN && Uuid.isEqual(location.domainID, userStory.domain_id)) { // avoid placing stars up that point to the current domain. return; } starOverlayManager.updateStar.call(starOverlayManager, userStory); }); starOverlayManager.finalizeUpdate.call(starOverlayManager); }); }, updateStar: function(userStory) { var starHash = getStarHash(userStory.domain_id, userStory.place_name); if (starHash in starOverlayManager._starOverlays) { var starOverlay = starOverlayManager._starOverlays[starHash]; starOverlay.update.call(starOverlay, userStory); return; } var newStarOverlay = new StarOverlay(starOverlayManager, userStory); starOverlayManager._starOverlays[starHash] = newStarOverlay; }, cleanUp: function() { Object.keys(this._starOverlays).forEach(function(starHash) { var starOverlay = this._starOverlays[starHash]; starOverlay.cleanUp.call(starOverlay); }, this); this.cancelTeleportMode(); } }; return StarOverlayManager; })(); var starOverlayManager = null; var updateInterval = null; var mousePressEvent = function(event) { if (!event.isLeftButton || Settings.getValue(EDIT_SETTING, false)) { return; } var pickRay = Camera.computePickRay(event.x, event.y); starOverlayManager.handlePickRay(pickRay); }; this.preload = function(entityID) { Controller.mousePressEvent.connect(mousePressEvent); starOverlayManager = new StarOverlayManager(entityID); starOverlayManager.updateStars(); updateInterval = Script.setInterval(function() { starOverlayManager.updateStars.call(starOverlayManager); }, UPDATE_TIMEOUT); triggerMapping = Controller.newMapping(entityID + '-click'); [Controller.Standard.RT, Controller.Standard.LT].map(function(trigger) { var triggered = false; var MIN_TRIGGER_VALUE = 0.8; triggerMapping.from(trigger).peek().to(function(value) { if (!triggered && value >= MIN_TRIGGER_VALUE) { triggered = true; var hand = (trigger === Controller.Standard.LT ? Controller.Standard.LeftHand : Controller.Standard.RightHand); var pickRay = controllerUtils.controllerComputePickRay(hand); if (pickRay === null) { console.error('There was a problem computing the pickRay'); return; } starOverlayManager.handlePickRay(pickRay); } else if (triggered && value < MIN_TRIGGER_VALUE) { triggered = false; } }); }); triggerMapping.enable(); }; this.unload = function() { starOverlayManager.cleanUp.call(starOverlayManager); Script.clearInterval(updateInterval); Controller.mousePressEvent.disconnect(mousePressEvent); triggerMapping.disable(); }; });