content/hifi-content/Experiences/Releases/marketPlaceItems/RPO360/v1.2/RPO360.js
2022-02-13 23:16:46 +01:00

507 lines
18 KiB
JavaScript

"use strict";
/*jslint vars:true, plusplus:true, forin:true*/
/*global Tablet, Script, */
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
//
// RPO360.js
//
// Created by Zach Fox on 2018-10-26
// 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 () { // BEGIN LOCAL_SCOPE
var AppUi = Script.require('appUi');
// Function Name: inFrontOf()
//
// Description:
// - Returns the position in front of the given "position" argument, where the forward vector is based off
// the "orientation" argument and the amount in front is based off the "distance" argument.
function inFrontOf(distance, position, orientation) {
return Vec3.sum(position || MyAvatar.position,
Vec3.multiply(distance, Quat.getForward(orientation || MyAvatar.orientation)));
}
// Function Name: rpo360On()
var secondaryCameraConfig = Render.getConfig("SecondaryCamera");
var camera = false;
var cameraRotation;
var cameraPosition;
var cameraGravity = {x: 0, y: -5, z: 0};
var velocityLoopInterval = false;
function rpo360On() {
// Rez the camera model, and attach
// the secondary camera to the rezzed model.
cameraRotation = MyAvatar.orientation;
cameraPosition = inFrontOf(1.0, Vec3.sum(MyAvatar.position, { x: 0, y: 0.3, z: 0 }));
camera = Entities.addEntity({
"angularDamping": 0.08,
"canCastShadow": false,
"damping": 0.01,
"collidesWith": "static,dynamic,kinematic,",
"collisionMask": 7,
"modelURL": Script.resolvePath("resources/models/rpo360.fbx"),
"name": "RPO360 Camera",
"rotation": cameraRotation,
"position": cameraPosition,
"shapeType": "simple-compound",
"type": "Model",
"userData": '{"grabbableKey":{"grabbable":true}}',
"isVisibleInSecondaryCamera": false,
"gravity": cameraGravity,
"dynamic": true
}, true);
secondaryCameraConfig.attachedEntityId = camera;
// Make the button go active if the UI is open OR the camera is on
// (in this case it'll be both)
buttonActive(ui.isOpen);
// Play a little sound to let the user know we've rezzed the camera
Audio.playSound(SOUND_CAMERA_ON, {
volume: 0.15,
position: cameraPosition,
localOnly: true
});
// Remove the existing camera model from the domain if one exists.
// It's easy for this to happen if the user crashes while the RPO360 Camera is on.
// We do this down here (after the new one is rezzed) so that we don't accidentally delete
// the newly-rezzed model.
var entityIDs = Entities.findEntitiesByName("RPO360 Camera", MyAvatar.position, 100, false);
entityIDs.forEach(function (currentEntityID) {
var currentEntityOwner = Entities.getEntityProperties(currentEntityID, ['owningAvatarID']).owningAvatarID;
if (currentEntityOwner === MyAvatar.sessionUUID && currentEntityID !== camera) {
Entities.deleteEntity(currentEntityID);
}
});
// Start the velocity loop interval at 70ms
// This is used to determine when the 360 photo should be snapped
velocityLoopInterval = Script.setInterval(velocityLoop, 70);
}
// Function Name: velocityLoop()
var hasBeenThrown = false;
var hasBeenGrabbed = false;
var snapshotVelocity = false;
var snapshotAngularVelocity = false;
var velocityWasPositive = false;
var cameraReleaseTime = false;
var MIN_AIRTIME_MS = 500;
var flash = false;
function velocityLoop() {
// Get the velocity and angular velocity of the camera model
var properties = Entities.getEntityProperties(camera, [
'velocity',
'angularVelocity'
]);
var velocity = properties.velocity;
var angularVelocity = properties.angularVelocity;
// ActionIDs refer to the actions on the entity
// Actions can be things like: NearGrab, FarGrab, Equip, etc.
var actionIDs = Entities.getActionIDs(camera);
// If there's an action on the entity...
if (actionIDs.length > 0) {
// Set the "hasBeenGrabbed" flag to "true"
hasBeenGrabbed = true;
// Make sure we record that we haven't yet been thrown
hasBeenThrown = false;
// If we've previously been grabbed, and there are currently no actions
// on the camera model...
} else if (hasBeenGrabbed) {
// Reset this flag to false
hasBeenGrabbed = false;
// We've been thrown now!
hasBeenThrown = true;
// Record the time at which a user has thrown the camera
cameraReleaseTime = Date.now();
}
// If we've been thrown UP...
if (hasBeenThrown && velocity.y > 0) {
// Set this flag to true
velocityWasPositive = true;
}
// If we've been thrown UP in the past, but now we're coming DOWN...
if (hasBeenThrown && velocityWasPositive && velocity.y < 0) {
// Reset the state machine
hasBeenThrown = false;
velocityWasPositive = false;
// Don't take a snapshot if the camera hasn't been in the air for very long
if (Date.now() - cameraReleaseTime <= MIN_AIRTIME_MS) {
return;
}
// Save these properties so that the camera falls realistically
// after it's taken the 360 snapshot
snapshotVelocity = velocity;
snapshotAngularVelocity = angularVelocity;
// Freeze the camera model and make it not grabbable
Entities.editEntity(camera, {
velocity: {x: 0, y: 0, z: 0},
angularVelocity: {x: 0, y: 0, z: 0},
gravity: {x: 0, y: 0, z: 0},
"userData": '{"grabbableKey":{"grabbable":false}}',
});
// Add a "flash" to the camera that illuminates the ground below the camera
flash = Entities.addEntity({
"collidesWith": "",
"collisionMask": 0,
"color": {
"blue": 173,
"green": 252,
"red": 255
},
"dimensions": {
"x": 100,
"y": 100,
"z": 100
},
"dynamic": false,
"falloffRadius": 10,
"intensity": 1,
"isSpotlight": false,
"localRotation": { w: 1, x: 0, y: 0, z: 0 },
"name": "RPO360 Camera Flash",
"type": "Light",
"parentID": camera
});
// Take the snapshot!
maybeTake360Snapshot();
}
}
// Function Name: rpo360Off()
var WAIT_AFTER_DOMAIN_SWITCH_BEFORE_CAMERA_DELETE_MS = 1 * 1000;
function rpo360Off(isChangingDomains) {
if (velocityLoopInterval) {
Script.clearInterval(velocityLoopInterval);
velocityLoopInterval = false;
}
function deleteCamera() {
if (flash) {
Entities.deleteEntity(flash);
flash = false;
}
if (camera) {
Entities.deleteEntity(camera);
camera = false;
}
buttonActive(ui.isOpen);
}
secondaryCameraConfig.attachedEntityId = false;
if (camera) {
// Workaround for Avatar Entities not immediately having properties after
// the "Window.domainChanged()" signal is emitted.
// May no longer be necessary; untested...
if (isChangingDomains) {
Script.setTimeout(function () {
deleteCamera();
rpo360On();
}, WAIT_AFTER_DOMAIN_SWITCH_BEFORE_CAMERA_DELETE_MS);
} else {
deleteCamera();
}
}
}
var isCurrentlyTaking360Snapshot = false;
var processing360Snapshot = false;
function maybeTake360Snapshot() {
// Don't take a snapshot if we're currently in the middle of taking one
// or if the camera entity doesn't exist
if (!isCurrentlyTaking360Snapshot && camera) {
isCurrentlyTaking360Snapshot = true;
var currentCameraPosition = Entities.getEntityProperties(camera, ['position']).position;
// Play a sound at the current camera position
Audio.playSound(SOUND_SNAPSHOT, {
position: { x: currentCameraPosition.x, y: currentCameraPosition.y, z: currentCameraPosition.z },
localOnly: false,
volume: 0.8
});
Window.takeSecondaryCamera360Snapshot(currentCameraPosition);
used360AppToTakeThisSnapshot = true;
processing360Snapshot = true;
// Let the QML know we're processing a 360 snapshot now
ui.sendMessage({
method: 'startedProcessing360Snapshot'
});
}
}
function on360SnapshotTaken(path) {
isCurrentlyTaking360Snapshot = false;
// Make the camera fall back to the ground with the same
// physical properties as when it froze in the air
Entities.editEntity(camera, {
velocity: snapshotVelocity,
angularVelocity: snapshotAngularVelocity,
gravity: cameraGravity,
"userData": '{"grabbableKey":{"grabbable":true}}',
});
// Delete the flash entity
if (flash) {
Entities.deleteEntity(flash);
flash = false;
}
console.log('360 Snapshot taken. Path: ' + path + "\nUploading now...");
// Upload the 360 snapshot to our S3
Window.shareSnapshot(path);
}
var last360SnapshotUrl = false;
var last360ThumbnailURL = false;
var used360AppToTakeThisSnapshot = false;
// We process this signal after any snapshot is uploaded to the HiFi website
function snapshotUploaded(isError, reply) {
if (!isError) {
// This isn't foolproof - there's a race condition here. A user could
// take a snapshot using the Snap app or some other method, upload it,
// just before using this app to take a snapshot. At that point,
// it'd be possible to enter this conditional accidentally.
// But the likelihood of that happening is very low.
if (used360AppToTakeThisSnapshot) {
var replyJson = JSON.parse(reply),
storyID = replyJson.user_story.id,
imageURL = replyJson.user_story.details.image_url,
thumbnailURL = replyJson.user_story.thumbnail_url;
used360AppToTakeThisSnapshot = false;
last360SnapshotUrl = imageURL;
last360ThumbnailURL = thumbnailURL;
console.log('SUCCESS: Snapshot uploaded! Story with audience:for_url created! ID:', storyID);
console.log("Image URL: " + last360SnapshotUrl);
console.log("Thumbnail URL: " + last360ThumbnailURL);
ui.sendMessage({
method: 'last360ThumbnailURL',
last360ThumbnailURL: last360ThumbnailURL
});
processing360Snapshot = false;
ui.sendMessage({
method: 'finishedProcessing360Snapshot'
});
}
}
}
// This is the globe with the 360 image printed on the OUTSIDE.
var globe = false;
function rez360Globe() {
var properties = {
"type": 'Sphere',
"name": "Globe by " + MyAvatar.sessionDisplayName,
"description": "Created with RPO360 Cam",
"dimensions": { "x": 1.0, "y": 1.0, "z": 1.0 },
"position": inFrontOf(2.0, Vec3.sum(MyAvatar.position, { x: 0, y: 0.3, z: 0 })),
"density": 200,
"restitution": 0.15,
"gravity": { "x": 0, "y": -0.5, "z": 0 },
"damping": 0.45,
"dynamic": true,
"collisionsWillMove": true,
"grab": { "grabbable": true }
};
globe = Entities.addEntity(properties);
var globeMaterial = Entities.addEntity({
type: "Material",
name: "Globe Texture",
parentID: globe,
materialURL: "materialData",
priority: 1,
materialData: JSON.stringify({
materialVersion: 1,
materials: {
"model": "hifi_pbr",
"albedoMap": last360SnapshotUrl,
"emissiveMap": last360SnapshotUrl
}
})
});
}
// This is the globe with the 360 image printed on the INSIDE.
var streetViewGlobe = false;
function rezStreetViewGlobe() {
var properties = {
"type": 'Model',
"shapeType": "simple-compound",
"name": "Street View Globe by " + MyAvatar.sessionDisplayName,
"description": "Created with RPO360 Cam",
"dimensions": { "x": 3.0, "y": 3.0, "z": 3.0 },
"modelURL": Script.resolvePath("resources/models/invertedSphere.fbx"),
"position": inFrontOf(3.0, Vec3.sum(MyAvatar.position, { x: 0, y: 0.55, z: 0 })),
"density": 200,
"restitution": 0.15,
"gravity": { "x": 0, "y": -0.5, "z": 0 },
"damping": 0.45,
"dynamic": true,
"collidesWith": "static,dynamic,kinematic,",
"collisionMask": 7,
"collisionsWillMove": true,
"userData": "{\"grabbableKey\":{\"grabbable\":true}}"
};
streetViewGlobe = Entities.addEntity(properties);
var globeMaterial = Entities.addEntity({
type: "Material",
name: "Globe Texture",
parentID: streetViewGlobe,
materialURL: "materialData",
priority: 1,
materialData: JSON.stringify({
materialVersion: 1,
materials: {
"model": "hifi_pbr",
"albedoMap": last360SnapshotUrl,
"emissiveMap": last360SnapshotUrl
}
})
});
}
// Stolen from `controllerDispatcherUtils.js`
// The client team probably wouldn't like the "disable/enable grab highlighting"
// feature that this app implements :)
const DISPATCHER_HOVERING_LIST = "dispactherHoveringList";
const DISPATCHER_HOVERING_STYLE = {
isOutlineSmooth: true,
outlineWidth: 0,
outlineUnoccludedColor: {red: 255, green: 128, blue: 128},
outlineUnoccludedAlpha: 0.0,
outlineOccludedColor: {red: 255, green: 128, blue: 128},
outlineOccludedAlpha:0.0,
fillUnoccludedColor: {red: 255, green: 255, blue: 255},
fillUnoccludedAlpha: 0.12,
fillOccludedColor: {red: 255, green: 255, blue: 255},
fillOccludedAlpha: 0.0
};
// Function Name: fromQml()
function fromQml(message) {
switch (message.method) {
case 'rpo360On':
rpo360On();
break;
case 'rpo360Off':
rpo360Off();
break;
case 'openSettings':
if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false))
|| (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) {
Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog");
} else {
tablet.pushOntoStack("hifi/tablet/TabletGeneralPreferences.qml");
}
break;
case 'rezGlobe':
rez360Globe();
break;
case 'rezStreetViewGlobe':
rezStreetViewGlobe();
break;
case 'disableGrabHighlighting':
Selection.disableListHighlight(DISPATCHER_HOVERING_LIST);
break;
case 'enableGrabHighlighting':
Selection.enableListHighlight(DISPATCHER_HOVERING_LIST, DISPATCHER_HOVERING_STYLE);
break;
default:
print('Unrecognized message from RPO360.qml:', JSON.stringify(message));
}
}
// Function Name: shutdown()
//
// Description:
// -shutdown() will be called when the script ends (i.e. is stopped).
function shutdown() {
rpo360Off();
Window.domainChanged.disconnect(onDomainChanged);
Window.snapshot360Taken.disconnect(on360SnapshotTaken);
Window.snapshotShared.disconnect(snapshotUploaded);
ui.tablet.tabletShownChanged.disconnect(tabletVisibilityChanged);
}
// Function Name: onDomainChanged()
//
// Description:
// -A small utility function used when the Window.domainChanged() signal is fired.
function onDomainChanged() {
rpo360Off(true);
}
// These functions will be called when the script is loaded.
var SOUND_CAMERA_ON = SoundCache.getSound(Script.resolvePath("resources/sounds/cameraOn.wav"));
var SOUND_SNAPSHOT = SoundCache.getSound(Script.resolvePath("resources/sounds/snap.wav"));
function buttonActive(isActive) {
ui.button.editProperties({isActive: isActive || camera});
}
function onOpened() {
// In the case of a remote QML app, it takes a bit of time
// for the event bridge to actually connect, so we have to wait...
Script.setTimeout(function () {
if (ui.isOpen) {
ui.sendMessage({
method: 'initializeUI',
masterSwitchOn: !!camera,
last360ThumbnailURL: last360ThumbnailURL || "",
processing360Snapshot: processing360Snapshot
});
}
}, 700);
}
function tabletVisibilityChanged() {
if (!ui.tablet.tabletShown && ui.isOpen) {
ui.close();
}
}
// Function Name: startup()
var ui;
var RPO_360_QML_SOURCE = Script.resolvePath("RPO360.qml");
function startup() {
ui = new AppUi({
buttonName: "RPO360",
home: RPO_360_QML_SOURCE,
onMessage: fromQml,
buttonActive: buttonActive,
onOpened: onOpened,
graphicsDirectory: Script.resolvePath("./resources/images/icons/")
});
Window.domainChanged.connect(onDomainChanged);
Window.snapshot360Taken.connect(on360SnapshotTaken);
Window.snapshotShared.connect(snapshotUploaded);
ui.tablet.tabletShownChanged.connect(tabletVisibilityChanged);
camera = false;
}
startup();
Script.scriptEnding.connect(shutdown);
}()); // END LOCAL_SCOPE