// // 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 () { "use strict"; 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", VR_EDIT_SETTING = "io.highfidelity.isVREditing", // Note: This constant is duplicated in utils.js. // Application state isAppActive = false, isAppScaleWithHandles = false, dominantHand, // Primary objects editors = [], LEFT_HAND = 0, RIGHT_HAND = 1, toolMenu, // Modules Hand, Handles, Highlights, Laser, Selection, ToolMenu, Editor, // Miscellaneous UPDATE_LOOP_TIMEOUT = 16, updateTimer = null, tablet, button, DEBUG = true; // TODO: Set false. // Utilities Script.include("./utilities/utilities.js"); // Modules Script.include("./modules/hand.js"); Script.include("./modules/handles.js"); Script.include("./modules/highlights.js"); Script.include("./modules/laser.js"); Script.include("./modules/selection.js"); Script.include("./modules/toolMenu.js"); function log(message) { print(APP_NAME + ": " + message); } function debug(side, message) { // Optional parameter: side. var hand = "", HAND_LETTERS = ["L", "R"]; if (DEBUG) { if (side === 0 || side === 1) { hand = HAND_LETTERS[side] + " "; } else { message = side; } log(hand + message); } } Editor = function (side) { // Each controller has a hand, laser, an entity selection, entity highlighter, and entity handles. var otherEditor, // Other hand's Editor object. // Editor states. EDITOR_IDLE = 0, EDITOR_SEARCHING = 1, EDITOR_HIGHLIGHTING = 2, // Highlighting an entity (not hovering a handle). EDITOR_GRABBING = 3, EDITOR_DIRECT_SCALING = 4, // Scaling data are sent to other editor's EDITOR_GRABBING state. EDITOR_HANDLE_SCALING = 5, // "" EDITOR_STATE_STRINGS = ["EDITOR_IDLE", "EDITOR_SEARCHING", "EDITOR_HIGHLIGHTING", "EDITOR_GRABBING", "EDITOR_DIRECT_SCALING", "EDITOR_HANDLE_SCALING"], editorState = EDITOR_IDLE, // State machine. STATE_MACHINE, highlightedEntityID = null, // Root entity of highlighted entity set. wasAppScaleWithHandles = false, isOtherEditorEditingEntityID = false, hoveredOverlayID = null, doDeleteEntities = false, // Primary objects. hand, laser, selection, highlights, handles, // Position values. initialHandOrientationInverse, initialHandToSelectionVector, initialSelectionOrientation, // Scaling values. isScalingWithHand = false, isDirectScaling = false, // Modifies EDITOR_GRABBING state. isHandleScaling = false, // "" initialTargetsSeparation, initialtargetsDirection, initialTargetToBoundingBoxCenter, otherTargetPosition, handleUnitScaleAxis, handleScaleDirections, handleHandOffset, initialHandleDistance, initialHandleOrientationInverse, initialHandleRegistrationOffset, initialSelectionOrientationInverse, laserOffset, MIN_SCALE = 0.001, intersection; function onGripClicked() { // Delete entity set grabbed by this hand. if (editorState === EDITOR_GRABBING) { doDeleteEntities = true; } } hand = new Hand(side, onGripClicked); laser = new Laser(side); selection = new Selection(side); highlights = new Highlights(side); handles = new Handles(side); laserOffset = laser.handOffset(); function setOtherEditor(editor) { otherEditor = editor; } function hoverHandle(overlayID) { // Highlights handle if overlayID is a handle, otherwise unhighlights currently highlighted handle if any. handles.hover(overlayID); } function isHandle(overlayID) { return handles.isHandle(overlayID); } function isEditing(rootEntityID) { // rootEntityID is an optional parameter. return editorState > EDITOR_HIGHLIGHTING && (rootEntityID === undefined || rootEntityID === selection.rootEntityID()); } function isScaling() { return editorState === EDITOR_DIRECT_SCALING || editorState === EDITOR_HANDLE_SCALING; } function rootEntityID() { return selection.rootEntityID(); } function startEditing() { var selectionPositionAndOrientation; initialHandOrientationInverse = Quat.inverse(hand.orientation()); selectionPositionAndOrientation = selection.getPositionAndOrientation(); initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, hand.position()); initialSelectionOrientation = selectionPositionAndOrientation.orientation; selection.startEditing(); } function stopEditing() { selection.finishEditing(); } function getScaleTargetPosition() { if (isScalingWithHand) { return side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); } return Vec3.sum(Vec3.sum(hand.position(), Vec3.multiplyQbyV(hand.orientation(), laserOffset)), Vec3.multiply(laser.length(), Quat.getUp(hand.orientation()))); } function startDirectScaling(targetPosition) { // Called on grabbing hand by scaling hand. var initialTargetPosition, initialTargetsCenter; isScalingWithHand = intersection.handIntersected; otherTargetPosition = targetPosition; initialTargetPosition = getScaleTargetPosition(); initialTargetsCenter = Vec3.multiply(0.5, Vec3.sum(initialTargetPosition, otherTargetPosition)); initialTargetsSeparation = Vec3.distance(initialTargetPosition, otherTargetPosition); initialtargetsDirection = Vec3.subtract(otherTargetPosition, initialTargetPosition); selection.startDirectScaling(initialTargetsCenter); isDirectScaling = true; } function updateDirectScaling(targetPosition) { // Called on grabbing hand by scaling hand. otherTargetPosition = targetPosition; } function stopDirectScaling() { // Called on grabbing hand by scaling hand. selection.finishDirectScaling(); isDirectScaling = false; } function startHandleScaling(targetPosition, overlayID) { // Called on grabbing hand by scaling hand. var initialTargetPosition, boundingBox, selectionPositionAndOrientation, scaleAxis, handDistance; isScalingWithHand = intersection.handIntersected; otherTargetPosition = targetPosition; // Keep grabbed handle highlighted and hide other handles. handles.grab(overlayID); // Vector from target to bounding box center. initialTargetPosition = getScaleTargetPosition(); boundingBox = selection.boundingBox(); initialTargetToBoundingBoxCenter = Vec3.subtract(boundingBox.center, initialTargetPosition); // Selection information. selectionPositionAndOrientation = selection.getPositionAndOrientation(); initialSelectionOrientationInverse = Quat.inverse(selectionPositionAndOrientation.orientation); // Handle information. initialHandleOrientationInverse = Quat.inverse(hand.orientation()); handleUnitScaleAxis = handles.scalingAxis(overlayID); // Unit vector in direction of scaling. handleScaleDirections = handles.scalingDirections(overlayID); // Which axes to scale the selection on. initialHandleDistance = Vec3.length(Vec3.multiplyVbyV(boundingBox.dimensions, handleScaleDirections)) / 2; initialHandleRegistrationOffset = Vec3.multiplyQbyV(initialSelectionOrientationInverse, Vec3.subtract(selectionPositionAndOrientation.position, boundingBox.center)); // Distance from hand to handle in direction of handle. scaleAxis = Vec3.multiplyQbyV(selectionPositionAndOrientation.orientation, handleUnitScaleAxis); handDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, boundingBox.center), scaleAxis)); handleHandOffset = handDistance - initialHandleDistance; selection.startHandleScaling(); handles.startScaling(); isHandleScaling = true; } function updateHandleScaling(targetPosition) { // Called on grabbing hand by scaling hand. otherTargetPosition = targetPosition; } function stopHandleScaling() { // Called on grabbing hand by scaling hand. handles.finishScaling(); selection.finishHandleScaling(); handles.grab(null); // Stop highlighting grabbed handle and resume displaying all handles. isHandleScaling = false; } function applyGrab() { // Sets position and orientation of selection per grabbing hand. var deltaOrientation, selectionPosition, selectionOrientation; deltaOrientation = Quat.multiply(hand.orientation(), initialHandOrientationInverse); selectionPosition = Vec3.sum(hand.position(), Vec3.multiplyQbyV(deltaOrientation, initialHandToSelectionVector)); selectionOrientation = Quat.multiply(deltaOrientation, initialSelectionOrientation); selection.setPositionAndOrientation(selectionPosition, selectionOrientation); } function applyDirectScale() { // Scales, rotates, and positions selection per changing length, orientation, and position of vector between hands. var targetPosition, targetsSeparation, scale, rotation, center, selectionPositionAndOrientation; // Scale selection. targetPosition = getScaleTargetPosition(); targetsSeparation = Vec3.distance(targetPosition, otherTargetPosition); scale = targetsSeparation / initialTargetsSeparation; scale = Math.max(scale, MIN_SCALE); rotation = Quat.rotationBetween(initialtargetsDirection, Vec3.subtract(otherTargetPosition, targetPosition)); center = Vec3.multiply(0.5, Vec3.sum(targetPosition, otherTargetPosition)); selection.directScale(scale, rotation, center); // Update grab offset. selectionPositionAndOrientation = selection.getPositionAndOrientation(); initialHandOrientationInverse = Quat.inverse(hand.orientation()); initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, hand.position()); initialSelectionOrientation = selectionPositionAndOrientation.orientation; } function applyHandleScale() { // Scales selection per changing position of scaling hand; positions and orients per grabbing hand. var targetPosition, deltaHandOrientation, deltaHandleOrientation, selectionPosition, selectionOrientation, boundingBoxCenter, scaleAxis, handleDistance, scale, scale3D, selectionPositionAndOrientation; // Orient selection per grabbing hand. deltaHandOrientation = Quat.multiply(hand.orientation(), initialHandOrientationInverse); selectionOrientation = Quat.multiply(deltaHandOrientation, initialSelectionOrientation); // Desired distance of handle from center of bounding box. targetPosition = getScaleTargetPosition(); deltaHandleOrientation = Quat.multiply(hand.orientation(), initialHandleOrientationInverse); boundingBoxCenter = Vec3.sum(targetPosition, Vec3.multiplyQbyV(deltaHandleOrientation, initialTargetToBoundingBoxCenter)); scaleAxis = Vec3.multiplyQbyV(selection.getPositionAndOrientation().orientation, handleUnitScaleAxis); handleDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, boundingBoxCenter), scaleAxis)); handleDistance -= handleHandOffset; handleDistance = Math.max(handleDistance, MIN_SCALE); // Scale selection relative to initial dimensions. scale = handleDistance / initialHandleDistance; scale3D = Vec3.multiply(scale, handleScaleDirections); scale3D = { x: handleScaleDirections.x !== 0 ? scale3D.x : 1, y: handleScaleDirections.y !== 0 ? scale3D.y : 1, z: handleScaleDirections.z !== 0 ? scale3D.z : 1 }; // Reposition selection per scale. selectionPosition = Vec3.sum(boundingBoxCenter, Vec3.multiplyQbyV(selectionOrientation, Vec3.multiplyVbyV(scale3D, initialHandleRegistrationOffset))); // Scale. handles.scale(scale3D); selection.handleScale(scale3D, selectionPosition, selectionOrientation); // Update grab offset. selectionPositionAndOrientation = selection.getPositionAndOrientation(); initialHandOrientationInverse = Quat.inverse(hand.orientation()); initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, hand.position()); initialSelectionOrientation = selectionPositionAndOrientation.orientation; } function enterEditorIdle() { laser.clear(); selection.clear(); } function exitEditorIdle() { // Nothing to do. } function enterEditorSearching() { selection.clear(); hoveredOverlayID = intersection.overlayID; otherEditor.hoverHandle(hoveredOverlayID); } function updateEditorSearching() { if (isAppScaleWithHandles && intersection.overlayID !== hoveredOverlayID && otherEditor.isEditing()) { hoveredOverlayID = intersection.overlayID; otherEditor.hoverHandle(hoveredOverlayID); } } function exitEditorSearching() { otherEditor.hoverHandle(null); } function enterEditorHighlighting() { selection.select(highlightedEntityID); if (!isAppScaleWithHandles || !otherEditor.isEditing(highlightedEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), isAppScaleWithHandles || otherEditor.isEditing(highlightedEntityID)); } isOtherEditorEditingEntityID = otherEditor.isEditing(highlightedEntityID); wasAppScaleWithHandles = isAppScaleWithHandles; } function updateEditorHighlighting() { selection.select(highlightedEntityID); if (!isAppScaleWithHandles || !otherEditor.isEditing(highlightedEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), isAppScaleWithHandles || otherEditor.isEditing(highlightedEntityID)); } else { highlights.clear(); } isOtherEditorEditingEntityID = !isOtherEditorEditingEntityID; } function exitEditorHighlighting() { highlights.clear(); isOtherEditorEditingEntityID = false; } function enterEditorGrabbing() { selection.select(highlightedEntityID); // For when transitioning from EDITOR_SEARCHING. if (intersection.laserIntersected) { laser.setLength(laser.length()); } else { laser.disable(); } if (isAppScaleWithHandles) { handles.display(highlightedEntityID, selection.boundingBox(), selection.count() > 1); } startEditing(); wasAppScaleWithHandles = isAppScaleWithHandles; doDeleteEntities = false; } function updateEditorGrabbing() { selection.select(highlightedEntityID); if (isAppScaleWithHandles) { handles.display(highlightedEntityID, selection.boundingBox(), selection.count() > 1); } else { handles.clear(); } } function exitEditorGrabbing() { stopEditing(); handles.clear(); laser.clearLength(); laser.enable(); } function enterEditorDirectScaling() { selection.select(highlightedEntityID); // In case need to transition to EDITOR_GRABBING. isScalingWithHand = intersection.handIntersected; if (intersection.laserIntersected) { laser.setLength(laser.length()); } else { laser.disable(); } otherEditor.startDirectScaling(getScaleTargetPosition()); } function updateEditorDirectScaling() { otherEditor.updateDirectScaling(getScaleTargetPosition()); } function exitEditorDirectScaling() { otherEditor.stopDirectScaling(); laser.clearLength(); laser.enable(); } function enterEditorHandleScaling() { selection.select(highlightedEntityID); // In case need to transition to EDITOR_GRABBING. isScalingWithHand = intersection.handIntersected; if (intersection.laserIntersected) { laser.setLength(laser.length()); } else { laser.disable(); } otherEditor.startHandleScaling(getScaleTargetPosition(), intersection.overlayID); } function updateEditorHandleScaling() { otherEditor.updateHandleScaling(getScaleTargetPosition()); } function exitEditorHandleScaling() { otherEditor.stopHandleScaling(); laser.clearLength(); laser.enable(); } STATE_MACHINE = { EDITOR_IDLE: { enter: enterEditorIdle, update: null, exit: exitEditorIdle }, EDITOR_SEARCHING: { enter: enterEditorSearching, update: updateEditorSearching, exit: exitEditorSearching }, EDITOR_HIGHLIGHTING: { enter: enterEditorHighlighting, update: updateEditorHighlighting, exit: exitEditorHighlighting }, EDITOR_GRABBING: { enter: enterEditorGrabbing, update: updateEditorGrabbing, exit: exitEditorGrabbing }, EDITOR_DIRECT_SCALING: { enter: enterEditorDirectScaling, update: updateEditorDirectScaling, exit: exitEditorDirectScaling }, EDITOR_HANDLE_SCALING: { enter: enterEditorHandleScaling, update: updateEditorHandleScaling, exit: exitEditorHandleScaling } }; function setState(state) { if (state !== editorState) { STATE_MACHINE[EDITOR_STATE_STRINGS[editorState]].exit(); STATE_MACHINE[EDITOR_STATE_STRINGS[state]].enter(); editorState = state; } else if (DEBUG) { log("ERROR: Null state transition: " + state + "!"); } } function updateState() { STATE_MACHINE[EDITOR_STATE_STRINGS[editorState]].update(); } function update() { var previousState = editorState, doUpdateState; // Hand update. hand.update(); intersection = hand.intersection(); // Laser update. // Displays laser if hand has no intersection and trigger is pressed. if (hand.valid()) { laser.update(hand); if (!intersection.intersects) { intersection = laser.intersection(); } } // State update. switch (editorState) { case EDITOR_IDLE: if (!hand.valid()) { // No transition. break; } setState(EDITOR_SEARCHING); break; case EDITOR_SEARCHING: if (hand.valid() && !intersection.entityID && !(intersection.overlayID && hand.triggerClicked() && otherEditor.isHandle(intersection.overlayID))) { // No transition. updateState(); break; } if (!hand.valid()) { setState(EDITOR_IDLE); } else if (intersection.overlayID && hand.triggerClicked() && otherEditor.isHandle(intersection.overlayID)) { highlightedEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); } else if (intersection.entityID && !hand.triggerClicked()) { highlightedEntityID = Entities.rootOf(intersection.entityID); setState(EDITOR_HIGHLIGHTING); } else if (intersection.entityID && hand.triggerClicked()) { highlightedEntityID = Entities.rootOf(intersection.entityID); if (otherEditor.isEditing(highlightedEntityID)) { if (!isAppScaleWithHandles) { setState(EDITOR_DIRECT_SCALING); } } else { setState(EDITOR_GRABBING); } } else { debug(side, "ERROR: Unexpected condition in EDITOR_SEARCHING!"); } break; case EDITOR_HIGHLIGHTING: if (hand.valid() && intersection.entityID && !(hand.triggerClicked() && (!otherEditor.isEditing(highlightedEntityID) || !isAppScaleWithHandles)) && !(hand.triggerClicked() && intersection.overlayID && otherEditor.isHandle(intersection.overlayID))) { // No transition. doUpdateState = false; if (otherEditor.isEditing(highlightedEntityID) !== isOtherEditorEditingEntityID) { doUpdateState = true; } if (Entities.rootOf(intersection.entityID) !== highlightedEntityID) { highlightedEntityID = Entities.rootOf(intersection.entityID); doUpdateState = true; } if (isAppScaleWithHandles !== wasAppScaleWithHandles) { wasAppScaleWithHandles = isAppScaleWithHandles; doUpdateState = true; } if (doUpdateState) { updateState(); } break; } if (!hand.valid()) { setState(EDITOR_IDLE); } else if (intersection.overlayID && hand.triggerClicked() && otherEditor.isHandle(intersection.overlayID)) { highlightedEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); } else if (intersection.entityID && hand.triggerClicked()) { highlightedEntityID = Entities.rootOf(intersection.entityID); // May be a different entityID. if (otherEditor.isEditing(highlightedEntityID)) { if (!isAppScaleWithHandles) { setState(EDITOR_DIRECT_SCALING); } else { debug(side, "ERROR: Unexpected condition in EDITOR_HIGHLIGHTING! A"); } } else { setState(EDITOR_GRABBING); } } else if (!intersection.entityID) { setState(EDITOR_SEARCHING); } else { debug(side, "ERROR: Unexpected condition in EDITOR_HIGHLIGHTING! B"); } break; case EDITOR_GRABBING: if (hand.valid() && hand.triggerClicked() && !doDeleteEntities) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // No transition. if (isAppScaleWithHandles !== wasAppScaleWithHandles) { updateState(); wasAppScaleWithHandles = isAppScaleWithHandles; } break; } if (!hand.valid()) { setState(EDITOR_IDLE); } else if (!hand.triggerClicked()) { if (intersection.entityID) { highlightedEntityID = Entities.rootOf(intersection.entityID); setState(EDITOR_HIGHLIGHTING); } else { setState(EDITOR_SEARCHING); } } else if (doDeleteEntities) { selection.deleteEntities(); setState(EDITOR_SEARCHING); } else { debug(side, "ERROR: Unexpected condition in EDITOR_GRABBING!"); } break; case EDITOR_DIRECT_SCALING: if (hand.valid() && hand.triggerClicked() && (otherEditor.isEditing(highlightedEntityID) || otherEditor.isHandle(intersection.overlayID))) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // Don't test isAppScaleWithHandles because this will eventually be a UI element and so not able to be // changed while scaling with two hands. // No transition. updateState(); break; } if (!hand.valid()) { setState(EDITOR_IDLE); } else if (!hand.triggerClicked()) { if (!intersection.entityID) { setState(EDITOR_SEARCHING); } else { highlightedEntityID = Entities.rootOf(intersection.entityID); setState(EDITOR_HIGHLIGHTING); } } else if (!otherEditor.isEditing(highlightedEntityID)) { // Grab highlightEntityID that was scaling and has already been set. setState(EDITOR_GRABBING); } break; case EDITOR_HANDLE_SCALING: if (hand.valid() && hand.triggerClicked() && otherEditor.isEditing(highlightedEntityID)) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // Don't test isAppScaleWithHandles because this will eventually be a UI element and so not able to be // changed while scaling with two hands. // No transition. updateState(); break; } if (!hand.valid()) { setState(EDITOR_IDLE); } else if (!hand.triggerClicked()) { if (!intersection.entityID) { setState(EDITOR_SEARCHING); } else { highlightedEntityID = Entities.rootOf(intersection.entityID); setState(EDITOR_HIGHLIGHTING); } } else if (!otherEditor.isEditing(highlightedEntityID)) { // Grab highlightEntityID that was scaling and has already been set. setState(EDITOR_GRABBING); } break; } if (DEBUG && editorState !== previousState) { debug(side, EDITOR_STATE_STRINGS[editorState]); } } function apply() { switch (editorState) { case EDITOR_GRABBING: if (isDirectScaling) { applyDirectScale(); } else if (isHandleScaling) { applyHandleScale(); } else { applyGrab(); } break; } } function clear() { setState(EDITOR_IDLE); hand.clear(); laser.clear(); selection.clear(); highlights.clear(); handles.clear(); } function destroy() { if (hand) { hand.destroy(); hand = null; } if (laser) { laser.destroy(); laser = null; } if (selection) { selection.destroy(); selection = null; } if (highlights) { highlights.destroy(); highlights = null; } if (handles) { handles.destroy(); handles = null; } } if (!this instanceof Editor) { return new Editor(); } return { setOtherEditor: setOtherEditor, hoverHandle: hoverHandle, isHandle: isHandle, isEditing: isEditing, isScaling: isScaling, rootEntityID: rootEntityID, startDirectScaling: startDirectScaling, updateDirectScaling: updateDirectScaling, stopDirectScaling: stopDirectScaling, startHandleScaling: startHandleScaling, updateHandleScaling: updateHandleScaling, stopHandleScaling: stopHandleScaling, update: update, apply: apply, clear: clear, destroy: destroy }; }; function update() { // Main update loop. updateTimer = null; // Each hand's action depends on the state of the other hand, so update the states first then apply actions. editors[LEFT_HAND].update(); editors[RIGHT_HAND].update(); editors[LEFT_HAND].apply(); editors[RIGHT_HAND].apply(); updateTimer = Script.setTimeout(update, UPDATE_LOOP_TIMEOUT); } function updateHandControllerGrab() { // Communicate app status to handControllerGrab.js. Settings.setValue(VR_EDIT_SETTING, isAppActive); } function onAppButtonClicked() { // Application tablet/toolbar button clicked. isAppActive = !isAppActive; updateHandControllerGrab(); button.editProperties({ isActive: isAppActive }); if (isAppActive) { toolMenu.display(); update(); } else { Script.clearTimeout(updateTimer); updateTimer = null; editors[LEFT_HAND].clear(); editors[RIGHT_HAND].clear(); toolMenu.clear(); } } function otherHand(hand) { return (hand + 1) % 2; } function onDominantHandChanged() { /* // TODO: API coming. dominantHand = TODO; */ toolMenu.setHand(otherHand(dominantHand)); } 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(onAppButtonClicked); } // Hands, each with a laser, selection, etc. editors[LEFT_HAND] = new Editor(LEFT_HAND); editors[RIGHT_HAND] = new Editor(RIGHT_HAND); editors[LEFT_HAND].setOtherEditor(editors[RIGHT_HAND]); editors[RIGHT_HAND].setOtherEditor(editors[LEFT_HAND]); // Dominant hand from settings. // TODO: API coming. dominantHand = RIGHT_HAND; /* dominantHand = TODO; TODO.change.connect(onDominantHandChanged); */ toolMenu = new ToolMenu(otherHand(dominantHand)); if (isAppActive) { update(); } } function tearDown() { if (updateTimer) { Script.clearTimeout(updateTimer); } isAppActive = false; updateHandControllerGrab(); if (!tablet) { return; } if (button) { button.clicked.disconnect(onAppButtonClicked); tablet.removeButton(button); button = null; } if (toolMenu) { toolMenu.destroy(); toolMenu = null; } if (editors[LEFT_HAND]) { editors[LEFT_HAND].destroy(); editors[LEFT_HAND] = null; } if (editors[RIGHT_HAND]) { editors[RIGHT_HAND].destroy(); editors[RIGHT_HAND] = null; } tablet = null; } setUp(); Script.scriptEnding.connect(tearDown); }());