overte/examples/newEditEntities.js
2014-10-27 14:34:43 -07:00

698 lines
25 KiB
JavaScript

//
// newEditEntities.js
// examples
//
// Created by Brad Hefta-Gaub on 10/2/14.
// 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
//
Script.include("libraries/globals.js");
Script.include("libraries/stringHelpers.js");
Script.include("libraries/dataviewHelpers.js");
Script.include("libraries/httpMultiPart.js");
Script.include("libraries/modelUploader.js");
Script.include("libraries/toolBars.js");
Script.include("libraries/progressDialog.js");
Script.include("libraries/entitySelectionTool.js");
var selectionDisplay = SelectionDisplay;
var selectionManager = SelectionManager;
Script.include("libraries/ModelImporter.js");
var modelImporter = new ModelImporter();
Script.include("libraries/ExportMenu.js");
Script.include("libraries/ToolTip.js");
Script.include("libraries/entityPropertyDialogBox.js");
var entityPropertyDialogBox = EntityPropertyDialogBox;
Script.include("libraries/entityCameraTool.js");
var entityCameraTool = new EntityCameraTool();
selectionManager.setEventListener(selectionDisplay.updateHandles());
var windowDimensions = Controller.getViewportDimensions();
var toolIconUrl = HIFI_PUBLIC_BUCKET + "images/tools/";
var toolHeight = 50;
var toolWidth = 50;
var MIN_ANGULAR_SIZE = 2;
var MAX_ANGULAR_SIZE = 45;
var allowLargeModels = false;
var allowSmallModels = false;
var wantEntityGlow = false;
var SPAWN_DISTANCE = 1;
var DEFAULT_DIMENSION = 0.20;
var modelURLs = [
HIFI_PUBLIC_BUCKET + "meshes/Feisar_Ship.FBX",
HIFI_PUBLIC_BUCKET + "meshes/birarda/birarda_head.fbx",
HIFI_PUBLIC_BUCKET + "meshes/pug.fbx",
HIFI_PUBLIC_BUCKET + "meshes/newInvader16x16-large-purple.svo",
HIFI_PUBLIC_BUCKET + "meshes/minotaur/mino_full.fbx",
HIFI_PUBLIC_BUCKET + "meshes/Combat_tank_V01.FBX",
HIFI_PUBLIC_BUCKET + "meshes/orc.fbx",
HIFI_PUBLIC_BUCKET + "meshes/slimer.fbx"
];
var mode = 0;
var isActive = false;
var toolBar = (function () {
var that = {},
toolBar,
activeButton,
newModelButton,
newCubeButton,
newSphereButton,
newLightButton,
browseModelsButton,
loadURLMenuItem,
loadFileMenuItem,
menuItemWidth = 125,
menuItemOffset,
menuItemHeight,
menuItemMargin = 5,
menuTextColor = { red: 255, green: 255, blue: 255 },
menuBackgoundColor = { red: 18, green: 66, blue: 66 };
function initialize() {
toolBar = new ToolBar(0, 0, ToolBar.VERTICAL);
activeButton = toolBar.addTool({
imageURL: toolIconUrl + "models-tool.svg",
subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT },
width: toolWidth,
height: toolHeight,
alpha: 0.9,
visible: true
}, true, false);
newModelButton = toolBar.addTool({
imageURL: toolIconUrl + "add-model-tool.svg",
subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT },
width: toolWidth,
height: toolHeight,
alpha: 0.9,
visible: true
}, true, false);
browseModelsButton = toolBar.addTool({
imageURL: toolIconUrl + "list-icon.svg",
width: toolWidth,
height: toolHeight,
alpha: 0.9,
visible: true
});
menuItemOffset = toolBar.height / 3 + 2;
menuItemHeight = Tool.IMAGE_HEIGHT / 2 - 2;
loadURLMenuItem = Overlays.addOverlay("text", {
x: newModelButton.x - menuItemWidth,
y: newModelButton.y + menuItemOffset,
width: menuItemWidth,
height: menuItemHeight,
backgroundColor: menuBackgoundColor,
topMargin: menuItemMargin,
text: "Model URL",
alpha: 0.9,
visible: false
});
loadFileMenuItem = Overlays.addOverlay("text", {
x: newModelButton.x - menuItemWidth,
y: newModelButton.y + menuItemOffset + menuItemHeight,
width: menuItemWidth,
height: menuItemHeight,
backgroundColor: menuBackgoundColor,
topMargin: menuItemMargin,
text: "Model File",
alpha: 0.9,
visible: false
});
newCubeButton = toolBar.addTool({
imageURL: toolIconUrl + "add-cube.svg",
subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT },
width: toolWidth,
height: toolHeight,
alpha: 0.9,
visible: true
});
newSphereButton = toolBar.addTool({
imageURL: toolIconUrl + "add-sphere.svg",
subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT },
width: toolWidth,
height: toolHeight,
alpha: 0.9,
visible: true
});
newLightButton = toolBar.addTool({
imageURL: toolIconUrl + "add-fire.svg",
subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT },
width: toolWidth,
height: toolHeight,
alpha: 0.9,
visible: true
});
}
function toggleNewModelButton(active) {
if (active === undefined) {
active = !toolBar.toolSelected(newModelButton);
}
toolBar.selectTool(newModelButton, active);
Overlays.editOverlay(loadURLMenuItem, { visible: active });
Overlays.editOverlay(loadFileMenuItem, { visible: active });
}
var RESIZE_INTERVAL = 50;
var RESIZE_TIMEOUT = 20000;
var RESIZE_MAX_CHECKS = RESIZE_TIMEOUT / RESIZE_INTERVAL;
function addModel(url) {
var position;
position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), SPAWN_DISTANCE));
if (position.x > 0 && position.y > 0 && position.z > 0) {
Entities.addEntity({
type: "Model",
position: position,
dimensions: { x: DEFAULT_DIMENSION, y: DEFAULT_DIMENSION, z: DEFAULT_DIMENSION },
modelURL: url
});
print("Model added: " + url);
var checkCount = 0;
function resize() {
var entityProperties = Entities.getEntityProperties(entityId);
var naturalDimensions = entityProperties.naturalDimensions;
checkCount++;
if (naturalDimensions.x == 0 && naturalDimensions.y == 0 && naturalDimensions.z == 0) {
if (checkCount < RESIZE_MAX_CHECKS) {
Script.setTimeout(resize, RESIZE_INTERVAL);
} else {
print("Resize failed: timed out waiting for model (" + url + ") to load");
}
} else {
entityProperties.dimensions = naturalDimensions;
Entities.editEntity(entityId, entityProperties);
}
}
Script.setTimeout(resize, RESIZE_INTERVAL);
} else {
print("Can't add model: Model would be out of bounds.");
}
}
that.move = function () {
var newViewPort,
toolsX,
toolsY;
newViewPort = Controller.getViewportDimensions();
if (toolBar === undefined) {
initialize();
} else if (windowDimensions.x === newViewPort.x &&
windowDimensions.y === newViewPort.y) {
return;
}
windowDimensions = newViewPort;
toolsX = windowDimensions.x - 8 - toolBar.width;
toolsY = (windowDimensions.y - toolBar.height) / 2;
toolBar.move(toolsX, toolsY);
Overlays.editOverlay(loadURLMenuItem, { x: toolsX - menuItemWidth, y: toolsY + menuItemOffset });
Overlays.editOverlay(loadFileMenuItem, { x: toolsX - menuItemWidth, y: toolsY + menuItemOffset + menuItemHeight });
};
that.mousePressEvent = function (event) {
var clickedOverlay,
url,
file;
clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y });
if (activeButton === toolBar.clicked(clickedOverlay)) {
isActive = !isActive;
if (!isActive) {
selectionDisplay.unselectAll();
entityCameraTool.disable();
} else {
entityCameraTool.enable();
}
return true;
}
if (newModelButton === toolBar.clicked(clickedOverlay)) {
toggleNewModelButton();
return true;
}
if (clickedOverlay === loadURLMenuItem) {
toggleNewModelButton(false);
url = Window.prompt("Model URL", modelURLs[Math.floor(Math.random() * modelURLs.length)]);
if (url !== null && url !== "") {
addModel(url);
}
return true;
}
if (clickedOverlay === loadFileMenuItem) {
toggleNewModelButton(false);
file = Window.browse("Select your model file ...",
Settings.getValue("LastModelUploadLocation").path(),
"Model files (*.fst *.fbx)");
//"Model files (*.fst *.fbx *.svo)");
if (file !== null) {
Settings.setValue("LastModelUploadLocation", file);
modelUploader.upload(file, addModel);
}
return true;
}
if (browseModelsButton === toolBar.clicked(clickedOverlay)) {
toggleNewModelButton(false);
url = Window.s3Browse(".*(fbx|FBX)");
if (url !== null && url !== "") {
addModel(url);
}
return true;
}
if (newCubeButton === toolBar.clicked(clickedOverlay)) {
var position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), SPAWN_DISTANCE));
if (position.x > 0 && position.y > 0 && position.z > 0) {
Entities.addEntity({
type: "Box",
position: position,
dimensions: { x: DEFAULT_DIMENSION, y: DEFAULT_DIMENSION, z: DEFAULT_DIMENSION },
color: { red: 255, green: 0, blue: 0 }
});
} else {
print("Can't create box: Box would be out of bounds.");
}
return true;
}
if (newSphereButton === toolBar.clicked(clickedOverlay)) {
var position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), SPAWN_DISTANCE));
if (position.x > 0 && position.y > 0 && position.z > 0) {
Entities.addEntity({
type: "Sphere",
position: position,
dimensions: { x: DEFAULT_DIMENSION, y: DEFAULT_DIMENSION, z: DEFAULT_DIMENSION },
color: { red: 255, green: 0, blue: 0 }
});
} else {
print("Can't create sphere: Sphere would be out of bounds.");
}
return true;
}
if (newLightButton === toolBar.clicked(clickedOverlay)) {
var position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), SPAWN_DISTANCE));
if (position.x > 0 && position.y > 0 && position.z > 0) {
Entities.addEntity({
type: "Light",
position: position,
dimensions: { x: DEFAULT_DIMENSION, y: DEFAULT_DIMENSION, z: DEFAULT_DIMENSION },
color: { red: 255, green: 0, blue: 0 }
});
} else {
print("Can't create Light: Light would be out of bounds.");
}
return true;
}
return false;
};
that.cleanup = function () {
toolBar.cleanup();
Overlays.deleteOverlay(loadURLMenuItem);
Overlays.deleteOverlay(loadFileMenuItem);
};
return that;
}());
var exportMenu = null;
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 entitySelected = false;
var selectedEntityID;
var selectedEntityProperties;
var mouseLastPosition;
var orientation;
var intersection;
var SCALE_FACTOR = 200.0;
function rayPlaneIntersection(pickRay, point, normal) {
var d = -Vec3.dot(point, normal);
var t = -(Vec3.dot(pickRay.origin, normal) + d) / Vec3.dot(pickRay.direction, normal);
return Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction, t));
}
function mousePressEvent(event) {
mouseLastPosition = { x: event.x, y: event.y };
var clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y });
if (toolBar.mousePressEvent(event) || progressDialog.mousePressEvent(event)
|| entityCameraTool.mousePressEvent(event) || selectionDisplay.mousePressEvent(event)) {
// Event handled; do nothing.
return;
} else {
entitySelected = false;
selectionDisplay.unselectAll();
// If we aren't active and didn't click on an overlay: quit
if (!isActive) {
return;
}
var pickRay = Camera.computePickRay(event.x, event.y);
Vec3.print("[Mouse] Looking at: ", pickRay.origin);
var foundIntersection = Entities.findRayIntersection(pickRay);
if(!foundIntersection.accurate) {
return;
}
var foundEntity = foundIntersection.entityID;
if (!foundEntity.isKnownID) {
var identify = Entities.identifyEntity(foundEntity);
if (!identify.isKnownID) {
print("Unknown ID " + identify.id + " (update loop " + foundEntity.id + ")");
return;
}
foundEntity = identify;
}
var properties = Entities.getEntityProperties(foundEntity);
if (isLocked(properties)) {
print("Model locked " + properties.id);
} else {
var halfDiagonal = Vec3.length(properties.dimensions) / 2.0;
print("Checking properties: " + properties.id + " " + properties.isKnownID + " - 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 X = Vec3.sum(A, Vec3.multiply(B, x));
var d = Vec3.length(Vec3.subtract(P, X));
var halfDiagonal = Vec3.length(properties.dimensions) / 2.0;
var angularSize = 2 * Math.atan(halfDiagonal / Vec3.distance(Camera.getPosition(), properties.position)) * 180 / 3.14;
var sizeOK = (allowLargeModels || angularSize < MAX_ANGULAR_SIZE)
&& (allowSmallModels || angularSize > MIN_ANGULAR_SIZE);
if (0 < x && sizeOK) {
entitySelected = true;
selectedEntityID = foundEntity;
selectedEntityProperties = properties;
orientation = MyAvatar.orientation;
intersection = rayPlaneIntersection(pickRay, P, Quat.getFront(orientation));
if (!event.isShifted) {
selectionManager.clearSelections();
}
selectionManager.addEntity(foundEntity);
print("Model selected selectedEntityID:" + selectedEntityID.id);
}
}
}
if (entitySelected) {
selectedEntityProperties.oldDimensions = selectedEntityProperties.dimensions;
selectedEntityProperties.oldPosition = {
x: selectedEntityProperties.position.x,
y: selectedEntityProperties.position.y,
z: selectedEntityProperties.position.z,
};
selectedEntityProperties.oldRotation = {
x: selectedEntityProperties.rotation.x,
y: selectedEntityProperties.rotation.y,
z: selectedEntityProperties.rotation.z,
w: selectedEntityProperties.rotation.w,
};
selectedEntityProperties.glowLevel = 0.0;
print("Clicked on " + selectedEntityID.id + " " + entitySelected);
tooltip.updateText(selectedEntityProperties);
tooltip.show(true);
selectionDisplay.select(selectedEntityID, event);
}
}
var highlightedEntityID = { isKnownID: false };
function mouseMoveEvent(event) {
if (!isActive) {
return;
}
// allow the selectionDisplay and entityCameraTool to handle the event first, if it doesn't handle it, then do our own thing
if (selectionDisplay.mouseMoveEvent(event) || entityCameraTool.mouseMoveEvent(event)) {
return;
}
var pickRay = Camera.computePickRay(event.x, event.y);
var entityIntersection = Entities.findRayIntersection(pickRay);
if (entityIntersection.accurate) {
if(highlightedEntityID.isKnownID && highlightedEntityID.id != entityIntersection.entityID.id) {
selectionDisplay.unhighlightSelectable(highlightedEntityID);
highlightedEntityID = { id: -1, isKnownID: false };
}
var halfDiagonal = Vec3.length(entityIntersection.properties.dimensions) / 2.0;
var angularSize = 2 * Math.atan(halfDiagonal / Vec3.distance(Camera.getPosition(),
entityIntersection.properties.position)) * 180 / 3.14;
var sizeOK = (allowLargeModels || angularSize < MAX_ANGULAR_SIZE)
&& (allowSmallModels || angularSize > MIN_ANGULAR_SIZE);
if (entityIntersection.entityID.isKnownID && sizeOK) {
if (wantEntityGlow) {
Entities.editEntity(entityIntersection.entityID, { glowLevel: 0.25 });
}
highlightedEntityID = entityIntersection.entityID;
selectionDisplay.highlightSelectable(entityIntersection.entityID);
}
}
}
function mouseReleaseEvent(event) {
if (!isActive) {
return;
}
if (entitySelected) {
tooltip.show(false);
}
entityCameraTool.mouseReleaseEvent(event);
}
Controller.mousePressEvent.connect(mousePressEvent);
Controller.mouseMoveEvent.connect(mouseMoveEvent);
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;
function setupModelMenus() {
print("setupModelMenus()");
// adj our menuitems
Menu.addMenuItem({ menuName: "Edit", menuItemName: "Models", isSeparator: true, beforeItem: "Physics" });
Menu.addMenuItem({ menuName: "Edit", menuItemName: "Edit Properties...",
shortcutKeyEvent: { text: "`" }, afterItem: "Models" });
if (!Menu.menuItemExists("Edit", "Delete")) {
print("no delete... adding ours");
Menu.addMenuItem({ menuName: "Edit", menuItemName: "Delete",
shortcutKeyEvent: { text: "backspace" }, afterItem: "Models" });
modelMenuAddedDelete = true;
} else {
print("delete exists... don't add ours");
}
Menu.addMenuItem({ menuName: "Edit", menuItemName: "Paste Models", shortcutKey: "CTRL+META+V", afterItem: "Edit Properties..." });
Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Select Large Models", shortcutKey: "CTRL+META+L",
afterItem: "Paste Models", isCheckable: true });
Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Select Small Models", shortcutKey: "CTRL+META+S",
afterItem: "Allow Select Large Models", isCheckable: true });
Menu.addMenuItem({ menuName: "File", menuItemName: "Models", isSeparator: true, beforeItem: "Settings" });
Menu.addMenuItem({ menuName: "File", menuItemName: "Export Models", shortcutKey: "CTRL+META+E", afterItem: "Models" });
Menu.addMenuItem({ menuName: "File", menuItemName: "Import Models", shortcutKey: "CTRL+META+I", afterItem: "Export Models" });
Menu.addMenuItem({ menuName: "Developer", menuItemName: "Debug Ryans Rotation Problems", isCheckable: true });
}
setupModelMenus(); // do this when first running our script.
function cleanupModelMenus() {
Menu.removeSeparator("Edit", "Models");
Menu.removeMenuItem("Edit", "Edit Properties...");
if (modelMenuAddedDelete) {
// delete our menuitems
Menu.removeMenuItem("Edit", "Delete");
}
Menu.removeMenuItem("Edit", "Paste Models");
Menu.removeMenuItem("Edit", "Allow Select Large Models");
Menu.removeMenuItem("Edit", "Allow Select Small Models");
Menu.removeSeparator("File", "Models");
Menu.removeMenuItem("File", "Export Models");
Menu.removeMenuItem("File", "Import Models");
Menu.removeMenuItem("Developer", "Debug Ryans Rotation Problems");
}
Script.scriptEnding.connect(function() {
progressDialog.cleanup();
toolBar.cleanup();
cleanupModelMenus();
tooltip.cleanup();
modelImporter.cleanup();
selectionDisplay.cleanup();
if (exportMenu) {
exportMenu.close();
}
});
// Do some stuff regularly, like check for placement of various overlays
Script.update.connect(function (deltaTime) {
toolBar.move();
progressDialog.move();
selectionDisplay.checkMove();
});
function handeMenuEvent(menuItem) {
if (menuItem == "Allow Select Small Models") {
allowSmallModels = Menu.isOptionChecked("Allow Select Small Models");
} else if (menuItem == "Allow Select Large Models") {
allowLargeModels = Menu.isOptionChecked("Allow Select Large Models");
} else if (menuItem == "Delete") {
if (entitySelected) {
print(" Delete Entity.... selectedEntityID="+ selectedEntityID);
Entities.deleteEntity(selectedEntityID);
selectionDisplay.unselect(selectedEntityID);
entitySelected = false;
} else {
print(" Delete Entity.... not holding...");
}
} else if (menuItem == "Edit Properties...") {
// good place to put the properties dialog
editModelID = -1;
if (entitySelected) {
print(" Edit Properties.... selectedEntityID="+ selectedEntityID);
editModelID = selectedEntityID;
} else {
print(" Edit Properties.... not holding...");
}
if (editModelID != -1) {
print(" Edit Properties.... about to edit properties...");
entityPropertyDialogBox.openDialog(editModelID);
}
} else if (menuItem == "Paste Models") {
modelImporter.paste();
} else if (menuItem == "Export Models") {
if (!exportMenu) {
exportMenu = new ExportMenu({
onClose: function () {
exportMenu = null;
}
});
}
} else if (menuItem == "Import Models") {
modelImporter.doImport();
}
tooltip.show(false);
}
Menu.menuItemEvent.connect(handeMenuEvent);
Controller.keyReleaseEvent.connect(function (event) {
// since sometimes our menu shortcut keys don't work, trap our menu items here also and fire the appropriate menu items
print(event.text);
if (event.text == "`") {
handeMenuEvent("Edit Properties...");
}
if (event.text == "BACKSPACE") {
handeMenuEvent("Delete");
} else if (event.text == "TAB") {
selectionDisplay.toggleSpaceMode();
} else if (event.text == "ESC") {
selectionDisplay.cancelTool();
} else if (event.text == "f") {
if (entitySelected) {
// Get latest properties
var properties = Entities.getEntityProperties(selectedEntityID);
entityCameraTool.focus(properties);
}
}
});