diff --git a/examples/blocks.js b/examples/blocks.js new file mode 100644 index 0000000000..30c2126096 --- /dev/null +++ b/examples/blocks.js @@ -0,0 +1,118 @@ +// +// Blocks.js +// +// Created by Philip Rosedale on January 26, 2015 +// Copyright 2015 High Fidelity, Inc. +// +// Create a bunch of building blocks and drop them onto a playing surface in front of you. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +var FLOOR_SIZE = 7.5; +var FLOOR_THICKNESS = 0.10; +var EDGE_THICKESS = 0.25; +var SCALE = 0.25; + +var NUM_BLOCKS = 25; +var DROP_HEIGHT = SCALE * 8.0; + +var GRAVITY = -1.0; +var LIFETIME = 6000; +var DAMPING = 0.50; + +var blockTypes = []; +blockTypes.push({ x: 1, y: 1, z: 1, red: 255, green: 0, blue: 0 }); +blockTypes.push({ x: 1, y: 1, z: 2, red: 0, green: 255, blue: 0 }); +blockTypes.push({ x: 1, y: 2, z: 5, red: 0, green: 0, blue: 255 }); +blockTypes.push({ x: 1, y: 2, z: 2, red: 255, green: 255, blue: 0 }); +blockTypes.push({ x: 1, y: 1, z: 5, red: 0, green: 255, blue: 255 }); + +var center = Vec3.sum(MyAvatar.position, Vec3.multiply(FLOOR_SIZE * 2.0, Quat.getFront(Camera.getOrientation()))); + +var floor = Entities.addEntity( + { type: "Box", + position: Vec3.subtract(center, { x: 0, y: SCALE / 2.0, z: 0 }), + dimensions: { x: FLOOR_SIZE, y: FLOOR_THICKNESS, z: FLOOR_SIZE }, + color: { red: 128, green: 128, blue: 128 }, + gravity: { x: 0, y: 0, z: 0 }, + ignoreCollisions: false, + locked: true, + lifetime: LIFETIME }); + +var edge1 = Entities.addEntity( + { type: "Box", + position: Vec3.sum(center, { x: FLOOR_SIZE / 2.0, y: FLOOR_THICKNESS / 2.0, z: 0 }), + dimensions: { x: EDGE_THICKESS, y: EDGE_THICKESS, z: FLOOR_SIZE }, + color: { red: 128, green: 128, blue: 128 }, + gravity: { x: 0, y: 0, z: 0 }, + ignoreCollisions: false, + visible: true, + locked: true, + lifetime: LIFETIME }); + +var edge2 = Entities.addEntity( + { type: "Box", + position: Vec3.sum(center, { x: -FLOOR_SIZE / 2.0, y: FLOOR_THICKNESS / 2.0, z: 0 }), + dimensions: { x: EDGE_THICKESS, y: EDGE_THICKESS, z: FLOOR_SIZE }, + color: { red: 128, green: 128, blue: 128 }, + gravity: { x: 0, y: 0, z: 0 }, + ignoreCollisions: false, + visible: true, + locked: true, + lifetime: LIFETIME }); + +var edge3 = Entities.addEntity( + { type: "Box", + position: Vec3.sum(center, { x: 0, y: FLOOR_THICKNESS / 2.0, z: -FLOOR_SIZE / 2.0 }), + dimensions: { x: FLOOR_SIZE, y: EDGE_THICKESS, z: EDGE_THICKESS }, + color: { red: 128, green: 128, blue: 128 }, + gravity: { x: 0, y: 0, z: 0 }, + ignoreCollisions: false, + visible: true, + locked: true, + lifetime: LIFETIME }); + +var edge4 = Entities.addEntity( + { type: "Box", + position: Vec3.sum(center, { x: 0, y: FLOOR_THICKNESS / 2.0, z: FLOOR_SIZE / 2.0 }), + dimensions: { x: FLOOR_SIZE, y: EDGE_THICKESS, z: EDGE_THICKESS }, + color: { red: 128, green: 128, blue: 128 }, + gravity: { x: 0, y: 0, z: 0 }, + ignoreCollisions: false, + visible: true, + locked: true, + lifetime: LIFETIME }); + +blocks = []; + +for (var i = 0; i < NUM_BLOCKS; i++) { + var which = Math.floor(Math.random() * blockTypes.length); + var type = blockTypes[which]; + blocks.push(Entities.addEntity( + { type: "Box", + position: { x: center.x + (Math.random() - 0.5) * (FLOOR_SIZE * 0.75), + y: center.y + DROP_HEIGHT, + z: center.z + (Math.random() - 0.5) * (FLOOR_SIZE * 0.75) }, + dimensions: { x: type.x * SCALE, y: type.y * SCALE, z: type.z * SCALE }, + color: { red: type.red, green: type.green, blue: type.blue }, + gravity: { x: 0, y: GRAVITY, z: 0 }, + ignoreCollisions: false, + damping: DAMPING, + lifetime: LIFETIME, + collisionsWillMove: true })); +} + +function scriptEnding() { + Entities.deleteEntity(edge1); + Entities.deleteEntity(edge2); + Entities.deleteEntity(edge3); + Entities.deleteEntity(edge4); + Entities.deleteEntity(floor); + + for (var i = 0; i < NUM_BLOCKS; i++) { + Entities.deleteEntity(blocks[i]); + } +} + +Script.scriptEnding.connect(scriptEnding); \ No newline at end of file diff --git a/examples/controllers/hydra/gun.js b/examples/controllers/hydra/gun.js index f3c1e14001..8bea5d623e 100644 --- a/examples/controllers/hydra/gun.js +++ b/examples/controllers/hydra/gun.js @@ -304,7 +304,6 @@ function makePlatform(gravity, scale, size) { } function entityCollisionWithEntity(entity1, entity2, collision) { - if (((entity1.id == bulletID.id) || (entity1.id == targetID.id)) && ((entity2.id == bulletID.id) || (entity2.id == targetID.id))) { score++; @@ -502,6 +501,7 @@ function scriptEnding() { Overlays.deleteOverlay(pointer[1]); Overlays.deleteOverlay(text); MyAvatar.detachOne(gunModel); + MyAvatar.detachOne(gunModel); clearPose(); } diff --git a/examples/controllers/hydra/hydraGrab.js b/examples/controllers/hydra/hydraGrab.js index 202a226f1e..dc8cd14eaa 100644 --- a/examples/controllers/hydra/hydraGrab.js +++ b/examples/controllers/hydra/hydraGrab.js @@ -19,8 +19,8 @@ var entityPropertyDialogBox = EntityPropertyDialogBox; var MIN_ANGULAR_SIZE = 2; var MAX_ANGULAR_SIZE = 45; -var allowLargeModels = false; -var allowSmallModels = false; +var allowLargeModels = true; +var allowSmallModels = true; var wantEntityGlow = false; var LEFT = 0; @@ -28,15 +28,16 @@ var RIGHT = 1; var jointList = MyAvatar.getJointNames(); -var STICKS = 0; -var MAPPED = 1; -var mode = STICKS; +var LASER_WIDTH = 3; +var LASER_COLOR = { red: 50, green: 150, blue: 200 }; +var DROP_COLOR = { red: 200, green: 200, blue: 200 }; +var DROP_WIDTH = 4; +var DROP_DISTANCE = 5.0; -var LASER_WIDTH = 4; -var LASER_COLOR = [{ red: 200, green: 150, blue: 50 }, // STICKS - { red: 50, green: 150, blue: 200 }]; // MAPPED var LASER_LENGTH_FACTOR = 500; +var velocity = { x: 0, y: 0, z: 0 }; + var lastAccurateIntersection = null; var accurateIntersections = 0; var totalIntersections = 0; @@ -107,6 +108,7 @@ function controller(wichSide) { this.positionAtGrab; this.rotationAtGrab; + this.gravityAtGrab; this.modelPositionAtGrab; this.modelRotationAtGrab; this.jointsIntersectingFromStart = []; @@ -114,13 +116,21 @@ function controller(wichSide) { this.laser = Overlays.addOverlay("line3d", { start: { x: 0, y: 0, z: 0 }, end: { x: 0, y: 0, z: 0 }, - color: LASER_COLOR[mode], + color: LASER_COLOR, alpha: 1, visible: false, lineWidth: LASER_WIDTH, anchor: "MyAvatar" }); + this.dropLine = Overlays.addOverlay("line3d", { + start: { x: 0, y: 0, z: 0 }, + end: { x: 0, y: 0, z: 0 }, + color: DROP_COLOR, + alpha: 1, + visible: false, + lineWidth: DROP_WIDTH }); + this.guideScale = 0.02; this.ball = Overlays.addOverlay("sphere", { position: { x: 0, y: 0, z: 0 }, @@ -158,6 +168,7 @@ function controller(wichSide) { this.entityID = entityID; this.modelURL = properties.modelURL; + this.oldModelPosition = properties.position; this.oldModelRotation = properties.rotation; this.oldModelHalfDiagonal = Vec3.length(properties.dimensions) / 2.0; @@ -166,6 +177,10 @@ function controller(wichSide) { this.rotationAtGrab = this.rotation; this.modelPositionAtGrab = properties.position; this.modelRotationAtGrab = properties.rotation; + this.gravityAtGrab = properties.gravity; + Entities.editEntity(entityID, { gravity: { x: 0, y: 0, z: 0 }, velocity: { x: 0, y: 0, z: 0 } }); + + this.jointsIntersectingFromStart = []; for (var i = 0; i < jointList.length; i++) { var distance = Vec3.distance(MyAvatar.getJointPosition(jointList[i]), this.oldModelPosition); @@ -174,10 +189,14 @@ function controller(wichSide) { } } this.showLaser(false); + Overlays.editOverlay(this.dropLine, { visible: true }); } this.release = function () { if (this.grabbing) { + + Entities.editEntity(this.entityID, { gravity: this.gravityAtGrab }); + jointList = MyAvatar.getJointNames(); var closestJointIndex = -1; @@ -216,6 +235,8 @@ function controller(wichSide) { Entities.deleteEntity(this.entityID); } } + + Overlays.editOverlay(this.dropLine, { visible: false }); } this.grabbing = false; @@ -288,7 +309,6 @@ function controller(wichSide) { end: endPosition }); - Overlays.editOverlay(this.ball, { position: endPosition }); @@ -300,7 +320,7 @@ function controller(wichSide) { start: Vec3.sum(endPosition, Vec3.multiply(this.up, 2 * this.guideScale)), end: Vec3.sum(endPosition, Vec3.multiply(this.up, -2 * this.guideScale)) }); - this.showLaser(!this.grabbing || mode == STICKS); + this.showLaser(!this.grabbing); if (this.glowedIntersectingModel.isKnownID) { Entities.editEntity(this.glowedIntersectingModel, { glowLevel: 0.0 }); @@ -332,7 +352,7 @@ function controller(wichSide) { Overlays.editOverlay(this.leftRight, { visible: show }); Overlays.editOverlay(this.topDown, { visible: show }); } - this.moveEntity = function () { + this.moveEntity = function (deltaTime) { if (this.grabbing) { if (!this.entityID.isKnownID) { print("Unknown grabbed ID " + this.entityID.id + ", isKnown: " + this.entityID.isKnownID); @@ -344,55 +364,34 @@ function controller(wichSide) { var newPosition; var newRotation; - switch (mode) { - case STICKS: - newPosition = Vec3.sum(this.palmPosition, - Vec3.multiply(this.front, this.x)); - newPosition = Vec3.sum(newPosition, - Vec3.multiply(this.up, this.y)); - newPosition = Vec3.sum(newPosition, - Vec3.multiply(this.right, this.z)); - - - newRotation = Quat.multiply(this.rotation, - Quat.inverse(this.oldRotation)); - newRotation = Quat.multiply(newRotation, - this.oldModelRotation); - break; - case MAPPED: - var forward = Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -1 }); - var d = Vec3.dot(forward, MyAvatar.position); - - var factor1 = Vec3.dot(forward, this.positionAtGrab) - d; - var factor2 = Vec3.dot(forward, this.modelPositionAtGrab) - d; - var vector = Vec3.subtract(this.palmPosition, this.positionAtGrab); - - if (factor2 < 0) { - factor2 = 0; - } - if (factor1 <= 0) { - factor1 = 1; - factor2 = 1; - } - - newPosition = Vec3.sum(this.modelPositionAtGrab, - Vec3.multiply(vector, - factor2 / factor1)); - - newRotation = Quat.multiply(this.rotation, - Quat.inverse(this.rotationAtGrab)); - newRotation = Quat.multiply(newRotation, newRotation); - newRotation = Quat.multiply(newRotation, - this.modelRotationAtGrab); - break; + var CONSTANT_SCALING_FACTOR = 5.0; + var MINIMUM_SCALING_DISTANCE = 2.0; + var distanceToModel = Vec3.length(Vec3.subtract(this.oldModelPosition, this.palmPosition)); + if (distanceToModel < MINIMUM_SCALING_DISTANCE) { + distanceToModel = MINIMUM_SCALING_DISTANCE; } + + var deltaPalm = Vec3.multiply(distanceToModel * CONSTANT_SCALING_FACTOR, Vec3.subtract(this.palmPosition, this.oldPalmPosition)); + newPosition = Vec3.sum(this.oldModelPosition, deltaPalm); + + newRotation = Quat.multiply(this.rotation, + Quat.inverse(this.rotationAtGrab)); + newRotation = Quat.multiply(newRotation, newRotation); + newRotation = Quat.multiply(newRotation, + this.modelRotationAtGrab); + + velocity = Vec3.multiply(1.0 / deltaTime, Vec3.subtract(newPosition, this.oldModelPosition)); + Entities.editEntity(this.entityID, { position: newPosition, - rotation: newRotation + rotation: newRotation, + velocity: velocity }); this.oldModelRotation = newRotation; this.oldModelPosition = newPosition; + Overlays.editOverlay(this.dropLine, { start: newPosition, end: Vec3.sum(newPosition, { x: 0, y: -DROP_DISTANCE, z: 0 }) }); + var indicesToRemove = []; for (var i = 0; i < this.jointsIntersectingFromStart.length; ++i) { var distance = Vec3.distance(MyAvatar.getJointPosition(this.jointsIntersectingFromStart[i]), this.oldModelPosition); @@ -428,15 +427,6 @@ function controller(wichSide) { this.triggerValue = Controller.getTriggerValue(this.trigger); var bumperValue = Controller.isButtonPressed(this.bumper); - if (bumperValue && !this.bumperValue) { - if (mode === STICKS) { - mode = MAPPED; - } else if (mode === MAPPED) { - mode = STICKS; - } - Overlays.editOverlay(leftController.laser, { color: LASER_COLOR[mode] }); - Overlays.editOverlay(rightController.laser, { color: LASER_COLOR[mode] }); - } this.bumperValue = bumperValue; @@ -548,61 +538,37 @@ function controller(wichSide) { var leftController = new controller(LEFT); var rightController = new controller(RIGHT); -function moveEntities() { +function moveEntities(deltaTime) { if (leftController.grabbing && rightController.grabbing && rightController.entityID.id == leftController.entityID.id) { var newPosition = leftController.oldModelPosition; var rotation = leftController.oldModelRotation; var ratio = 1; + var u = Vec3.normalize(Vec3.subtract(rightController.oldPalmPosition, leftController.oldPalmPosition)); + var v = Vec3.normalize(Vec3.subtract(rightController.palmPosition, leftController.palmPosition)); - switch (mode) { - case STICKS: - var oldLeftPoint = Vec3.sum(leftController.oldPalmPosition, Vec3.multiply(leftController.oldFront, leftController.x)); - var oldRightPoint = Vec3.sum(rightController.oldPalmPosition, Vec3.multiply(rightController.oldFront, rightController.x)); - - var oldMiddle = Vec3.multiply(Vec3.sum(oldLeftPoint, oldRightPoint), 0.5); - var oldLength = Vec3.length(Vec3.subtract(oldLeftPoint, oldRightPoint)); - - - var leftPoint = Vec3.sum(leftController.palmPosition, Vec3.multiply(leftController.front, leftController.x)); - var rightPoint = Vec3.sum(rightController.palmPosition, Vec3.multiply(rightController.front, rightController.x)); - - var middle = Vec3.multiply(Vec3.sum(leftPoint, rightPoint), 0.5); - var length = Vec3.length(Vec3.subtract(leftPoint, rightPoint)); - - - ratio = length / oldLength; - newPosition = Vec3.sum(middle, - Vec3.multiply(Vec3.subtract(leftController.oldModelPosition, oldMiddle), ratio)); - break; - case MAPPED: - var u = Vec3.normalize(Vec3.subtract(rightController.oldPalmPosition, leftController.oldPalmPosition)); - var v = Vec3.normalize(Vec3.subtract(rightController.palmPosition, leftController.palmPosition)); - - var cos_theta = Vec3.dot(u, v); - if (cos_theta > 1) { - cos_theta = 1; - } - var angle = Math.acos(cos_theta) / Math.PI * 180; - if (angle < 0.1) { - return; - - } - var w = Vec3.normalize(Vec3.cross(u, v)); - - rotation = Quat.multiply(Quat.angleAxis(angle, w), leftController.oldModelRotation); - - - leftController.positionAtGrab = leftController.palmPosition; - leftController.rotationAtGrab = leftController.rotation; - leftController.modelPositionAtGrab = leftController.oldModelPosition; - leftController.modelRotationAtGrab = rotation; - rightController.positionAtGrab = rightController.palmPosition; - rightController.rotationAtGrab = rightController.rotation; - rightController.modelPositionAtGrab = rightController.oldModelPosition; - rightController.modelRotationAtGrab = rotation; - break; + var cos_theta = Vec3.dot(u, v); + if (cos_theta > 1) { + cos_theta = 1; } + var angle = Math.acos(cos_theta) / Math.PI * 180; + if (angle < 0.1) { + return; + } + var w = Vec3.normalize(Vec3.cross(u, v)); + + rotation = Quat.multiply(Quat.angleAxis(angle, w), leftController.oldModelRotation); + + + leftController.positionAtGrab = leftController.palmPosition; + leftController.rotationAtGrab = leftController.rotation; + leftController.modelPositionAtGrab = leftController.oldModelPosition; + leftController.modelRotationAtGrab = rotation; + rightController.positionAtGrab = rightController.palmPosition; + rightController.rotationAtGrab = rightController.rotation; + rightController.modelPositionAtGrab = rightController.oldModelPosition; + rightController.modelRotationAtGrab = rotation; + Entities.editEntity(leftController.entityID, { position: newPosition, rotation: rotation, @@ -612,7 +578,6 @@ function moveEntities() { y: leftController.oldModelHalfDiagonal * ratio, z: leftController.oldModelHalfDiagonal * ratio } - }); leftController.oldModelPosition = newPosition; leftController.oldModelRotation = rotation; @@ -623,8 +588,8 @@ function moveEntities() { rightController.oldModelHalfDiagonal *= ratio; return; } - leftController.moveEntity(); - rightController.moveEntity(); + leftController.moveEntity(deltaTime); + rightController.moveEntity(deltaTime); } var hydraConnected = false; @@ -642,7 +607,7 @@ function checkController(deltaTime) { leftController.update(); rightController.update(); - moveEntities(); + moveEntities(deltaTime); } else { if (hydraConnected) { hydraConnected = false; diff --git a/examples/controllers/hydra/paddleBall.js b/examples/controllers/hydra/paddleBall.js index 85b025e4cd..fb312739f4 100644 --- a/examples/controllers/hydra/paddleBall.js +++ b/examples/controllers/hydra/paddleBall.js @@ -10,29 +10,42 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; + +hitSound = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Collisions-ballhitsandcatches/billiards/collision1.wav"); +var rightHandAnimation = HIFI_PUBLIC_BUCKET + "animations/RightHandAnimPhilip.fbx"; +var leftHandAnimation = HIFI_PUBLIC_BUCKET + "animations/LeftHandAnimPhilip.fbx"; + var BALL_SIZE = 0.08; var PADDLE_SIZE = 0.20; var PADDLE_THICKNESS = 0.06; var PADDLE_COLOR = { red: 184, green: 134, blue: 11 }; var BALL_COLOR = { red: 255, green: 0, blue: 0 }; var LINE_COLOR = { red: 255, green: 255, blue: 0 }; -var PADDLE_OFFSET = { x: 0.05, y: 0.0, z: 0.0 }; +var PADDLE_BOX_OFFSET = { x: 0.05, y: 0.0, z: 0.0 }; + +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; var lastSoundTime = 0; var gameOn = false; -var leftHanded = false; +var leftHanded = true; var controllerID; -if (leftHanded) { - controllerID = 1; -} else { - controllerID = 3; + +function setControllerID() { + if (leftHanded) { + controllerID = 1; + } else { + controllerID = 3; + } } - -HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; -hitSound = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Collisions-ballhitsandcatches/billiards/collision1.wav"); +setControllerID(); +Menu.addMenu("PaddleBall"); +Menu.addMenuItem({ menuName: "PaddleBall", menuItemName: "Left-Handed", isCheckable: true, isChecked: true }); var screenSize = Controller.getViewportDimensions(); var offButton = Overlays.addOverlay("image", { @@ -73,7 +86,7 @@ function createEntities() { modelURL = "http://public.highfidelity.io/models/attachments/pong_paddle.fbx"; paddleModel = Entities.addEntity( { type: "Model", - position: Vec3.sum(Controller.getSpatialControlPosition(controllerID), PADDLE_OFFSET), + position: Vec3.sum(Controller.getSpatialControlPosition(controllerID), PADDLE_BOX_OFFSET), dimensions: { x: PADDLE_SIZE * 1.5, y: PADDLE_THICKNESS, z: PADDLE_SIZE * 1.25 }, color: PADDLE_COLOR, gravity: { x: 0, y: 0, z: 0 }, @@ -90,6 +103,10 @@ function createEntities() { alpha: 1, visible: true, lineWidth: 2 }); + + MyAvatar.stopAnimation(leftHandAnimation); + MyAvatar.stopAnimation(rightHandAnimation); + MyAvatar.startAnimation(leftHanded ? leftHandAnimation: rightHandAnimation, 15.0, 1.0, false, true, 0.0, 6); } function deleteEntities() { @@ -97,6 +114,7 @@ function deleteEntities() { Entities.deleteEntity(paddle); Entities.deleteEntity(paddleModel); Overlays.deleteOverlay(line); + MyAvatar.stopAnimation(leftHanded ? leftHandAnimation: rightHandAnimation); } function update(deltaTime) { @@ -120,18 +138,23 @@ function update(deltaTime) { if (!ball.isKnownID) { ball = Entities.identifyEntity(ball); } else { + 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, leftHanded ? HOLD_POSITION_LEFT_OFFSET : HOLD_POSITION_RIGHT_OFFSET )); + var props = Entities.getEntityProperties(ball); - var spring = Vec3.subtract(palmPosition, props.position); - var paddleWorldOrientation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(controllerID)); + var spring = Vec3.subtract(holdPosition, props.position); var springLength = Vec3.length(spring); + spring = Vec3.normalize(spring); var ballVelocity = Vec3.sum(props.velocity, Vec3.multiply(springLength * SPRING_FORCE * deltaTime, spring)); Entities.editEntity(ball, { velocity: ballVelocity }); - Overlays.editOverlay(line, { start: props.position, end: palmPosition }); - Entities.editEntity(paddle, { position: palmPosition, + Overlays.editOverlay(line, { start: props.position, end: holdPosition }); + Entities.editEntity(paddle, { position: holdPosition, velocity: Controller.getSpatialControlVelocity(controllerID), rotation: paddleWorldOrientation }); - Entities.editEntity(paddleModel, { position: Vec3.sum(palmPosition, Vec3.multiplyQbyV(paddleWorldOrientation, PADDLE_OFFSET)), + Entities.editEntity(paddleModel, { position: Vec3.sum(holdPosition, Vec3.multiplyQbyV(paddleWorldOrientation, PADDLE_BOX_OFFSET)), velocity: Controller.getSpatialControlVelocity(controllerID), rotation: paddleWorldOrientation }); } @@ -159,14 +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(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 43c36a0846..1b551ab2e9 100644 --- a/examples/editEntities.js +++ b/examples/editEntities.js @@ -12,35 +12,38 @@ // 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 }); gridTool.setVisible(false); -Script.include("libraries/entityList.js"); var entityListTool = EntityListTool(); var hasShownPropertiesTool = false; @@ -822,13 +825,15 @@ function handeMenuEvent(menuItem) { Menu.menuItemEvent.connect(handeMenuEvent); Controller.keyPressEvent.connect(function(event) { - if (event.text == 'w' || event.text == 'a' || event.text == 's' || event.text == 'd' - || event.text == 'UP' || event.text == 'DOWN' || event.text == 'LEFT' || event.text == 'RIGHT') { - toolBar.setActive(false); + if (isActive) { + cameraManager.keyPressEvent(event); } }); Controller.keyReleaseEvent.connect(function (event) { + if (isActive) { + cameraManager.keyReleaseEvent(event); + } // since sometimes our menu shortcut keys don't work, trap our menu items here also and fire the appropriate menu items if (event.text == "BACKSPACE" || event.text == "DELETE") { deleteSelectedEntities(); @@ -852,55 +857,6 @@ Controller.keyReleaseEvent.connect(function (event) { newPosition = Vec3.subtract(newPosition, { x: 0, y: selectionManager.worldDimensions.y * 0.5, z: 0 }); grid.setPosition(newPosition); } - } else if (isActive) { - var delta = null; - var increment = event.isShifted ? grid.getMajorIncrement() : grid.getMinorIncrement(); - - if (event.text == 'UP') { - if (event.isControl || event.isAlt) { - delta = { x: 0, y: increment, z: 0 }; - } else { - delta = { x: 0, y: 0, z: -increment }; - } - } else if (event.text == 'DOWN') { - if (event.isControl || event.isAlt) { - delta = { x: 0, y: -increment, z: 0 }; - } else { - delta = { x: 0, y: 0, z: increment }; - } - } else if (event.text == 'LEFT') { - delta = { x: -increment, y: 0, z: 0 }; - } else if (event.text == 'RIGHT') { - delta = { x: increment, y: 0, z: 0 }; - } - - if (delta != null) { - // Adjust delta so that movements are relative to the current camera orientation - var lookDirection = Quat.getFront(Camera.getOrientation()); - lookDirection.z *= -1; - - var angle = Math.atan2(lookDirection.z, lookDirection.x); - angle -= (Math.PI / 4); - - var rotation = Math.floor(angle / (Math.PI / 2)) * (Math.PI / 2); - var rotator = Quat.fromPitchYawRollRadians(0, rotation, 0); - - delta = Vec3.multiplyQbyV(rotator, delta); - - SelectionManager.saveProperties(); - - for (var i = 0; i < selectionManager.selections.length; i++) { - var entityID = selectionManager.selections[i]; - var properties = Entities.getEntityProperties(entityID); - Entities.editEntity(entityID, { - position: Vec3.sum(properties.position, delta) - }); - } - - pushCommandForSelections(); - - selectionManager._update(); - } } }); diff --git a/examples/example/games/billiards.js b/examples/example/games/billiards.js index d671addbe6..fbd41e8939 100644 --- a/examples/example/games/billiards.js +++ b/examples/example/games/billiards.js @@ -1,4 +1,17 @@ -// Pool Table +// Billiards.js +// +// Created by Philip Rosedale on January 21, 2015 +// Copyright 2014 High Fidelity, Inc. +// +// Creates a pool table in front of you. Hold and release space ball to shoot a ball. +// Cue ball will return if falls off table. Delete and reset to restart. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; + var tableParts = []; var balls = []; var cueBall; @@ -19,6 +32,10 @@ var cuePosition; var startStroke = 0; +// Sounds to use +hitSounds = []; +hitSounds.push(SoundCache.getSound(HIFI_PUBLIC_BUCKET + "Collisions-ballhitsandcatches/billiards/collision1.wav")); + HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; var screenSize = Controller.getViewportDimensions(); var reticle = Overlays.addOverlay("image", { @@ -122,6 +139,7 @@ function makeBalls(pos) { } ballPosition.x += (BALL_GAP + Math.sqrt(3.0) / 2.0 * BALL_SIZE) * SCALE; } + // Cue Ball cuePosition = { x: pos.x - (LENGTH / 4.0) * SCALE, y: pos.y + HEIGHT / 2.0 + DROP_HEIGHT, z: pos.z }; cueBall = Entities.addEntity( @@ -138,6 +156,15 @@ function makeBalls(pos) { } +function isObjectBall(id) { + for (var i; i < balls.length; i++) { + if (balls[i].id == id) { + return true; + } + } + return false; +} + function shootCue(velocity) { var DISTANCE_FROM_CAMERA = BALL_SIZE * 5.0 * SCALE; var camera = Camera.getPosition(); @@ -213,12 +240,28 @@ function update(deltaTime) { } +function entityCollisionWithEntity(entity1, entity2, collision) { + /* + NOT WORKING YET + if ((entity1.id == cueBall.id) || (entity2.id == cueBall.id)) { + print("Cue ball collision!"); + //audioOptions.position = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation())); + //Audio.playSound(hitSounds[0], { position: Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation())) }); + } + + else if (isObjectBall(entity1.id) || isObjectBall(entity2.id)) { + print("Object ball collision"); + } */ +} + tableCenter = Vec3.sum(MyAvatar.position, Vec3.multiply(4.0, Quat.getFront(Camera.getOrientation()))); makeTable(tableCenter); makeBalls(tableCenter); +Entities.entityCollisionWithEntity.connect(entityCollisionWithEntity); Script.scriptEnding.connect(cleanup); Controller.keyPressEvent.connect(keyPressEvent); Controller.keyReleaseEvent.connect(keyReleaseEvent); Script.update.connect(update); + diff --git a/examples/libraries/entityCameraTool.js b/examples/libraries/entityCameraTool.js index f5095bb149..609c1db076 100644 --- a/examples/libraries/entityCameraTool.js +++ b/examples/libraries/entityCameraTool.js @@ -15,6 +15,9 @@ var MOUSE_SENSITIVITY = 0.9; var SCROLL_SENSITIVITY = 0.05; var PAN_ZOOM_SCALE_RATIO = 0.4; +var KEY_ORBIT_SENSITIVITY = 40; +var KEY_ZOOM_SENSITIVITY = 10; + // Scaling applied based on the size of the object being focused var FOCUS_ZOOM_SCALE = 1.3; @@ -43,6 +46,10 @@ var easeOutCubic = function(t) { EASE_TIME = 0.5; +function clamp(value, minimum, maximum) { + return Math.min(Math.max(value, minimum), maximum); +} + function mergeObjects(obj1, obj2) { var newObj = {}; for (key in obj1) { @@ -60,6 +67,49 @@ CameraManager = function() { that.enabled = false; that.mode = MODE_INACTIVE; + var actions = { + orbitLeft: 0, + orbitRight: 0, + orbitUp: 0, + orbitDown: 0, + orbitForward: 0, + orbitBackward: 0, + } + + var keyToActionMapping = { + "a": "orbitLeft", + "d": "orbitRight", + "w": "orbitForward", + "s": "orbitBackward", + "e": "orbitUp", + "c": "orbitDown", + + "LEFT": "orbitLeft", + "RIGHT": "orbitRight", + "UP": "orbitForward", + "DOWN": "orbitBackward", + } + + var CAPTURED_KEYS = []; + for (key in keyToActionMapping) { + CAPTURED_KEYS.push(key); + } + + function getActionForKeyEvent(event) { + var action = keyToActionMapping[event.text]; + if (action !== undefined) { + if (event.isShifted) { + if (action == "orbitForward") { + action = "orbitUp"; + } else if (action == "orbitBackward") { + action = "orbitDown"; + } + } + return action; + } + return null; + } + that.zoomDistance = INITIAL_ZOOM_DISTANCE; that.targetZoomDistance = INITIAL_ZOOM_DISTANCE; @@ -82,6 +132,10 @@ CameraManager = function() { that.enable = function() { if (Camera.mode == "independent" || that.enabled) return; + for (var i = 0; i < CAPTURED_KEYS.length; i++) { + Controller.captureKeyEvents({ text: CAPTURED_KEYS[i] }); + } + that.enabled = true; that.mode = MODE_INACTIVE; @@ -112,6 +166,11 @@ CameraManager = function() { that.disable = function(ignoreCamera) { if (!that.enabled) return; + + for (var i = 0; i < CAPTURED_KEYS.length; i++) { + Controller.releaseKeyEvents({ text: CAPTURED_KEYS[i] }); + } + that.enabled = false; that.mode = MODE_INACTIVE; @@ -280,6 +339,20 @@ CameraManager = function() { that.mode = MODE_INACTIVE; } + that.keyPressEvent = function(event) { + var action = getActionForKeyEvent(event); + if (action) { + actions[action] = 1; + } + }; + + that.keyReleaseEvent = function(event) { + var action = getActionForKeyEvent(event); + if (action) { + actions[action] = 0; + } + }; + that.wheelEvent = function(event) { if (!that.enabled) return; @@ -333,6 +406,14 @@ CameraManager = function() { return; } + // Update based on current actions + that.targetYaw += (actions.orbitRight - actions.orbitLeft) * dt * KEY_ORBIT_SENSITIVITY; + that.targetPitch += (actions.orbitUp - actions.orbitDown) * dt * KEY_ORBIT_SENSITIVITY; + that.targetPitch = clamp(that.targetPitch, -90, 90); + that.targetZoomDistance += (actions.orbitBackward - actions.orbitForward) * dt * KEY_ZOOM_SENSITIVITY; + that.targetZoomDistance = clamp(that.targetZoomDistance, MIN_ZOOM_DISTANCE, MAX_ZOOM_DISTANCE); + + if (easing) { easingTime = Math.min(EASE_TIME, easingTime + dt); } @@ -384,6 +465,7 @@ CameraManager = function() { }); Script.update.connect(that.update); + Script.scriptEnding.connect(that.disable); Controller.wheelEvent.connect(that.wheelEvent); 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/examples/popcorn.js b/examples/popcorn.js new file mode 100644 index 0000000000..567953ac60 --- /dev/null +++ b/examples/popcorn.js @@ -0,0 +1,182 @@ +// +// popcorn.js +// examples +// +// Created by Philip Rosedale on January 25, 2014 +// Copyright 2015 High Fidelity, Inc. +// +// Creates a bunch of physical balls trapped in a box with a rotating wall in the middle that smacks them around, +// and a periodic 'pop' force that shoots them into the air. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var BALL_SIZE = 0.07; +var WALL_THICKNESS = 0.10; +var SCALE = 1.0; + +var GRAVITY = -1.0; +var LIFETIME = 600; +var DAMPING = 0.50; + +var center = Vec3.sum(MyAvatar.position, Vec3.multiply(SCALE * 3.0, Quat.getFront(Camera.getOrientation()))); + +var floor = Entities.addEntity( + { type: "Box", + position: Vec3.subtract(center, { x: 0, y: SCALE / 2.0, z: 0 }), + dimensions: { x: SCALE, y: WALL_THICKNESS, z: SCALE }, + color: { red: 0, green: 255, blue: 0 }, + gravity: { x: 0, y: 0, z: 0 }, + ignoreCollisions: false, + lifetime: LIFETIME }); + +var ceiling = Entities.addEntity( + { type: "Box", + position: Vec3.sum(center, { x: 0, y: SCALE / 2.0, z: 0 }), + dimensions: { x: SCALE, y: WALL_THICKNESS, z: SCALE }, + color: { red: 128, green: 128, blue: 128 }, + gravity: { x: 0, y: 0, z: 0 }, + ignoreCollisions: false, + visible: true, + lifetime: LIFETIME }); + +var wall1 = Entities.addEntity( + { type: "Box", + position: Vec3.sum(center, { x: SCALE / 2.0, y: 0, z: 0 }), + dimensions: { x: WALL_THICKNESS, y: SCALE, z: SCALE }, + color: { red: 0, green: 255, blue: 0 }, + gravity: { x: 0, y: 0, z: 0 }, + ignoreCollisions: false, + visible: false, + lifetime: LIFETIME }); + +var wall2 = Entities.addEntity( + { type: "Box", + position: Vec3.subtract(center, { x: SCALE / 2.0, y: 0, z: 0 }), + dimensions: { x: WALL_THICKNESS, y: SCALE, z: SCALE }, + color: { red: 0, green: 255, blue: 0 }, + gravity: { x: 0, y: 0, z: 0 }, + ignoreCollisions: false, + visible: false, + lifetime: LIFETIME }); + +var wall3 = Entities.addEntity( + { type: "Box", + position: Vec3.subtract(center, { x: 0, y: 0, z: SCALE / 2.0 }), + dimensions: { x: SCALE, y: SCALE, z: WALL_THICKNESS }, + color: { red: 0, green: 255, blue: 0 }, + gravity: { x: 0, y: 0, z: 0 }, + ignoreCollisions: false, + visible: false, + lifetime: LIFETIME }); + +var wall4 = Entities.addEntity( + { type: "Box", + position: Vec3.sum(center, { x: 0, y: 0, z: SCALE / 2.0 }), + dimensions: { x: SCALE, y: SCALE, z: WALL_THICKNESS }, + color: { red: 0, green: 255, blue: 0 }, + gravity: { x: 0, y: 0, z: 0 }, + ignoreCollisions: false, + visible: false, + lifetime: LIFETIME }); + +var corner1 = Entities.addEntity( + { type: "Box", + position: Vec3.sum(center, { x: -SCALE / 2.0, y: 0, z: SCALE / 2.0 }), + dimensions: { x: WALL_THICKNESS, y: SCALE, z: WALL_THICKNESS }, + color: { red: 128, green: 128, blue: 128 }, + ignoreCollisions: false, + visible: true, + lifetime: LIFETIME }); + +var corner2 = Entities.addEntity( + { type: "Box", + position: Vec3.sum(center, { x: -SCALE / 2.0, y: 0, z: -SCALE / 2.0 }), + dimensions: { x: WALL_THICKNESS, y: SCALE, z: WALL_THICKNESS }, + color: { red: 128, green: 128, blue: 128 }, + ignoreCollisions: false, + visible: true, + lifetime: LIFETIME }); + +var corner3 = Entities.addEntity( + { type: "Box", + position: Vec3.sum(center, { x: SCALE / 2.0, y: 0, z: SCALE / 2.0 }), + dimensions: { x: WALL_THICKNESS, y: SCALE, z: WALL_THICKNESS }, + color: { red: 128, green: 128, blue: 128 }, + ignoreCollisions: false, + visible: true, + lifetime: LIFETIME }); + +var corner4 = Entities.addEntity( + { type: "Box", + position: Vec3.sum(center, { x: SCALE / 2.0, y: 0, z: -SCALE / 2.0 }), + dimensions: { x: WALL_THICKNESS, y: SCALE, z: WALL_THICKNESS }, + color: { red: 128, green: 128, blue: 128 }, + ignoreCollisions: false, + visible: true, + lifetime: LIFETIME }); + +var spinner = Entities.addEntity( + { type: "Box", + position: center, + dimensions: { x: SCALE / 1.5, y: SCALE / 3.0, z: SCALE / 8.0 }, + color: { red: 255, green: 0, blue: 0 }, + angularVelocity: { x: 0, y: 360, z: 0 }, + angularDamping: 0.0, + gravity: { x: 0, y: 0, z: 0 }, + ignoreCollisions: false, + visible: true, + lifetime: LIFETIME }); + +var NUM_BALLS = 70; + +balls = []; + +for (var i = 0; i < NUM_BALLS; i++) { + balls.push(Entities.addEntity( + { type: "Sphere", + position: { x: center.x + (Math.random() - 0.5) * (SCALE - BALL_SIZE - WALL_THICKNESS), + y: center.y + (Math.random() - 0.5) * (SCALE - BALL_SIZE - WALL_THICKNESS) , + z: center.z + (Math.random() - 0.5) * (SCALE - BALL_SIZE - WALL_THICKNESS) }, + dimensions: { x: BALL_SIZE, y: BALL_SIZE, z: BALL_SIZE }, + color: { red: Math.random() * 255, green: Math.random() * 255, blue: Math.random() * 255 }, + gravity: { x: 0, y: GRAVITY, z: 0 }, + ignoreCollisions: false, + damping: DAMPING, + lifetime: LIFETIME, + collisionsWillMove: true })); +} + +var VEL_MAG = 2.0; +var CHANCE_OF_POP = 0.007; // 0.01; +function update(deltaTime) { + for (var i = 0; i < NUM_BALLS; i++) { + if (Math.random() < CHANCE_OF_POP) { + Entities.editEntity(balls[i], { velocity: { x: (Math.random() - 0.5) * VEL_MAG, y: Math.random() * VEL_MAG, z: (Math.random() - 0.5) * VEL_MAG }}); + } + } + +} + + +function scriptEnding() { + Entities.deleteEntity(wall1); + Entities.deleteEntity(wall2); + Entities.deleteEntity(wall3); + Entities.deleteEntity(wall4); + Entities.deleteEntity(corner1); + Entities.deleteEntity(corner2); + Entities.deleteEntity(corner3); + Entities.deleteEntity(corner4); + Entities.deleteEntity(floor); + Entities.deleteEntity(ceiling); + Entities.deleteEntity(spinner); + + for (var i = 0; i < NUM_BALLS; i++) { + Entities.deleteEntity(balls[i]); + } +} + +Script.scriptEnding.connect(scriptEnding); +Script.update.connect(update); \ No newline at end of file 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/interface/src/ui/overlays/ImageOverlay.cpp b/interface/src/ui/overlays/ImageOverlay.cpp index 0c5d9a7737..e354ea539e 100644 --- a/interface/src/ui/overlays/ImageOverlay.cpp +++ b/interface/src/ui/overlays/ImageOverlay.cpp @@ -45,7 +45,6 @@ ImageOverlay::~ImageOverlay() { // TODO: handle setting image multiple times, how do we manage releasing the bound texture? void ImageOverlay::setImageURL(const QUrl& url) { _imageURL = url; - if (url.isEmpty()) { _isLoaded = true; _renderImage = false; diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index d0779afb42..f9fbcb72df 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -254,7 +254,16 @@ bool Texture::assignStoredMip(uint16 level, const Element& format, Size size, co } // THen check that the mem buffer passed make sense with its format - if (size == evalStoredMipSize(level, format)) { + Size expectedSize = evalStoredMipSize(level, format); + if (size == expectedSize) { + _storage->assignMipData(level, format, size, bytes); + _stamp++; + return true; + } else if (size > expectedSize) { + // NOTE: We are facing this case sometime because apparently QImage (from where we get the bits) is generating images + // and alligning the line of pixels to 32 bits. + // We should probably consider something a bit more smart to get the correct result but for now (UI elements) + // it seems to work... _storage->assignMipData(level, format, size, bytes); _stamp++; return true; diff --git a/libraries/render-utils/src/TextureCache.cpp b/libraries/render-utils/src/TextureCache.cpp index 3644ded81c..aac2ec1b8c 100644 --- a/libraries/render-utils/src/TextureCache.cpp +++ b/libraries/render-utils/src/TextureCache.cpp @@ -387,7 +387,7 @@ NetworkTexture::NetworkTexture(const QUrl& url, TextureType type, const QByteArr if (!url.isValid()) { _loaded = true; } - + // default to white/blue/black /* glBindTexture(GL_TEXTURE_2D, getID()); switch (type) { 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;