"use strict"; // // vr-edit.js // // Created by David Rowe on 27 Jun 2017. // Copyright 2017 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // (function () { var APP_NAME = "VR EDIT", // TODO: App name. APP_ICON_INACTIVE = "icons/tablet-icons/edit-i.svg", // TODO: App icons. APP_ICON_ACTIVE = "icons/tablet-icons/edit-a.svg", tablet, button, isAppActive = false, VR_EDIT_SETTING = "io.highfidelity.isVREditing", // Note: This constant is duplicated in utils.js. hands = [], LEFT_HAND = 0, RIGHT_HAND = 1, UPDATE_LOOP_TIMEOUT = 16, updateTimer = null, Selection, Laser, Hand, AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}", NULL_UUID = "{00000000-0000-0000-0000-000000000000}"; Selection = function () { // Manages set of selected entities. Currently supports just one set of linked entities. var selection = [], selectedEntityID = null, selectionPosition = null, selectionOrientation, rootEntityID; function traverseEntityTree(id, result) { // Recursively traverses tree of entities and their children, gather IDs and properties. var children, properties, SELECTION_PROPERTIES = ["position", "registrationPoint", "rotation", "dimensions"], i, length; properties = Entities.getEntityProperties(id, SELECTION_PROPERTIES); result.push({ id: id, position: properties.position, registrationPoint: properties.registrationPoint, rotation: properties.rotation, dimensions: properties.dimensions }); children = Entities.getChildrenIDs(id); for (i = 0, length = children.length; i < length; i += 1) { traverseEntityTree(children[i], result); } } function select(entityID) { var entityProperties, PARENT_PROPERTIES = ["parentID", "position", "rotation"]; // Find root parent. rootEntityID = entityID; entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); while (entityProperties.parentID !== NULL_UUID) { rootEntityID = entityProperties.parentID; entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES); } // Selection position and orientation is that of the root entity. selectionPosition = entityProperties.position; selectionOrientation = entityProperties.rotation; // Find all children. selection = []; traverseEntityTree(rootEntityID, selection); selectedEntityID = entityID; } function getSelection() { return selection; } function getPositionAndOrientation() { return { position: selectionPosition, orientation: selectionOrientation }; } function setPositionAndOrientation(position, orientation) { selectionPosition = position; selectionOrientation = orientation; Entities.editEntity(rootEntityID, { position: position, rotation: orientation }); } function clear() { selection = []; selectedEntityID = null; rootEntityID = null; } function destroy() { clear(); } if (!this instanceof Selection) { return new Selection(); } return { select: select, selection: getSelection, getPositionAndOrientation: getPositionAndOrientation, setPositionAndOrientation: setPositionAndOrientation, clear: clear, destroy: destroy }; }; Laser = function (side) { // Draws hand lasers. // May intersect with entities or bounding box of other hand's selection. var hand, laserLine = null, laserSphere = null, searchDistance = 0.0, SEARCH_SPHERE_SIZE = 0.013, // Per handControllerGrab.js multiplied by 1.2 per handControllerGrab.js. SEARCH_SPHERE_FOLLOW_RATE = 0.5, // Per handControllerGrab.js. COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { red: 10, green: 10, blue: 255 }, // Per handControllgerGrab.js. COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }; // Per handControllgerGrab.js. hand = side; laserLine = Overlays.addOverlay("line3d", { lineWidth: 5, alpha: 1.0, glow: 1.0, ignoreRayIntersection: true, drawInFront: true, parentID: AVATAR_SELF_ID, parentJointIndex: MyAvatar.getJointIndex(hand === LEFT_HAND ? "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND" : "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"), visible: false }); laserSphere = Overlays.addOverlay("circle3d", { innerAlpha: 1.0, outerAlpha: 0.0, solid: true, ignoreRayIntersection: true, drawInFront: true, visible: false }); function colorPow(color, power) { return { red: Math.pow(color.red / 255, power) * 255, green: Math.pow(color.green / 255, power) * 255, blue: Math.pow(color.blue / 255, power) * 255 }; } function updateLine(start, end, color) { Overlays.editOverlay(laserLine, { start: start, end: end, color: color, visible: true }); } function updateSphere(location, size, color) { var rotation, brightColor; rotation = Quat.lookAt(location, Camera.getPosition(), Vec3.UP); brightColor = colorPow(color, 0.06); Overlays.editOverlay(laserSphere, { position: location, rotation: rotation, innerColor: brightColor, outerColor: color, outerRadius: size, visible: true }); } function update(origin, direction, distance, isClicked) { var searchTarget, sphereSize, color; searchDistance = SEARCH_SPHERE_FOLLOW_RATE * searchDistance + (1.0 - SEARCH_SPHERE_FOLLOW_RATE) * distance; searchTarget = Vec3.sum(origin, Vec3.multiply(searchDistance, direction)); sphereSize = SEARCH_SPHERE_SIZE * searchDistance; color = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE : COLORS_GRAB_SEARCHING_HALF_SQUEEZE; updateLine(origin, searchTarget, color); updateSphere(searchTarget, sphereSize, color); } function clear() { Overlays.editOverlay(laserLine, { visible: false }); Overlays.editOverlay(laserSphere, { visible: false }); } function destroy() { Overlays.deleteOverlay(laserLine); Overlays.deleteOverlay(laserSphere); } if (!this instanceof Laser) { return new Laser(); } return { update: update, clear: clear, destroy: destroy }; }; Hand = function (side) { // Hand controller input. // Each hand has a laser, an entity selection, and entity highlighter. var hand, handController, controllerTrigger, controllerTriggerClicked, TRIGGER_ON_VALUE = 0.15, // Per handControllerGrab.js. TRIGGER_OFF_VALUE = 0.1, // Per handControllerGrab.js. GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js. PICK_MAX_DISTANCE = 500, // Per handControllerGrab.js. PRECISION_PICKING = true, NO_INCLUDE_IDS = [], NO_EXCLUDE_IDS = [], VISIBLE_ONLY = true, isLaserOn = false, hoveredEntityID = null, laser, selection; hand = side; if (hand === LEFT_HAND) { handController = Controller.Standard.LeftHand; controllerTrigger = Controller.Standard.LT; controllerTriggerClicked = Controller.Standard.LTClick; GRAB_POINT_SPHERE_OFFSET.x = -GRAB_POINT_SPHERE_OFFSET.x; } else { handController = Controller.Standard.RightHand; controllerTrigger = Controller.Standard.RT; controllerTriggerClicked = Controller.Standard.RTClick; } laser = new Laser(hand); selection = new Selection(); function update() { var wasLaserOn, handPose, handPosition, handOrientation, deltaOrigin, pickRay, intersection, distance; // Controller trigger. wasLaserOn = isLaserOn; isLaserOn = Controller.getValue(controllerTrigger) > (isLaserOn ? TRIGGER_OFF_VALUE : TRIGGER_ON_VALUE); if (!isLaserOn) { if (wasLaserOn) { laser.clear(); selection.clear(); hoveredEntityID = null; } return; } // Hand position and orientation. handPose = Controller.getPoseValue(handController); if (!handPose.valid) { isLaserOn = false; if (wasLaserOn) { laser.clear(); selection.clear(); hoveredEntityID = null; } return; } handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position); handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); // Entity intersection, if any. deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET); pickRay = { origin: Vec3.sum(handPosition, deltaOrigin), // Add a bit to ... direction: Quat.getUp(handOrientation), length: PICK_MAX_DISTANCE }; intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS, VISIBLE_ONLY); distance = intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE; // Hover entities. laser.update(pickRay.origin, pickRay.direction, distance, Controller.getValue(controllerTriggerClicked)); if (intersection.intersects) { if (intersection.entityID !== hoveredEntityID) { hoveredEntityID = intersection.entityID; selection.select(hoveredEntityID); } } else { if (hoveredEntityID) { selection.clear(); hoveredEntityID = null; } } } function destroy() { if (laser) { laser.destroy(); laser = null; } if (selection) { selection.destroy(); selection = null; } } if (!this instanceof Hand) { return new Hand(); } return { update: update, destroy: destroy }; }; function update() { // Main update loop. updateTimer = null; hands[LEFT_HAND].update(); hands[RIGHT_HAND].update(); updateTimer = Script.setTimeout(update, UPDATE_LOOP_TIMEOUT); } function updateHandControllerGrab() { // Communicate app status to handControllerGrab.js. Settings.setValue(VR_EDIT_SETTING, isAppActive); } function onButtonClicked() { isAppActive = !isAppActive; updateHandControllerGrab(); button.editProperties({ isActive: isAppActive }); if (isAppActive) { update(); } else { Script.clearTimeout(updateTimer); updateTimer = null; } } function setUp() { updateHandControllerGrab(); tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); if (!tablet) { return; } // Tablet/toolbar button. button = tablet.addButton({ icon: APP_ICON_INACTIVE, activeIcon: APP_ICON_ACTIVE, text: APP_NAME, isActive: isAppActive }); if (button) { button.clicked.connect(onButtonClicked); } // Hands, each with a laser, selection, etc. hands[LEFT_HAND] = new Hand(LEFT_HAND); hands[RIGHT_HAND] = new Hand(RIGHT_HAND); if (isAppActive) { update(); } } function tearDown() { if (updateTimer) { Script.clearTimeout(updateTimer); } isAppActive = false; updateHandControllerGrab(); if (!tablet) { return; } if (button) { button.clicked.disconnect(onButtonClicked); tablet.removeButton(button); button = null; } if (hands[LEFT_HAND]) { hands[LEFT_HAND].destroy(); hands[LEFT_HAND] = null; } if (hands[RIGHT_HAND]) { hands[RIGHT_HAND].destroy(); hands[RIGHT_HAND] = null; } tablet = null; } setUp(); Script.scriptEnding.connect(tearDown); }());