diff --git a/examples/controllers/hydra/paddleBall.js b/examples/controllers/hydra/paddleBall.js index d6c39131c8..fb312739f4 100644 --- a/examples/controllers/hydra/paddleBall.js +++ b/examples/controllers/hydra/paddleBall.js @@ -24,7 +24,8 @@ var BALL_COLOR = { red: 255, green: 0, blue: 0 }; var LINE_COLOR = { red: 255, green: 255, blue: 0 }; var PADDLE_BOX_OFFSET = { x: 0.05, y: 0.0, z: 0.0 }; -var HOLD_POSITION_OFFSET = { x: -0.15, y: 0.05, z: -0.05 }; +var HOLD_POSITION_LEFT_OFFSET = { x: -0.15, y: 0.05, z: -0.05 }; +var HOLD_POSITION_RIGHT_OFFSET = { x: -0.15, y: 0.05, z: 0.05 }; var PADDLE_ORIENTATION = Quat.fromPitchYawRollDegrees(0,0,0); var GRAVITY = 0.0; var SPRING_FORCE = 15.0; @@ -33,12 +34,19 @@ var gameOn = false; var leftHanded = true; var controllerID; -if (leftHanded) { - controllerID = 1; -} else { - controllerID = 3; + +function setControllerID() { + if (leftHanded) { + controllerID = 1; + } else { + controllerID = 3; + } } +setControllerID(); +Menu.addMenu("PaddleBall"); +Menu.addMenuItem({ menuName: "PaddleBall", menuItemName: "Left-Handed", isCheckable: true, isChecked: true }); + var screenSize = Controller.getViewportDimensions(); var offButton = Overlays.addOverlay("image", { x: screenSize.x - 48, @@ -96,7 +104,8 @@ function createEntities() { visible: true, lineWidth: 2 }); - MyAvatar.stopAnimation(leftHanded ? leftHandAnimation: rightHandAnimation); + MyAvatar.stopAnimation(leftHandAnimation); + MyAvatar.stopAnimation(rightHandAnimation); MyAvatar.startAnimation(leftHanded ? leftHandAnimation: rightHandAnimation, 15.0, 1.0, false, true, 0.0, 6); } @@ -129,9 +138,10 @@ function update(deltaTime) { if (!ball.isKnownID) { ball = Entities.identifyEntity(ball); } else { - var paddleWorldOrientation = Quat.multiply(Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(controllerID)), PADDLE_ORIENTATION); + var paddleOrientation = leftHanded ? PADDLE_ORIENTATION : Quat.multiply(PADDLE_ORIENTATION, Quat.fromPitchYawRollDegrees(0, 180, 0)); + var paddleWorldOrientation = Quat.multiply(Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(controllerID)), paddleOrientation); var holdPosition = Vec3.sum(leftHanded ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(), - Vec3.multiplyQbyV(paddleWorldOrientation, HOLD_POSITION_OFFSET)); + Vec3.multiplyQbyV(paddleWorldOrientation, leftHanded ? HOLD_POSITION_LEFT_OFFSET : HOLD_POSITION_RIGHT_OFFSET )); var props = Entities.getEntityProperties(ball); var spring = Vec3.subtract(holdPosition, props.position); @@ -172,15 +182,30 @@ function mousePressEvent(event) { } } +function menuItemEvent(menuItem) { + oldHanded = leftHanded; + if (menuItem == "Left-Handed") { + leftHanded = Menu.isOptionChecked("Left-Handed"); + } + if ((leftHanded != oldHanded) && gameOn) { + setControllerID(); + deleteEntities(); + createEntities(); + } +} + function scriptEnding() { if (gameOn) { deleteEntities(); } Overlays.deleteOverlay(offButton); - MyAvatar.stopAnimation(leftHanded ? leftHandAnimation: rightHandAnimation); + MyAvatar.stopAnimation(leftHandAnimation); + MyAvatar.stopAnimation(rightHandAnimation); + Menu.removeMenu("PaddleBall"); } Entities.entityCollisionWithEntity.connect(entityCollisionWithEntity); +Menu.menuItemEvent.connect(menuItemEvent); Controller.mousePressEvent.connect(mousePressEvent); Script.scriptEnding.connect(scriptEnding); Script.update.connect(update); diff --git a/examples/editEntities.js b/examples/editEntities.js index 083887819f..b446fe8fa7 100644 --- a/examples/editEntities.js +++ b/examples/editEntities.js @@ -12,34 +12,37 @@ // HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; -Script.include("libraries/stringHelpers.js"); -Script.include("libraries/dataviewHelpers.js"); -Script.include("libraries/httpMultiPart.js"); -Script.include("libraries/modelUploader.js"); -Script.include("libraries/toolBars.js"); -Script.include("libraries/progressDialog.js"); -Script.include("libraries/entitySelectionTool.js"); +Script.include([ + "http://public.highfidelity.io/scripts/libraries/stringHelpers.js", + "http://public.highfidelity.io/scripts/libraries/dataviewHelpers.js", + "http://public.highfidelity.io/scripts/libraries/httpMultiPart.js", + "http://public.highfidelity.io/scripts/libraries/modelUploader.js", + "http://public.highfidelity.io/scripts/libraries/toolBars.js", + "http://public.highfidelity.io/scripts/libraries/progressDialog.js", + + "http://public.highfidelity.io/scripts/libraries/entitySelectionTool.js", + "http://public.highfidelity.io/scripts/libraries/ModelImporter.js", + + "http://public.highfidelity.io/scripts/libraries/ExportMenu.js", + "http://public.highfidelity.io/scripts/libraries/ToolTip.js", + + "http://public.highfidelity.io/scripts/libraries/entityPropertyDialogBox.js", + "http://public.highfidelity.io/scripts/libraries/entityCameraTool.js", + "http://public.highfidelity.io/scripts/libraries/gridTool.js", + "http://public.highfidelity.io/scripts/libraries/entityList.js", +]); + var selectionDisplay = SelectionDisplay; var selectionManager = SelectionManager; - -Script.include("libraries/ModelImporter.js"); var modelImporter = new ModelImporter(); - -Script.include("libraries/ExportMenu.js"); -Script.include("libraries/ToolTip.js"); - -Script.include("libraries/entityPropertyDialogBox.js"); var entityPropertyDialogBox = EntityPropertyDialogBox; -Script.include("libraries/entityCameraTool.js"); var cameraManager = new CameraManager(); -Script.include("libraries/gridTool.js"); var grid = Grid(); gridTool = GridTool({ horizontalGrid: grid }); -Script.include("libraries/entityList.js"); var entityListTool = EntityListTool(); var hasShownPropertiesTool = false; diff --git a/examples/notifications.js b/examples/notifications.js index 9a6fbbce29..1b512634d7 100644 --- a/examples/notifications.js +++ b/examples/notifications.js @@ -52,18 +52,16 @@ // 2. Declare a text string. // 3. Call createNotifications(text) parsing the text. // example: +// var welcome; // if (key.text == "q") { //queries number of users online -// var numUsers = GlobalServices.onlineUsers.length; -// var welcome = "There are " + numUsers + " users online now."; -// createNotification(welcome); +// var welcome = "There are " + GlobalServices.onlineUsers.length + " users online now."; +// createNotification(welcome); // } - var width = 340.0; //width of notification overlay -var height = 40.0; // height of a single line notification overlay var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window -var overlayLocationX = (windowDimensions.x - (width + 20.0));// positions window 20px from the right of the interface window -var buttonLocationX = overlayLocationX + (width - 28.0); +var overlayLocationX = (windowDimensions.x - (width + 20.0)); // positions window 20px from the right of the interface window +var buttonLocationX = overlayLocationX + (width - 28.0); var locationY = 20.0; // position down from top of interface window var topMargin = 13.0; var leftMargin = 10.0; @@ -71,264 +69,79 @@ var textColor = { red: 228, green: 228, blue: 228}; // text color var backColor = { red: 2, green: 2, blue: 2}; // background color was 38,38,38 var backgroundAlpha = 0; var fontSize = 12.0; -var persistTime = 10.0; // time in seconds before notification fades +var PERSIST_TIME_2D = 10.0; // Time in seconds before notification fades +var PERSIST_TIME_3D = 15.0; +var persistTime = PERSIST_TIME_2D; var clickedText = false; var frame = 0; -var ourWidth = Window.innerWidth; -var ourHeight = Window.innerHeight; +var ourWidth = Window.innerWidth; +var ourHeight = Window.innerHeight; var text = "placeholder"; var last_users = GlobalServices.onlineUsers; var users = []; var ctrlIsPressed = false; var ready = true; - -// When our script shuts down, we should clean up all of our overlays -function scriptEnding() { - for (i = 0; i < notifications.length; i++) { - Overlays.deleteOverlay(notifications[i]); - Overlays.deleteOverlay(buttons[i]); - } -} -Script.scriptEnding.connect(scriptEnding); - var notifications = []; -var buttons = []; +var buttons = []; var times = []; var heights = []; var myAlpha = []; var arrays = []; +var isOnHMD = false, + ENABLE_VR_MODE = "Enable VR Mode", + NOTIFICATIONS_3D_DIRECTION = 0.0, // Degrees from avatar orientation. + NOTIFICATIONS_3D_DISTANCE = 0.6, // Horizontal distance from avatar position. + NOTIFICATIONS_3D_ELEVATION = -0.8, // Height of top middle of top notification relative to avatar eyes. + NOTIFICATIONS_3D_YAW = 0.0, // Degrees relative to notifications direction. + NOTIFICATIONS_3D_PITCH = -60.0, // Degrees from vertical. + NOTIFICATION_3D_SCALE = 0.002, // Multiplier that converts 2D overlay dimensions to 3D overlay dimensions. + NOTIFICATION_3D_BUTTON_WIDTH = 40 * NOTIFICATION_3D_SCALE, // Need a little more room for button in 3D. + overlay3DDetails = []; -// This function creates and sizes the overlays -function createNotification(text) { - var count = (text.match(/\n/g) || []).length; - var breakPoint = 43.0; // length when new line is added - var extraLine = 0; - var breaks = 0; - var height = 40.0; - var stack = 0; - if (text.length >= breakPoint) { - breaks = count; - } - var extraLine = breaks * 16.0; - for (i = 0; i < heights.length; i++) { - stack = stack + heights[i]; - } - var level = (stack + 20.0); - height = height + extraLine; - var overlayProperties = { - x: overlayLocationX, - y: level, - width: width, - height: height, - color: textColor, - backgroundColor: backColor, - alpha: backgroundAlpha, - topMargin: topMargin, - leftMargin: leftMargin, - font: {size: fontSize}, - text: text, - }; - var bLevel = level + 12.0; - var buttonProperties = { - x: buttonLocationX, - y: bLevel, - width: 10.0, - height: 10.0, - subImage: { x: 0, y: 0, width: 10, height: 10 }, - imageURL: "http://hifi-public.s3.amazonaws.com/images/close-small-light.svg", - color: { red: 255, green: 255, blue: 255}, - visible: true, - alpha: backgroundAlpha, - }; - - Notify(overlayProperties, buttonProperties, height); - +// push data from above to the 2 dimensional array +function createArrays(notice, button, createTime, height, myAlpha) { + arrays.push([notice, button, createTime, height, myAlpha]); } -// Pushes data to each array and sets up data for 2nd dimension array -// to handle auxiliary data not carried by the overlay class -// specifically notification "heights", "times" of creation, and . -function Notify(notice, button, height){ - - notifications.push((Overlays.addOverlay("text", notice))); - buttons.push((Overlays.addOverlay("image",button))); - times.push(new Date().getTime() / 1000); - height = height + 1.0; - heights.push(height); - myAlpha.push(0); - var last = notifications.length - 1; - createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); - fadeIn(notifications[last], buttons[last]) +// This handles the final dismissal of a notification after fading +function dismiss(firstNoteOut, firstButOut, firstOut) { + Overlays.deleteOverlay(firstNoteOut); + Overlays.deleteOverlay(firstButOut); + notifications.splice(firstOut, 1); + buttons.splice(firstOut, 1); + times.splice(firstOut, 1); + heights.splice(firstOut, 1); + myAlpha.splice(firstOut, 1); + overlay3DDetails.splice(firstOut, 1); } function fadeIn(noticeIn, buttonIn) { - var myLength = arrays.length; - var q = 0; - var pauseTimer = null; - pauseTimer = Script.setInterval(function() { - q++; + var q = 0, + qFade, + pauseTimer = null; + + pauseTimer = Script.setInterval(function () { + q += 1; qFade = q / 10.0; - Overlays.editOverlay(noticeIn, {alpha: qFade}); - Overlays.editOverlay(buttonIn, {alpha: qFade}); + Overlays.editOverlay(noticeIn, { alpha: qFade }); + Overlays.editOverlay(buttonIn, { alpha: qFade }); if (q >= 9.0) { Script.clearInterval(pauseTimer); } }, 10); } - -// push data from above to the 2 dimensional array -function createArrays(notice, button, createTime, height, myAlpha) { - arrays.push([notice, button, createTime, height, myAlpha]); -} -// handles mouse clicks on buttons -function mousePressEvent(event) { - var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); //identify which overlay was clicked - for (i = 0; i < buttons.length; i++) { //if user clicked a button - if(clickedOverlay == buttons[i]) { - Overlays.deleteOverlay(notifications[i]); - Overlays.deleteOverlay(buttons[i]); - notifications.splice(i, 1); - buttons.splice(i, 1); - times.splice(i, 1); - heights.splice(i, 1); - myAlpha.splice(i, 1); - arrays.splice(i, 1); - } - } -} - -// Control key remains active only while key is held down -function keyReleaseEvent(key) { - if (key.key == 16777249) { - ctrlIsPressed = false; - } -} - -// Triggers notification on specific key driven events -function keyPressEvent(key) { - if (key.key == 16777249) { - ctrlIsPressed = true; - } - if (key.text == "q") { //queries number of users online - var numUsers = GlobalServices.onlineUsers.length; - var welcome = "There are " + numUsers + " users online now."; - createNotification(welcome); - } - - if (key.text == "s") { - if (ctrlIsPressed == true){ - var noteString = "Snapshot taken."; - createNotification(noteString); - } - } -} - -// formats string to add newline every 43 chars -function wordWrap(str) { - var result = stringDivider(str, 43.0, "\n"); - createNotification(result); -} -// wraps whole word to newline -function stringDivider(str, slotWidth, spaceReplacer) { - if (str.length > slotWidth) { - var p = slotWidth; - for (; p > 0 && str[p] != ' '; p--) { - } - if (p > 0) { - var left = str.substring(0, p); - var right = str.substring(p + 1); - return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); - } - } - return str; -} - -// This fires a notification on window resize -function checkSize(){ - if((Window.innerWidth != ourWidth)||(Window.innerHeight != ourHeight)) { - var windowResize = "Window has been resized"; - ourWidth = Window.innerWidth; - ourHeight = Window.innerHeight; - windowDimensions = Controller.getViewportDimensions(); - overlayLocationX = (windowDimensions.x - (width + 60.0)); - buttonLocationX = overlayLocationX + (width - 35.0); - createNotification(windowResize) - } -} - -// Triggers notification if a user logs on or off -function onOnlineUsersChanged(users) { - if (!isStartingUp()) { // Skip user notifications at startup. - for (user in users) { - if (last_users.indexOf(users[user]) == -1.0) { - createNotification(users[user] + " has joined"); - } - } - for (user in last_users) { - if (users.indexOf(last_users[user]) == -1.0) { - createNotification(last_users[user] + " has left"); - } - } - } - last_users = users; -} - -// Triggers notification if @MyUserName is mentioned in chat and returns the message to the notification. -function onIncomingMessage(user, message) { - var myMessage = message; - var alertMe = "@" + GlobalServices.myUsername; - var thisAlert = user + ": " + myMessage; - if (myMessage.indexOf(alertMe) > -1.0) { - wordWrap(thisAlert); - } -} -// Triggers mic mute notification -function onMuteStateChanged() { - var muteState = AudioDevice.getMuted() ? "muted" : "unmuted"; - var muteString = "Microphone is now " + muteState; - createNotification(muteString); -} - -function update(){ - frame++; - if ((frame % 60.0) == 0) { // only update once a second - checkSize(); // checks for size change to trigger windowResize notification - locationY = 20.0; - for (var i = 0; i < arrays.length; i++) { //repositions overlays as others fade - var nextOverlay = Overlays.getOverlayAtPoint({x: overlayLocationX, y: locationY}); - Overlays.editOverlay(notifications[i], { x:overlayLocationX, y:locationY}); - Overlays.editOverlay(buttons[i], { x:buttonLocationX, y:locationY + 12.0}); - locationY = locationY + arrays[i][3]; - } - } - -// This checks the age of the notification and prepares to fade it after 9.0 seconds (var persistTime - 1) - for (var i = 0; i < arrays.length; i++) { - if (ready){ - var j = arrays[i][2]; - var k = j + persistTime; - if (k < (new Date().getTime() / 1000)) { - ready = false; - noticeOut = arrays[i][0]; - buttonOut = arrays[i][1]; - var arraysOut = i; - fadeOut(noticeOut, buttonOut, arraysOut); - } - } - } -} - // this fades the notification ready for dismissal, and removes it from the arrays function fadeOut(noticeOut, buttonOut, arraysOut) { - var myLength = arrays.length; - var r = 9.0; - var pauseTimer = null; - pauseTimer = Script.setInterval(function() { - r--; - rFade = r / 10.0; - Overlays.editOverlay(noticeOut, {alpha: rFade}); - Overlays.editOverlay(buttonOut, {alpha: rFade}); + var r = 9.0, + rFade, + pauseTimer = null; + + pauseTimer = Script.setInterval(function () { + r -= 1; + rFade = r / 10.0; + Overlays.editOverlay(noticeOut, { alpha: rFade }); + Overlays.editOverlay(buttonOut, { alpha: rFade }); if (r < 0) { dismiss(noticeOut, buttonOut, arraysOut); arrays.splice(arraysOut, 1); @@ -338,29 +151,283 @@ function fadeOut(noticeOut, buttonOut, arraysOut) { }, 20); } -// This handles the final dismissal of a notification after fading -function dismiss(firstNoteOut, firstButOut, firstOut) { - var working = firstOut - Overlays.deleteOverlay(firstNoteOut); - Overlays.deleteOverlay(firstButOut); - notifications.splice(firstOut, 1); - buttons.splice(firstOut, 1); - times.splice(firstOut, 1); - heights.splice(firstOut, 1); - myAlpha.splice(firstOut,1); +function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) { + // Calculates overlay positions and orientations in avatar coordinates. + var noticeY, + originOffset, + notificationOrientation, + notificationPosition, + buttonPosition; + + // Notification plane positions + noticeY = -y * NOTIFICATION_3D_SCALE - noticeHeight / 2; + notificationPosition = { x: 0, y: noticeY, z: 0 }; + buttonPosition = { x: (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH) / 2, y: noticeY, z: 0.001 }; + + // Rotate plane + notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, + NOTIFICATIONS_3D_DIRECTION + NOTIFICATIONS_3D_YAW, 0); + notificationPosition = Vec3.multiplyQbyV(notificationOrientation, notificationPosition); + buttonPosition = Vec3.multiplyQbyV(notificationOrientation, buttonPosition); + + // Translate plane + originOffset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, NOTIFICATIONS_3D_DIRECTION, 0), + { x: 0, y: 0, z: -NOTIFICATIONS_3D_DISTANCE }); + originOffset.y += NOTIFICATIONS_3D_ELEVATION; + notificationPosition = Vec3.sum(originOffset, notificationPosition); + buttonPosition = Vec3.sum(originOffset, buttonPosition); + + return { + notificationOrientation: notificationOrientation, + notificationPosition: notificationPosition, + buttonPosition: buttonPosition + }; } -// This reports the number of users online at startup -function reportUsers() { - var numUsers = GlobalServices.onlineUsers.length; - var welcome = "Welcome! There are " + numUsers + " users online now."; - createNotification(welcome); +// Pushes data to each array and sets up data for 2nd dimension array +// to handle auxiliary data not carried by the overlay class +// specifically notification "heights", "times" of creation, and . +function notify(notice, button, height) { + var noticeWidth, + noticeHeight, + positions, + last; + + if (isOnHMD) { + // Calculate 3D values from 2D overlay properties. + + noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; + noticeHeight = notice.height * NOTIFICATION_3D_SCALE; + + notice.size = { x: noticeWidth, y: noticeHeight }; + notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; + notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; + notice.bottomMargin = 0; + notice.rightMargin = 0; + notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; + notice.isFacingAvatar = false; + + button.url = button.imageURL; + button.scale = button.width * NOTIFICATION_3D_SCALE; + button.isFacingAvatar = false; + + positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); + + notifications.push((Overlays.addOverlay("text3d", notice))); + buttons.push((Overlays.addOverlay("billboard", button))); + overlay3DDetails.push({ + notificationOrientation: positions.notificationOrientation, + notificationPosition: positions.notificationPosition, + buttonPosition: positions.buttonPosition, + width: noticeWidth, + height: noticeHeight + }); + } else { + notifications.push((Overlays.addOverlay("text", notice))); + buttons.push((Overlays.addOverlay("image", button))); + } + + height = height + 1.0; + heights.push(height); + times.push(new Date().getTime() / 1000); + myAlpha.push(0); + last = notifications.length - 1; + createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); + fadeIn(notifications[last], buttons[last]); +} + +// This function creates and sizes the overlays +function createNotification(text) { + var count = (text.match(/\n/g) || []).length, + breakPoint = 43.0, // length when new line is added + extraLine = 0, + breaks = 0, + height = 40.0, + stack = 0, + level, + noticeProperties, + bLevel, + buttonProperties, + i; + + if (text.length >= breakPoint) { + breaks = count; + } + extraLine = breaks * 16.0; + for (i = 0; i < heights.length; i += 1) { + stack = stack + heights[i]; + } + + level = (stack + 20.0); + height = height + extraLine; + noticeProperties = { + x: overlayLocationX, + y: level, + width: width, + height: height, + color: textColor, + backgroundColor: backColor, + alpha: backgroundAlpha, + topMargin: topMargin, + leftMargin: leftMargin, + font: {size: fontSize}, + text: text + }; + + bLevel = level + 12.0; + buttonProperties = { + x: buttonLocationX, + y: bLevel, + width: 10.0, + height: 10.0, + subImage: { x: 0, y: 0, width: 10, height: 10 }, + imageURL: "http://hifi-public.s3.amazonaws.com/images/close-small-light.svg", + color: { red: 255, green: 255, blue: 255}, + visible: true, + alpha: backgroundAlpha + }; + + notify(noticeProperties, buttonProperties, height); +} + +function deleteNotification(index) { + Overlays.deleteOverlay(notifications[index]); + Overlays.deleteOverlay(buttons[index]); + notifications.splice(index, 1); + buttons.splice(index, 1); + times.splice(index, 1); + heights.splice(index, 1); + myAlpha.splice(index, 1); + overlay3DDetails.splice(index, 1); + arrays.splice(index, 1); +} + +// wraps whole word to newline +function stringDivider(str, slotWidth, spaceReplacer) { + var p, + left, + right; + + if (str.length > slotWidth) { + p = slotWidth; + while (p > 0 && str[p] !== ' ') { + p -= 1; + } + + if (p > 0) { + left = str.substring(0, p); + right = str.substring(p + 1); + return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); + } + } + return str; +} + +// formats string to add newline every 43 chars +function wordWrap(str) { + var result = stringDivider(str, 43.0, "\n"); + createNotification(result); +} + +// This fires a notification on window resize +function checkSize() { + if ((Window.innerWidth !== ourWidth) || (Window.innerHeight !== ourHeight)) { + var windowResize = "Window has been resized"; + ourWidth = Window.innerWidth; + ourHeight = Window.innerHeight; + windowDimensions = Controller.getViewportDimensions(); + overlayLocationX = (windowDimensions.x - (width + 60.0)); + buttonLocationX = overlayLocationX + (width - 35.0); + createNotification(windowResize); + } +} + +function update() { + var nextOverlay, + noticeOut, + buttonOut, + arraysOut, + defaultEyePosition, + avatarOrientation, + notificationPosition, + notificationOrientation, + buttonPosition, + positions, + i, + j, + k; + + if (isOnHMD !== Menu.isOptionChecked(ENABLE_VR_MODE)) { + while (arrays.length > 0) { + deleteNotification(0); + } + isOnHMD = !isOnHMD; + persistTime = isOnHMD ? PERSIST_TIME_3D : PERSIST_TIME_2D; + return; + } + + frame += 1; + if ((frame % 60.0) === 0) { // only update once a second + checkSize(); // checks for size change to trigger windowResize notification + locationY = 20.0; + for (i = 0; i < arrays.length; i += 1) { //repositions overlays as others fade + nextOverlay = Overlays.getOverlayAtPoint({ x: overlayLocationX, y: locationY }); + Overlays.editOverlay(notifications[i], { x: overlayLocationX, y: locationY }); + Overlays.editOverlay(buttons[i], { x: buttonLocationX, y: locationY + 12.0 }); + if (isOnHMD) { + positions = calculate3DOverlayPositions(overlay3DDetails[i].width, overlay3DDetails[i].height, locationY); + overlay3DDetails[i].notificationOrientation = positions.notificationOrientation; + overlay3DDetails[i].notificationPosition = positions.notificationPosition; + overlay3DDetails[i].buttonPosition = positions.buttonPosition; + } + locationY = locationY + arrays[i][3]; + } + } + + // This checks the age of the notification and prepares to fade it after 9.0 seconds (var persistTime - 1) + for (i = 0; i < arrays.length; i += 1) { + if (ready) { + j = arrays[i][2]; + k = j + persistTime; + if (k < (new Date().getTime() / 1000)) { + ready = false; + noticeOut = arrays[i][0]; + buttonOut = arrays[i][1]; + arraysOut = i; + fadeOut(noticeOut, buttonOut, arraysOut); + } + } + } + + if (isOnHMD && notifications.length > 0) { + // Update 3D overlays to maintain positions relative to avatar + defaultEyePosition = MyAvatar.getDefaultEyePosition(); + avatarOrientation = MyAvatar.orientation; + + for (i = 0; i < notifications.length; i += 1) { + notificationPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, overlay3DDetails[i].notificationPosition)); + notificationOrientation = Quat.multiply(avatarOrientation, overlay3DDetails[i].notificationOrientation); + buttonPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, overlay3DDetails[i].buttonPosition)); + Overlays.editOverlay(notifications[i], { position: notificationPosition, rotation: notificationOrientation }); + Overlays.editOverlay(buttons[i], { position: buttonPosition, rotation: notificationOrientation }); + } + } } var STARTUP_TIMEOUT = 500, // ms startingUp = true, startupTimer = null; +// This reports the number of users online at startup +function reportUsers() { + var welcome; + + welcome = "Welcome! There are " + GlobalServices.onlineUsers.length + " users online now."; + createNotification(welcome); +} + function finishStartup() { startingUp = false; Script.clearTimeout(startupTimer); @@ -378,6 +445,113 @@ function isStartingUp() { return startingUp; } +// Triggers notification if a user logs on or off +function onOnlineUsersChanged(users) { + var i; + + if (!isStartingUp()) { // Skip user notifications at startup. + for (i = 0; i < users.length; i += 1) { + if (last_users.indexOf(users[i]) === -1.0) { + createNotification(users[i] + " has joined"); + } + + } + + for (i = 0; i < last_users.length; i += 1) { + if (users.indexOf(last_users[i]) === -1.0) { + createNotification(last_users[i] + " has left"); + } + } + } + + last_users = users; +} + +// Triggers notification if @MyUserName is mentioned in chat and returns the message to the notification. +function onIncomingMessage(user, message) { + var myMessage, + alertMe, + thisAlert; + + myMessage = message; + alertMe = "@" + GlobalServices.myUsername; + thisAlert = user + ": " + myMessage; + + if (myMessage.indexOf(alertMe) > -1.0) { + wordWrap(thisAlert); + } +} + +// Triggers mic mute notification +function onMuteStateChanged() { + var muteState, + muteString; + + muteState = AudioDevice.getMuted() ? "muted" : "unmuted"; + muteString = "Microphone is now " + muteState; + createNotification(muteString); +} + +// handles mouse clicks on buttons +function mousePressEvent(event) { + var pickRay, + clickedOverlay, + i; + + if (isOnHMD) { + pickRay = Camera.computePickRay(event.x, event.y); + clickedOverlay = Overlays.findRayIntersection(pickRay).overlayID; + } else { + clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + } + + for (i = 0; i < buttons.length; i += 1) { + if (clickedOverlay === buttons[i]) { + deleteNotification(i); + } + } +} + +// Control key remains active only while key is held down +function keyReleaseEvent(key) { + if (key.key === 16777249) { + ctrlIsPressed = false; + } +} + +// Triggers notification on specific key driven events +function keyPressEvent(key) { + var numUsers, + welcome, + noteString; + + if (key.key === 16777249) { + ctrlIsPressed = true; + } + + if (key.text === "q") { //queries number of users online + numUsers = GlobalServices.onlineUsers.length; + welcome = "There are " + numUsers + " users online now."; + createNotification(welcome); + } + + if (key.text === "s") { + if (ctrlIsPressed === true) { + noteString = "Snapshot taken."; + createNotification(noteString); + } + } +} + +// When our script shuts down, we should clean up all of our overlays +function scriptEnding() { + var i; + + for (i = 0; i < notifications.length; i += 1) { + Overlays.deleteOverlay(notifications[i]); + Overlays.deleteOverlay(buttons[i]); + } +} AudioDevice.muteToggled.connect(onMuteStateChanged); Controller.keyPressEvent.connect(keyPressEvent); @@ -386,3 +560,4 @@ GlobalServices.onlineUsersChanged.connect(onOnlineUsersChanged); GlobalServices.incomingMessage.connect(onIncomingMessage); Controller.keyReleaseEvent.connect(keyReleaseEvent); Script.update.connect(update); +Script.scriptEnding.connect(scriptEnding); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 9147cb34ba..dd5c09405c 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -51,7 +51,7 @@ public: // getters float getLeanScale() const { return _leanScale; } glm::vec3 getGravity() const { return _gravity; } - glm::vec3 getDefaultEyePosition() const; + Q_INVOKABLE glm::vec3 getDefaultEyePosition() const; bool getShouldRenderLocally() const { return _shouldRender; } const QList& getAnimationHandles() const { return _animationHandles; } diff --git a/libraries/script-engine/src/BatchLoader.cpp b/libraries/script-engine/src/BatchLoader.cpp new file mode 100644 index 0000000000..e2c345ce16 --- /dev/null +++ b/libraries/script-engine/src/BatchLoader.cpp @@ -0,0 +1,79 @@ +// +// BatchLoader.cpp +// libraries/script-engine/src +// +// Created by Ryan Huffman on 01/22/15 +// 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 +// + +#include +#include + +#include +#include "BatchLoader.h" +#include + +BatchLoader::BatchLoader(const QList& urls) + : QObject(), + _started(false), + _finished(false), + _urls(urls.toSet()), + _data() { +} + +void BatchLoader::start() { + if (_started) { + return; + } + + _started = true; + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + for (QUrl url : _urls) { + if (url.scheme() == "http" || url.scheme() == "https" || url.scheme() == "ftp") { + QNetworkReply* reply = networkAccessManager.get(QNetworkRequest(url)); + + qDebug() << "Downloading file at" << url; + + connect(reply, &QNetworkReply::finished, [=]() { + if (reply->error()) { + _data.insert(url, QString()); + } else { + _data.insert(url, reply->readAll()); + } + reply->deleteLater(); + checkFinished(); + }); + + // If we end up being destroyed before the reply finishes, clean it up + connect(this, &QObject::destroyed, reply, &QObject::deleteLater); + + } else { +#ifdef _WIN32 + QString fileName = url.toString(); +#else + QString fileName = url.toLocalFile(); +#endif + + qDebug() << "Reading file at " << fileName; + + QFile scriptFile(fileName); + if (scriptFile.open(QFile::ReadOnly | QFile::Text)) { + QTextStream in(&scriptFile); + _data.insert(url, in.readAll()); + } else { + _data.insert(url, QString()); + } + } + } + checkFinished(); +} + +void BatchLoader::checkFinished() { + if (!_finished && _urls.size() == _data.size()) { + _finished = true; + emit finished(_data); + } +} diff --git a/libraries/script-engine/src/BatchLoader.h b/libraries/script-engine/src/BatchLoader.h new file mode 100644 index 0000000000..cda040d219 --- /dev/null +++ b/libraries/script-engine/src/BatchLoader.h @@ -0,0 +1,42 @@ +// +// BatchLoader.h +// libraries/script-engine/src +// +// Created by Ryan Huffman on 01/22/15 +// 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 +// + +#ifndef hifi_BatchLoader_h +#define hifi_BatchLoader_h + +#include +#include +#include +#include +#include +#include + +class BatchLoader : public QObject { + Q_OBJECT +public: + BatchLoader(const QList& urls) ; + + void start(); + bool isFinished() const { return _finished; }; + +signals: + void finished(const QMap& data); + +private: + void checkFinished(); + + bool _started; + bool _finished; + QSet _urls; + QMap _data; +}; + +#endif // hifi_BatchLoader_h diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 0f860208f4..a002950d46 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -31,6 +31,7 @@ #include "AnimationObject.h" #include "ArrayBufferViewClass.h" +#include "BatchLoader.h" #include "DataViewClass.h" #include "EventTypes.h" #include "MenuItemProperties.h" @@ -111,7 +112,7 @@ void ScriptEngine::setIsAvatar(bool isAvatar) { _avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); _avatarBillboardTimer->start(AVATAR_BILLBOARD_PACKET_SEND_INTERVAL_MSECS); } - + if (!_isAvatar) { delete _avatarIdentityTimer; _avatarIdentityTimer = NULL; @@ -304,7 +305,7 @@ QScriptValue ScriptEngine::evaluate(const QString& program, const QString& fileN QScriptValue result = QScriptEngine::evaluate(program, fileName, lineNumber); if (hasUncaughtException()) { int line = uncaughtExceptionLineNumber(); - qDebug() << "Uncaught exception at (" << _fileNameString << ") line" << line << ": " << result.toString(); + qDebug() << "Uncaught exception at (" << _fileNameString << " : " << fileName << ") line" << line << ": " << result.toString(); } emit evaluationFinished(result, hasUncaughtException()); clearExceptions(); @@ -595,46 +596,57 @@ void ScriptEngine::print(const QString& message) { emit printedMessage(message); } -void ScriptEngine::include(const QString& includeFile) { - QUrl url = resolvePath(includeFile); - QString includeContents; +/** + * If a callback is specified, the included files will be loaded asynchronously and the callback will be called + * when all of the files have finished loading. + * If no callback is specified, the included files will be loaded synchronously and will block execution until + * all of the files have finished loading. + */ +void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { + QList urls; + for (QString file : includeFiles) { + urls.append(resolvePath(file)); + } - if (url.scheme() == "http" || url.scheme() == "https" || url.scheme() == "ftp") { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkReply* reply = networkAccessManager.get(QNetworkRequest(url)); - qDebug() << "Downloading included script at" << includeFile; - QEventLoop loop; - QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - loop.exec(); - includeContents = reply->readAll(); - reply->deleteLater(); - } else { -#ifdef _WIN32 - QString fileName = url.toString(); -#else - QString fileName = url.toLocalFile(); -#endif - - QFile scriptFile(fileName); - if (scriptFile.open(QFile::ReadOnly | QFile::Text)) { - qDebug() << "Including file:" << fileName; - QTextStream in(&scriptFile); - includeContents = in.readAll(); - } else { - qDebug() << "ERROR Including file:" << fileName; - emit errorMessage("ERROR Including file:" + fileName); + BatchLoader* loader = new BatchLoader(urls); + + auto evaluateScripts = [=](const QMap& data) { + for (QUrl url : urls) { + QString contents = data[url]; + if (contents.isNull()) { + qDebug() << "Error loading file: " << url; + } else { + QScriptValue result = evaluate(contents, url.toString()); + } } - } - QScriptValue result = evaluate(includeContents); - if (hasUncaughtException()) { - int line = uncaughtExceptionLineNumber(); - qDebug() << "Uncaught exception at (" << includeFile << ") line" << line << ":" << result.toString(); - emit errorMessage("Uncaught exception at (" + includeFile + ") line" + QString::number(line) + ":" + result.toString()); - clearExceptions(); + if (callback.isFunction()) { + QScriptValue(callback).call(); + } + + loader->deleteLater(); + }; + + connect(loader, &BatchLoader::finished, this, evaluateScripts); + + // If we are destroyed before the loader completes, make sure to clean it up + connect(this, &QObject::destroyed, loader, &QObject::deleteLater); + + loader->start(); + + if (!callback.isFunction() && !loader->isFinished()) { + QEventLoop loop; + QObject::connect(loader, &BatchLoader::finished, &loop, &QEventLoop::quit); + loop.exec(); } } +void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { + QStringList urls; + urls.append(includeFile); + include(urls, callback); +} + void ScriptEngine::load(const QString& loadFile) { QUrl url = resolvePath(loadFile); emit loadScript(url.toString(), false); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 10f419937a..f78a14bffa 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -96,7 +96,8 @@ public slots: QObject* setTimeout(const QScriptValue& function, int timeoutMS); void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); } void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast(timer)); } - void include(const QString& includeFile); + void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue()); + void include(const QString& includeFile, QScriptValue callback = QScriptValue()); void load(const QString& loadfile); void print(const QString& message); QUrl resolvePath(const QString& path) const;