// // shapes.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 // /* global Feedback, History, Preload */ (function () { "use strict"; var APP_NAME = "SHAPES", APP_ICON_INACTIVE = Script.resolvePath("./assets/shapes-i.svg"), APP_ICON_ACTIVE = Script.resolvePath("./assets/shapes-a.svg"), APP_ICON_DISABLED = Script.resolvePath("./assets/shapes-d.svg"), ENABLED_CAPTION_COLOR_OVERRIDE = "#ffffff", DISABLED_CAPTION_COLOR_OVERRIDE = "#888888", START_DELAY = 2000, // ms // Application state isAppActive, dominantHand, // Tool state TOOL_NONE = 0, TOOL_SCALE = 1, TOOL_CLONE = 2, TOOL_GROUP = 3, TOOL_GROUP_BOX = 4, TOOL_COLOR = 5, TOOL_PICK_COLOR = 6, TOOL_PHYSICS = 7, TOOL_DELETE = 8, toolSelected = TOOL_NONE, colorToolColor = { red: 128, green: 128, blue: 128 }, physicsToolPhysics = { userData: { grabbableKey: {} } }, EARTH_GRAVITY = -9.80665, physicsToolGravity = EARTH_GRAVITY, // Primary objects App, Inputs, inputs = [], UI, ui, Editor, editors = [], LEFT_HAND = 0, RIGHT_HAND = 1, Grouping, grouping, // Modules CreatePalette, Groups, Hand, Handles, Highlights, Laser, SelectionManager, ToolIcon, ToolsMenu, // Miscellaneous UPDATE_LOOP_TIMEOUT = 16, updateTimer = null, tablet, button, DOMAIN_CHANGED_MESSAGE = "Toolbar-DomainChanged", DEBUG = false; // Utilities Script.include("./utilities/utilities.js"); // Modules Script.include("./modules/createPalette.js"); Script.include("./modules/feedback.js"); Script.include("./modules/groups.js"); Script.include("./modules/hand.js"); Script.include("./modules/handles.js"); Script.include("./modules/highlights.js"); Script.include("./modules/history.js"); Script.include("./modules/laser.js"); Script.include("./modules/preload.js"); Script.include("./modules/selection.js"); Script.include("./modules/toolIcon.js"); Script.include("./modules/toolsMenu.js"); Script.include("./modules/uit.js"); function log(side, message) { // Optional parameter: side. var hand = "", HAND_LETTERS = ["L", "R"]; if (side === 0 || side === 1) { hand = HAND_LETTERS[side] + " "; } else { message = side; } print(APP_NAME + ": " + hand + message); } function debug(side, message) { // Optional parameter: side. if (DEBUG) { log(side, message); } } function otherHand(hand) { var NUMBER_OF_HANDS = 2; return (hand + 1) % NUMBER_OF_HANDS; } App = { log: log, debug: debug, DEBUG: DEBUG }; Inputs = function (side) { // A hand plus a laser. var // Primary objects. hand, laser, intersection = {}; if (!(this instanceof Inputs)) { return new Inputs(); } hand = new Hand(side); laser = new Laser(side); function setUIOverlays(overlayIDs) { laser.setUIOverlays(overlayIDs); } function getHand() { return hand; } function getLaser() { return laser; } function getIntersection() { return intersection; } function update() { var laserIntersection, handIntersection; hand.update(); if (hand.valid()) { laser.update(hand); // Use intersections in order to achieve entity manipulation while inside an entity: // - Use laser overlay intersection if there is one (for UI). // - Otherwise use hand overlay if there is one (for UI). // - Otherwise use laser entity intersection if there is one (for entity manipulation). // Except if hand intersection is for same entity. // - Otherwise use hand entity intersection if there is one (for entity manipulation). laserIntersection = laser.intersection(); if (laserIntersection.intersects && laserIntersection.overlayID !== null) { intersection = laserIntersection; } else { handIntersection = hand.intersection(); if (handIntersection.intersects && handIntersection.overlayID !== null) { intersection = handIntersection; } else if (laserIntersection.intersects && laserIntersection.entityID !== handIntersection.entityID) { intersection = laserIntersection; } else { intersection = handIntersection; } } } else { intersection = {}; } } function clear() { hand.clear(); laser.clear(); } function destroy() { if (hand) { hand.destroy(); hand = null; } if (laser) { laser.destroy(); laser = null; } } return { setUIOverlays: setUIOverlays, hand: getHand, laser: getLaser, intersection: getIntersection, update: update, clear: clear, destroy: destroy }; }; UI = function (side, leftInputs, rightInputs, uiCommandCallback) { // Tool menu and Create palette. var // Primary objects. toolsMenu, toolIcon, createPalette, isDisplaying = false, getIntersection; // Function. if (!(this instanceof UI)) { return new UI(); } toolIcon = new ToolIcon(otherHand(side)); createPalette = new CreatePalette(side, leftInputs, rightInputs, uiCommandCallback); toolsMenu = new ToolsMenu(side, leftInputs, rightInputs, uiCommandCallback); Preload.load(toolIcon.assetURLs()); Preload.load(createPalette.assetURLs()); Preload.load(toolsMenu.assetURLs()); getIntersection = side === LEFT_HAND ? rightInputs.intersection : leftInputs.intersection; function setHand(newSide) { side = newSide; toolIcon.setHand(otherHand(side)); toolsMenu.setHand(side); createPalette.setHand(side); getIntersection = side === LEFT_HAND ? rightInputs.intersection : leftInputs.intersection; } function setToolIcon(icon) { toolIcon.display(toolsMenu.iconInfo(icon)); } function clearTool() { toolIcon.clear(); toolsMenu.clearTool(); } function setUIOverlays() { var uiOverlayIDs = [].concat(toolsMenu.overlayIDs(), createPalette.overlayIDs()); leftInputs.setUIOverlays(side === RIGHT_HAND ? uiOverlayIDs : []); rightInputs.setUIOverlays(side === LEFT_HAND ? uiOverlayIDs : []); } function display() { toolsMenu.display(); createPalette.display(); setUIOverlays(); isDisplaying = true; } function update() { var intersection; if (isDisplaying) { intersection = getIntersection(); toolsMenu.update(intersection, grouping.groupsCount(), grouping.entitiesCount()); createPalette.update(intersection.overlayID); } } function doPickColor(color) { toolsMenu.doCommand("setColorFromPick", color); } function clear() { leftInputs.setUIOverlays([]); rightInputs.setUIOverlays([]); toolIcon.clear(); toolsMenu.clear(); createPalette.clear(); isDisplaying = false; } function setVisible(visible) { toolsMenu.setVisible(visible); createPalette.setVisible(visible); } function destroy() { if (createPalette) { createPalette.destroy(); createPalette = null; } if (toolsMenu) { toolsMenu.destroy(); toolsMenu = null; } if (toolIcon) { toolIcon.destroy(); toolIcon = null; } } return { setHand: setHand, setToolIcon: setToolIcon, clearTool: clearTool, COLOR_TOOL: toolsMenu.COLOR_TOOL, SCALE_TOOL: toolsMenu.SCALE_TOOL, CLONE_TOOL: toolsMenu.CLONE_TOOL, GROUP_TOOL: toolsMenu.GROUP_TOOL, PHYSICS_TOOL: toolsMenu.PHYSICS_TOOL, DELETE_TOOL: toolsMenu.DELETE_TOOL, display: display, setVisible: setVisible, updateUIOverlays: setUIOverlays, doPickColor: doPickColor, update: update, clear: clear, destroy: destroy }; }; Editor = function (side) { // An entity selection, entity highlights, and entity handles. var // Primary objects. selection, highlights, handles, // References. otherEditor, // Other hand's Editor object. hand, laser, // 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_CLONING = 6, EDITOR_GROUPING = 7, EDITOR_STATE_STRINGS = ["EDITOR_IDLE", "EDITOR_SEARCHING", "EDITOR_HIGHLIGHTING", "EDITOR_GRABBING", "EDITOR_DIRECT_SCALING", "EDITOR_HANDLE_SCALING", "EDITOR_CLONING", "EDITOR_GROUPING"], editorState = EDITOR_IDLE, // State machine. STATE_MACHINE, intersectedEntityID = null, // Intersected entity of highlighted entity set. rootEntityID = null, // Root entity of highlighted entity set. wasScaleTool = false, isOtherEditorEditingEntityID = false, isTriggerClicked = false, wasTriggerClicked = false, isGripClicked = false, wasGripClicked = false, hoveredOverlayID = null, isAutoGrab = false, // Position values. initialHandOrientationInverse, initialHandToSelectionVector, initialSelectionOrientation, // Scaling values. isScalingWithHand = false, isDirectScaling = false, // Modifies EDITOR_GRABBING state. isHandleScaling = false, // "" initialTargetsSeparation, initialtargetsDirection, otherTargetPosition, handleUnitScaleAxis, handleScaleDirections, handleTargetOffset, initialHandToTargetOffset, initialHandleDistance, laserOffset, MIN_SCALE = 0.001, MIN_SCALE_HANDLE_DISTANCE = 0.0001, getIntersection, // Function. intersection, isUIVisible = true; if (!(this instanceof Editor)) { return new Editor(); } selection = new SelectionManager(side); highlights = new Highlights(side); handles = new Handles(side); function setReferences(inputs, editor) { hand = inputs.hand(); // Object. laser = inputs.laser(); // Object. getIntersection = inputs.intersection; // Function. otherEditor = editor; // Object. laserOffset = laser.handOffset(); // Value. highlights.setHandHighlightRadius(hand.getNearGrabRadius()); } function hoverHandle(overlayID) { // Highlights handle if overlayID is a handle, otherwise unhighlights currently highlighted handle if any. handles.hover(overlayID); } function enableAutoGrab() { // Used to grab entity created from Create palette. isAutoGrab = true; } function isHandle(overlayID) { return handles.isHandle(overlayID); } function setHandleOverlays(overlayIDs) { hand.setHandleOverlays(overlayIDs); } function isEditing(aRootEntityID) { // aRootEntityID is an optional parameter. return editorState > EDITOR_HIGHLIGHTING && (aRootEntityID === undefined || aRootEntityID === rootEntityID); } function isScaling() { return editorState === EDITOR_DIRECT_SCALING || editorState === EDITOR_HANDLE_SCALING; } function getIntersectedEntityID() { return intersectedEntityID; } function getRootEntityID() { return rootEntityID; } function isCameraOutsideEntity(entityID, testPosition) { var cameraPosition, pickRay, PRECISION_PICKING = true, NO_EXCLUDE_IDS = [], VISIBLE_ONLY = true, intersection; cameraPosition = Camera.position; pickRay = { origin: cameraPosition, direction: Vec3.normalize(Vec3.subtract(testPosition, cameraPosition)), length: Vec3.distance(testPosition, cameraPosition) }; intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, [entityID], NO_EXCLUDE_IDS, VISIBLE_ONLY); return !intersection.intersects || intersection.distance < pickRay.length; } 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 finishEditing() { selection.finishEditing(); } function getScaleTargetPosition() { if (isScalingWithHand) { return hand.palmPosition(); } 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. if (isDirectScaling) { selection.finishDirectScaling(); isDirectScaling = false; } } function startHandleScaling(targetPosition, overlayID) { // Called on grabbing hand by scaling hand. var initialTargetPosition, selectionPositionAndOrientation, scaleAxis; // Keep grabbed handle highlighted and hide other handles. handles.grab(overlayID); isScalingWithHand = intersection.handIntersected; // Grab center of selection. otherTargetPosition = targetPosition; initialTargetPosition = selection.boundingBox().center; initialHandToTargetOffset = Vec3.subtract(initialTargetPosition, hand.position()); // Initial handle offset from center of selection. selectionPositionAndOrientation = selection.getPositionAndOrientation(); handleUnitScaleAxis = handles.scalingAxis(overlayID); // Unit vector in direction of scaling. handleScaleDirections = handles.scalingDirections(overlayID); // Which axes to scale the selection on. scaleAxis = Vec3.multiplyQbyV(selectionPositionAndOrientation.orientation, handleUnitScaleAxis); handleTargetOffset = handles.handleOffset(overlayID) + Vec3.dot(Vec3.subtract(otherTargetPosition, Overlays.getProperty(overlayID, "position")), scaleAxis); initialHandleDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, initialTargetPosition), scaleAxis)); initialHandleDistance -= handleTargetOffset; initialHandleDistance = Math.max(initialHandleDistance, MIN_SCALE_HANDLE_DISTANCE); // Start scaling. selection.startHandleScaling(initialTargetPosition); 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. if (isHandleScaling) { 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, selectionOrientation, scaleAxis, handleDistance, scale, scale3D, selectionPositionAndOrientation; // Orient selection per grabbing hand. deltaHandOrientation = Quat.multiply(hand.orientation(), initialHandOrientationInverse); selectionOrientation = Quat.multiply(deltaHandOrientation, initialSelectionOrientation); // Position selection per grabbing hand. targetPosition = Vec3.sum(hand.position(), Vec3.multiplyQbyV(deltaHandOrientation, initialHandToTargetOffset)); // Desired distance of handle from other hand scaleAxis = Vec3.multiplyQbyV(selection.getPositionAndOrientation().orientation, handleUnitScaleAxis); handleDistance = Vec3.dot(Vec3.subtract(otherTargetPosition, targetPosition), scaleAxis); handleDistance -= handleTargetOffset; handleDistance = Math.max(handleDistance, MIN_SCALE_HANDLE_DISTANCE); // Scale selection relative to initial dimensions. scale = handleDistance / initialHandleDistance; scale = Math.max(scale, MIN_SCALE); 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 }; // Scale. handles.scale(scale3D); selection.handleScale(scale3D, targetPosition, 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(); intersectedEntityID = null; rootEntityID = null; hoveredOverlayID = intersection.overlayID; otherEditor.hoverHandle(hoveredOverlayID); } function updateEditorSearching() { if (toolSelected === TOOL_SCALE && intersection.overlayID !== hoveredOverlayID && otherEditor.isEditing()) { hoveredOverlayID = intersection.overlayID; otherEditor.hoverHandle(hoveredOverlayID); } } function exitEditorSearching() { otherEditor.hoverHandle(null); } function enterEditorHighlighting() { selection.select(intersectedEntityID); if (!intersection.laserIntersected && !isUIVisible) { laser.disable(); } if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(rootEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), toolSelected === TOOL_COLOR || toolSelected === TOOL_PICK_COLOR ? selection.intersectedEntityIndex() : null, null, toolSelected === TOOL_SCALE || otherEditor.isEditing(rootEntityID) ? highlights.SCALE_COLOR : highlights.HIGHLIGHT_COLOR); } isOtherEditorEditingEntityID = otherEditor.isEditing(rootEntityID); wasScaleTool = toolSelected === TOOL_SCALE; } function updateEditorHighlighting() { selection.select(intersectedEntityID); if (toolSelected !== TOOL_SCALE || !otherEditor.isEditing(rootEntityID)) { highlights.display(intersection.handIntersected, selection.selection(), toolSelected === TOOL_COLOR || toolSelected === TOOL_PICK_COLOR ? selection.intersectedEntityIndex() : null, null, toolSelected === TOOL_SCALE || otherEditor.isEditing(rootEntityID) ? highlights.SCALE_COLOR : highlights.HIGHLIGHT_COLOR); if (!intersection.laserIntersected && !isUIVisible) { laser.disable(); } else { laser.enable(); } } else { highlights.clear(); laser.enable(); } isOtherEditorEditingEntityID = otherEditor.isEditing(rootEntityID); } function exitEditorHighlighting() { highlights.clear(); isOtherEditorEditingEntityID = false; laser.enable(); } function enterEditorGrabbing() { selection.select(intersectedEntityID); // For when transitioning from EDITOR_SEARCHING. if (intersection.laserIntersected) { laser.setLength(laser.length()); } else { laser.disable(); } if (toolSelected === TOOL_SCALE) { handles.display(rootEntityID, selection.boundingBox(), selection.count() > 1, selection.is2D()); otherEditor.setHandleOverlays(handles.overlays()); } startEditing(); wasScaleTool = toolSelected === TOOL_SCALE; } function updateEditorGrabbing() { selection.select(intersectedEntityID); if (toolSelected === TOOL_SCALE) { handles.display(rootEntityID, selection.boundingBox(), selection.count() > 1, selection.is2D()); otherEditor.setHandleOverlays(handles.overlays()); } else { handles.clear(); otherEditor.setHandleOverlays([]); } } function exitEditorGrabbing() { stopDirectScaling(); stopHandleScaling(); finishEditing(); handles.clear(); otherEditor.setHandleOverlays([]); laser.clearLength(); laser.enable(); } function enterEditorDirectScaling() { selection.select(intersectedEntityID); // 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(intersectedEntityID); // 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(); } function enterEditorCloning() { Feedback.play(side, Feedback.CLONE_ENTITY); selection.select(intersectedEntityID); // For when transitioning from EDITOR_SEARCHING. selection.cloneEntities(); intersectedEntityID = selection.intersectedEntityID(); rootEntityID = selection.rootEntityID(); intersectedEntityID = rootEntityID; } function exitEditorCloning() { // Nothing to do. } function enterEditorGrouping() { if (!grouping.includes(rootEntityID)) { highlights.display(false, selection.selection(), null, null, highlights.GROUP_COLOR); } if (toolSelected === TOOL_GROUP_BOX) { if (!grouping.includes(rootEntityID)) { Feedback.play(side, Feedback.SELECT_ENTITY); grouping.toggle(selection.selection()); grouping.selectInBox(); } else { Feedback.play(side, Feedback.GENERAL_ERROR); } } else { Feedback.play(side, Feedback.SELECT_ENTITY); grouping.toggle(selection.selection()); } } function exitEditorGrouping() { highlights.clear(); } 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 }, EDITOR_CLONING: { enter: enterEditorCloning, update: null, exit: exitEditorCloning }, EDITOR_GROUPING: { enter: enterEditorGrouping, update: null, exit: exitEditorGrouping } }; function setState(state) { if (state !== editorState) { STATE_MACHINE[EDITOR_STATE_STRINGS[editorState]].exit(); STATE_MACHINE[EDITOR_STATE_STRINGS[state]].enter(); editorState = state; } else { log(side, "ERROR: Editor: Null state transition: " + state + "!"); } } function updateState() { STATE_MACHINE[EDITOR_STATE_STRINGS[editorState]].update(); } function updateTool() { if (!wasGripClicked && isGripClicked && (toolSelected !== TOOL_NONE)) { Feedback.play(side, Feedback.DROP_TOOL); toolSelected = TOOL_NONE; grouping.clear(); ui.clearTool(); ui.updateUIOverlays(); } } function update() { var isTriggerPressed, showUI, previousState = editorState, doUpdateState, color; intersection = getIntersection(); isTriggerClicked = hand.triggerClicked(); isGripClicked = hand.gripClicked(); isTriggerPressed = hand.triggerPressed(); // Hide UI if hand is intersecting entity and camera is outside entity, or if hand is intersecting stretch handle. if (side !== dominantHand) { showUI = !intersection.handIntersected || (intersection.entityID !== null && !isCameraOutsideEntity(intersection.entityID, intersection.intersection)); if (showUI !== isUIVisible) { isUIVisible = !isUIVisible; ui.setVisible(isUIVisible); } } // State update. switch (editorState) { case EDITOR_IDLE: if (!hand.valid()) { // No transition. break; } setState(EDITOR_SEARCHING); break; case EDITOR_SEARCHING: if (hand.valid() && !(intersection.overlayID && !wasTriggerClicked && isTriggerClicked && otherEditor.isHandle(intersection.overlayID)) && !(intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && (wasTriggerClicked || !isTriggerClicked) && !isAutoGrab && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, intersection.intersection))) && !(intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && (!wasTriggerClicked || isAutoGrab) && isTriggerClicked)) { // No transition. updateState(); updateTool(); break; } if (!hand.valid()) { setState(EDITOR_IDLE); } else if (intersection.overlayID && !wasTriggerClicked && isTriggerClicked && otherEditor.isHandle(intersection.overlayID)) { intersectedEntityID = otherEditor.intersectedEntityID(); rootEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); } else if (intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && (wasTriggerClicked || !isTriggerClicked) && !isAutoGrab && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, intersection.intersection))) { intersectedEntityID = intersection.entityID; rootEntityID = Entities.rootOf(intersectedEntityID); setState(EDITOR_HIGHLIGHTING); } else if (intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && (!wasTriggerClicked || isAutoGrab) && isTriggerClicked) { intersectedEntityID = intersection.entityID; rootEntityID = Entities.rootOf(intersectedEntityID); if (isAutoGrab) { setState(EDITOR_GRABBING); } else if (otherEditor.isEditing(rootEntityID)) { if (toolSelected !== TOOL_SCALE) { setState(EDITOR_DIRECT_SCALING); } } else if (toolSelected === TOOL_CLONE) { setState(EDITOR_CLONING); } else if (toolSelected === TOOL_GROUP || toolSelected === TOOL_GROUP_BOX) { setState(EDITOR_GROUPING); } else if (toolSelected === TOOL_COLOR) { setState(EDITOR_HIGHLIGHTING); if (selection.applyColor(colorToolColor, false)) { Feedback.play(side, Feedback.APPLY_PROPERTY); } else { Feedback.play(side, Feedback.APPLY_ERROR); } } else if (toolSelected === TOOL_PICK_COLOR) { color = selection.getColor(intersection.entityID); if (color) { colorToolColor = color; ui.doPickColor(colorToolColor); } else { Feedback.play(side, Feedback.APPLY_ERROR); } toolSelected = TOOL_COLOR; ui.setToolIcon(ui.COLOR_TOOL); } else if (toolSelected === TOOL_PHYSICS) { setState(EDITOR_HIGHLIGHTING); selection.applyPhysics(physicsToolPhysics); } else if (toolSelected === TOOL_DELETE) { setState(EDITOR_HIGHLIGHTING); Feedback.play(side, Feedback.DELETE_ENTITY); selection.deleteEntities(); setState(EDITOR_SEARCHING); } else { log(side, "ERROR: Editor: Unexpected condition A in EDITOR_SEARCHING!"); } } else { log(side, "ERROR: Editor: Unexpected condition B in EDITOR_SEARCHING!"); } break; case EDITOR_HIGHLIGHTING: if (hand.valid() && intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && (isTriggerPressed || isCameraOutsideEntity(intersection.entityID, intersection.intersection)) && !(!wasTriggerClicked && isTriggerClicked && (!otherEditor.isEditing(rootEntityID) || toolSelected !== TOOL_SCALE)) && !(!wasTriggerClicked && isTriggerClicked && intersection.overlayID && otherEditor.isHandle(intersection.overlayID))) { // No transition. doUpdateState = false; if (otherEditor.isEditing(rootEntityID) !== isOtherEditorEditingEntityID) { doUpdateState = true; } if (Entities.rootOf(intersection.entityID) !== rootEntityID) { intersectedEntityID = intersection.entityID; rootEntityID = Entities.rootOf(intersectedEntityID); doUpdateState = true; } if ((toolSelected === TOOL_SCALE) !== wasScaleTool) { wasScaleTool = toolSelected === TOOL_SCALE; doUpdateState = true; } if ((toolSelected === TOOL_COLOR || toolSelected === TOOL_PHYSICS) && intersection.entityID !== intersectedEntityID) { intersectedEntityID = intersection.entityID; doUpdateState = true; } if (doUpdateState) { updateState(); } updateTool(); break; } if (!hand.valid()) { setState(EDITOR_IDLE); } else if (intersection.overlayID && !wasTriggerClicked && isTriggerClicked && otherEditor.isHandle(intersection.overlayID)) { intersectedEntityID = otherEditor.intersectedEntityID(); rootEntityID = otherEditor.rootEntityID(); setState(EDITOR_HANDLE_SCALING); } else if (intersection.entityID && (intersection.editableEntity || toolSelected === TOOL_PICK_COLOR) && !wasTriggerClicked && isTriggerClicked) { intersectedEntityID = intersection.entityID; // May be a different entityID. rootEntityID = Entities.rootOf(intersectedEntityID); if (otherEditor.isEditing(rootEntityID)) { if (toolSelected !== TOOL_SCALE) { setState(EDITOR_DIRECT_SCALING); } else { log(side, "ERROR: Editor: Unexpected condition A in EDITOR_HIGHLIGHTING!"); } } else if (toolSelected === TOOL_CLONE) { setState(EDITOR_CLONING); } else if (toolSelected === TOOL_GROUP || toolSelected === TOOL_GROUP_BOX) { setState(EDITOR_GROUPING); } else if (toolSelected === TOOL_COLOR) { if (selection.applyColor(colorToolColor, false)) { Feedback.play(side, Feedback.APPLY_PROPERTY); } else { Feedback.play(side, Feedback.APPLY_ERROR); } } else if (toolSelected === TOOL_PICK_COLOR) { color = selection.getColor(intersection.entityID); if (color) { colorToolColor = color; ui.doPickColor(colorToolColor); toolSelected = TOOL_COLOR; ui.setToolIcon(ui.COLOR_TOOL); } else { Feedback.play(side, Feedback.APPLY_ERROR); } } else if (toolSelected === TOOL_PHYSICS) { selection.applyPhysics(physicsToolPhysics); } else if (toolSelected === TOOL_DELETE) { Feedback.play(side, Feedback.DELETE_ENTITY); selection.deleteEntities(); setState(EDITOR_SEARCHING); } else { setState(EDITOR_GRABBING); } } else if (!intersection.entityID || !intersection.editableEntity || (!isTriggerPressed && !isCameraOutsideEntity(intersection.entityID, intersection.intersection))) { setState(EDITOR_SEARCHING); } else { log(side, "ERROR: Editor: Unexpected condition B in EDITOR_HIGHLIGHTING!"); } break; case EDITOR_GRABBING: if (hand.valid() && isTriggerClicked && !isGripClicked) { // Don't test intersection.intersected because when scaling with handles intersection may lag behind. // No transition. if ((toolSelected === TOOL_SCALE) !== wasScaleTool) { updateState(); wasScaleTool = toolSelected === TOOL_SCALE; } // updateTool(); Don't updateTool() because grip button is used to delete grabbed entity. break; } if (!hand.valid()) { setState(EDITOR_IDLE); } else if (!isTriggerClicked) { if (intersection.entityID && intersection.editableEntity) { intersectedEntityID = intersection.entityID; rootEntityID = Entities.rootOf(intersectedEntityID); setState(EDITOR_HIGHLIGHTING); } else { setState(EDITOR_SEARCHING); } } else if (isGripClicked) { if (!wasGripClicked) { Feedback.play(side, Feedback.DELETE_ENTITY); selection.deleteEntities(); setState(EDITOR_SEARCHING); } } else { log(side, "ERROR: Editor: Unexpected condition in EDITOR_GRABBING!"); } break; case EDITOR_DIRECT_SCALING: if (hand.valid() && isTriggerClicked && (otherEditor.isEditing(rootEntityID) || otherEditor.isHandle(intersection.overlayID))) { // Don't test for intersection.intersected because when scaling with handles intersection may lag behind. // Don't test toolSelected === TOOL_SCALE because this is a UI element and so not able to be changed while // scaling with two hands. // No transition. updateState(); // updateTool(); Don't updateTool() because this hand is currently using the scaling tool. break; } if (!hand.valid()) { setState(EDITOR_IDLE); } else if (!isTriggerClicked) { if (!intersection.entityID || !intersection.editableEntity) { setState(EDITOR_SEARCHING); } else { intersectedEntityID = intersection.entityID; rootEntityID = Entities.rootOf(intersectedEntityID); setState(EDITOR_HIGHLIGHTING); } } else if (!otherEditor.isEditing(rootEntityID)) { // Grab highlightEntityID that was scaling and has already been set. setState(EDITOR_GRABBING); } else { log(side, "ERROR: Editor: Unexpected condition in EDITOR_DIRECT_SCALING!"); } break; case EDITOR_HANDLE_SCALING: if (hand.valid() && isTriggerClicked && otherEditor.isEditing(rootEntityID)) { // Don't test intersection.intersected because when scaling with handles intersection may lag behind. // Don't test toolSelected === TOOL_SCALE because this is a UI element and so not able to be changed // while scaling with two hands. // No transition. updateState(); updateTool(); break; } if (!hand.valid()) { setState(EDITOR_IDLE); } else if (!isTriggerClicked) { if (!intersection.entityID || !intersection.editableEntity) { setState(EDITOR_SEARCHING); } else { intersectedEntityID = intersection.entityID; rootEntityID = Entities.rootOf(intersectedEntityID); setState(EDITOR_HIGHLIGHTING); } } else if (!otherEditor.isEditing(rootEntityID)) { // Grab highlightEntityID that was scaling and has already been set. setState(EDITOR_GRABBING); } else { log(side, "ERROR: Editor: Unexpected condition in EDITOR_HANDLE_SCALING!"); } break; case EDITOR_CLONING: // Immediate transition out of state after cloning entities during state entry. if (hand.valid() && isTriggerClicked) { setState(EDITOR_GRABBING); } else if (!hand.valid()) { setState(EDITOR_IDLE); } else if (!isTriggerClicked) { if (intersection.entityID && intersection.editableEntity) { intersectedEntityID = intersection.entityID; rootEntityID = Entities.rootOf(intersectedEntityID); setState(EDITOR_HIGHLIGHTING); } else { setState(EDITOR_SEARCHING); } } else { log(side, "ERROR: Editor: Unexpected condition in EDITOR_CLONING!"); } break; case EDITOR_GROUPING: // Immediate transition out of state after updating group data during state entry. if (hand.valid() && isTriggerClicked) { // No transition. break; } if (!hand.valid()) { setState(EDITOR_IDLE); } else { setState(EDITOR_SEARCHING); } break; } wasTriggerClicked = isTriggerClicked; wasGripClicked = isGripClicked; isAutoGrab = isAutoGrab && isTriggerClicked && !isGripClicked; 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() { if (editorState !== EDITOR_IDLE) { setState(EDITOR_IDLE); } selection.clear(); highlights.clear(); handles.clear(); otherEditor.setHandleOverlays([]); } function destroy() { if (selection) { selection.destroy(); selection = null; } if (highlights) { highlights.destroy(); highlights = null; } if (handles) { handles.destroy(); handles = null; } } return { setReferences: setReferences, hoverHandle: hoverHandle, enableAutoGrab: enableAutoGrab, isHandle: isHandle, setHandleOverlays: setHandleOverlays, isEditing: isEditing, isScaling: isScaling, intersectedEntityID: getIntersectedEntityID, rootEntityID: getRootEntityID, startDirectScaling: startDirectScaling, updateDirectScaling: updateDirectScaling, stopDirectScaling: stopDirectScaling, startHandleScaling: startHandleScaling, updateHandleScaling: updateHandleScaling, stopHandleScaling: stopHandleScaling, update: update, apply: apply, clear: clear, destroy: destroy }; }; Grouping = function () { // Grouping highlights and functions. var groups, highlights, selectInBoxSelection, // Selection of all entities selected. groupSelection, // New group to add to selection. exludedLeftRootEntityID = null, exludedrightRootEntityID = null, excludedRootEntityIDs = [], hasHighlights = false, hasSelectionChanged = false, isSelectInBox = false; if (!(this instanceof Grouping)) { return new Grouping(); } groups = new Groups(); highlights = new Highlights(); selectInBoxSelection = new SelectionManager(); groupSelection = new SelectionManager(); function getAllChildrenIDs(entityID) { var childrenIDs = [], ENTITY_TYPE = "entity"; function traverseEntityTree(id) { var children, i, length; children = Entities.getChildrenIDs(id); for (i = 0, length = children.length; i < length; i++) { if (Entities.getNestableType(children[i]) === ENTITY_TYPE) { childrenIDs.push(children[i]); traverseEntityTree(children[i]); } } } traverseEntityTree(entityID); return childrenIDs; } function isInsideBoundingBox(entityID, boundingBox) { // Are all 8 corners of entityID's bounding box inside boundingBox? var entityProperties, cornerPosition, boundingBoxInverseRotation, boundingBoxHalfDimensions, isInside = true, i, CORNER_REGISTRATION_OFFSETS = [ { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 1 }, { x: 0, y: 1, z: 0 }, { x: 0, y: 1, z: 1 }, { x: 1, y: 0, z: 0 }, { x: 1, y: 0, z: 1 }, { x: 1, y: 1, z: 0 }, { x: 1, y: 1, z: 1 } ], NUM_CORNERS = 8; entityProperties = Entities.getEntityProperties(entityID, ["position", "rotation", "dimensions", "registrationPoint"]); // Convert entity coordinates into boundingBox coordinates. boundingBoxInverseRotation = Quat.inverse(boundingBox.orientation); entityProperties.position = Vec3.multiplyQbyV(boundingBoxInverseRotation, Vec3.subtract(entityProperties.position, boundingBox.center)); entityProperties.rotation = Quat.multiply(boundingBoxInverseRotation, entityProperties.rotation); // Check all 8 corners of entity's bounding box are inside the given bounding box. boundingBoxHalfDimensions = Vec3.multiply(0.5, boundingBox.dimensions); i = 0; while (isInside && i < NUM_CORNERS) { cornerPosition = Vec3.sum(entityProperties.position, Vec3.multiplyQbyV(entityProperties.rotation, Vec3.multiplyVbyV(Vec3.subtract(CORNER_REGISTRATION_OFFSETS[i], entityProperties.registrationPoint), entityProperties.dimensions))); isInside = Math.abs(cornerPosition.x) <= boundingBoxHalfDimensions.x && Math.abs(cornerPosition.y) <= boundingBoxHalfDimensions.y && Math.abs(cornerPosition.z) <= boundingBoxHalfDimensions.z; i++; } return isInside; } function toggle(selection) { groups.toggle(selection); if (isSelectInBox) { // When selecting in a box, toggle() is only called to add entities to the selection. if (selectInBoxSelection.count() === 0) { selectInBoxSelection.select(selection[0].id); } else { selectInBoxSelection.append(selection[0].id); } } if (groups.groupsCount() === 0) { hasHighlights = false; highlights.clear(); } else { hasHighlights = true; hasSelectionChanged = true; } } function selectInBox() { // Add any entities or groups of entities wholly within bounding box of current selection. // Must be wholly within otherwise selection could grow uncontrollably. var boundingBox, entityIDs, checkedEntityIDs = [], entityID, rootID, groupIDs, doIncludeGroup, i, lengthI, j, lengthJ; if (selectInBoxSelection.count() > 1) { boundingBox = selectInBoxSelection.boundingBox(); entityIDs = Entities.findEntities(boundingBox.center, Vec3.length(boundingBox.dimensions) / 2); for (i = 0, lengthI = entityIDs.length; i < lengthI; i++) { entityID = entityIDs[i]; if (checkedEntityIDs.indexOf(entityID) === -1) { rootID = Entities.rootOf(entityID); if (!selectInBoxSelection.contains(entityID) && Entities.hasEditableRoot(rootID)) { groupIDs = [rootID].concat(getAllChildrenIDs(rootID)); doIncludeGroup = true; j = 0; lengthJ = groupIDs.length; while (doIncludeGroup && j < lengthJ) { doIncludeGroup = isInsideBoundingBox(groupIDs[j], boundingBox); j++; } checkedEntityIDs = checkedEntityIDs.concat(groupIDs); if (doIncludeGroup) { groupSelection.select(rootID); groups.toggle(groupSelection.selection()); groupSelection.clear(); selectInBoxSelection.append(rootID); hasSelectionChanged = true; } } else { checkedEntityIDs.push(rootID); } } } } } function startSelectInBox() { // Start automatically selecting entities in bounding box of current selection. var rootEntityIDs, i, length; isSelectInBox = true; // Select entities current groups combined. rootEntityIDs = groups.rootEntityIDs(); if (rootEntityIDs.length > 0) { selectInBoxSelection.select(rootEntityIDs[0]); for (i = 1, length = rootEntityIDs.length; i < length; i++) { selectInBoxSelection.append(rootEntityIDs[i]); } } // Add any enclosed entities. selectInBox(); // Show bounding box overlay plus any newly selected entities. hasSelectionChanged = true; } function stopSelectInBox() { // Stop automatically selecting entities within bounding box of current selection. // Hide bounding box overlay. selectInBoxSelection.clear(); hasSelectionChanged = true; isSelectInBox = false; } function includes(rootEntityID) { return groups.includes(rootEntityID); } function groupsCount() { return groups.groupsCount(); } function entitiesCount() { return groups.entitiesCount(); } function group() { groups.group(); } function ungroup() { groups.ungroup(); } function update(leftRootEntityID, rightRootEntityID) { // Update highlights displayed, excluding entities highlighted by left or right hands. var hasExludedRootEntitiesChanged, boundingBox; hasExludedRootEntitiesChanged = leftRootEntityID !== exludedLeftRootEntityID || rightRootEntityID !== exludedrightRootEntityID; if (!hasHighlights || (!hasSelectionChanged && !hasExludedRootEntitiesChanged)) { return; } if (hasExludedRootEntitiesChanged) { excludedRootEntityIDs = []; if (leftRootEntityID) { excludedRootEntityIDs.push(leftRootEntityID); } if (rightRootEntityID) { excludedRootEntityIDs.push(rightRootEntityID); } exludedLeftRootEntityID = leftRootEntityID; exludedrightRootEntityID = rightRootEntityID; } boundingBox = isSelectInBox && selectInBoxSelection.count() > 1 ? selectInBoxSelection.boundingBox() : null; highlights.display(false, groups.selection(excludedRootEntityIDs), null, boundingBox, highlights.GROUP_COLOR); hasSelectionChanged = false; } function clear() { if (isSelectInBox) { stopSelectInBox(); } groups.clear(); highlights.clear(); } function destroy() { if (groups) { groups.destroy(); groups = null; } if (highlights) { highlights.destroy(); highlights = null; } if (selectInBoxSelection) { selectInBoxSelection.destroy(); selectInBoxSelection = null; } if (groupSelection) { groupSelection.destroy(); groupSelection = null; } } return { toggle: toggle, startSelectInBox: startSelectInBox, selectInBox: selectInBox, stopSelectInBox: stopSelectInBox, includes: includes, groupsCount: groupsCount, entitiesCount: entitiesCount, group: group, ungroup: ungroup, update: update, clear: clear, destroy: destroy }; }; function update() { // Main update loop. updateTimer = null; // Update inputs - hands and lasers. inputs[LEFT_HAND].update(); inputs[RIGHT_HAND].update(); // UI has first dibs on handling inputs. ui.update(); // Each hand's edit 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(); // Grouping display. grouping.update(editors[LEFT_HAND].rootEntityID(), editors[RIGHT_HAND].rootEntityID()); updateTimer = Script.setTimeout(update, UPDATE_LOOP_TIMEOUT); } function updateControllerDispatcher() { // Communicate app status to controllerDispatcher.js. var DISABLE_HANDS = "both", ENABLE_HANDS = "none"; Messages.sendLocalMessage("Hifi-InVREdit-Disabler", isAppActive ? DISABLE_HANDS : ENABLE_HANDS); } function onUICommand(command, parameter) { switch (command) { case "scaleTool": Feedback.play(dominantHand, Feedback.EQUIP_TOOL); grouping.clear(); toolSelected = TOOL_SCALE; ui.setToolIcon(ui.SCALE_TOOL); ui.updateUIOverlays(); break; case "cloneTool": Feedback.play(dominantHand, Feedback.EQUIP_TOOL); grouping.clear(); toolSelected = TOOL_CLONE; ui.setToolIcon(ui.CLONE_TOOL); ui.updateUIOverlays(); break; case "groupTool": Feedback.play(dominantHand, Feedback.EQUIP_TOOL); toolSelected = TOOL_GROUP; ui.setToolIcon(ui.GROUP_TOOL); ui.updateUIOverlays(); break; case "colorTool": Feedback.play(dominantHand, Feedback.EQUIP_TOOL); grouping.clear(); toolSelected = TOOL_COLOR; ui.setToolIcon(ui.COLOR_TOOL); colorToolColor = parameter; ui.updateUIOverlays(); break; case "pickColorTool": if (parameter) { grouping.clear(); toolSelected = TOOL_PICK_COLOR; ui.updateUIOverlays(); } else { Feedback.play(dominantHand, Feedback.EQUIP_TOOL); grouping.clear(); toolSelected = TOOL_COLOR; ui.updateUIOverlays(); } break; case "physicsTool": Feedback.play(dominantHand, Feedback.EQUIP_TOOL); grouping.clear(); toolSelected = TOOL_PHYSICS; ui.setToolIcon(ui.PHYSICS_TOOL); ui.updateUIOverlays(); break; case "deleteTool": Feedback.play(dominantHand, Feedback.EQUIP_TOOL); grouping.clear(); toolSelected = TOOL_DELETE; ui.setToolIcon(ui.DELETE_TOOL); ui.updateUIOverlays(); break; case "clearTool": Feedback.play(dominantHand, Feedback.DROP_TOOL); grouping.clear(); toolSelected = TOOL_NONE; ui.clearTool(); ui.updateUIOverlays(); break; case "groupButton": Feedback.play(dominantHand, Feedback.APPLY_PROPERTY); grouping.group(); grouping.clear(); toolSelected = TOOL_NONE; ui.clearTool(); ui.updateUIOverlays(); break; case "ungroupButton": Feedback.play(dominantHand, Feedback.APPLY_PROPERTY); grouping.ungroup(); grouping.clear(); toolSelected = TOOL_NONE; ui.clearTool(); ui.updateUIOverlays(); break; case "toggleGroupSelectionBoxTool": toolSelected = parameter ? TOOL_GROUP_BOX : TOOL_GROUP; if (toolSelected === TOOL_GROUP_BOX) { grouping.startSelectInBox(); } else { grouping.stopSelectInBox(); } break; case "clearGroupSelectionTool": if (grouping.groupsCount() > 0) { Feedback.play(dominantHand, Feedback.SELECT_ENTITY); } grouping.clear(); if (toolSelected === TOOL_GROUP_BOX) { grouping.startSelectInBox(); } break; case "setColor": if (toolSelected === TOOL_PICK_COLOR) { toolSelected = TOOL_COLOR; ui.setToolIcon(ui.COLOR_TOOL); } colorToolColor = parameter; break; case "setGravityOn": // Dynamic is true if the entity has gravity or is grabbable. if (parameter) { physicsToolPhysics.gravity = { x: 0, y: physicsToolGravity, z: 0 }; physicsToolPhysics.dynamic = true; } else { physicsToolPhysics.gravity = Vec3.ZERO; physicsToolPhysics.dynamic = physicsToolPhysics.userData.grabbableKey.grabbable === true; } break; case "setGrabOn": // Dynamic is true if the entity has gravity or is grabbable. physicsToolPhysics.userData.grabbableKey.grabbable = parameter; physicsToolPhysics.dynamic = parameter || (physicsToolPhysics.gravity && Vec3.length(physicsToolPhysics.gravity) > 0); break; case "setCollideOn": if (parameter) { physicsToolPhysics.collisionless = false; physicsToolPhysics.collidesWith = "static,dynamic,kinematic,myAvatar,otherAvatar"; } else { physicsToolPhysics.collisionless = true; physicsToolPhysics.collidesWith = ""; } break; case "setGravity": if (parameter !== undefined) { // Power range 0.0, 0.5, 1.0 maps to -50.0, -9.80665, 50.0. physicsToolGravity = 82.36785162 * Math.pow(2.214065901, parameter) - 132.36785; if (physicsToolPhysics.dynamic === true) { // Only apply if gravity is turned on. physicsToolPhysics.gravity = { x: 0, y: physicsToolGravity, z: 0 }; } } break; case "setBounce": if (parameter !== undefined) { // Linear range from 0.0, 0.5, 1.0 maps to 0.0, 0.5, 1.0; physicsToolPhysics.restitution = parameter; } break; case "setFriction": if (parameter !== undefined) { // Power range 0.0, 0.5, 1.0 maps to 0, 0.39, 1.0. physicsToolPhysics.damping = 0.69136364 * Math.pow(2.446416831, parameter) - 0.691364; // Power range 0.0, 0.5, 1.0 maps to 0, 0.3935, 1.0. physicsToolPhysics.angularDamping = 0.72695892 * Math.pow(2.375594, parameter) - 0.726959; // Linear range from 0.0, 0.5, 1.0 maps to 0.0, 0.5, 1.0; physicsToolPhysics.friction = parameter; } break; case "setDensity": if (parameter !== undefined) { // Power range 0.0, 0.5, 1.0 maps to 100, 1000, 10000. physicsToolPhysics.density = Math.pow(10, 2 + 2 * parameter); } break; case "autoGrab": if (dominantHand === LEFT_HAND) { editors[LEFT_HAND].enableAutoGrab(); } else { editors[RIGHT_HAND].enableAutoGrab(); } break; case "undoAction": if (History.hasUndo()) { Feedback.play(dominantHand, Feedback.UNDO_ACTION); History.undo(); } else { Feedback.play(dominantHand, Feedback.GENERAL_ERROR); } break; case "redoAction": if (History.hasRedo()) { Feedback.play(dominantHand, Feedback.REDO_ACTION); History.redo(); } else { Feedback.play(dominantHand, Feedback.GENERAL_ERROR); } break; default: log("ERROR: Unexpected command in onUICommand(): " + command + ", " + parameter); } } function startApp() { ui.display(); update(); // Start main update loop. } function stopApp() { Script.clearTimeout(updateTimer); updateTimer = null; inputs[LEFT_HAND].clear(); inputs[RIGHT_HAND].clear(); ui.clear(); grouping.clear(); editors[LEFT_HAND].clear(); editors[RIGHT_HAND].clear(); toolSelected = TOOL_NONE; } function onAppButtonClicked() { var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications", EDIT_ERROR = 4, // Per notifications.js. INSUFFICIENT_PERMISSIONS_ERROR_MSG = "You do not have the necessary permissions to edit on this domain."; // Same as edit.js. // Application tablet/toolbar button clicked. if (!isAppActive && !(Entities.canRez() || Entities.canRezTmp())) { Feedback.play(dominantHand, Feedback.GENERAL_ERROR); Messages.sendLocalMessage(NOTIFICATIONS_MESSAGE_CHANNEL, JSON.stringify({ message: INSUFFICIENT_PERMISSIONS_ERROR_MSG, notificationType: EDIT_ERROR })); return; } isAppActive = !isAppActive; updateControllerDispatcher(); button.editProperties({ isActive: isAppActive }); if (isAppActive) { startApp(); } else { stopApp(); } } function onDomainChanged() { // Fires when domain starts or domain changes; does not fire when domain stops. var hasRezPermissions = Entities.canRez() || Entities.canRezTmp(); if (isAppActive && !hasRezPermissions) { isAppActive = false; updateControllerDispatcher(); stopApp(); } button.editProperties({ icon: hasRezPermissions ? APP_ICON_INACTIVE : APP_ICON_DISABLED, captionColor: hasRezPermissions ? ENABLED_CAPTION_COLOR_OVERRIDE : DISABLED_CAPTION_COLOR_OVERRIDE, isActive: isAppActive }); } function onCanRezChanged() { // canRez or canRezTmp changed. var hasRezPermissions = Entities.canRez() || Entities.canRezTmp(); if (isAppActive && !hasRezPermissions) { isAppActive = false; updateControllerDispatcher(); stopApp(); } button.editProperties({ icon: hasRezPermissions ? APP_ICON_INACTIVE : APP_ICON_DISABLED, captionColor: hasRezPermissions ? ENABLED_CAPTION_COLOR_OVERRIDE : DISABLED_CAPTION_COLOR_OVERRIDE, isActive: isAppActive }); } function onMessageReceived(channel) { // Hacky but currently the only way of detecting server stopping or restarting. Also occurs if changing domains. // TODO: Remove this when Window.domainChanged or other signal is emitted when you disconnect from a domain. if (channel === DOMAIN_CHANGED_MESSAGE) { // Happens a little while after server goes away. if (isAppActive && !location.isConnected) { // Interface deletes all overlays when domain connection is lost; restart app to work around this. stopApp(); startApp(); } } } function onDominantHandChanged(hand) { dominantHand = hand === "left" ? LEFT_HAND : RIGHT_HAND; if (isAppActive) { // Stop operations. stopApp(); } // Swap UI hands. ui.setHand(otherHand(dominantHand)); if (isAppActive) { // Resume operations. startApp(); } } function onSkeletonChanged() { if (isAppActive) { // Close the app because the new avatar may have different joint numbers meaning that the UI would be attached // incorrectly. Let the user reopen the app because it can take some time for the new avatar to load. isAppActive = false; updateControllerDispatcher(); button.editProperties({ isActive: false }); stopApp(); } } function setUp() { var hasRezPermissions; tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); if (!tablet) { App.log("ERROR: Tablet not found! App not started."); return; } // Application state. isAppActive = false; updateControllerDispatcher(); dominantHand = MyAvatar.getDominantHand() === "left" ? LEFT_HAND : RIGHT_HAND; // Tablet/toolbar button. hasRezPermissions = Entities.canRez() || Entities.canRezTmp(); button = tablet.addButton({ icon: hasRezPermissions ? APP_ICON_INACTIVE : APP_ICON_DISABLED, captionColor: hasRezPermissions ? ENABLED_CAPTION_COLOR_OVERRIDE : DISABLED_CAPTION_COLOR_OVERRIDE, activeIcon: APP_ICON_ACTIVE, text: APP_NAME, isActive: isAppActive }); if (button) { button.clicked.connect(onAppButtonClicked); } // Input objects. inputs[LEFT_HAND] = new Inputs(LEFT_HAND); inputs[RIGHT_HAND] = new Inputs(RIGHT_HAND); // UI object. ui = new UI(otherHand(dominantHand), inputs[LEFT_HAND], inputs[RIGHT_HAND], onUICommand); // Editor objects. editors[LEFT_HAND] = new Editor(LEFT_HAND); editors[RIGHT_HAND] = new Editor(RIGHT_HAND); editors[LEFT_HAND].setReferences(inputs[LEFT_HAND], editors[RIGHT_HAND]); editors[RIGHT_HAND].setReferences(inputs[RIGHT_HAND], editors[LEFT_HAND]); // Grouping object. grouping = new Grouping(); // Changes. Window.domainChanged.connect(onDomainChanged); Entities.canRezChanged.connect(onCanRezChanged); Entities.canRezTmpChanged.connect(onCanRezChanged); Messages.subscribe(DOMAIN_CHANGED_MESSAGE); Messages.messageReceived.connect(onMessageReceived); MyAvatar.dominantHandChanged.connect(onDominantHandChanged); MyAvatar.skeletonChanged.connect(onSkeletonChanged); } function tearDown() { if (!tablet) { return; } if (updateTimer) { Script.clearTimeout(updateTimer); } Window.domainChanged.disconnect(onDomainChanged); Entities.canRezChanged.disconnect(onCanRezChanged); Entities.canRezTmpChanged.disconnect(onCanRezChanged); Messages.messageReceived.disconnect(onMessageReceived); // Messages.unsubscribe(DOMAIN_CHANGED_MESSAGE); Do not unsubscribe because edit.js also subscribes and // Messages.subscribe works script engine-wide which would mess things up if they're both run in the same engine. MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged); MyAvatar.skeletonChanged.disconnect(onSkeletonChanged); isAppActive = false; updateControllerDispatcher(); if (button) { button.clicked.disconnect(onAppButtonClicked); tablet.removeButton(button); button = null; } if (grouping) { grouping.destroy(); grouping = 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; } if (ui) { ui.destroy(); ui = null; } if (inputs[LEFT_HAND]) { inputs[LEFT_HAND].destroy(); inputs[LEFT_HAND] = null; } if (inputs[RIGHT_HAND]) { inputs[RIGHT_HAND].destroy(); inputs[RIGHT_HAND] = null; } tablet = null; } Script.setTimeout(setUp, START_DELAY); // Delay start so that Entities.canRez() work; button is enabled correctly. Script.scriptEnding.connect(tearDown); }());