overte/scripts/system/marketplaces/marketplaces.js
2018-10-22 13:59:57 -07:00

1293 lines
50 KiB
JavaScript

//
// marketplaces.js
//
// Created by Eric Levin on 8 Jan 2016
// Copyright 2016 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
//
/* global Tablet, Script, HMD, UserActivityLogger, Entities, Account, Wallet, ContextOverlay, Settings, Camera, Vec3,
Quat, MyAvatar, Clipboard, Menu, Grid, Uuid, GlobalServices, openLoginWindow, getConnectionData, Overlays, SoundCache,
DesktopPreviewProvider */
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
var selectionDisplay = null; // for gridTool.js to ignore
(function () { // BEGIN LOCAL_SCOPE
var AppUi = Script.require('appUi');
Script.include("/~/system/libraries/gridTool.js");
Script.include("/~/system/libraries/connectionUtils.js");
var MARKETPLACE_CHECKOUT_QML_PATH = "hifi/commerce/checkout/Checkout.qml";
var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertificate/InspectionCertificate.qml";
var MARKETPLACE_ITEM_TESTER_QML_PATH = "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml";
var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml";
var MARKETPLACE_WALLET_QML_PATH = "hifi/commerce/wallet/Wallet.qml";
var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js");
var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html");
var METAVERSE_SERVER_URL = Account.metaverseServerURL;
var REZZING_SOUND = SoundCache.getSound(Script.resolvePath("../assets/sounds/rezzing.wav"));
// Event bridge messages.
var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD";
var CLARA_IO_STATUS = "CLARA.IO STATUS";
var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD";
var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD";
var GOTO_DIRECTORY = "GOTO_DIRECTORY";
var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS";
var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS";
var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS";
var CLARA_DOWNLOAD_TITLE = "Preparing Download";
var messageBox = null;
var isDownloadBeingCancelled = false;
var CANCEL_BUTTON = 4194304; // QMessageBox::Cancel
var NO_BUTTON = 0; // QMessageBox::NoButton
var NO_PERMISSIONS_ERROR_MESSAGE = "Cannot download model because you can't write to \nthe domain's Asset Server.";
var resourceRequestEvents = [];
function signalResourceRequestEvent(data) {
// Once we can tie resource request events to specific resources,
// we will have to update the "0" in here.
resourceObjectsInTest[0].resourceAccessEventText += "[" + data.date.toISOString() + "] " +
data.url.toString().replace("__NONE__,", "") + "\n";
ui.tablet.sendToQml({
method: "resourceRequestEvent",
data: data,
resourceAccessEventText: resourceObjectsInTest[0].resourceAccessEventText
});
}
function onResourceRequestEvent(data) {
// Once we can tie resource request events to specific resources,
// we will have to update the "0" in here.
if (resourceObjectsInTest[0] && resourceObjectsInTest[0].currentlyRecordingResources) {
var resourceRequestEvent = {
"date": new Date(),
"url": data.url,
"callerId": data.callerId,
"extra": data.extra
};
resourceRequestEvents.push(resourceRequestEvent);
signalResourceRequestEvent(resourceRequestEvent);
}
}
function onMessageBoxClosed(id, button) {
if (id === messageBox && button === CANCEL_BUTTON) {
isDownloadBeingCancelled = true;
messageBox = null;
ui.sendToHtml({
type: CLARA_IO_CANCEL_DOWNLOAD
});
}
}
function onCanWriteAssetsChanged() {
ui.sendToHtml({
type: CAN_WRITE_ASSETS,
canWriteAssets: Entities.canWriteAssets()
});
}
var tabletShouldBeVisibleInSecondaryCamera = false;
function setTabletVisibleInSecondaryCamera(visibleInSecondaryCam) {
if (visibleInSecondaryCam) {
// if we're potentially showing the tablet, only do so if it was visible before
if (!tabletShouldBeVisibleInSecondaryCamera) {
return;
}
} else {
// if we're hiding the tablet, check to see if it was visible in the first place
tabletShouldBeVisibleInSecondaryCamera = Overlays.getProperty(HMD.tabletID, "isVisibleInSecondaryCamera");
}
Overlays.editOverlay(HMD.tabletID, { isVisibleInSecondaryCamera : visibleInSecondaryCam });
Overlays.editOverlay(HMD.homeButtonID, { isVisibleInSecondaryCamera : visibleInSecondaryCam });
Overlays.editOverlay(HMD.homeButtonHighlightID, { isVisibleInSecondaryCamera : visibleInSecondaryCam });
Overlays.editOverlay(HMD.tabletScreenID, { isVisibleInSecondaryCamera : visibleInSecondaryCam });
}
function openWallet() {
ui.open(MARKETPLACE_WALLET_QML_PATH);
}
function setupWallet(referrer) {
// Needs to be done within the QML page in order to get access to QmlCommerce
openWallet();
var ALLOWANCE_FOR_EVENT_BRIDGE_SETUP = 0;
Script.setTimeout(function () {
ui.tablet.sendToQml({
method: 'updateWalletReferrer',
referrer: referrer
});
}, ALLOWANCE_FOR_EVENT_BRIDGE_SETUP);
}
function onMarketplaceOpen(referrer) {
var cta = referrer, match;
if (Account.loggedIn && walletNeedsSetup()) {
if (referrer === MARKETPLACE_URL_INITIAL) {
setupWallet('marketplace cta');
} else {
match = referrer.match(/\/item\/(\w+)$/);
if (match && match[1]) {
setupWallet(match[1]);
} else if (referrer.indexOf(METAVERSE_SERVER_URL) === -1) { // not a url
setupWallet(referrer);
} else {
print("WARNING: opening marketplace to", referrer, "without wallet setup.");
}
}
}
}
function openMarketplace(optionalItemOrUrl) {
// This is a bit of a kluge, but so is the whole file.
// If given a whole path, use it with no cta.
// If given an id, build the appropriate url and use the id as the cta.
// Otherwise, use home and 'marketplace cta'.
// AND... if call onMarketplaceOpen to setupWallet if we need to.
var url = optionalItemOrUrl || MARKETPLACE_URL_INITIAL;
// If optionalItemOrUrl contains the metaverse base, then it's a url, not an item id.
if (optionalItemOrUrl && optionalItemOrUrl.indexOf(METAVERSE_SERVER_URL) === -1) {
url = MARKETPLACE_URL + '/items/' + optionalItemOrUrl;
}
ui.open(url, MARKETPLACES_INJECT_SCRIPT_URL);
}
// Function Name: wireQmlEventBridge()
//
// Description:
// -Used to connect/disconnect the script's response to the tablet's "fromQml" signal. Set the "on" argument to enable or
// disable to event bridge.
//
// Relevant Variables:
// -hasEventBridge: true/false depending on whether we've already connected the event bridge.
var hasEventBridge = false;
function wireQmlEventBridge(on) {
if (!ui.tablet) {
print("Warning in wireQmlEventBridge(): 'tablet' undefined!");
return;
}
if (on) {
if (!hasEventBridge) {
ui.tablet.fromQml.connect(onQmlMessageReceived);
hasEventBridge = true;
}
} else {
if (hasEventBridge) {
ui.tablet.fromQml.disconnect(onQmlMessageReceived);
hasEventBridge = false;
}
}
}
var contextOverlayEntity = "";
function openInspectionCertificateQML(currentEntityWithContextOverlay) {
ui.open(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH);
contextOverlayEntity = currentEntityWithContextOverlay;
}
function setCertificateInfo(currentEntityWithContextOverlay, itemCertificateId) {
var certificateId = itemCertificateId ||
(Entities.getEntityProperties(currentEntityWithContextOverlay, ['certificateID']).certificateID);
ui.tablet.sendToQml({
method: 'inspectionCertificate_setCertificateId',
entityId: currentEntityWithContextOverlay,
certificateId: certificateId
});
}
function onUsernameChanged() {
if (onMarketplaceScreen) {
openMarketplace();
}
}
function walletNeedsSetup() {
return Wallet.walletStatus === 1;
}
function sendCommerceSettings() {
ui.sendToHtml({
type: "marketplaces",
action: "commerceSetting",
data: {
commerceMode: Settings.getValue("commerce", true),
userIsLoggedIn: Account.loggedIn,
walletNeedsSetup: walletNeedsSetup(),
metaverseServerURL: Account.metaverseServerURL,
messagesWaiting: shouldShowDot
}
});
}
// BEGIN AVATAR SELECTOR LOGIC
var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6 };
var SELECTED_COLOR = { red: 0xF3, green: 0x91, blue: 0x29 };
var HOVER_COLOR = { red: 0xD0, green: 0xD0, blue: 0xD0 };
var overlays = {}; // Keeps track of all our extended overlay data objects, keyed by target identifier.
function ExtendedOverlay(key, type, properties) { // A wrapper around overlays to store the key it is associated with.
overlays[key] = this;
this.key = key;
this.selected = false;
this.hovering = false;
this.activeOverlay = Overlays.addOverlay(type, properties); // We could use different overlays for (un)selected...
}
// Instance methods:
ExtendedOverlay.prototype.deleteOverlay = function () { // remove display and data of this overlay
Overlays.deleteOverlay(this.activeOverlay);
delete overlays[this.key];
};
ExtendedOverlay.prototype.editOverlay = function (properties) { // change display of this overlay
Overlays.editOverlay(this.activeOverlay, properties);
};
function color(selected, hovering) {
var base = hovering ? HOVER_COLOR : selected ? SELECTED_COLOR : UNSELECTED_COLOR;
function scale(component) {
return component;
}
return { red: scale(base.red), green: scale(base.green), blue: scale(base.blue) };
}
// so we don't have to traverse the overlays to get the last one
var lastHoveringId = 0;
ExtendedOverlay.prototype.hover = function (hovering) {
this.hovering = hovering;
if (this.key === lastHoveringId) {
if (hovering) {
return;
}
lastHoveringId = 0;
}
this.editOverlay({ color: color(this.selected, hovering) });
if (hovering) {
// un-hover the last hovering overlay
if (lastHoveringId && lastHoveringId !== this.key) {
ExtendedOverlay.get(lastHoveringId).hover(false);
}
lastHoveringId = this.key;
}
};
ExtendedOverlay.prototype.select = function (selected) {
if (this.selected === selected) {
return;
}
this.editOverlay({ color: color(selected, this.hovering) });
this.selected = selected;
};
// Class methods:
var selectedId = false;
ExtendedOverlay.isSelected = function (id) {
return selectedId === id;
};
ExtendedOverlay.get = function (key) { // answer the extended overlay data object associated with the given avatar identifier
return overlays[key];
};
ExtendedOverlay.some = function (iterator) { // Bails early as soon as iterator returns truthy.
var key;
for (key in overlays) {
if (iterator(ExtendedOverlay.get(key))) {
return;
}
}
};
ExtendedOverlay.unHover = function () { // calls hover(false) on lastHoveringId (if any)
if (lastHoveringId) {
ExtendedOverlay.get(lastHoveringId).hover(false);
}
};
// hit(overlay) on the one overlay intersected by pickRay, if any.
// noHit() if no ExtendedOverlay was intersected (helps with hover)
ExtendedOverlay.applyPickRay = function (pickRay, hit, noHit) {
var pickedOverlay = Overlays.findRayIntersection(pickRay); // Depends on nearer coverOverlays to extend closer to us than farther ones.
if (!pickedOverlay.intersects) {
if (noHit) {
return noHit();
}
return;
}
ExtendedOverlay.some(function (overlay) { // See if pickedOverlay is one of ours.
if ((overlay.activeOverlay) === pickedOverlay.overlayID) {
hit(overlay);
return true;
}
});
};
function addAvatarNode(id) {
return new ExtendedOverlay(id, "sphere", {
drawInFront: true,
solid: true,
alpha: 0.8,
color: color(false, false),
ignoreRayIntersection: false
});
}
var pingPong = true;
function updateOverlays() {
var eye = Camera.position;
AvatarList.getAvatarIdentifiers().forEach(function (id) {
if (!id) {
return; // don't update ourself, or avatars we're not interested in
}
var avatar = AvatarList.getAvatar(id);
if (!avatar) {
return; // will be deleted below if there had been an overlay.
}
var overlay = ExtendedOverlay.get(id);
if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back.
overlay = addAvatarNode(id);
}
var target = avatar.position;
var distance = Vec3.distance(target, eye);
var offset = 0.2;
var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position)
var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can
if (headIndex > 0) {
offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2;
}
// move a bit in front, towards the camera
target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset));
// now bump it up a bit
target.y = target.y + offset;
overlay.ping = pingPong;
overlay.editOverlay({
color: color(ExtendedOverlay.isSelected(id), overlay.hovering),
position: target,
dimensions: 0.032 * distance
});
});
pingPong = !pingPong;
ExtendedOverlay.some(function (overlay) { // Remove any that weren't updated. (User is gone.)
if (overlay.ping === pingPong) {
overlay.deleteOverlay();
}
});
}
function removeOverlays() {
selectedId = false;
lastHoveringId = 0;
ExtendedOverlay.some(function (overlay) {
overlay.deleteOverlay();
});
}
//
// Clicks.
//
function usernameFromIDReply(id, username, machineFingerprint, isAdmin) {
if (selectedId === id) {
var message = {
method: 'updateSelectedRecipientUsername',
userName: username === "" ? "unknown username" : username
};
ui.tablet.sendToQml(message);
}
}
function handleClick(pickRay) {
ExtendedOverlay.applyPickRay(pickRay, function (overlay) {
var nextSelectedStatus = !overlay.selected;
var avatarId = overlay.key;
selectedId = nextSelectedStatus ? avatarId : false;
if (nextSelectedStatus) {
Users.requestUsernameFromID(avatarId);
}
var message = {
method: 'selectRecipient',
id: avatarId,
isSelected: nextSelectedStatus,
displayName: '"' + AvatarList.getAvatar(avatarId).sessionDisplayName + '"',
userName: ''
};
ui.tablet.sendToQml(message);
ExtendedOverlay.some(function (overlay) {
var id = overlay.key;
var selected = ExtendedOverlay.isSelected(id);
overlay.select(selected);
});
return true;
});
}
function handleMouseEvent(mousePressEvent) { // handleClick if we get one.
if (!mousePressEvent.isLeftButton) {
return;
}
handleClick(Camera.computePickRay(mousePressEvent.x, mousePressEvent.y));
}
function handleMouseMove(pickRay) { // given the pickRay, just do the hover logic
ExtendedOverlay.applyPickRay(pickRay, function (overlay) {
overlay.hover(true);
}, function () {
ExtendedOverlay.unHover();
});
}
// handy global to keep track of which hand is the mouse (if any)
var currentHandPressed = 0;
var TRIGGER_CLICK_THRESHOLD = 0.85;
var TRIGGER_PRESS_THRESHOLD = 0.05;
function handleMouseMoveEvent(event) { // find out which overlay (if any) is over the mouse position
var pickRay;
if (HMD.active) {
if (currentHandPressed !== 0) {
pickRay = controllerComputePickRay(currentHandPressed);
} else {
// nothing should hover, so
ExtendedOverlay.unHover();
return;
}
} else {
pickRay = Camera.computePickRay(event.x, event.y);
}
handleMouseMove(pickRay);
}
function handleTriggerPressed(hand, value) {
// The idea is if you press one trigger, it is the one
// we will consider the mouse. Even if the other is pressed,
// we ignore it until this one is no longer pressed.
var isPressed = value > TRIGGER_PRESS_THRESHOLD;
if (currentHandPressed === 0) {
currentHandPressed = isPressed ? hand : 0;
return;
}
if (currentHandPressed === hand) {
currentHandPressed = isPressed ? hand : 0;
return;
}
// otherwise, the other hand is still triggered
// so do nothing.
}
// We get mouseMoveEvents from the handControllers, via handControllerPointer.
// But we don't get mousePressEvents.
var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click');
var triggerPressMapping = Controller.newMapping(Script.resolvePath('') + '-press');
function controllerComputePickRay(hand) {
var controllerPose = getControllerWorldLocation(hand, true);
if (controllerPose.valid) {
return { origin: controllerPose.position, direction: Quat.getUp(controllerPose.orientation) };
}
}
function makeClickHandler(hand) {
return function (clicked) {
if (clicked > TRIGGER_CLICK_THRESHOLD) {
var pickRay = controllerComputePickRay(hand);
handleClick(pickRay);
}
};
}
function makePressHandler(hand) {
return function (value) {
handleTriggerPressed(hand, value);
};
}
triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand));
triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand));
triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand));
triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand));
// END AVATAR SELECTOR LOGIC
var grid = new Grid();
function adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation) {
// Adjust the position such that the bounding box (registration, dimenions, and orientation) lies behind the original
// position in the given direction.
var CORNERS = [
{ x: 0, y: 0, z: 0 },
{ x: 0, y: 0, z: 1 },
{ x: 0, y: 1, z: 0 },
{ x: 0, y: 1, z: 1 },
{ x: 1, y: 0, z: 0 },
{ x: 1, y: 0, z: 1 },
{ x: 1, y: 1, z: 0 },
{ x: 1, y: 1, z: 1 }
];
// Go through all corners and find least (most negative) distance in front of position.
var distance = 0;
for (var i = 0, length = CORNERS.length; i < length; i++) {
var cornerVector =
Vec3.multiplyQbyV(orientation, Vec3.multiplyVbyV(Vec3.subtract(CORNERS[i], registration), dimensions));
var cornerDistance = Vec3.dot(cornerVector, direction);
distance = Math.min(cornerDistance, distance);
}
position = Vec3.sum(Vec3.multiply(distance, direction), position);
return position;
}
var HALF_TREE_SCALE = 16384;
function getPositionToCreateEntity(extra) {
var CREATE_DISTANCE = 2;
var position;
var delta = extra !== undefined ? extra : 0;
if (Camera.mode === "entity" || Camera.mode === "independent") {
position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), CREATE_DISTANCE + delta));
} else {
position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getForward(MyAvatar.orientation), CREATE_DISTANCE + delta));
position.y += 0.5;
}
if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) {
return null;
}
return position;
}
function defaultFor(arg, val) {
return typeof arg !== 'undefined' ? arg : val;
}
function rezEntity(itemHref, itemType, marketplaceItemTesterId) {
var isWearable = itemType === "wearable";
print("!!!!! Clipboard.importEntities " + marketplaceItemTesterId);
var success = Clipboard.importEntities(itemHref, true, marketplaceItemTesterId);
var wearableLocalPosition = null;
var wearableLocalRotation = null;
var wearableLocalDimensions = null;
var wearableDimensions = null;
marketplaceItemTesterId = defaultFor(marketplaceItemTesterId, -1);
if (itemType === "contentSet") {
console.log("Item is a content set; codepath shouldn't go here.");
return;
}
if (isWearable) {
var wearableTransforms = Settings.getValue("io.highfidelity.avatarStore.checkOut.transforms");
if (!wearableTransforms) {
// TODO delete this clause
wearableTransforms = Settings.getValue("io.highfidelity.avatarStore.checkOut.tranforms");
}
var certPos = itemHref.search("certificate_id="); // TODO how do I parse a URL from here?
if (certPos >= 0) {
certPos += 15; // length of "certificate_id="
var certURLEncoded = itemHref.substring(certPos);
var certB64Encoded = decodeURIComponent(certURLEncoded);
for (var key in wearableTransforms) {
if (wearableTransforms.hasOwnProperty(key)) {
var certificateTransforms = wearableTransforms[key].certificateTransforms;
if (certificateTransforms) {
for (var certID in certificateTransforms) {
if (certificateTransforms.hasOwnProperty(certID) &&
certID == certB64Encoded) {
var certificateTransform = certificateTransforms[certID];
wearableLocalPosition = certificateTransform.localPosition;
wearableLocalRotation = certificateTransform.localRotation;
wearableLocalDimensions = certificateTransform.localDimensions;
wearableDimensions = certificateTransform.dimensions;
}
}
}
}
}
}
}
if (success) {
var VERY_LARGE = 10000;
var isLargeImport = Clipboard.getClipboardContentsLargestDimension() >= VERY_LARGE;
var position = Vec3.ZERO;
if (!isLargeImport) {
position = getPositionToCreateEntity(Clipboard.getClipboardContentsLargestDimension() / 2);
}
if (position !== null && position !== undefined) {
var pastedEntityIDs = Clipboard.pasteEntities(position);
if (!isLargeImport) {
// The first entity in Clipboard gets the specified position with the rest being relative to it. Therefore, move
// entities after they're imported so that they're all the correct distance in front of and with geometric mean
// centered on the avatar/camera direction.
var deltaPosition = Vec3.ZERO;
var entityPositions = [];
var entityParentIDs = [];
var propType = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]).type;
var NO_ADJUST_ENTITY_TYPES = ["Zone", "Light", "ParticleEffect"];
if (NO_ADJUST_ENTITY_TYPES.indexOf(propType) === -1) {
var targetDirection;
if (Camera.mode === "entity" || Camera.mode === "independent") {
targetDirection = Camera.orientation;
} else {
targetDirection = MyAvatar.orientation;
}
targetDirection = Vec3.multiplyQbyV(targetDirection, Vec3.UNIT_Z);
var targetPosition = getPositionToCreateEntity();
var deltaParallel = HALF_TREE_SCALE; // Distance to move entities parallel to targetDirection.
var deltaPerpendicular = Vec3.ZERO; // Distance to move entities perpendicular to targetDirection.
for (var i = 0, length = pastedEntityIDs.length; i < length; i++) {
var curLoopEntityProps = Entities.getEntityProperties(pastedEntityIDs[i], ["position", "dimensions",
"registrationPoint", "rotation", "parentID"]);
var adjustedPosition = adjustPositionPerBoundingBox(targetPosition, targetDirection,
curLoopEntityProps.registrationPoint, curLoopEntityProps.dimensions, curLoopEntityProps.rotation);
var delta = Vec3.subtract(adjustedPosition, curLoopEntityProps.position);
var distance = Vec3.dot(delta, targetDirection);
deltaParallel = Math.min(distance, deltaParallel);
deltaPerpendicular = Vec3.sum(Vec3.subtract(delta, Vec3.multiply(distance, targetDirection)),
deltaPerpendicular);
entityPositions[i] = curLoopEntityProps.position;
entityParentIDs[i] = curLoopEntityProps.parentID;
}
deltaPerpendicular = Vec3.multiply(1 / pastedEntityIDs.length, deltaPerpendicular);
deltaPosition = Vec3.sum(Vec3.multiply(deltaParallel, targetDirection), deltaPerpendicular);
}
if (grid.getSnapToGrid()) {
var firstEntityProps = Entities.getEntityProperties(pastedEntityIDs[0], ["position", "dimensions",
"registrationPoint"]);
var positionPreSnap = Vec3.sum(deltaPosition, firstEntityProps.position);
position = grid.snapToSurface(grid.snapToGrid(positionPreSnap, false, firstEntityProps.dimensions,
firstEntityProps.registrationPoint), firstEntityProps.dimensions, firstEntityProps.registrationPoint);
deltaPosition = Vec3.subtract(position, firstEntityProps.position);
}
if (!Vec3.equal(deltaPosition, Vec3.ZERO)) {
for (var editEntityIndex = 0, numEntities = pastedEntityIDs.length; editEntityIndex < numEntities; editEntityIndex++) {
if (Uuid.isNull(entityParentIDs[editEntityIndex])) {
Entities.editEntity(pastedEntityIDs[editEntityIndex], {
position: Vec3.sum(deltaPosition, entityPositions[editEntityIndex])
});
}
}
}
}
if (isWearable) {
// apply the relative offsets saved during checkout
var offsets = {};
if (wearableLocalPosition) {
offsets.localPosition = wearableLocalPosition;
}
if (wearableLocalRotation) {
offsets.localRotation = wearableLocalRotation;
}
if (wearableLocalDimensions) {
offsets.localDimensions = wearableLocalDimensions;
} else if (wearableDimensions) {
offsets.dimensions = wearableDimensions;
}
// we currently assume a wearable is a single entity
Entities.editEntity(pastedEntityIDs[0], offsets);
}
var rezPosition = Entities.getEntityProperties(pastedEntityIDs[0], "position").position;
Audio.playSound(REZZING_SOUND, {
volume: 1.0,
position: rezPosition,
localOnly: true
});
} else {
Window.notifyEditError("Can't import entities: entities would be out of bounds.");
}
} else {
Window.notifyEditError("There was an error importing the entity file.");
}
}
var referrerURL; // Used for updating Purchases QML
var filterText; // Used for updating Purchases QML
function onWebEventReceived(message) {
message = JSON.parse(message);
if (message.type === GOTO_DIRECTORY) {
// This is the chooser between marketplaces. Only OUR markteplace
// requires/makes-use-of wallet, so doesn't go through openMarketplace bottleneck.
ui.open(MARKETPLACES_URL, MARKETPLACES_INJECT_SCRIPT_URL);
} else if (message.type === QUERY_CAN_WRITE_ASSETS) {
ui.sendToHtml(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets());
} else if (message.type === WARN_USER_NO_PERMISSIONS) {
Window.alert(NO_PERMISSIONS_ERROR_MESSAGE);
} else if (message.type === CLARA_IO_STATUS) {
if (isDownloadBeingCancelled) {
return;
}
var text = message.status;
if (messageBox === null) {
messageBox = Window.openMessageBox(CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON);
} else {
Window.updateMessageBox(messageBox, CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON);
}
return;
} else if (message.type === CLARA_IO_DOWNLOAD) {
if (messageBox !== null) {
Window.closeMessageBox(messageBox);
messageBox = null;
}
return;
} else if (message.type === CLARA_IO_CANCELLED_DOWNLOAD) {
isDownloadBeingCancelled = false;
} else if (message.type === "CHECKOUT") {
wireQmlEventBridge(true);
ui.open(MARKETPLACE_CHECKOUT_QML_PATH);
ui.tablet.sendToQml({
method: 'updateCheckoutQML',
params: message
});
} else if (message.type === "REQUEST_SETTING") {
sendCommerceSettings();
} else if (message.type === "PURCHASES") {
referrerURL = message.referrerURL;
filterText = "";
ui.open(MARKETPLACE_PURCHASES_QML_PATH);
} else if (message.type === "LOGIN") {
openLoginWindow();
} else if (message.type === "WALLET_SETUP") {
setupWallet('marketplace cta');
} else if (message.type === "MY_ITEMS") {
referrerURL = MARKETPLACE_URL_INITIAL;
filterText = "";
ui.open(MARKETPLACE_PURCHASES_QML_PATH);
wireQmlEventBridge(true);
ui.tablet.sendToQml({
method: 'purchases_showMyItems'
});
}
}
var sendAssetRecipient;
var sendAssetParticleEffectUpdateTimer;
var particleEffectTimestamp;
var sendAssetParticleEffect;
var SEND_ASSET_PARTICLE_TIMER_UPDATE = 250;
var SEND_ASSET_PARTICLE_EMITTING_DURATION = 3000;
var SEND_ASSET_PARTICLE_LIFETIME_SECONDS = 8;
var SEND_ASSET_PARTICLE_PROPERTIES = {
accelerationSpread: { x: 0, y: 0, z: 0 },
alpha: 1,
alphaFinish: 1,
alphaSpread: 0,
alphaStart: 1,
azimuthFinish: 0,
azimuthStart: -6,
color: { red: 255, green: 222, blue: 255 },
colorFinish: { red: 255, green: 229, blue: 225 },
colorSpread: { red: 0, green: 0, blue: 0 },
colorStart: { red: 243, green: 255, blue: 255 },
emitAcceleration: { x: 0, y: 0, z: 0 }, // Immediately gets updated to be accurate
emitDimensions: { x: 0, y: 0, z: 0 },
emitOrientation: { x: 0, y: 0, z: 0 },
emitRate: 4,
emitSpeed: 2.1,
emitterShouldTrail: true,
isEmitting: 1,
lifespan: SEND_ASSET_PARTICLE_LIFETIME_SECONDS + 1, // Immediately gets updated to be accurate
lifetime: SEND_ASSET_PARTICLE_LIFETIME_SECONDS + 1,
maxParticles: 20,
name: 'asset-particles',
particleRadius: 0.2,
polarFinish: 0,
polarStart: 0,
radiusFinish: 0.05,
radiusSpread: 0,
radiusStart: 0.2,
speedSpread: 0,
textures: "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle-HFC.png",
type: 'ParticleEffect'
};
function updateSendAssetParticleEffect() {
var timestampNow = Date.now();
if ((timestampNow - particleEffectTimestamp) > (SEND_ASSET_PARTICLE_LIFETIME_SECONDS * 1000)) {
deleteSendAssetParticleEffect();
return;
} else if ((timestampNow - particleEffectTimestamp) > SEND_ASSET_PARTICLE_EMITTING_DURATION) {
Entities.editEntity(sendAssetParticleEffect, {
isEmitting: 0
});
} else if (sendAssetParticleEffect) {
var recipientPosition = AvatarList.getAvatar(sendAssetRecipient).position;
var distance = Vec3.distance(recipientPosition, MyAvatar.position);
var accel = Vec3.subtract(recipientPosition, MyAvatar.position);
accel.y -= 3.0;
var life = Math.sqrt(2 * distance / Vec3.length(accel));
Entities.editEntity(sendAssetParticleEffect, {
emitAcceleration: accel,
lifespan: life
});
}
}
function deleteSendAssetParticleEffect() {
if (sendAssetParticleEffectUpdateTimer) {
Script.clearInterval(sendAssetParticleEffectUpdateTimer);
sendAssetParticleEffectUpdateTimer = null;
}
if (sendAssetParticleEffect) {
sendAssetParticleEffect = Entities.deleteEntity(sendAssetParticleEffect);
}
sendAssetRecipient = null;
}
var savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview");
var UI_FADE_TIMEOUT_MS = 150;
function maybeEnableHMDPreview() {
// Set a small timeout to prevent sensitive data from being shown during UI fade
Script.setTimeout(function () {
setTabletVisibleInSecondaryCamera(true);
DesktopPreviewProvider.setPreviewDisabledReason("USER");
Menu.setIsOptionChecked("Disable Preview", savedDisablePreviewOption);
}, UI_FADE_TIMEOUT_MS);
}
var resourceObjectsInTest = [];
function signalNewResourceObjectInTest(resourceObject) {
ui.tablet.sendToQml({
method: "newResourceObjectInTest",
resourceObject: resourceObject
});
}
var onQmlMessageReceived = function onQmlMessageReceived(message) {
if (message.messageSrc === "HTML") {
return;
}
switch (message.method) {
case 'gotoBank':
ui.close();
if (Account.metaverseServerURL.indexOf("staging") >= 0) {
Window.location = "hifi://hifiqa-master-metaverse-staging"; // So that we can test in staging.
} else {
Window.location = "hifi://BankOfHighFidelity";
}
break;
case 'purchases_openWallet':
case 'checkout_openWallet':
case 'checkout_setUpClicked':
openWallet();
break;
case 'purchases_walletNotSetUp':
wireQmlEventBridge(true);
ui.tablet.sendToQml({
method: 'updateWalletReferrer',
referrer: "purchases"
});
openWallet();
break;
case 'checkout_walletNotSetUp':
wireQmlEventBridge(true);
ui.tablet.sendToQml({
method: 'updateWalletReferrer',
referrer: message.referrer === "itemPage" ? message.itemId : message.referrer
});
openWallet();
break;
case 'checkout_cancelClicked':
openMarketplace(message.params);
break;
case 'header_goToPurchases':
case 'checkout_goToPurchases':
referrerURL = MARKETPLACE_URL_INITIAL;
filterText = message.filterText;
ui.open(MARKETPLACE_PURCHASES_QML_PATH);
break;
case 'checkout_itemLinkClicked':
openMarketplace(message.itemId);
break;
case 'checkout_continueShopping':
openMarketplace();
break;
case 'purchases_itemInfoClicked':
var itemId = message.itemId;
if (itemId && itemId !== "") {
openMarketplace(itemId);
}
break;
case 'checkout_rezClicked':
case 'purchases_rezClicked':
case 'tester_rezClicked':
print("!!!!! marketplaces tester_rezClicked");
rezEntity(message.itemHref, message.itemType, message.itemId);
break;
case 'tester_newResourceObject':
var resourceObject = message.resourceObject;
resourceObjectsInTest = []; // REMOVE THIS once we support specific referrers
resourceObject.currentlyRecordingResources = false;
resourceObject.resourceAccessEventText = "";
resourceObjectsInTest[resourceObject.resourceObjectId] = resourceObject;
signalNewResourceObjectInTest(resourceObject);
break;
case 'tester_updateResourceObjectAssetType':
resourceObjectsInTest[message.objectId].assetType = message.assetType;
break;
case 'tester_deleteResourceObject':
delete resourceObjectsInTest[message.objectId];
break;
case 'tester_updateResourceRecordingStatus':
resourceObjectsInTest[message.objectId].currentlyRecordingResources = message.status;
if (message.status) {
resourceObjectsInTest[message.objectId].resourceAccessEventText = "";
}
break;
case 'header_marketplaceImageClicked':
case 'purchases_backClicked':
openMarketplace(message.referrerURL);
break;
case 'purchases_goToMarketplaceClicked':
openMarketplace();
break;
case 'updateItemClicked':
openMarketplace(message.upgradeUrl + "?edition=" + message.itemEdition);
break;
case 'giftAsset':
break;
case 'passphrasePopup_cancelClicked':
case 'needsLogIn_cancelClicked':
// Should/must NOT check for wallet setup.
ui.open(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL);
break;
case 'needsLogIn_loginClicked':
openLoginWindow();
break;
case 'disableHmdPreview':
if (!savedDisablePreviewOption) {
DesktopPreviewProvider.setPreviewDisabledReason("SECURE_SCREEN");
Menu.setIsOptionChecked("Disable Preview", true);
setTabletVisibleInSecondaryCamera(false);
}
break;
case 'maybeEnableHmdPreview':
maybeEnableHMDPreview();
break;
case 'purchases_openGoTo':
ui.open("hifi/tablet/TabletAddressDialog.qml");
break;
case 'purchases_itemCertificateClicked':
contextOverlayEntity = "";
setCertificateInfo(contextOverlayEntity, message.itemCertificateId);
break;
case 'inspectionCertificate_closeClicked':
ui.close();
break;
case 'inspectionCertificate_requestOwnershipVerification':
ContextOverlay.requestOwnershipVerification(message.entity);
break;
case 'inspectionCertificate_showInMarketplaceClicked':
openMarketplace(message.marketplaceUrl);
break;
case 'header_myItemsClicked':
referrerURL = MARKETPLACE_URL_INITIAL;
filterText = "";
ui.open(MARKETPLACE_PURCHASES_QML_PATH);
wireQmlEventBridge(true);
ui.tablet.sendToQml({
method: 'purchases_showMyItems'
});
break;
case 'refreshConnections':
// Guard to prevent this code from being executed while sending money --
// we only want to execute this while sending non-HFC gifts
if (!onWalletScreen) {
print('Refreshing Connections...');
getConnectionData(false);
}
break;
case 'enable_ChooseRecipientNearbyMode':
// Guard to prevent this code from being executed while sending money --
// we only want to execute this while sending non-HFC gifts
if (!onWalletScreen) {
if (!isUpdateOverlaysWired) {
Script.update.connect(updateOverlays);
isUpdateOverlaysWired = true;
}
}
break;
case 'disable_ChooseRecipientNearbyMode':
// Guard to prevent this code from being executed while sending money --
// we only want to execute this while sending non-HFC gifts
if (!onWalletScreen) {
if (isUpdateOverlaysWired) {
Script.update.disconnect(updateOverlays);
isUpdateOverlaysWired = false;
}
removeOverlays();
}
break;
case 'purchases_availableUpdatesReceived':
shouldShowDot = message.numUpdates > 0;
ui.messagesWaiting(shouldShowDot && !ui.isOpen);
break;
case 'purchases_updateWearables':
var currentlyWornWearables = [];
var ATTACHMENT_SEARCH_RADIUS = 100; // meters (just in case)
var nearbyEntities = Entities.findEntitiesByType('Model', MyAvatar.position, ATTACHMENT_SEARCH_RADIUS);
for (var i = 0; i < nearbyEntities.length; i++) {
var currentProperties = Entities.getEntityProperties(
nearbyEntities[i], ['certificateID', 'editionNumber', 'parentID']
);
if (currentProperties.parentID === MyAvatar.sessionUUID) {
currentlyWornWearables.push({
entityID: nearbyEntities[i],
entityCertID: currentProperties.certificateID,
entityEdition: currentProperties.editionNumber
});
}
}
ui.tablet.sendToQml({ method: 'updateWearables', wornWearables: currentlyWornWearables });
break;
case 'sendAsset_sendPublicly':
if (message.assetName !== "") {
deleteSendAssetParticleEffect();
sendAssetRecipient = message.recipient;
var props = SEND_ASSET_PARTICLE_PROPERTIES;
props.parentID = MyAvatar.sessionUUID;
props.position = MyAvatar.position;
props.position.y += 0.2;
if (message.effectImage) {
props.textures = message.effectImage;
}
sendAssetParticleEffect = Entities.addEntity(props, true);
particleEffectTimestamp = Date.now();
updateSendAssetParticleEffect();
sendAssetParticleEffectUpdateTimer = Script.setInterval(updateSendAssetParticleEffect,
SEND_ASSET_PARTICLE_TIMER_UPDATE);
}
break;
case 'http.request':
// Handled elsewhere, don't log.
break;
case 'goToPurchases_fromWalletHome': // HRS FIXME What's this about?
break;
default:
print('Unrecognized message from Checkout.qml or Purchases.qml: ' + JSON.stringify(message));
}
};
function pushResourceObjectsInTest() {
var maxResourceObjectId = -1;
var length = resourceObjectsInTest.length;
for (var i = 0; i < length; i++) {
if (i in resourceObjectsInTest) {
signalNewResourceObjectInTest(resourceObjectsInTest[i]);
var resourceObjectId = resourceObjectsInTest[i].resourceObjectId;
maxResourceObjectId = (maxResourceObjectId < resourceObjectId) ? parseInt(resourceObjectId) : maxResourceObjectId;
}
}
// N.B. Thinking about removing the following sendToQml? Be sure
// that the marketplace item tester QML has heard from us, at least
// so that it can indicate to the user that all of the resoruce
// objects in test have been transmitted to it.
//ui.tablet.sendToQml({ method: "nextObjectIdInTest", id: maxResourceObjectId + 1 });
// Since, for now, we only support 1 object in test, always send id: 0
ui.tablet.sendToQml({ method: "nextObjectIdInTest", id: 0 });
}
// Function Name: onTabletScreenChanged()
//
// Description:
// -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string
// value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML.
var onCommerceScreen = false;
var onInspectionCertificateScreen = false;
var onMarketplaceItemTesterScreen = false;
var onMarketplaceScreen = false;
var onWalletScreen = false;
var onTabletScreenChanged = function onTabletScreenChanged(type, url) {
ui.setCurrentVisibleScreenMetadata(type, url);
onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1;
onInspectionCertificateScreen = type === "QML" && url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1;
var onWalletScreenNow = url.indexOf(MARKETPLACE_WALLET_QML_PATH) !== -1;
var onCommerceScreenNow = type === "QML" && (
url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH) !== -1 ||
url === MARKETPLACE_PURCHASES_QML_PATH ||
url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1);
var onMarketplaceItemTesterScreenNow = (
url.indexOf(MARKETPLACE_ITEM_TESTER_QML_PATH) !== -1 ||
url === MARKETPLACE_ITEM_TESTER_QML_PATH);
if ((!onWalletScreenNow && onWalletScreen) ||
(!onCommerceScreenNow && onCommerceScreen) ||
(!onMarketplaceItemTesterScreenNow && onMarketplaceScreen)
) {
// exiting wallet, commerce, or marketplace item tester screen
maybeEnableHMDPreview();
}
onCommerceScreen = onCommerceScreenNow;
onWalletScreen = onWalletScreenNow;
onMarketplaceItemTesterScreen = onMarketplaceItemTesterScreenNow;
wireQmlEventBridge(onCommerceScreen || onWalletScreen || onMarketplaceItemTesterScreen);
if (url === MARKETPLACE_PURCHASES_QML_PATH) {
// FIXME? Is there a race condition here in which the event
// bridge may not be up yet? If so, Script.setTimeout(..., 750)
// may help avoid the condition.
ui.tablet.sendToQml({
method: 'updatePurchases',
referrerURL: referrerURL,
filterText: filterText
});
}
ui.isOpen = (onMarketplaceScreen || onCommerceScreen) && !onWalletScreen;
ui.buttonActive(ui.isOpen);
if (type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1) {
ContextOverlay.isInMarketplaceInspectionMode = true;
} else {
ContextOverlay.isInMarketplaceInspectionMode = false;
}
if (onInspectionCertificateScreen) {
setCertificateInfo(contextOverlayEntity);
}
if (onCommerceScreen) {
if (!isWired) {
Users.usernameFromIDReply.connect(usernameFromIDReply);
Controller.mousePressEvent.connect(handleMouseEvent);
Controller.mouseMoveEvent.connect(handleMouseMoveEvent);
triggerMapping.enable();
triggerPressMapping.enable();
}
isWired = true;
Wallet.refreshWalletStatus();
} else {
if (onMarketplaceScreen) {
onMarketplaceOpen('marketplace cta');
}
ui.tablet.sendToQml({
method: 'inspectionCertificate_resetCert'
});
off();
}
if (onMarketplaceItemTesterScreen) {
// Why setTimeout? The QML event bridge, wired above, requires a
// variable amount of time to come up, in practice less than
// 750ms.
Script.setTimeout(pushResourceObjectsInTest, 750);
}
console.debug(ui.buttonName + " app reports: Tablet screen changed.\nNew screen type: " + type +
"\nNew screen URL: " + url + "\nCurrent app open status: " + ui.isOpen + "\n");
};
function notificationDataProcessPage(data) {
return data.data.updates;
}
var shouldShowDot = false;
function notificationPollCallback(updatesArray) {
shouldShowDot = shouldShowDot || updatesArray.length > 0;
ui.messagesWaiting(shouldShowDot && !ui.isOpen);
if (updatesArray.length > 0) {
var message;
if (!ui.notificationInitialCallbackMade) {
message = updatesArray.length + " of your purchased items " +
(updatesArray.length === 1 ? "has an update " : "have updates ") +
"available. Open MARKET to update.";
ui.notificationDisplayBanner(message);
ui.notificationPollCaresAboutSince = true;
} else {
for (var i = 0; i < updatesArray.length; i++) {
message = "Update available for \"" +
updatesArray[i].base_item_title + "\"." +
"Open MARKET to update.";
ui.notificationDisplayBanner(message);
}
}
}
}
function isReturnedDataEmpty(data) {
var historyArray = data.data.updates;
return historyArray.length === 0;
}
var BUTTON_NAME = "MARKET";
var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace";
var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page.
var ui;
function startup() {
ui = new AppUi({
buttonName: BUTTON_NAME,
sortOrder: 9,
inject: MARKETPLACES_INJECT_SCRIPT_URL,
home: MARKETPLACE_URL_INITIAL,
onScreenChanged: onTabletScreenChanged,
onMessage: onQmlMessageReceived,
notificationPollEndpoint: "/api/v1/commerce/available_updates?per_page=10",
notificationPollTimeoutMs: 300000,
notificationDataProcessPage: notificationDataProcessPage,
notificationPollCallback: notificationPollCallback,
notificationPollStopPaginatingConditionMet: isReturnedDataEmpty,
notificationPollCaresAboutSince: false // Changes to true after first poll
});
ContextOverlay.contextOverlayClicked.connect(openInspectionCertificateQML);
Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged);
GlobalServices.myUsernameChanged.connect(onUsernameChanged);
ui.tablet.webEventReceived.connect(onWebEventReceived);
Wallet.walletStatusChanged.connect(sendCommerceSettings);
Window.messageBoxClosed.connect(onMessageBoxClosed);
ResourceRequestObserver.resourceRequestEvent.connect(onResourceRequestEvent);
Wallet.refreshWalletStatus();
}
var isWired = false;
var isUpdateOverlaysWired = false;
function off() {
if (isWired) {
Users.usernameFromIDReply.disconnect(usernameFromIDReply);
Controller.mousePressEvent.disconnect(handleMouseEvent);
Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent);
triggerMapping.disable();
triggerPressMapping.disable();
isWired = false;
}
if (isUpdateOverlaysWired) {
Script.update.disconnect(updateOverlays);
isUpdateOverlaysWired = false;
}
removeOverlays();
}
function shutdown() {
maybeEnableHMDPreview();
deleteSendAssetParticleEffect();
Window.messageBoxClosed.disconnect(onMessageBoxClosed);
Wallet.walletStatusChanged.disconnect(sendCommerceSettings);
ui.tablet.webEventReceived.disconnect(onWebEventReceived);
GlobalServices.myUsernameChanged.disconnect(onUsernameChanged);
Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged);
ContextOverlay.contextOverlayClicked.disconnect(openInspectionCertificateQML);
ResourceRequestObserver.resourceRequestEvent.disconnect(onResourceRequestEvent);
off();
}
//
// Run the functions.
//
startup();
Script.scriptEnding.connect(shutdown);
}()); // END LOCAL_SCOPE