// edit.js // // Created by Brad Hefta-Gaub on 10/2/14. // Persist toolbar by HRS 6/11/15. // Copyright 2014 High Fidelity, Inc. // // This script allows you to edit entities with a new UI/UX for mouse and trackpad based editing // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // /* global Script, SelectionDisplay, LightOverlayManager, CameraManager, Grid, GridTool, EntityListTool, Vec3, SelectionManager, Overlays, OverlayWebWindow, UserActivityLogger, Settings, Entities, Tablet, Toolbars, Messages, Menu, Camera, progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, ParticleExplorerTool */ (function() { // BEGIN LOCAL_SCOPE "use strict"; var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; var EDIT_TOGGLE_BUTTON = "com.highfidelity.interface.system.editButton"; var SYSTEM_TOOLBAR = "com.highfidelity.interface.toolbar.system"; var EDIT_TOOLBAR = "com.highfidelity.interface.toolbar.edit"; Script.include([ "libraries/stringHelpers.js", "libraries/dataViewHelpers.js", "libraries/progressDialog.js", "libraries/entitySelectionTool.js", "libraries/ToolTip.js", "libraries/entityCameraTool.js", "libraries/gridTool.js", "libraries/entityList.js", "particle_explorer/particleExplorerTool.js", "libraries/entityIconOverlayManager.js" ]); var selectionDisplay = SelectionDisplay; var selectionManager = SelectionManager; var PARTICLE_SYSTEM_URL = Script.resolvePath("assets/images/icon-particles.svg"); var POINT_LIGHT_URL = Script.resolvePath("assets/images/icon-point-light.svg"); var SPOT_LIGHT_URL = Script.resolvePath("assets/images/icon-spot-light.svg"); entityIconOverlayManager = new EntityIconOverlayManager(['Light', 'ParticleEffect'], function(entityID) { var properties = Entities.getEntityProperties(entityID, ['type', 'isSpotlight']); if (properties.type === 'Light') { return { url: properties.isSpotlight ? SPOT_LIGHT_URL : POINT_LIGHT_URL, }; } else { return { url: PARTICLE_SYSTEM_URL, }; } }); var cameraManager = new CameraManager(); var grid = new Grid(); var gridTool = new GridTool({ horizontalGrid: grid }); gridTool.setVisible(false); var entityListTool = new EntityListTool(); selectionManager.addEventListener(function () { selectionDisplay.updateHandles(); entityIconOverlayManager.updatePositions(); // Update particle explorer var needToDestroyParticleExplorer = false; if (selectionManager.selections.length === 1) { var selectedEntityID = selectionManager.selections[0]; if (selectedEntityID === selectedParticleEntityID) { return; } var type = Entities.getEntityProperties(selectedEntityID, "type").type; if (type === "ParticleEffect") { selectParticleEntity(selectedEntityID); } else { needToDestroyParticleExplorer = true; } } else { needToDestroyParticleExplorer = true; } if (needToDestroyParticleExplorer && selectedParticleEntityID !== null) { selectedParticleEntityID = null; particleExplorerTool.destroyWebView(); } }); var KEY_P = 80; //Key code for letter p used for Parenting hotkey. var DEGREES_TO_RADIANS = Math.PI / 180.0; var RADIANS_TO_DEGREES = 180.0 / Math.PI; var MIN_ANGULAR_SIZE = 2; var MAX_ANGULAR_SIZE = 45; var allowLargeModels = true; var allowSmallModels = true; var DEFAULT_DIMENSION = 0.20; var DEFAULT_DIMENSIONS = { x: DEFAULT_DIMENSION, y: DEFAULT_DIMENSION, z: DEFAULT_DIMENSION }; var DEFAULT_LIGHT_DIMENSIONS = Vec3.multiply(20, DEFAULT_DIMENSIONS); var MENU_AUTO_FOCUS_ON_SELECT = "Auto Focus on Select"; var MENU_EASE_ON_FOCUS = "Ease Orientation on Focus"; var MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "Show Lights and Particle Systems in Edit Mode"; var MENU_SHOW_ZONES_IN_EDIT_MODE = "Show Zones in Edit Mode"; var SETTING_AUTO_FOCUS_ON_SELECT = "autoFocusOnSelect"; var SETTING_EASE_ON_FOCUS = "cameraEaseOnFocus"; var SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "showLightsAndParticlesInEditMode"; var SETTING_SHOW_ZONES_IN_EDIT_MODE = "showZonesInEditMode"; var CREATE_ENABLED_ICON = "icons/tablet-icons/edit-i.svg"; var CREATE_DISABLED_ICON = "icons/tablet-icons/edit-disabled.svg"; // marketplace info, etc. not quite ready yet. var SHOULD_SHOW_PROPERTY_MENU = false; var INSUFFICIENT_PERMISSIONS_ERROR_MSG = "You do not have the necessary permissions to edit on this domain."; var INSUFFICIENT_PERMISSIONS_IMPORT_ERROR_MSG = "You do not have the necessary permissions to place items on this domain."; var isActive = false; var createButton = null; var IMPORTING_SVO_OVERLAY_WIDTH = 144; var IMPORTING_SVO_OVERLAY_HEIGHT = 30; var IMPORTING_SVO_OVERLAY_MARGIN = 5; var IMPORTING_SVO_OVERLAY_LEFT_MARGIN = 34; var importingSVOImageOverlay = Overlays.addOverlay("image", { imageURL: Script.resolvePath("assets") + "/images/hourglass.svg", width: 20, height: 20, alpha: 1.0, x: Window.innerWidth - IMPORTING_SVO_OVERLAY_WIDTH, y: Window.innerHeight - IMPORTING_SVO_OVERLAY_HEIGHT, visible: false }); var importingSVOTextOverlay = Overlays.addOverlay("text", { font: { size: 14 }, text: "Importing SVO...", leftMargin: IMPORTING_SVO_OVERLAY_LEFT_MARGIN, x: Window.innerWidth - IMPORTING_SVO_OVERLAY_WIDTH - IMPORTING_SVO_OVERLAY_MARGIN, y: Window.innerHeight - IMPORTING_SVO_OVERLAY_HEIGHT - IMPORTING_SVO_OVERLAY_MARGIN, width: IMPORTING_SVO_OVERLAY_WIDTH, height: IMPORTING_SVO_OVERLAY_HEIGHT, backgroundColor: { red: 80, green: 80, blue: 80 }, backgroundAlpha: 0.7, visible: false }); var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace"; var marketplaceWindow = new OverlayWebWindow({ title: 'Marketplace', source: "about:blank", width: 900, height: 700, visible: false }); function showMarketplace(marketplaceID) { var url = MARKETPLACE_URL; if (marketplaceID) { url = url + "/items/" + marketplaceID; } marketplaceWindow.setURL(url); marketplaceWindow.setVisible(true); marketplaceWindow.raise(); UserActivityLogger.logAction("opened_marketplace"); } function hideMarketplace() { marketplaceWindow.setVisible(false); marketplaceWindow.setURL("about:blank"); } // function toggleMarketplace() { // if (marketplaceWindow.visible) { // hideMarketplace(); // } else { // showMarketplace(); // } // } function adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation) { // Adjust the position such that the bounding box (registration, dimenions, and orientation) lies behind the original // position in the given direction. var CORNERS = [ { 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 }, ]; // Go through all corners and find least (most negative) distance in front of position. var distance = 0; for (var i = 0, length = CORNERS.length; i < length; i++) { var cornerVector = Vec3.multiplyQbyV(orientation, Vec3.multiplyVbyV(Vec3.subtract(CORNERS[i], registration), dimensions)); var cornerDistance = Vec3.dot(cornerVector, direction); distance = Math.min(cornerDistance, distance); } position = Vec3.sum(Vec3.multiply(distance, direction), position); return position; } var TOOLS_PATH = Script.resolvePath("assets/images/tools/"); var GRABBABLE_ENTITIES_MENU_CATEGORY = "Edit"; var GRABBABLE_ENTITIES_MENU_ITEM = "Create Entities As Grabbable"; var toolBar = (function () { var EDIT_SETTING = "io.highfidelity.isEditting"; // for communication with other scripts var that = {}, toolBar, activeButton = null, systemToolbar = null, tablet = null; function createNewEntity(properties) { var dimensions = properties.dimensions ? properties.dimensions : DEFAULT_DIMENSIONS; var position = getPositionToCreateEntity(); var entityID = null; if (position !== null && position !== undefined) { var direction; if (Camera.mode === "entity" || Camera.mode === "independent") { direction = Camera.orientation; } else { direction = MyAvatar.orientation; } direction = Vec3.multiplyQbyV(direction, Vec3.UNIT_Z); var PRE_ADJUST_ENTITY_TYPES = ["Box", "Sphere", "Shape", "Text", "Web"]; if (PRE_ADJUST_ENTITY_TYPES.indexOf(properties.type) !== -1) { // Adjust position of entity per bounding box prior to creating it. var registration = properties.registration; if (registration === undefined) { var DEFAULT_REGISTRATION = { x: 0.5, y: 0.5, z: 0.5 }; registration = DEFAULT_REGISTRATION; } var orientation = properties.orientation; if (orientation === undefined) { var DEFAULT_ORIENTATION = Quat.fromPitchYawRollDegrees(0, 0, 0); orientation = DEFAULT_ORIENTATION; } position = adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation); } position = grid.snapToSurface(grid.snapToGrid(position, false, dimensions), dimensions); properties.position = position; if (Menu.isOptionChecked(GRABBABLE_ENTITIES_MENU_ITEM)) { properties.userData = JSON.stringify({ grabbableKey: { grabbable: true } }); } entityID = Entities.addEntity(properties); if (properties.type === "ParticleEffect") { selectParticleEntity(entityID); } var POST_ADJUST_ENTITY_TYPES = ["Model"]; if (POST_ADJUST_ENTITY_TYPES.indexOf(properties.type) !== -1) { // Adjust position of entity per bounding box after it has been created and auto-resized. var initialDimensions = Entities.getEntityProperties(entityID, ["dimensions"]).dimensions; var DIMENSIONS_CHECK_INTERVAL = 200; var MAX_DIMENSIONS_CHECKS = 10; var dimensionsCheckCount = 0; var dimensionsCheckFunction = function () { dimensionsCheckCount++; var properties = Entities.getEntityProperties(entityID, ["dimensions", "registrationPoint", "rotation"]); if (!Vec3.equal(properties.dimensions, initialDimensions)) { position = adjustPositionPerBoundingBox(position, direction, properties.registrationPoint, properties.dimensions, properties.rotation); position = grid.snapToSurface(grid.snapToGrid(position, false, properties.dimensions), properties.dimensions); Entities.editEntity(entityID, { position: position }); selectionManager._update(); } else if (dimensionsCheckCount < MAX_DIMENSIONS_CHECKS) { Script.setTimeout(dimensionsCheckFunction, DIMENSIONS_CHECK_INTERVAL); } }; Script.setTimeout(dimensionsCheckFunction, DIMENSIONS_CHECK_INTERVAL); } } else { Window.notifyEditError("Can't create " + properties.type + ": " + properties.type + " would be out of bounds."); } selectionManager.clearSelections(); entityListTool.sendUpdate(); selectionManager.setSelections([entityID]); return entityID; } function cleanup() { that.setActive(false); if (tablet) { tablet.removeButton(activeButton); } if (systemToolbar) { systemToolbar.removeButton(EDIT_TOGGLE_BUTTON); } Menu.removeMenuItem(GRABBABLE_ENTITIES_MENU_CATEGORY, GRABBABLE_ENTITIES_MENU_ITEM); } var buttonHandlers = {}; // only used to tablet mode function addButton(name, image, handler) { buttonHandlers[name] = handler; } var SHAPE_TYPE_NONE = 0; var SHAPE_TYPE_SIMPLE_HULL = 1; var SHAPE_TYPE_SIMPLE_COMPOUND = 2; var SHAPE_TYPE_STATIC_MESH = 3; var SHAPE_TYPE_BOX = 4; var SHAPE_TYPE_SPHERE = 5; var DYNAMIC_DEFAULT = false; function handleNewModelDialogResult(result) { if (result) { var url = result.textInput; var shapeType; switch (result.comboBox) { case SHAPE_TYPE_SIMPLE_HULL: shapeType = "simple-hull"; break; case SHAPE_TYPE_SIMPLE_COMPOUND: shapeType = "simple-compound"; break; case SHAPE_TYPE_STATIC_MESH: shapeType = "static-mesh"; break; case SHAPE_TYPE_BOX: shapeType = "box"; break; case SHAPE_TYPE_SPHERE: shapeType = "sphere"; break; default: shapeType = "none"; } var dynamic = result.checkBox !== null ? result.checkBox : DYNAMIC_DEFAULT; if (shapeType === "static-mesh" && dynamic) { // The prompt should prevent this case print("Error: model cannot be both static mesh and dynamic. This should never happen."); } else if (url) { createNewEntity({ type: "Model", modelURL: url, shapeType: shapeType, dynamic: dynamic, gravity: dynamic ? { x: 0, y: -10, z: 0 } : { x: 0, y: 0, z: 0 } }); } } } function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); tablet.popFromStack(); switch (message.method) { case "newModelDialogAdd": handleNewModelDialogResult(message.params); break; case "newEntityButtonClicked": buttonHandlers[message.params.buttonName](); break; } } function initialize() { Script.scriptEnding.connect(cleanup); Window.domainChanged.connect(function () { that.setActive(false); that.clearEntityList(); }); Entities.canAdjustLocksChanged.connect(function (canAdjustLocks) { if (isActive && !canAdjustLocks) { that.setActive(false); } }); var createButtonIconRsrc = ((Entities.canRez() || Entities.canRezTmp()) ? CREATE_ENABLED_ICON : CREATE_DISABLED_ICON); tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); activeButton = tablet.addButton({ icon: createButtonIconRsrc, activeIcon: "icons/tablet-icons/edit-a.svg", text: "CREATE", sortOrder: 10 }); createButton = activeButton; tablet.screenChanged.connect(function (type, url) { if (isActive && (type !== "QML" || url !== "Edit.qml")) { that.setActive(false) } }); tablet.fromQml.connect(fromQml); createButton.clicked.connect(function() { if ( ! (Entities.canRez() || Entities.canRezTmp()) ) { Window.notifyEditError(INSUFFICIENT_PERMISSIONS_ERROR_MSG); return; } that.toggle(); }); addButton("importEntitiesButton", "assets-01.svg", function() { var importURL = null; var fullPath = Window.browse("Select Model to Import", "", "*.json"); if (fullPath) { importURL = "file:///" + fullPath; } if (importURL) { if (!isActive && (Entities.canRez() && Entities.canRezTmp())) { toolBar.toggle(); } importSVO(importURL); } }); addButton("openAssetBrowserButton", "assets-01.svg", function() { Window.showAssetServer(); }); addButton("newModelButton", "model-01.svg", function () { var SHAPE_TYPES = []; SHAPE_TYPES[SHAPE_TYPE_NONE] = "No Collision"; SHAPE_TYPES[SHAPE_TYPE_SIMPLE_HULL] = "Basic - Whole model"; SHAPE_TYPES[SHAPE_TYPE_SIMPLE_COMPOUND] = "Good - Sub-meshes"; SHAPE_TYPES[SHAPE_TYPE_STATIC_MESH] = "Exact - All polygons"; SHAPE_TYPES[SHAPE_TYPE_BOX] = "Box"; SHAPE_TYPES[SHAPE_TYPE_SPHERE] = "Sphere"; var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_STATIC_MESH; // tablet version of new-model dialog var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); tablet.pushOntoStack("NewModelDialog.qml"); }); addButton("newCubeButton", "cube-01.svg", function () { createNewEntity({ type: "Box", dimensions: DEFAULT_DIMENSIONS, color: { red: 255, green: 0, blue: 0 } }); }); addButton("newSphereButton", "sphere-01.svg", function () { createNewEntity({ type: "Sphere", dimensions: DEFAULT_DIMENSIONS, color: { red: 255, green: 0, blue: 0 } }); }); addButton("newLightButton", "light-01.svg", function () { createNewEntity({ type: "Light", dimensions: DEFAULT_LIGHT_DIMENSIONS, isSpotlight: false, color: { red: 150, green: 150, blue: 150 }, constantAttenuation: 1, linearAttenuation: 0, quadraticAttenuation: 0, exponent: 0, cutoff: 180 // in degrees }); }); addButton("newTextButton", "text-01.svg", function () { createNewEntity({ type: "Text", dimensions: { x: 0.65, y: 0.3, z: 0.01 }, backgroundColor: { red: 64, green: 64, blue: 64 }, textColor: { red: 255, green: 255, blue: 255 }, text: "some text", lineHeight: 0.06 }); }); addButton("newWebButton", "web-01.svg", function () { createNewEntity({ type: "Web", dimensions: { x: 1.6, y: 0.9, z: 0.01 }, sourceUrl: "https://highfidelity.com/" }); }); addButton("newZoneButton", "zone-01.svg", function () { createNewEntity({ type: "Zone", dimensions: { x: 10, y: 10, z: 10 } }); }); addButton("newParticleButton", "particle-01.svg", function () { createNewEntity({ type: "ParticleEffect", isEmitting: true, emitterShouldTrail: true, color: { red: 200, green: 200, blue: 200 }, colorSpread: { red: 0, green: 0, blue: 0 }, colorStart: { red: 200, green: 200, blue: 200 }, colorFinish: { red: 0, green: 0, blue: 0 }, emitAcceleration: { x: -0.5, y: 2.5, z: -0.5 }, accelerationSpread: { x: 0.5, y: 1, z: 0.5 }, emitRate: 5.5, emitSpeed: 0, speedSpread: 0, lifespan: 1.5, maxParticles: 10, particleRadius: 0.25, radiusStart: 0, radiusFinish: 0.1, radiusSpread: 0, alpha: 0, alphaStart: 1, alphaFinish: 0, polarStart: 0, polarFinish: 0, textures: "https://content.highfidelity.com/DomainContent/production/Particles/wispy-smoke.png" }); }); that.setActive(false); } that.clearEntityList = function () { entityListTool.clearEntityList(); }; that.toggle = function () { that.setActive(!isActive); if (!isActive) { tablet.gotoHomeScreen(); } }; that.setActive = function (active) { ContextOverlay.enabled = !active; Settings.setValue(EDIT_SETTING, active); if (active) { Controller.captureEntityClickEvents(); } else { Controller.releaseEntityClickEvents(); } if (active === isActive) { return; } if (active && !Entities.canRez() && !Entities.canRezTmp()) { Window.notifyEditError(INSUFFICIENT_PERMISSIONS_ERROR_MSG); return; } Messages.sendLocalMessage("edit-events", JSON.stringify({ enabled: active })); isActive = active; activeButton.editProperties({isActive: isActive}); var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); if (!isActive) { entityListTool.setVisible(false); gridTool.setVisible(false); grid.setEnabled(false); propertiesTool.setVisible(false); selectionManager.clearSelections(); cameraManager.disable(); selectionDisplay.triggerMapping.disable(); tablet.landscape = false; } else { tablet.loadQMLSource("Edit.qml"); UserActivityLogger.enabledEdit(); entityListTool.setVisible(true); gridTool.setVisible(true); grid.setEnabled(true); propertiesTool.setVisible(true); selectionDisplay.triggerMapping.enable(); print("starting tablet in landscape mode"); tablet.landscape = true; entityIconOverlayManager.setIconsSelectable(null,false); // Not sure what the following was meant to accomplish, but it currently causes // everybody else to think that Interface has lost focus overall. fogbugzid:558 // Window.setFocus(); } entityIconOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); }; initialize(); return that; })(); function isLocked(properties) { // special case to lock the ground plane model in hq. if (location.hostname === "hq.highfidelity.io" && properties.modelURL === HIFI_PUBLIC_BUCKET + "ozan/Terrain_Reduce_forAlpha.fbx") { return true; } return false; } var selectedEntityID; var orientation; var intersection; function rayPlaneIntersection(pickRay, point, normal) { // // // This version of the test returns the intersection of a line with a plane // var collides = Vec3.dot(pickRay.direction, normal); var d = -Vec3.dot(point, normal); var t = -(Vec3.dot(pickRay.origin, normal) + d) / collides; return Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction, t)); } function rayPlaneIntersection2(pickRay, point, normal) { // // This version of the test returns false if the ray is directed away from the plane // var collides = Vec3.dot(pickRay.direction, normal); var d = -Vec3.dot(point, normal); var t = -(Vec3.dot(pickRay.origin, normal) + d) / collides; if (t < 0.0) { return false; } else { return Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction, t)); } } function findClickedEntity(event) { var pickZones = event.isControl; if (pickZones) { Entities.setZonesArePickable(true); } var pickRay = Camera.computePickRay(event.x, event.y); var overlayResult = Overlays.findRayIntersection(pickRay, true, [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID]); if (overlayResult.intersects) { return null; } var entityResult = Entities.findRayIntersection(pickRay, true); // want precision picking var iconResult = entityIconOverlayManager.findRayIntersection(pickRay); iconResult.accurate = true; if (pickZones) { Entities.setZonesArePickable(false); } var result; if (iconResult.intersects) { result = iconResult; } else if (entityResult.intersects) { result = entityResult; } else { return null; } if (!result.accurate) { return null; } var foundEntity = result.entityID; return { pickRay: pickRay, entityID: foundEntity, intersection: result.intersection }; } // Handles selections on overlays while in edit mode by querying entities from // entityIconOverlayManager. function handleOverlaySelectionToolUpdates(channel, message, sender) { if (sender !== MyAvatar.sessionUUID || channel !== 'entityToolUpdates') return; var data = JSON.parse(message); if (data.method === "selectOverlay") { print("setting selection to overlay " + data.overlayID); var entity = entityIconOverlayManager.findEntity(data.overlayID); if (entity !== null) { selectionManager.setSelections([entity]); } } } // Handles any edit mode updates required when domains have switched function handleDomainChange() { if ( (createButton === null) || (createButton === undefined) ){ //--EARLY EXIT--( nothing to safely update ) return; } var hasRezPermissions = (Entities.canRez() || Entities.canRezTmp()); createButton.editProperties({ icon: (hasRezPermissions ? CREATE_ENABLED_ICON : CREATE_DISABLED_ICON), }); } function handleMessagesReceived(channel, message, sender) { switch( channel ){ case 'entityToolUpdates': { handleOverlaySelectionToolUpdates( channel, message, sender ); break; } case 'Toolbar-DomainChanged': { handleDomainChange(); break; } default: { return; } } } Messages.subscribe('Toolbar-DomainChanged'); Messages.subscribe("entityToolUpdates"); Messages.messageReceived.connect(handleMessagesReceived); var mouseHasMovedSincePress = false; var mousePressStartTime = 0; var mousePressStartPosition = { x: 0, y: 0 }; var mouseDown = false; function mousePressEvent(event) { mouseDown = true; mousePressStartPosition = { x: event.x, y: event.y }; mousePressStartTime = Date.now(); mouseHasMovedSincePress = false; mouseCapturedByTool = false; if (propertyMenu.mousePressEvent(event) || progressDialog.mousePressEvent(event)) { mouseCapturedByTool = true; return; } if (isActive) { if (cameraManager.mousePressEvent(event) || selectionDisplay.mousePressEvent(event)) { // Event handled; do nothing. return; } } } var mouseCapturedByTool = false; var lastMousePosition = null; var CLICK_TIME_THRESHOLD = 500 * 1000; // 500 ms var CLICK_MOVE_DISTANCE_THRESHOLD = 20; var IDLE_MOUSE_TIMEOUT = 200; var lastMouseMoveEvent = null; function mouseMoveEventBuffered(event) { lastMouseMoveEvent = event; } function mouseMove(event) { if (mouseDown && !mouseHasMovedSincePress) { var timeSincePressMicro = Date.now() - mousePressStartTime; var dX = mousePressStartPosition.x - event.x; var dY = mousePressStartPosition.y - event.y; var sqDist = (dX * dX) + (dY * dY); // If less than CLICK_TIME_THRESHOLD has passed since the mouse click AND the mouse has moved // less than CLICK_MOVE_DISTANCE_THRESHOLD distance, then don't register this as a mouse move // yet. The goal is to provide mouse clicks that are more lenient to small movements. if (timeSincePressMicro < CLICK_TIME_THRESHOLD && sqDist < CLICK_MOVE_DISTANCE_THRESHOLD) { return; } mouseHasMovedSincePress = true; } if (!isActive) { return; } // allow the selectionDisplay and cameraManager to handle the event first, if it doesn't handle it, then do our own thing if (selectionDisplay.mouseMoveEvent(event) || propertyMenu.mouseMoveEvent(event) || cameraManager.mouseMoveEvent(event)) { return; } lastMousePosition = { x: event.x, y: event.y }; } function mouseReleaseEvent(event) { mouseDown = false; if (lastMouseMoveEvent) { mouseMove(lastMouseMoveEvent); lastMouseMoveEvent = null; } if (propertyMenu.mouseReleaseEvent(event)) { return true; } if (isActive && selectionManager.hasSelection()) { tooltip.show(false); } if (mouseCapturedByTool) { return; } cameraManager.mouseReleaseEvent(event); if (!mouseHasMovedSincePress) { mouseClickEvent(event); } } function wasTabletClicked(event) { var rayPick = Camera.computePickRay(event.x, event.y); var result = Overlays.findRayIntersection(rayPick, true, [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID]); return result.intersects; } function mouseClickEvent(event) { var wantDebug = false; var result, properties, tabletClicked; if (isActive && event.isLeftButton) { result = findClickedEntity(event); tabletClicked = wasTabletClicked(event); if (tabletClicked) { return; } if (result === null || result === undefined) { if (!event.isShifted) { selectionManager.clearSelections(); } return; } toolBar.setActive(true); var pickRay = result.pickRay; var foundEntity = result.entityID; if (foundEntity === HMD.tabletID) { return; } properties = Entities.getEntityProperties(foundEntity); if (isLocked(properties)) { if (wantDebug) { print("Model locked " + properties.id); } } else { var halfDiagonal = Vec3.length(properties.dimensions) / 2.0; if (wantDebug) { print("Checking properties: " + properties.id + " " + " - Half Diagonal:" + halfDiagonal); } // P P - Model // /| A - Palm // / | d B - unit vector toward tip // / | X - base of the perpendicular line // A---X----->B d - distance fom axis // x x - distance from A // // |X-A| = (P-A).B // X === A + ((P-A).B)B // d = |P-X| var A = pickRay.origin; var B = Vec3.normalize(pickRay.direction); var P = properties.position; var x = Vec3.dot(Vec3.subtract(P, A), B); var angularSize = 2 * Math.atan(halfDiagonal / Vec3.distance(Camera.getPosition(), properties.position)) * 180 / Math.PI; var sizeOK = (allowLargeModels || angularSize < MAX_ANGULAR_SIZE) && (allowSmallModels || angularSize > MIN_ANGULAR_SIZE); if (0 < x && sizeOK) { selectedEntityID = foundEntity; orientation = MyAvatar.orientation; intersection = rayPlaneIntersection(pickRay, P, Quat.getForward(orientation)); if (event.isShifted) { particleExplorerTool.destroyWebView(); } if (properties.type !== "ParticleEffect") { particleExplorerTool.destroyWebView(); } if (!event.isShifted) { selectionManager.setSelections([foundEntity]); } else { selectionManager.addEntity(foundEntity, true); } if (wantDebug) { print("Model selected: " + foundEntity); } selectionDisplay.select(selectedEntityID, event); if (Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)) { cameraManager.enable(); cameraManager.focus(selectionManager.worldPosition, selectionManager.worldDimensions, Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); } } } } else if (event.isRightButton) { result = findClickedEntity(event); if (result) { if (SHOULD_SHOW_PROPERTY_MENU !== true) { return; } properties = Entities.getEntityProperties(result.entityID); if (properties.marketplaceID) { propertyMenu.marketplaceID = properties.marketplaceID; propertyMenu.updateMenuItemText(showMenuItem, "Show in Marketplace"); } else { propertyMenu.marketplaceID = null; propertyMenu.updateMenuItemText(showMenuItem, "No marketplace info"); } propertyMenu.setPosition(event.x, event.y); propertyMenu.show(); } else { propertyMenu.hide(); } } } Controller.mousePressEvent.connect(mousePressEvent); Controller.mouseMoveEvent.connect(mouseMoveEventBuffered); Controller.mouseReleaseEvent.connect(mouseReleaseEvent); // In order for editVoxels and editModels to play nice together, they each check to see if a "delete" menu item already // exists. If it doesn't they add it. If it does they don't. They also only delete the menu item if they were the one that // added it. var modelMenuAddedDelete = false; var originalLightsArePickable = Entities.getLightsArePickable(); function setupModelMenus() { // adj our menuitems Menu.addMenuItem({ menuName: "Edit", menuItemName: "Entities", isSeparator: true, grouping: "Advanced" }); if (!Menu.menuItemExists("Edit", "Delete")) { Menu.addMenuItem({ menuName: "Edit", menuItemName: "Delete", shortcutKeyEvent: { text: "delete" }, afterItem: "Entities", grouping: "Advanced" }); modelMenuAddedDelete = true; } Menu.addMenuItem({ menuName: "Edit", menuItemName: "Entity List...", afterItem: "Entities", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Parent Entity to Last", afterItem: "Entity List...", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Unparent Entity", afterItem: "Parent Entity to Last", grouping: "Advanced" }); Menu.addMenuItem({ menuName: GRABBABLE_ENTITIES_MENU_CATEGORY, menuItemName: GRABBABLE_ENTITIES_MENU_ITEM, afterItem: "Unparent Entity", isCheckable: true, isChecked: true, grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Selecting of Large Models", afterItem: GRABBABLE_ENTITIES_MENU_ITEM, isCheckable: true, isChecked: true, grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Selecting of Small Models", afterItem: "Allow Selecting of Large Models", isCheckable: true, isChecked: true, grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Selecting of Lights", afterItem: "Allow Selecting of Small Models", isCheckable: true, grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Select All Entities In Box", afterItem: "Allow Selecting of Lights", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Select All Entities Touching Box", afterItem: "Select All Entities In Box", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Export Entities", afterItem: "Entities", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Import Entities", afterItem: "Export Entities", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Import Entities from URL", afterItem: "Import Entities", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: MENU_AUTO_FOCUS_ON_SELECT, isCheckable: true, isChecked: Settings.getValue(SETTING_AUTO_FOCUS_ON_SELECT) === "true", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: MENU_EASE_ON_FOCUS, afterItem: MENU_AUTO_FOCUS_ON_SELECT, isCheckable: true, isChecked: Settings.getValue(SETTING_EASE_ON_FOCUS) === "true", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, afterItem: MENU_EASE_ON_FOCUS, isCheckable: true, isChecked: Settings.getValue(SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE) !== "false", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: MENU_SHOW_ZONES_IN_EDIT_MODE, afterItem: MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, isCheckable: true, isChecked: Settings.getValue(SETTING_SHOW_ZONES_IN_EDIT_MODE) !== "false", grouping: "Advanced" }); Entities.setLightsArePickable(false); } setupModelMenus(); // do this when first running our script. function cleanupModelMenus() { Menu.removeSeparator("Edit", "Entities"); if (modelMenuAddedDelete) { // delete our menuitems Menu.removeMenuItem("Edit", "Delete"); } Menu.removeMenuItem("Edit", "Parent Entity to Last"); Menu.removeMenuItem("Edit", "Unparent Entity"); Menu.removeMenuItem("Edit", "Entity List..."); Menu.removeMenuItem("Edit", "Allow Selecting of Large Models"); Menu.removeMenuItem("Edit", "Allow Selecting of Small Models"); Menu.removeMenuItem("Edit", "Allow Selecting of Lights"); Menu.removeMenuItem("Edit", "Select All Entities In Box"); Menu.removeMenuItem("Edit", "Select All Entities Touching Box"); Menu.removeMenuItem("Edit", "Export Entities"); Menu.removeMenuItem("Edit", "Import Entities"); Menu.removeMenuItem("Edit", "Import Entities from URL"); Menu.removeMenuItem("Edit", MENU_AUTO_FOCUS_ON_SELECT); Menu.removeMenuItem("Edit", MENU_EASE_ON_FOCUS); Menu.removeMenuItem("Edit", MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE); Menu.removeMenuItem("Edit", MENU_SHOW_ZONES_IN_EDIT_MODE); } Script.scriptEnding.connect(function () { toolBar.setActive(false); Settings.setValue(SETTING_AUTO_FOCUS_ON_SELECT, Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)); Settings.setValue(SETTING_EASE_ON_FOCUS, Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); Settings.setValue(SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); Settings.setValue(SETTING_SHOW_ZONES_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); progressDialog.cleanup(); cleanupModelMenus(); tooltip.cleanup(); selectionDisplay.cleanup(); Entities.setLightsArePickable(originalLightsArePickable); Overlays.deleteOverlay(importingSVOImageOverlay); Overlays.deleteOverlay(importingSVOTextOverlay); Controller.keyReleaseEvent.disconnect(keyReleaseEvent); Controller.keyPressEvent.disconnect(keyPressEvent); Controller.mousePressEvent.disconnect(mousePressEvent); Controller.mouseMoveEvent.disconnect(mouseMoveEventBuffered); Controller.mouseReleaseEvent.disconnect(mouseReleaseEvent); Messages.messageReceived.disconnect(handleOverlaySelectionToolUpdates); Messages.unsubscribe("entityToolUpdates"); Messages.unsubscribe("Toolbar-DomainChanged"); createButton = null; }); var lastOrientation = null; var lastPosition = null; // Do some stuff regularly, like check for placement of various overlays Script.update.connect(function (deltaTime) { progressDialog.move(); selectionDisplay.checkMove(); var dOrientation = Math.abs(Quat.dot(Camera.orientation, lastOrientation) - 1); var dPosition = Vec3.distance(Camera.position, lastPosition); if (dOrientation > 0.001 || dPosition > 0.001) { propertyMenu.hide(); lastOrientation = Camera.orientation; lastPosition = Camera.position; } if (lastMouseMoveEvent) { mouseMove(lastMouseMoveEvent); lastMouseMoveEvent = null; } }); function insideBox(center, dimensions, point) { return (Math.abs(point.x - center.x) <= (dimensions.x / 2.0)) && (Math.abs(point.y - center.y) <= (dimensions.y / 2.0)) && (Math.abs(point.z - center.z) <= (dimensions.z / 2.0)); } function selectAllEtitiesInCurrentSelectionBox(keepIfTouching) { if (selectionManager.hasSelection()) { // Get all entities touching the bounding box of the current selection var boundingBoxCorner = Vec3.subtract(selectionManager.worldPosition, Vec3.multiply(selectionManager.worldDimensions, 0.5)); var entities = Entities.findEntitiesInBox(boundingBoxCorner, selectionManager.worldDimensions); if (!keepIfTouching) { var isValid; if (selectionManager.localPosition === null || selectionManager.localPosition === undefined) { isValid = function (position) { return insideBox(selectionManager.worldPosition, selectionManager.worldDimensions, position); }; } else { isValid = function (position) { var localPosition = Vec3.multiplyQbyV(Quat.inverse(selectionManager.localRotation), Vec3.subtract(position, selectionManager.localPosition)); return insideBox({ x: 0, y: 0, z: 0 }, selectionManager.localDimensions, localPosition); }; } for (var i = 0; i < entities.length; ++i) { var properties = Entities.getEntityProperties(entities[i]); if (!isValid(properties.position)) { entities.splice(i, 1); --i; } } } selectionManager.setSelections(entities); } } function sortSelectedEntities(selected) { var sortedEntities = selected.slice(); var begin = 0; while (begin < sortedEntities.length) { var elementRemoved = false; var next = begin + 1; while (next < sortedEntities.length) { var beginID = sortedEntities[begin]; var nextID = sortedEntities[next]; if (Entities.isChildOfParent(beginID, nextID)) { sortedEntities[begin] = nextID; sortedEntities[next] = beginID; sortedEntities.splice(next, 1); elementRemoved = true; break; } else if (Entities.isChildOfParent(nextID, beginID)) { sortedEntities.splice(next, 1); elementRemoved = true; break; } next++; } if (!elementRemoved) { begin++; } } return sortedEntities; } function recursiveDelete(entities, childrenList) { var entitiesLength = entities.length; for (var i = 0; i < entitiesLength; i++) { var entityID = entities[i]; var children = Entities.getChildrenIDs(entityID); var grandchildrenList = []; recursiveDelete(children, grandchildrenList); var initialProperties = Entities.getEntityProperties(entityID); childrenList.push({ entityID: entityID, properties: initialProperties, children: grandchildrenList }); Entities.deleteEntity(entityID); } } function unparentSelectedEntities() { if (SelectionManager.hasSelection()) { var selectedEntities = selectionManager.selections; var parentCheck = false; if (selectedEntities.length < 1) { Window.notifyEditError("You must have an entity selected inorder to unparent it."); return; } selectedEntities.forEach(function (id, index) { var parentId = Entities.getEntityProperties(id, ["parentID"]).parentID; if (parentId !== null && parentId.length > 0 && parentId !== "{00000000-0000-0000-0000-000000000000}") { parentCheck = true; } Entities.editEntity(id, {parentID: null}); return true; }); if (parentCheck) { if (selectedEntities.length > 1) { Window.notify("Entities unparented"); } else { Window.notify("Entity unparented"); } } else { if (selectedEntities.length > 1) { Window.notify("Selected Entities have no parents"); } else { Window.notify("Selected Entity does not have a parent"); } } } else { Window.notifyEditError("You have nothing selected to unparent"); } } function parentSelectedEntities() { if (SelectionManager.hasSelection()) { var selectedEntities = selectionManager.selections; if (selectedEntities.length <= 1) { Window.notifyEditError("You must have multiple entities selected in order to parent them"); return; } var parentCheck = false; var lastEntityId = selectedEntities[selectedEntities.length-1]; selectedEntities.forEach(function (id, index) { if (lastEntityId !== id) { var parentId = Entities.getEntityProperties(id, ["parentID"]).parentID; if (parentId !== lastEntityId) { parentCheck = true; } Entities.editEntity(id, {parentID: lastEntityId}); } }); if (parentCheck) { Window.notify("Entities parented"); }else { Window.notify("Entities are already parented to last"); } } else { Window.notifyEditError("You have nothing selected to parent"); } } function deleteSelectedEntities() { if (SelectionManager.hasSelection()) { selectedParticleEntityID = null; particleExplorerTool.destroyWebView(); SelectionManager.saveProperties(); var savedProperties = []; var newSortedSelection = sortSelectedEntities(selectionManager.selections); for (var i = 0; i < newSortedSelection.length; i++) { var entityID = newSortedSelection[i]; var initialProperties = SelectionManager.savedProperties[entityID]; var children = Entities.getChildrenIDs(entityID); var childList = []; recursiveDelete(children, childList); savedProperties.push({ entityID: entityID, properties: initialProperties, children: childList }); Entities.deleteEntity(entityID); } SelectionManager.clearSelections(); pushCommandForSelections([], savedProperties); } } function toggleSelectedEntitiesLocked() { if (SelectionManager.hasSelection()) { var locked = !Entities.getEntityProperties(SelectionManager.selections[0], ["locked"]).locked; for (var i = 0; i < selectionManager.selections.length; i++) { var entityID = SelectionManager.selections[i]; Entities.editEntity(entityID, { locked: locked }); } entityListTool.sendUpdate(); selectionManager._update(); } } function toggleSelectedEntitiesVisible() { if (SelectionManager.hasSelection()) { var visible = !Entities.getEntityProperties(SelectionManager.selections[0], ["visible"]).visible; for (var i = 0; i < selectionManager.selections.length; i++) { var entityID = SelectionManager.selections[i]; Entities.editEntity(entityID, { visible: visible }); } entityListTool.sendUpdate(); selectionManager._update(); } } function handeMenuEvent(menuItem) { if (menuItem === "Allow Selecting of Small Models") { allowSmallModels = Menu.isOptionChecked("Allow Selecting of Small Models"); } else if (menuItem === "Allow Selecting of Large Models") { allowLargeModels = Menu.isOptionChecked("Allow Selecting of Large Models"); } else if (menuItem === "Allow Selecting of Lights") { Entities.setLightsArePickable(Menu.isOptionChecked("Allow Selecting of Lights")); } else if (menuItem === "Delete") { deleteSelectedEntities(); } else if (menuItem === "Parent Entity to Last") { parentSelectedEntities(); } else if (menuItem === "Unparent Entity") { unparentSelectedEntities(); } else if (menuItem === "Export Entities") { if (!selectionManager.hasSelection()) { Window.notifyEditError("No entities have been selected."); } else { var filename = Window.save("Select Where to Save", "", "*.json"); if (filename) { var success = Clipboard.exportEntities(filename, selectionManager.selections); if (!success) { Window.notifyEditError("Export failed."); } } } } else if (menuItem === "Import Entities" || menuItem === "Import Entities from URL") { var importURL = null; if (menuItem === "Import Entities") { var fullPath = Window.browse("Select Model to Import", "", "*.json"); if (fullPath) { importURL = "file:///" + fullPath; } } else { importURL = Window.prompt("URL of SVO to import", ""); } if (importURL) { if (!isActive && (Entities.canRez() && Entities.canRezTmp())) { toolBar.toggle(); } importSVO(importURL); } } else if (menuItem === "Entity List...") { entityListTool.toggleVisible(); } else if (menuItem === "Select All Entities In Box") { selectAllEtitiesInCurrentSelectionBox(false); } else if (menuItem === "Select All Entities Touching Box") { selectAllEtitiesInCurrentSelectionBox(true); } else if (menuItem === MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE) { entityIconOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); } else if (menuItem === MENU_SHOW_ZONES_IN_EDIT_MODE) { Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); } tooltip.show(false); } var HALF_TREE_SCALE = 16384; function getPositionToCreateEntity(extra) { var CREATE_DISTANCE = 2; var position; var delta = extra !== undefined ? extra : 0; if (Camera.mode === "entity" || Camera.mode === "independent") { position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), CREATE_DISTANCE + delta)); } else { position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getForward(MyAvatar.orientation), CREATE_DISTANCE + delta)); position.y += 0.5; } if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { return null; } return position; } function importSVO(importURL) { if (!Entities.canRez() && !Entities.canRezTmp()) { Window.notifyEditError(INSUFFICIENT_PERMISSIONS_IMPORT_ERROR_MSG); return; } Overlays.editOverlay(importingSVOTextOverlay, { visible: true }); Overlays.editOverlay(importingSVOImageOverlay, { visible: true }); var success = Clipboard.importEntities(importURL); if (success) { var VERY_LARGE = 10000; var isLargeImport = Clipboard.getClipboardContentsLargestDimension() >= VERY_LARGE; var position = Vec3.ZERO; if (!isLargeImport) { position = getPositionToCreateEntity(Clipboard.getClipboardContentsLargestDimension() / 2); } if (position !== null && position !== undefined) { var pastedEntityIDs = Clipboard.pasteEntities(position); if (!isLargeImport) { // The first entity in Clipboard gets the specified position with the rest being relative to it. Therefore, move // entities after they're imported so that they're all the correct distance in front of and with geometric mean // centered on the avatar/camera direction. var deltaPosition = Vec3.ZERO; var entityPositions = []; var entityParentIDs = []; var propType = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]).type; var NO_ADJUST_ENTITY_TYPES = ["Zone", "Light", "ParticleEffect"]; if (NO_ADJUST_ENTITY_TYPES.indexOf(propType) === -1) { var targetDirection; if (Camera.mode === "entity" || Camera.mode === "independent") { targetDirection = Camera.orientation; } else { targetDirection = MyAvatar.orientation; } targetDirection = Vec3.multiplyQbyV(targetDirection, Vec3.UNIT_Z); var targetPosition = getPositionToCreateEntity(); var deltaParallel = HALF_TREE_SCALE; // Distance to move entities parallel to targetDirection. var deltaPerpendicular = Vec3.ZERO; // Distance to move entities perpendicular to targetDirection. for (var i = 0, length = pastedEntityIDs.length; i < length; i++) { var curLoopEntityProps = Entities.getEntityProperties(pastedEntityIDs[i], ["position", "dimensions", "registrationPoint", "rotation", "parentID"]); var adjustedPosition = adjustPositionPerBoundingBox(targetPosition, targetDirection, curLoopEntityProps.registrationPoint, curLoopEntityProps.dimensions, curLoopEntityProps.rotation); var delta = Vec3.subtract(adjustedPosition, curLoopEntityProps.position); var distance = Vec3.dot(delta, targetDirection); deltaParallel = Math.min(distance, deltaParallel); deltaPerpendicular = Vec3.sum(Vec3.subtract(delta, Vec3.multiply(distance, targetDirection)), deltaPerpendicular); entityPositions[i] = curLoopEntityProps.position; entityParentIDs[i] = curLoopEntityProps.parentID; } deltaPerpendicular = Vec3.multiply(1 / pastedEntityIDs.length, deltaPerpendicular); deltaPosition = Vec3.sum(Vec3.multiply(deltaParallel, targetDirection), deltaPerpendicular); } if (grid.getSnapToGrid()) { var firstEntityProps = Entities.getEntityProperties(pastedEntityIDs[0], ["position", "dimensions", "registrationPoint"]); var positionPreSnap = Vec3.sum(deltaPosition, firstEntityProps.position); position = grid.snapToSurface(grid.snapToGrid(positionPreSnap, false, firstEntityProps.dimensions, firstEntityProps.registrationPoint), firstEntityProps.dimensions, firstEntityProps.registrationPoint); deltaPosition = Vec3.subtract(position, firstEntityProps.position); } if (!Vec3.equal(deltaPosition, Vec3.ZERO)) { for (var editEntityIndex = 0, numEntities = pastedEntityIDs.length; editEntityIndex < numEntities; editEntityIndex++) { if (Uuid.isNull(entityParentIDs[editEntityIndex])) { Entities.editEntity(pastedEntityIDs[editEntityIndex], { position: Vec3.sum(deltaPosition, entityPositions[editEntityIndex]) }); } } } } if (isActive) { selectionManager.setSelections(pastedEntityIDs); } } else { Window.notifyEditError("Can't import entities: entities would be out of bounds."); } } else { Window.notifyEditError("There was an error importing the entity file."); } Overlays.editOverlay(importingSVOTextOverlay, { visible: false }); Overlays.editOverlay(importingSVOImageOverlay, { visible: false }); } Window.svoImportRequested.connect(importSVO); Menu.menuItemEvent.connect(handeMenuEvent); var keyPressEvent = function (event) { if (isActive) { cameraManager.keyPressEvent(event); } }; var keyReleaseEvent = 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 === "DELETE") { deleteSelectedEntities(); } else if (event.text === "ESC") { selectionManager.clearSelections(); } else if (event.text === "TAB") { selectionDisplay.toggleSpaceMode(); } else if (event.text === "f") { if (isActive) { if (selectionManager.hasSelection()) { cameraManager.enable(); cameraManager.focus(selectionManager.worldPosition, selectionManager.worldDimensions, Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); } } } else if (event.text === '[') { if (isActive) { cameraManager.enable(); } } else if (event.text === 'g') { if (isActive && selectionManager.hasSelection()) { var newPosition = selectionManager.worldPosition; newPosition = Vec3.subtract(newPosition, { x: 0, y: selectionManager.worldDimensions.y * 0.5, z: 0 }); grid.setPosition(newPosition); } } else if (event.key === KEY_P && event.isControl && !event.isAutoRepeat ) { if (event.isShifted) { unparentSelectedEntities(); } else { parentSelectedEntities(); } } }; Controller.keyReleaseEvent.connect(keyReleaseEvent); Controller.keyPressEvent.connect(keyPressEvent); function recursiveAdd(newParentID, parentData) { var children = parentData.children; for (var i = 0; i < children.length; i++) { var childProperties = children[i].properties; childProperties.parentID = newParentID; var newChildID = Entities.addEntity(childProperties); recursiveAdd(newChildID, children[i]); } } // When an entity has been deleted we need a way to "undo" this deletion. Because it's not currently // possible to create an entity with a specific id, earlier undo commands to the deleted entity // will fail if there isn't a way to find the new entity id. var DELETED_ENTITY_MAP = {}; function applyEntityProperties(data) { var properties = data.setProperties; var selectedEntityIDs = []; var i, entityID; for (i = 0; i < properties.length; i++) { entityID = properties[i].entityID; if (DELETED_ENTITY_MAP[entityID] !== undefined) { entityID = DELETED_ENTITY_MAP[entityID]; } Entities.editEntity(entityID, properties[i].properties); selectedEntityIDs.push(entityID); } for (i = 0; i < data.createEntities.length; i++) { entityID = data.createEntities[i].entityID; var entityProperties = data.createEntities[i].properties; var newEntityID = Entities.addEntity(entityProperties); recursiveAdd(newEntityID, data.createEntities[i]); DELETED_ENTITY_MAP[entityID] = newEntityID; if (data.selectCreated) { selectedEntityIDs.push(newEntityID); } } for (i = 0; i < data.deleteEntities.length; i++) { entityID = data.deleteEntities[i].entityID; if (DELETED_ENTITY_MAP[entityID] !== undefined) { entityID = DELETED_ENTITY_MAP[entityID]; } Entities.deleteEntity(entityID); } selectionManager.setSelections(selectedEntityIDs); } // For currently selected entities, push a command to the UndoStack that uses the current entity properties for the // redo command, and the saved properties for the undo command. Also, include create and delete entity data. function pushCommandForSelections(createdEntityData, deletedEntityData) { var undoData = { setProperties: [], createEntities: deletedEntityData || [], deleteEntities: createdEntityData || [], selectCreated: true }; var redoData = { setProperties: [], createEntities: createdEntityData || [], deleteEntities: deletedEntityData || [], selectCreated: false }; for (var i = 0; i < SelectionManager.selections.length; i++) { var entityID = SelectionManager.selections[i]; var initialProperties = SelectionManager.savedProperties[entityID]; var currentProperties = Entities.getEntityProperties(entityID); if (!initialProperties) { continue; } undoData.setProperties.push({ entityID: entityID, properties: initialProperties }); redoData.setProperties.push({ entityID: entityID, properties: currentProperties }); } UndoStack.pushCommand(applyEntityProperties, undoData, applyEntityProperties, redoData); } var ENTITY_PROPERTIES_URL = Script.resolvePath('html/entityProperties.html'); var ServerScriptStatusMonitor = function(entityID, statusCallback) { var self = this; self.entityID = entityID; self.active = true; self.sendRequestTimerID = null; var onStatusReceived = function(success, isRunning, status, errorInfo) { if (self.active) { statusCallback({ statusRetrieved: success, isRunning: isRunning, status: status, errorInfo: errorInfo }); self.sendRequestTimerID = Script.setTimeout(function() { if (self.active) { Entities.getServerScriptStatus(entityID, onStatusReceived); } }, 1000); } }; self.stop = function() { self.active = false; }; Entities.getServerScriptStatus(entityID, onStatusReceived); }; var PropertiesTool = function (opts) { var that = {}; var webView = null; webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); webView.setVisible = function(value) {}; var visible = false; // This keeps track of the last entity ID that was selected. If multiple entities // are selected or if no entity is selected this will be `null`. var currentSelectedEntityID = null; var statusMonitor = null; webView.setVisible(visible); that.setVisible = function (newVisible) { visible = newVisible; webView.setVisible(visible); }; function updateScriptStatus(info) { info.type = "server_script_status"; webView.emitScriptEvent(JSON.stringify(info)); } function resetScriptStatus() { updateScriptStatus({ statusRetrieved: undefined, isRunning: undefined, status: "", errorInfo: "" }); } function updateSelections(selectionUpdated) { var data = { type: 'update' }; if (selectionUpdated) { resetScriptStatus(); if (selectionManager.selections.length !== 1) { if (statusMonitor !== null) { statusMonitor.stop(); statusMonitor = null; } currentSelectedEntityID = null; } else if (currentSelectedEntityID != selectionManager.selections[0]) { if (statusMonitor !== null) { statusMonitor.stop(); } var entityID = selectionManager.selections[0]; currentSelectedEntityID = entityID; statusMonitor = new ServerScriptStatusMonitor(entityID, updateScriptStatus); } } var selections = []; for (var i = 0; i < selectionManager.selections.length; i++) { var entity = {}; entity.id = selectionManager.selections[i]; entity.properties = Entities.getEntityProperties(selectionManager.selections[i]); if (entity.properties.rotation !== undefined) { entity.properties.rotation = Quat.safeEulerAngles(entity.properties.rotation); } if (entity.properties.keyLight !== undefined && entity.properties.keyLight.direction !== undefined) { entity.properties.keyLight.direction = Vec3.multiply(RADIANS_TO_DEGREES, Vec3.toPolar(entity.properties.keyLight.direction)); entity.properties.keyLight.direction.z = 0.0; } selections.push(entity); } data.selections = selections; webView.emitScriptEvent(JSON.stringify(data)); } selectionManager.addEventListener(updateSelections); webView.webEventReceived.connect(function (data) { try { data = JSON.parse(data); } catch(e) { print('Edit.js received web event that was not valid json.'); return; } var i, properties, dY, diff, newPosition; if (data.type === "print") { if (data.message) { print(data.message); } } else if (data.type === "update") { selectionManager.saveProperties(); if (selectionManager.selections.length > 1) { properties = { locked: data.properties.locked, visible: data.properties.visible }; for (i = 0; i < selectionManager.selections.length; i++) { Entities.editEntity(selectionManager.selections[i], properties); } } else if (data.properties) { if (data.properties.dynamic === false) { // this object is leaving dynamic, so we zero its velocities data.properties.velocity = { x: 0, y: 0, z: 0 }; data.properties.angularVelocity = { x: 0, y: 0, z: 0 }; } if (data.properties.rotation !== undefined) { var rotation = data.properties.rotation; data.properties.rotation = Quat.fromPitchYawRollDegrees(rotation.x, rotation.y, rotation.z); } if (data.properties.keyLight !== undefined && data.properties.keyLight.direction !== undefined) { data.properties.keyLight.direction = Vec3.fromPolar( data.properties.keyLight.direction.x * DEGREES_TO_RADIANS, data.properties.keyLight.direction.y * DEGREES_TO_RADIANS ); } Entities.editEntity(selectionManager.selections[0], data.properties); if (data.properties.name !== undefined || data.properties.modelURL !== undefined || data.properties.visible !== undefined || data.properties.locked !== undefined) { entityListTool.sendUpdate(); } } pushCommandForSelections(); selectionManager._update(); } else if (data.type === 'parent') { parentSelectedEntities(); } else if (data.type === 'unparent') { unparentSelectedEntities(); } else if (data.type === 'saveUserData'){ //the event bridge and json parsing handle our avatar id string differently. var actualID = data.id.split('"')[1]; Entities.editEntity(actualID, data.properties); } else if (data.type === "showMarketplace") { showMarketplace(); } else if (data.type === "action") { if (data.action === "moveSelectionToGrid") { if (selectionManager.hasSelection()) { selectionManager.saveProperties(); dY = grid.getOrigin().y - (selectionManager.worldPosition.y - selectionManager.worldDimensions.y / 2); diff = { x: 0, y: dY, z: 0 }; for (i = 0; i < selectionManager.selections.length; i++) { properties = selectionManager.savedProperties[selectionManager.selections[i]]; newPosition = Vec3.sum(properties.position, diff); Entities.editEntity(selectionManager.selections[i], { position: newPosition }); } pushCommandForSelections(); selectionManager._update(); } } else if (data.action === "moveAllToGrid") { if (selectionManager.hasSelection()) { selectionManager.saveProperties(); for (i = 0; i < selectionManager.selections.length; i++) { properties = selectionManager.savedProperties[selectionManager.selections[i]]; var bottomY = properties.boundingBox.center.y - properties.boundingBox.dimensions.y / 2; dY = grid.getOrigin().y - bottomY; diff = { x: 0, y: dY, z: 0 }; newPosition = Vec3.sum(properties.position, diff); Entities.editEntity(selectionManager.selections[i], { position: newPosition }); } pushCommandForSelections(); selectionManager._update(); } } else if (data.action === "resetToNaturalDimensions") { if (selectionManager.hasSelection()) { selectionManager.saveProperties(); for (i = 0; i < selectionManager.selections.length; i++) { properties = selectionManager.savedProperties[selectionManager.selections[i]]; var naturalDimensions = properties.naturalDimensions; // If any of the natural dimensions are not 0, resize if (properties.type === "Model" && naturalDimensions.x === 0 && naturalDimensions.y === 0 && naturalDimensions.z === 0) { Window.notifyEditError("Cannot reset entity to its natural dimensions: Model URL" + " is invalid or the model has not yet been loaded."); } else { Entities.editEntity(selectionManager.selections[i], { dimensions: properties.naturalDimensions }); } } pushCommandForSelections(); selectionManager._update(); } } else if (data.action === "previewCamera") { if (selectionManager.hasSelection()) { Camera.mode = "entity"; Camera.cameraEntity = selectionManager.selections[0]; } } else if (data.action === "rescaleDimensions") { var multiplier = data.percentage / 100.0; if (selectionManager.hasSelection()) { selectionManager.saveProperties(); for (i = 0; i < selectionManager.selections.length; i++) { properties = selectionManager.savedProperties[selectionManager.selections[i]]; Entities.editEntity(selectionManager.selections[i], { dimensions: Vec3.multiply(multiplier, properties.dimensions) }); } pushCommandForSelections(); selectionManager._update(); } } else if (data.action === "reloadClientScripts") { if (selectionManager.hasSelection()) { var timestamp = Date.now(); for (i = 0; i < selectionManager.selections.length; i++) { Entities.editEntity(selectionManager.selections[i], { scriptTimestamp: timestamp }); } } } else if (data.action === "reloadServerScripts") { if (selectionManager.hasSelection()) { for (i = 0; i < selectionManager.selections.length; i++) { Entities.reloadServerScripts(selectionManager.selections[i]); } } } } else if (data.type === "propertiesPageReady") { updateSelections(true); } }); return that; }; var PopupMenu = function () { var self = this; var MENU_ITEM_HEIGHT = 21; var MENU_ITEM_SPACING = 1; var TEXT_MARGIN = 7; var overlays = []; var overlayInfo = {}; var upColor = { red: 0, green: 0, blue: 0 }; var downColor = { red: 192, green: 192, blue: 192 }; var overColor = { red: 128, green: 128, blue: 128 }; self.onSelectMenuItem = function () {}; self.addMenuItem = function (name) { var id = Overlays.addOverlay("text", { text: name, backgroundAlpha: 1.0, backgroundColor: upColor, topMargin: TEXT_MARGIN, leftMargin: TEXT_MARGIN, width: 210, height: MENU_ITEM_HEIGHT, font: { size: 12 }, visible: false }); overlays.push(id); overlayInfo[id] = { name: name }; return id; }; self.updateMenuItemText = function (id, newText) { Overlays.editOverlay(id, { text: newText }); }; self.setPosition = function (x, y) { for (var key in overlayInfo) { Overlays.editOverlay(key, { x: x, y: y }); y += MENU_ITEM_HEIGHT + MENU_ITEM_SPACING; } }; self.onSelected = function () {}; var pressingOverlay = null; var hoveringOverlay = null; self.mousePressEvent = function (event) { if (event.isLeftButton) { var overlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); if (overlay in overlayInfo) { pressingOverlay = overlay; Overlays.editOverlay(pressingOverlay, { backgroundColor: downColor }); } else { self.hide(); } return false; } }; self.mouseMoveEvent = function (event) { if (visible) { var overlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); if (!pressingOverlay) { if (hoveringOverlay !== null && hoveringOverlay !== null && overlay !== hoveringOverlay) { Overlays.editOverlay(hoveringOverlay, { backgroundColor: upColor }); hoveringOverlay = null; } if (overlay !== hoveringOverlay && overlay in overlayInfo) { Overlays.editOverlay(overlay, { backgroundColor: overColor }); hoveringOverlay = overlay; } } } return false; }; self.mouseReleaseEvent = function (event) { var overlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); if (pressingOverlay !== null && pressingOverlay !== undefined) { if (overlay === pressingOverlay) { self.onSelectMenuItem(overlayInfo[overlay].name); } Overlays.editOverlay(pressingOverlay, { backgroundColor: upColor }); pressingOverlay = null; self.hide(); } }; var visible = false; self.setVisible = function (newVisible) { if (newVisible !== visible) { visible = newVisible; for (var key in overlayInfo) { Overlays.editOverlay(key, { visible: newVisible }); } } }; self.show = function () { self.setVisible(true); }; self.hide = function () { self.setVisible(false); }; function cleanup() { ContextOverlay.enabled = true; for (var i = 0; i < overlays.length; i++) { Overlays.deleteOverlay(overlays[i]); } Controller.mousePressEvent.disconnect(self.mousePressEvent); Controller.mouseMoveEvent.disconnect(self.mouseMoveEvent); Controller.mouseReleaseEvent.disconnect(self.mouseReleaseEvent); } Controller.mousePressEvent.connect(self.mousePressEvent); Controller.mouseMoveEvent.connect(self.mouseMoveEvent); Controller.mouseReleaseEvent.connect(self.mouseReleaseEvent); Script.scriptEnding.connect(cleanup); return this; }; var propertyMenu = new PopupMenu(); propertyMenu.onSelectMenuItem = function (name) { if (propertyMenu.marketplaceID) { showMarketplace(propertyMenu.marketplaceID); } }; var showMenuItem = propertyMenu.addMenuItem("Show in Marketplace"); var propertiesTool = new PropertiesTool(); var particleExplorerTool = new ParticleExplorerTool(); var selectedParticleEntity = 0; var selectedParticleEntityID = null; function selectParticleEntity(entityID) { var properties = Entities.getEntityProperties(entityID); selectedParticleEntityID = entityID; if (properties.emitOrientation) { properties.emitOrientation = Quat.safeEulerAngles(properties.emitOrientation); } var particleData = { messageType: "particle_settings", currentProperties: properties }; particleExplorerTool.destroyWebView(); particleExplorerTool.createWebView(); selectedParticleEntity = entityID; particleExplorerTool.setActiveParticleEntity(entityID); particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); // Switch to particle explorer var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); tablet.sendToQml({method: 'selectTab', params: {id: 'particle'}}); } entityListTool.webView.webEventReceived.connect(function (data) { try { data = JSON.parse(data); } catch(e) { print("edit.js: Error parsing JSON: " + e.name + " data " + data); return; } if (data.type === 'parent') { parentSelectedEntities(); } else if (data.type === 'unparent') { unparentSelectedEntities(); } else if (data.type === "selectionUpdate") { var ids = data.entityIds; if (ids.length === 1) { if (Entities.getEntityProperties(ids[0], "type").type === "ParticleEffect") { if (JSON.stringify(selectedParticleEntity) === JSON.stringify(ids[0])) { // This particle entity is already selected, so return return; } // Destroy the old particles web view first } else { selectedParticleEntity = 0; particleExplorerTool.destroyWebView(); } } } }); }()); // END LOCAL_SCOPE