437 lines
17 KiB
JavaScript
437 lines
17 KiB
JavaScript
'use strict';
|
|
// smartboardZoneClient.js
|
|
//
|
|
// Milad Nazeri and Zach Fox 10/30/2019
|
|
// Copyright 2019 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
|
|
|
|
/* globals Screenshare */
|
|
|
|
(function() {
|
|
var DEBUG = false;
|
|
|
|
// BOARD UI
|
|
// If a new avatar enters the Smartboard Zone:
|
|
// 1. This client script will call `registerParticipant()` on the Smartboard Zone server script.
|
|
// 2. The server script will do some stuff, then call `receiveBoardState()` (below) on the client
|
|
// who just registered.
|
|
// If the ESS detects that the broadcasting avatar has left the domain:
|
|
// 1. The Smartboard Zone server script will call `receiveBoardState()` (below) on all clients
|
|
// whose avatars are inside the Smartboard zone.
|
|
function receiveBoardState(id, args) {
|
|
_this.currentBoardState = args[0];
|
|
_this.activePresenterUUID = args[1];
|
|
|
|
if (DEBUG) {
|
|
console.log("args:", args, "\nsmartboardZoneClient.js: " + _this.entityID + ": `receiveBoardState()`." +
|
|
"\n`_this.currentBoardState`: " + _this.currentBoardState +
|
|
"\n`_this.activePresenterUUID`: " + _this.activePresenterUUID);
|
|
}
|
|
|
|
maybeRemoveLocalScreenshareButton();
|
|
maybeAddLocalButton();
|
|
|
|
if (_this.currentBoardState === "whiteboard") {
|
|
createRandomPaintSphere();
|
|
maybeRemoveLocalPresenterDisplayName();
|
|
maybeRemoveLocalSmartboardScreenshareGlass();
|
|
// This will also delete the local web entity if there is one.
|
|
Screenshare.stopScreenshare();
|
|
} else if (_this.currentBoardState === "screenshare") {
|
|
maybeCreateLocalPresenterDisplayName();
|
|
maybeCreateLocalSmartboardScreenshareGlass();
|
|
maybeRemovePaintSpheres();
|
|
Screenshare.startScreenshare(_this.entityID, _this.smartboard, _this.activePresenterUUID === MyAvatar.sessionUUID);
|
|
} else {
|
|
console.log("smartboardZoneClient.js: " + _this.entityID + ": `receiveBoardState()`." + " Unhandled state.");
|
|
}
|
|
|
|
setButtonActivePresenterUUID();
|
|
}
|
|
|
|
|
|
// Updates the state of the local entity buttons.
|
|
// Send the `activePresenterUUID` (even if it's empty) - that helps the buttons know who can change the Smartboard's state.
|
|
function setButtonActivePresenterUUID() {
|
|
if (!buttonIsReady || !_this.screenshareStartStopButtonID) {
|
|
return;
|
|
}
|
|
|
|
if (DEBUG) {
|
|
console.log("smartboardZoneClient.js: " + _this.entityID + ": `setButtonActivePresenterUUID()`.");
|
|
}
|
|
|
|
Entities.callEntityMethod(_this.screenshareStartStopButtonID, "setActivePresenterUUID",
|
|
[_this.currentBoardState, _this.activePresenterUUID]);
|
|
}
|
|
|
|
|
|
// Signal to make sure the buttons are loaded when first entering the zone
|
|
// for help avoiding race conditions on the initial state setup
|
|
var buttonIsReady = false;
|
|
function buttonPreloadComplete() {
|
|
if (DEBUG) {
|
|
console.log("smartboardZoneClient.js: " + _this.entityID + ": `buttonPreloadComplete()`.");
|
|
}
|
|
|
|
buttonIsReady = true;
|
|
setButtonActivePresenterUUID();
|
|
}
|
|
|
|
|
|
// Create the local screenshare and whiteboard buttons when someone enters
|
|
// Probably will need to add an additional "zoom" button
|
|
var boardDimensions;
|
|
var entityOffsetFromBoard;
|
|
var margin = 0.025;
|
|
var HALF = 2;
|
|
var STATIC_BUTTON_PROPS = {
|
|
type: "Model",
|
|
script: Script.resolvePath("./boardButtonClient.js"),
|
|
localPosition: {x: 1.2814, y: 1.1614, z: 0.0837},
|
|
dimensions: {x: 0.9985, y: 0.1249, z: 0.0234},
|
|
visible: false,
|
|
grab: {
|
|
grabbable: false
|
|
}
|
|
};
|
|
function maybeAddLocalButton() {
|
|
if ((_this.currentBoardState === "screenshare" && _this.activePresenterUUID !== MyAvatar.sessionUUID)
|
|
|| _this.screenshareStartStopButtonID) {
|
|
return;
|
|
}
|
|
|
|
if (DEBUG) {
|
|
console.log("smartboardZoneClient.js: " + _this.entityID + ": `maybeAddLocalButton()`.");
|
|
}
|
|
|
|
if (!_this.smartboard) {
|
|
_this.smartboard = Entities.getEntityProperties(_this.entityID, 'parentID').parentID;
|
|
}
|
|
|
|
if (!_this.smartboard) {
|
|
console.log("smartboardZoneClient.js: " + _this.entityID + ": Couldn't get smartboard ID. Can't setup local start/stop button.");
|
|
return;
|
|
}
|
|
|
|
if (!_this.whiteBoardOnlyZone) {
|
|
var buttonProps = STATIC_BUTTON_PROPS;
|
|
buttonProps.parentID = _this.smartboard;
|
|
buttonProps.name = "Smartboard ScreenshareStartStop Button";
|
|
_this.screenshareStartStopButtonID = Entities.addEntity(buttonProps, "local");
|
|
}
|
|
}
|
|
|
|
|
|
// remove the local buttons when someone leaves the zone
|
|
function maybeRemoveLocalScreenshareButton() {
|
|
if (DEBUG) {
|
|
console.log("smartboardZoneClient.js: " + _this.entityID + ": `maybeRemoveLocalScreenshareButton()`.");
|
|
}
|
|
|
|
if (_this.screenshareStartStopButtonID) {
|
|
Entities.deleteEntity(_this.screenshareStartStopButtonID);
|
|
}
|
|
_this.screenshareStartStopButtonID = null;
|
|
}
|
|
|
|
|
|
// Grab the presenter's display name and create a text entity above the smartboard.
|
|
// After it is created, get the text size for positioning.
|
|
var DEFAULT_TEXTBOX_PROPS = {
|
|
type: "Text",
|
|
name: "Smartboard Presenting Text",
|
|
backgroundAlpha: 0.0,
|
|
grab: {
|
|
grabbable: false
|
|
}
|
|
};
|
|
var LINE_HEIGHT = 0.1;
|
|
var PRESENTER_TEXT_DELAY_MS = 350;
|
|
var TEXT_SIZE_WIDTH_BUFFER = 1.15; // textsize calculation from engine is slightly off
|
|
// Empirical value at the moment. May adjust when moving to dynamic scaling.
|
|
var MAXIMUM_NAME_LENGTH_ALLOWED = 17;
|
|
var REPLACEMENT_TEXT = "...";
|
|
var DEFAULT_TEXT_ENTITY_WIDTH = 1.5;
|
|
function maybeCreateLocalPresenterDisplayName() {
|
|
if (_this.localPresenterDisplayName) {
|
|
return;
|
|
}
|
|
|
|
if (!_this.smartboard) {
|
|
_this.smartboard = Entities.getEntityProperties(_this.entityID, 'parentID').parentID;
|
|
}
|
|
|
|
if (!_this.smartboard) {
|
|
console.log("smartboardZoneClient.js: " + _this.entityID + ": Couldn't get smartboard ID. Can't create presenter display name.");
|
|
return;
|
|
}
|
|
|
|
var textProps = DEFAULT_TEXTBOX_PROPS;
|
|
var displayName = AvatarManager.getAvatar(_this.activePresenterUUID).displayName;
|
|
displayName = displayName.length > MAXIMUM_NAME_LENGTH_ALLOWED
|
|
? displayName.substring(0, MAXIMUM_NAME_LENGTH_ALLOWED) + REPLACEMENT_TEXT
|
|
: displayName;
|
|
|
|
textProps.parentID = _this.smartboard;
|
|
textProps.dimensions = {x: DEFAULT_TEXT_ENTITY_WIDTH, y: LINE_HEIGHT, z: 0.1009};
|
|
textProps.localPosition = {x: 0, y: 1.1563, z: entityOffsetFromBoard + margin};
|
|
textProps.text = displayName + " is presenting";
|
|
textProps.visible = false;
|
|
_this.localPresenterDisplayName = Entities.addEntity(textProps, "local");
|
|
|
|
Script.setTimeout(function() {
|
|
var textSize = Entities.textSize(_this.localPresenterDisplayName, textProps.text);
|
|
textProps.dimensions.x = textSize.width * TEXT_SIZE_WIDTH_BUFFER;
|
|
var newProps = {
|
|
dimensions: textProps.dimensions,
|
|
visible: true
|
|
};
|
|
Entities.editEntity(_this.localPresenterDisplayName, newProps);
|
|
}, PRESENTER_TEXT_DELAY_MS);
|
|
}
|
|
|
|
|
|
// Check to see if there is a presenter name text box to remove
|
|
function maybeRemoveLocalPresenterDisplayName() {
|
|
if (_this.localPresenterDisplayName) {
|
|
Entities.deleteEntity(_this.localPresenterDisplayName);
|
|
}
|
|
_this.localPresenterDisplayName = false;
|
|
}
|
|
|
|
var DEFAULT_SMARTBOARD_SCREENSHARE_GLASS_PROPS = {
|
|
type: "Model",
|
|
modelURL: Script.resolvePath("../resources/models/screen-share-glass.fbx"),
|
|
localPosition: {x: 0.0, y: -0.0748, z: 0.0475},
|
|
dimensions: {x: 3.8642, y: 2.2785, z: 0.0023},
|
|
grab: {
|
|
grabbable: false
|
|
}
|
|
}
|
|
function maybeCreateLocalSmartboardScreenshareGlass() {
|
|
if (_this.localSmartboardScreenshareGlass) {
|
|
return;
|
|
}
|
|
|
|
if (!_this.smartboard) {
|
|
_this.smartboard = Entities.getEntityProperties(_this.entityID, 'parentID').parentID;
|
|
}
|
|
|
|
if (!_this.smartboard) {
|
|
console.log("smartboardZoneClient.js: " + _this.entityID + ": Couldn't get smartboard ID. Can't create smartboard screenshare glass.");
|
|
return;
|
|
}
|
|
|
|
var localSmartboardScreenshareGlassProps = DEFAULT_SMARTBOARD_SCREENSHARE_GLASS_PROPS;
|
|
localSmartboardScreenshareGlassProps.parentID = _this.smartboard;
|
|
localSmartboardScreenshareGlassProps.name = "Smartboard Screenshare Glass";
|
|
|
|
_this.localSmartboardScreenshareGlass = Entities.addEntity(localSmartboardScreenshareGlassProps, "local");
|
|
|
|
}
|
|
|
|
function maybeRemoveLocalSmartboardScreenshareGlass() {
|
|
if (_this.localSmartboardScreenshareGlass) {
|
|
Entities.deleteEntity(_this.localSmartboardScreenshareGlass);
|
|
}
|
|
_this.localSmartboardScreenshareGlass = false;
|
|
}
|
|
|
|
// If there is a problem with screenshare, then update the board back to whiteboard mode
|
|
function onScreenshareError() {
|
|
if (DEBUG) {
|
|
console.log("smartboardZoneClient.js: " + _this.entityID + ": `onScreenshareError()`.");
|
|
}
|
|
|
|
// We pass `MyAvatar.sessionUUID` because the server needs to check if the user trying to update the current board
|
|
// state is allowed to do that.
|
|
Entities.callEntityServerMethod(_this.entityID, "updateCurrentBoardState", ["whiteboard", MyAvatar.sessionUUID]);
|
|
}
|
|
|
|
|
|
// Similar to function above but related to the process being terminated and not necessarily for an error
|
|
function onScreenshareProcessTerminated() {
|
|
if (DEBUG) {
|
|
console.log("smartboardZoneClient.js: " + _this.entityID + ": `onScreenshareProcessTerminated()`.");
|
|
}
|
|
|
|
// We pass `MyAvatar.sessionUUID` because the server needs to check if the user trying to update the current board
|
|
// state is allowed to do that.
|
|
Entities.callEntityServerMethod(_this.entityID, "updateCurrentBoardState", ["whiteboard", MyAvatar.sessionUUID]);
|
|
}
|
|
|
|
|
|
function getPaletteSquares() {
|
|
_this.paletteSquares = [];
|
|
_this.smartboardChildrenIDs = Entities.getChildrenIDs(_this.smartboard);
|
|
_this.smartboardChildrenIDs.forEach(function(smartboardPiece) {
|
|
MyAvatar.disableHandTouchForID(smartboardPiece);
|
|
var name = Entities.getEntityProperties(smartboardPiece, 'name').name;
|
|
if (name === "Smartboard Palette Square") {
|
|
_this.paletteSquares.push(smartboardPiece);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// ENTITY SIGNALS
|
|
// 1. get the smartboard parent, the dimensions/position of the board for button and web entity positioning
|
|
// 2. get the setup information: whether this is a whiteboard only zone
|
|
var signalsConnected = false;
|
|
var boardDimensions;
|
|
// Empirically determined. The following four values are closely linked:
|
|
// 1. The z-offset of whiteboard polylines (`STROKE_FORWARD_OFFSET_M` in `drawSphereClient.js`).
|
|
// 2. The z-offset of the screenshare local web entity (`LOCAL_WEB_ENTITY_Z_OFFSET` in `smartboardZoneClient.js`).
|
|
// 3. The z-offset of the screenshare "glass bezel" (`DEFAULT_SMARTBOARD_SCREENSHARE_GLASS_PROPS` in `smartboardZoneClient.js`).
|
|
// 4. The z-offset of the screenshare "status icon" (handled in the screenshare JSON file).
|
|
var LOCAL_WEB_ENTITY_Z_OFFSET = 0.0375;
|
|
function preload(entityID) {
|
|
_this.entityID = entityID;
|
|
_this.smartboard = Entities.getEntityProperties(_this.entityID, 'parentID').parentID;
|
|
|
|
if (DEBUG) {
|
|
console.log("smartboardZoneClient.js: " + _this.entityID + ": `preload()`.");
|
|
}
|
|
|
|
var boardProps = Entities.getEntityProperties(_this.smartboard, ['dimensions']);
|
|
boardDimensions = boardProps.dimensions;
|
|
entityOffsetFromBoard = boardDimensions.z / HALF + margin;
|
|
|
|
MyAvatar.disableHandTouchForID(_this.smartboard);
|
|
|
|
getPaletteSquares();
|
|
|
|
var userData = Entities.getEntityProperties(entityID, ['userData']).userData;
|
|
var parsedData = {};
|
|
try {
|
|
parsedData = JSON.parse(userData);
|
|
} catch (e) {
|
|
console.log("Error reading userData in smartboardZoneClient.js", e);
|
|
}
|
|
|
|
if (parsedData.whiteboardOnlyZone) {
|
|
_this.whiteboardOnlyZone = parsedData.whiteboardOnlyZone;
|
|
}
|
|
|
|
Screenshare.localWebEntityZOffset = LOCAL_WEB_ENTITY_Z_OFFSET;
|
|
}
|
|
|
|
|
|
// Make sure an avatar has a paint sphere when they enter the zone
|
|
function createRandomPaintSphere() {
|
|
if (DEBUG) {
|
|
console.log("smartboardZoneClient.js: " + _this.entityID +
|
|
": `createRandomPaintSphere()`. Making a random paintsphere...");
|
|
}
|
|
var numberPaletteSquares = _this.paletteSquares.length;
|
|
|
|
if (numberPaletteSquares === 0) {
|
|
getPaletteSquares();
|
|
}
|
|
|
|
if (numberPaletteSquares === 0) {
|
|
console.log("smartboardZoneClient.js: " + _this.entityID + ": Couldn't get palette squares.");
|
|
return;
|
|
}
|
|
|
|
var randomPaletteSquareIndex = Math.floor(Math.random() * numberPaletteSquares);
|
|
|
|
Entities.callEntityMethod(_this.paletteSquares[randomPaletteSquareIndex], 'createPaintSphere');
|
|
}
|
|
|
|
|
|
// 1. register the participant with the server to get the current board state
|
|
// 2. check to see if this is a smartboard only zone
|
|
// 3. enable the smartboard functions
|
|
function enterEntity() {
|
|
if (DEBUG) {
|
|
console.log("smartboardZoneClient.js: " + _this.entityID +
|
|
": `enterEntity()`. Registering participant and creating local buttons...");
|
|
}
|
|
|
|
if (!signalsConnected) {
|
|
Screenshare.screenshareProcessTerminated.connect(onScreenshareProcessTerminated);
|
|
Screenshare.screenshareError.connect(onScreenshareError);
|
|
signalsConnected = true;
|
|
}
|
|
|
|
Entities.callEntityServerMethod(_this.entityID, "registerParticipant", [MyAvatar.sessionUUID]);
|
|
}
|
|
|
|
|
|
// Check for existing paint sphere and delete if found
|
|
function maybeRemovePaintSpheres() {
|
|
MyAvatar.getAvatarEntitiesVariant().forEach(function(avatarEntity) {
|
|
var name = Entities.getEntityProperties(avatarEntity.id, 'name').name;
|
|
if (name && (name === "Smartboard Paint Sphere" || name === "Smartboard Paint Sphere Material")) {
|
|
Entities.deleteEntity(avatarEntity.id);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// remove the participant and remove paint sphere
|
|
function leaveEntity() {
|
|
if (DEBUG) {
|
|
console.log("smartboardZoneClient.js: " + _this.entityID + ": `leaveEntity()`. Stopping screenshare, " +
|
|
"removing local buttons, removing local web entity, and removing participant from server...");
|
|
}
|
|
Entities.callEntityServerMethod(_this.entityID, "removeParticipant", [MyAvatar.sessionUUID]);
|
|
unload();
|
|
}
|
|
|
|
|
|
// delete buttons and remove paintspheres
|
|
function unload() {
|
|
maybeRemovePaintSpheres();
|
|
// This will also delete the local web entity if there is one.
|
|
Screenshare.stopScreenshare();
|
|
maybeRemoveLocalScreenshareButton();
|
|
maybeRemoveLocalPresenterDisplayName();
|
|
maybeRemoveLocalSmartboardScreenshareGlass();
|
|
|
|
if (signalsConnected) {
|
|
Screenshare.screenshareError.disconnect(onScreenshareError);
|
|
Screenshare.screenshareProcessTerminated.disconnect(onScreenshareProcessTerminated);
|
|
}
|
|
signalsConnected = false;
|
|
}
|
|
|
|
|
|
// SMARTBOARD OBJECT
|
|
var _this;
|
|
function SmartboardZoneClient() {
|
|
_this = this;
|
|
this.activePresenterUUID = "";
|
|
this.currentBoardState = "screenshare";
|
|
this.entityID;
|
|
this.screenshareStartStopButtonID = null;
|
|
this.localPresenterDisplayName = null;
|
|
this.screenshareModeFirstActivated = false;
|
|
this.whiteboardOnlyZone = false;
|
|
this.smartboard;
|
|
this.smartboardChildrenIDs;
|
|
this.localSmartboardScreenshareGlass;
|
|
this.paletteSquares = [];
|
|
|
|
this.remotelyCallable = [
|
|
'receiveBoardState',
|
|
'buttonPreloadComplete'
|
|
];
|
|
}
|
|
|
|
SmartboardZoneClient.prototype = {
|
|
buttonPreloadComplete: buttonPreloadComplete,
|
|
setButtonActivePresenterUUID: setButtonActivePresenterUUID,
|
|
receiveBoardState: receiveBoardState,
|
|
preload: preload,
|
|
unload: unload,
|
|
enterEntity: enterEntity,
|
|
leaveEntity: leaveEntity
|
|
};
|
|
|
|
return new SmartboardZoneClient();
|
|
});
|