mirror of
https://github.com/overte-org/overte.git
synced 2025-05-06 16:09:49 +02:00
2107 lines
79 KiB
JavaScript
2107 lines
79 KiB
JavaScript
"use strict";
|
|
|
|
//
|
|
// vr-edit.js
|
|
//
|
|
// Created by David Rowe on 27 Jun 2017.
|
|
// Copyright 2017 High Fidelity, Inc.
|
|
//
|
|
// Distributed under the Apache License, Version 2.0.
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
//
|
|
|
|
(function () {
|
|
|
|
var APP_NAME = "VR EDIT", // TODO: App name.
|
|
APP_ICON_INACTIVE = "icons/tablet-icons/edit-i.svg", // TODO: App icons.
|
|
APP_ICON_ACTIVE = "icons/tablet-icons/edit-a.svg",
|
|
tablet,
|
|
button,
|
|
|
|
// Application state
|
|
isAppActive = false,
|
|
isAppScaleWithHandles = false,
|
|
|
|
VR_EDIT_SETTING = "io.highfidelity.isVREditing", // Note: This constant is duplicated in utils.js.
|
|
|
|
editors = [],
|
|
LEFT_HAND = 0,
|
|
RIGHT_HAND = 1,
|
|
|
|
UPDATE_LOOP_TIMEOUT = 16,
|
|
updateTimer = null,
|
|
|
|
Highlights,
|
|
Handles,
|
|
Selection,
|
|
Laser,
|
|
Hand,
|
|
Editor,
|
|
|
|
AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}",
|
|
NULL_UUID = "{00000000-0000-0000-0000-000000000000}",
|
|
HALF_TREE_SCALE = 16384,
|
|
|
|
DEBUG = true; // TODO: Set false.
|
|
|
|
|
|
if (typeof Vec3.min !== "function") {
|
|
Vec3.min = function (a, b) {
|
|
return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), z: Math.min(a.z, b.z) };
|
|
};
|
|
}
|
|
|
|
if (typeof Vec3.max !== "function") {
|
|
Vec3.max = function (a, b) {
|
|
return { x: Math.max(a.x, b.x), y: Math.max(a.y, b.y), z: Math.max(a.z, b.z) };
|
|
};
|
|
}
|
|
|
|
if (typeof Entities.rootOf !== "function") {
|
|
Entities.rootOf = function (entityID) {
|
|
var rootEntityID,
|
|
entityProperties,
|
|
PARENT_PROPERTIES = ["parentID"];
|
|
if (entityID === undefined || entityID === null) {
|
|
return null;
|
|
}
|
|
rootEntityID = entityID;
|
|
entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES);
|
|
while (entityProperties.parentID !== NULL_UUID) {
|
|
rootEntityID = entityProperties.parentID;
|
|
entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES);
|
|
}
|
|
return rootEntityID;
|
|
};
|
|
}
|
|
|
|
|
|
function log(message) {
|
|
print(APP_NAME + ": " + message);
|
|
}
|
|
|
|
function debug(side, message) {
|
|
// Optional parameter: side.
|
|
var hand = "",
|
|
HAND_LETTERS = ["L", "R"];
|
|
if (DEBUG) {
|
|
if (side === 0 || side === 1) {
|
|
hand = HAND_LETTERS[side] + " ";
|
|
} else {
|
|
message = side;
|
|
}
|
|
log(hand + message);
|
|
}
|
|
}
|
|
|
|
function isEditableRoot(entityID) {
|
|
var EDITIBLE_ENTITY_QUERY_PROPERTYES = ["parentID", "visible", "locked", "type"],
|
|
NONEDITABLE_ENTITY_TYPES = ["Unknown", "Zone", "Light"],
|
|
properties;
|
|
properties = Entities.getEntityProperties(entityID, EDITIBLE_ENTITY_QUERY_PROPERTYES);
|
|
while (properties.parentID && properties.parentID !== NULL_UUID) {
|
|
properties = Entities.getEntityProperties(properties.parentID, EDITIBLE_ENTITY_QUERY_PROPERTYES);
|
|
}
|
|
return properties.visible && !properties.locked && NONEDITABLE_ENTITY_TYPES.indexOf(properties.type) === -1;
|
|
}
|
|
|
|
|
|
Highlights = function (side) {
|
|
// Draws highlights on selected entities.
|
|
var handOverlay,
|
|
entityOverlays = [],
|
|
GRAB_HIGHLIGHT_COLOR = { red: 240, green: 240, blue: 0 },
|
|
SCALE_HIGHLIGHT_COLOR = { red: 0, green: 240, blue: 240 },
|
|
HAND_HIGHLIGHT_ALPHA = 0.35,
|
|
ENTITY_HIGHLIGHT_ALPHA = 0.8,
|
|
HAND_HIGHLIGHT_DIMENSIONS = { x: 0.2, y: 0.2, z: 0.2 },
|
|
HAND_HIGHLIGHT_OFFSET = { x: 0.0, y: 0.11, z: 0.02 };
|
|
|
|
handOverlay = Overlays.addOverlay("sphere", {
|
|
dimensions: HAND_HIGHLIGHT_DIMENSIONS,
|
|
parentID: AVATAR_SELF_ID,
|
|
parentJointIndex: MyAvatar.getJointIndex(side === LEFT_HAND
|
|
? "_CONTROLLER_LEFTHAND"
|
|
: "_CONTROLLER_RIGHTHAND"),
|
|
localPosition: HAND_HIGHLIGHT_OFFSET,
|
|
alpha: HAND_HIGHLIGHT_ALPHA,
|
|
solid: true,
|
|
drawInFront: true,
|
|
ignoreRayIntersection: true,
|
|
visible: false
|
|
});
|
|
|
|
function maybeAddEntityOverlay(index) {
|
|
if (index >= entityOverlays.length) {
|
|
entityOverlays.push(Overlays.addOverlay("cube", {
|
|
alpha: ENTITY_HIGHLIGHT_ALPHA,
|
|
solid: false,
|
|
drawInFront: true,
|
|
ignoreRayIntersection: true,
|
|
visible: false
|
|
}));
|
|
}
|
|
}
|
|
|
|
function editEntityOverlay(index, details, overlayColor) {
|
|
var offset = Vec3.multiplyQbyV(details.rotation,
|
|
Vec3.multiplyVbyV(Vec3.subtract(Vec3.HALF, details.registrationPoint), details.dimensions));
|
|
|
|
Overlays.editOverlay(entityOverlays[index], {
|
|
parentID: details.id,
|
|
position: Vec3.sum(details.position, offset),
|
|
rotation: details.rotation,
|
|
dimensions: details.dimensions,
|
|
color: overlayColor,
|
|
visible: true
|
|
});
|
|
}
|
|
|
|
function display(handIntersected, selection, isScale) {
|
|
var overlayColor = isScale ? SCALE_HIGHLIGHT_COLOR : GRAB_HIGHLIGHT_COLOR,
|
|
i,
|
|
length;
|
|
|
|
// Show/hide hand overlay.
|
|
Overlays.editOverlay(handOverlay, {
|
|
color: overlayColor,
|
|
visible: handIntersected
|
|
});
|
|
|
|
// Add/edit entity overlay.
|
|
for (i = 0, length = selection.length; i < length; i += 1) {
|
|
maybeAddEntityOverlay(i);
|
|
editEntityOverlay(i, selection[i], overlayColor);
|
|
}
|
|
|
|
// Delete extra entity overlays.
|
|
for (i = entityOverlays.length - 1, length = selection.length; i >= length; i -= 1) {
|
|
Overlays.deleteOverlay(entityOverlays[i]);
|
|
entityOverlays.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
function clear() {
|
|
var i,
|
|
length;
|
|
|
|
// Hide hand overlay.
|
|
Overlays.editOverlay(handOverlay, { visible: false });
|
|
|
|
// Delete entity overlays.
|
|
for (i = 0, length = entityOverlays.length; i < length; i += 1) {
|
|
Overlays.deleteOverlay(entityOverlays[i]);
|
|
}
|
|
entityOverlays = [];
|
|
}
|
|
|
|
function destroy() {
|
|
clear();
|
|
Overlays.deleteOverlay(handOverlay);
|
|
}
|
|
|
|
if (!this instanceof Highlights) {
|
|
return new Highlights();
|
|
}
|
|
|
|
return {
|
|
display: display,
|
|
clear: clear,
|
|
destroy: destroy
|
|
};
|
|
};
|
|
|
|
|
|
Handles = function (side) {
|
|
var boundingBoxOverlay,
|
|
boundingBoxDimensions,
|
|
boundingBoxLocalCenter,
|
|
cornerIndexes = [],
|
|
cornerHandleOverlays = [],
|
|
faceHandleOverlays = [],
|
|
faceHandleOffsets,
|
|
BOUNDING_BOX_COLOR = { red: 0, green: 240, blue: 240 },
|
|
BOUNDING_BOX_ALPHA = 0.8,
|
|
HANDLE_NORMAL_COLOR = { red: 0, green: 240, blue: 240 },
|
|
HANDLE_HOVER_COLOR = { red: 0, green: 255, blue: 120 },
|
|
HANDLE_NORMAL_ALPHA = 0.7,
|
|
HANDLE_HOVER_ALPHA = 0.9,
|
|
NUM_CORNERS = 8,
|
|
NUM_CORNER_HANDLES = 2,
|
|
CORNER_HANDLE_OVERLAY_DIMENSIONS = { x: 0.1, y: 0.1, z: 0.1 },
|
|
CORNER_HANDLE_OVERLAY_AXES,
|
|
NUM_FACE_HANDLES = 6,
|
|
FACE_HANDLE_OVERLAY_DIMENSIONS = { x: 0.1, y: 0.12, z: 0.1 },
|
|
FACE_HANDLE_OVERLAY_AXES,
|
|
FACE_HANDLE_OVERLAY_OFFSETS,
|
|
FACE_HANDLE_OVERLAY_ROTATIONS,
|
|
FACE_HANDLE_OVERLAY_SCALE_AXES,
|
|
ZERO_ROTATION = Quat.fromVec3Radians(Vec3.ZERO),
|
|
DISTANCE_MULTIPLIER_MULTIPLIER = 0.5,
|
|
hoveredOverlayID = null,
|
|
isVisible = false,
|
|
|
|
// Scaling.
|
|
scalingBoundingBoxDimensions,
|
|
scalingBoundingBoxLocalCenter,
|
|
|
|
i;
|
|
|
|
CORNER_HANDLE_OVERLAY_AXES = [
|
|
// Ordered such that items 4 apart are opposite corners - used in display().
|
|
{ x: -0.5, y: -0.5, z: -0.5 },
|
|
{ x: -0.5, y: -0.5, z: 0.5 },
|
|
{ x: -0.5, y: 0.5, z: -0.5 },
|
|
{ x: -0.5, y: 0.5, z: 0.5 },
|
|
{ x: 0.5, y: 0.5, z: 0.5 },
|
|
{ x: 0.5, y: 0.5, z: -0.5 },
|
|
{ x: 0.5, y: -0.5, z: 0.5 },
|
|
{ x: 0.5, y: -0.5, z: -0.5 }
|
|
];
|
|
|
|
FACE_HANDLE_OVERLAY_AXES = [
|
|
{ x: -0.5, y: 0, z: 0 },
|
|
{ x: 0.5, y: 0, z: 0 },
|
|
{ x: 0, y: -0.5, z: 0 },
|
|
{ x: 0, y: 0.5, z: 0 },
|
|
{ x: 0, y: 0, z: -0.5 },
|
|
{ x: 0, y: 0, z: 0.5 }
|
|
];
|
|
|
|
FACE_HANDLE_OVERLAY_OFFSETS = {
|
|
x: FACE_HANDLE_OVERLAY_DIMENSIONS.y,
|
|
y: FACE_HANDLE_OVERLAY_DIMENSIONS.y,
|
|
z: FACE_HANDLE_OVERLAY_DIMENSIONS.y
|
|
};
|
|
|
|
FACE_HANDLE_OVERLAY_ROTATIONS = [
|
|
Quat.fromVec3Degrees({ x: 0, y: 0, z: 90 }),
|
|
Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }),
|
|
Quat.fromVec3Degrees({ x: 180, y: 0, z: 0 }),
|
|
Quat.fromVec3Degrees({ x: 0, y: 0, z: 0 }),
|
|
Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }),
|
|
Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 })
|
|
];
|
|
|
|
FACE_HANDLE_OVERLAY_SCALE_AXES = [
|
|
Vec3.UNIT_X,
|
|
Vec3.UNIT_X,
|
|
Vec3.UNIT_Y,
|
|
Vec3.UNIT_Y,
|
|
Vec3.UNIT_Z,
|
|
Vec3.UNIT_Z
|
|
];
|
|
|
|
boundingBoxOverlay = Overlays.addOverlay("cube", {
|
|
color: BOUNDING_BOX_COLOR,
|
|
alpha: BOUNDING_BOX_ALPHA,
|
|
solid: false,
|
|
drawInFront: true,
|
|
ignoreRayIntersection: true,
|
|
visible: false
|
|
});
|
|
|
|
for (i = 0; i < NUM_CORNER_HANDLES; i += 1) {
|
|
cornerHandleOverlays[i] = Overlays.addOverlay("sphere", {
|
|
color: HANDLE_NORMAL_COLOR,
|
|
alpha: HANDLE_NORMAL_ALPHA,
|
|
solid: true,
|
|
drawInFront: true,
|
|
ignoreRayIntersection: false,
|
|
visible: false
|
|
});
|
|
}
|
|
|
|
for (i = 0; i < NUM_FACE_HANDLES; i += 1) {
|
|
faceHandleOverlays[i] = Overlays.addOverlay("shape", {
|
|
shape: "Cone",
|
|
color: HANDLE_NORMAL_COLOR,
|
|
alpha: HANDLE_NORMAL_ALPHA,
|
|
solid: true,
|
|
drawInFront: true,
|
|
ignoreRayIntersection: false,
|
|
visible: false
|
|
});
|
|
}
|
|
|
|
function isAxisHandle(overlayID) {
|
|
return faceHandleOverlays.indexOf(overlayID) !== -1;
|
|
}
|
|
|
|
function isCornerHandle(overlayID) {
|
|
return cornerHandleOverlays.indexOf(overlayID) !== -1;
|
|
}
|
|
|
|
function isHandle(overlayID) {
|
|
return isAxisHandle(overlayID) || isCornerHandle(overlayID);
|
|
}
|
|
|
|
function scalingAxis(overlayID) {
|
|
var axesIndex;
|
|
if (isCornerHandle(overlayID)) {
|
|
axesIndex = CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[cornerHandleOverlays.indexOf(overlayID)]];
|
|
return Vec3.normalize(Vec3.multiplyVbyV(axesIndex, boundingBoxDimensions));
|
|
}
|
|
return FACE_HANDLE_OVERLAY_SCALE_AXES[faceHandleOverlays.indexOf(overlayID)];
|
|
}
|
|
|
|
function scalingDirections(overlayID) {
|
|
if (isCornerHandle(overlayID)) {
|
|
return Vec3.ONE;
|
|
}
|
|
return FACE_HANDLE_OVERLAY_SCALE_AXES[faceHandleOverlays.indexOf(overlayID)];
|
|
}
|
|
|
|
function display(rootEntityID, boundingBox, isMultiple) {
|
|
var boundingBoxCenter,
|
|
boundingBoxOrientation,
|
|
cameraPosition,
|
|
boundingBoxVector,
|
|
distanceMultiplier,
|
|
cameraUp,
|
|
cornerPosition,
|
|
cornerVector,
|
|
crossProductScale,
|
|
maxCrossProductScale,
|
|
rightCornerIndex,
|
|
leftCornerIndex,
|
|
cornerHandleDimensions,
|
|
faceHandleDimensions,
|
|
i;
|
|
|
|
isVisible = true;
|
|
|
|
boundingBoxDimensions = boundingBox.dimensions;
|
|
boundingBoxCenter = boundingBox.center;
|
|
boundingBoxLocalCenter = boundingBox.localCenter;
|
|
boundingBoxOrientation = boundingBox.orientation;
|
|
|
|
// Selection bounding box.
|
|
Overlays.editOverlay(boundingBoxOverlay, {
|
|
parentID: rootEntityID,
|
|
localPosition: boundingBoxLocalCenter,
|
|
localRotation: ZERO_ROTATION,
|
|
dimensions: boundingBoxDimensions,
|
|
visible: true
|
|
});
|
|
|
|
// Somewhat maintain general angular size of scale handles per bounding box center but make more distance ones
|
|
// display smaller in order to give comfortable depth cue.
|
|
cameraPosition = Camera.position;
|
|
boundingBoxVector = Vec3.subtract(boundingBox.center, Camera.position);
|
|
distanceMultiplier = DISTANCE_MULTIPLIER_MULTIPLIER
|
|
* Vec3.dot(Quat.getForward(Camera.orientation), boundingBoxVector)
|
|
/ Math.sqrt(Vec3.length(boundingBoxVector));
|
|
|
|
// Corner scale handles.
|
|
// At right-most and opposite corners of bounding box.
|
|
cameraUp = Quat.getUp(Camera.orientation);
|
|
maxCrossProductScale = 0;
|
|
for (i = 0; i < NUM_CORNERS; i += 1) {
|
|
cornerPosition = Vec3.sum(boundingBoxCenter,
|
|
Vec3.multiplyQbyV(boundingBoxOrientation,
|
|
Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[i], boundingBoxDimensions)));
|
|
cornerVector = Vec3.subtract(cornerPosition, cameraPosition);
|
|
crossProductScale = Vec3.dot(Vec3.cross(cornerVector, boundingBoxVector), cameraUp);
|
|
if (crossProductScale > maxCrossProductScale) {
|
|
maxCrossProductScale = crossProductScale;
|
|
rightCornerIndex = i;
|
|
}
|
|
}
|
|
leftCornerIndex = (rightCornerIndex + 4) % NUM_CORNERS;
|
|
cornerIndexes[0] = leftCornerIndex;
|
|
cornerIndexes[1] = rightCornerIndex;
|
|
cornerHandleDimensions = Vec3.multiply(distanceMultiplier, CORNER_HANDLE_OVERLAY_DIMENSIONS);
|
|
for (i = 0; i < NUM_CORNER_HANDLES; i += 1) {
|
|
Overlays.editOverlay(cornerHandleOverlays[i], {
|
|
parentID: rootEntityID,
|
|
localPosition: Vec3.sum(boundingBoxLocalCenter,
|
|
Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[i]], boundingBoxDimensions)),
|
|
dimensions: cornerHandleDimensions,
|
|
visible: true
|
|
});
|
|
}
|
|
|
|
// Face scale handles.
|
|
// Only valid for a single entity because for multiple entities, some may be at an angle relative to the root entity
|
|
// which would necessitate a (non-existent) shear transform be applied to them when scaling a face of the set.
|
|
if (!isMultiple) {
|
|
faceHandleDimensions = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_DIMENSIONS);
|
|
faceHandleOffsets = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_OFFSETS);
|
|
for (i = 0; i < NUM_FACE_HANDLES; i += 1) {
|
|
Overlays.editOverlay(faceHandleOverlays[i], {
|
|
parentID: rootEntityID,
|
|
localPosition: Vec3.sum(boundingBoxLocalCenter,
|
|
Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], Vec3.sum(boundingBoxDimensions, faceHandleOffsets))),
|
|
localRotation: FACE_HANDLE_OVERLAY_ROTATIONS[i],
|
|
dimensions: faceHandleDimensions,
|
|
visible: true
|
|
});
|
|
}
|
|
} else {
|
|
for (i = 0; i < NUM_FACE_HANDLES; i += 1) {
|
|
Overlays.editOverlay(faceHandleOverlays[i], { visible: false });
|
|
}
|
|
}
|
|
}
|
|
|
|
function startScaling() {
|
|
// Nothing to do.
|
|
}
|
|
|
|
function scale(scale3D) {
|
|
// Scale relative to dimensions and positions at start of scaling.
|
|
|
|
// Selection bounding box.
|
|
scalingBoundingBoxDimensions = Vec3.multiplyVbyV(scale3D, boundingBoxLocalCenter);
|
|
scalingBoundingBoxLocalCenter = Vec3.multiplyVbyV(scale3D, boundingBoxDimensions);
|
|
Overlays.editOverlay(boundingBoxOverlay, {
|
|
localPosition: scalingBoundingBoxDimensions,
|
|
dimensions: scalingBoundingBoxLocalCenter
|
|
});
|
|
|
|
// Corner scale handles.
|
|
for (i = 0; i < NUM_CORNER_HANDLES; i += 1) {
|
|
Overlays.editOverlay(cornerHandleOverlays[i], {
|
|
localPosition: Vec3.sum(scalingBoundingBoxDimensions,
|
|
Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[i]], scalingBoundingBoxLocalCenter))
|
|
});
|
|
}
|
|
|
|
// Face scale handles.
|
|
for (i = 0; i < NUM_FACE_HANDLES; i += 1) {
|
|
Overlays.editOverlay(faceHandleOverlays[i], {
|
|
localPosition: Vec3.sum(scalingBoundingBoxDimensions,
|
|
Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i],
|
|
Vec3.sum(scalingBoundingBoxLocalCenter, faceHandleOffsets)))
|
|
});
|
|
}
|
|
}
|
|
|
|
function finishScaling() {
|
|
// Adopt final scale.
|
|
boundingBoxLocalCenter = scalingBoundingBoxDimensions;
|
|
boundingBoxDimensions = scalingBoundingBoxLocalCenter;
|
|
}
|
|
|
|
function hover(overlayID) {
|
|
if (overlayID !== hoveredOverlayID) {
|
|
if (hoveredOverlayID !== null) {
|
|
Overlays.editOverlay(hoveredOverlayID, { color: HANDLE_NORMAL_COLOR });
|
|
hoveredOverlayID = null;
|
|
}
|
|
|
|
if (overlayID !== null
|
|
&& (faceHandleOverlays.indexOf(overlayID) !== -1 || cornerHandleOverlays.indexOf(overlayID) !== -1)) {
|
|
Overlays.editOverlay(overlayID, {
|
|
color: HANDLE_HOVER_COLOR,
|
|
alpha: HANDLE_HOVER_ALPHA
|
|
});
|
|
hoveredOverlayID = overlayID;
|
|
}
|
|
}
|
|
}
|
|
|
|
function grab(overlayID) {
|
|
var overlay,
|
|
isShowAll = overlayID === null,
|
|
color = isShowAll ? HANDLE_NORMAL_COLOR : HANDLE_HOVER_COLOR,
|
|
alpha = isShowAll ? HANDLE_NORMAL_ALPHA : HANDLE_HOVER_ALPHA,
|
|
i,
|
|
length;
|
|
|
|
for (i = 0, length = cornerHandleOverlays.length; i < length; i += 1) {
|
|
overlay = cornerHandleOverlays[i];
|
|
Overlays.editOverlay(overlay, {
|
|
visible: isVisible && (isShowAll || overlay === overlayID),
|
|
color: color,
|
|
alpha: alpha
|
|
});
|
|
}
|
|
|
|
for (i = 0, length = faceHandleOverlays.length; i < length; i += 1) {
|
|
overlay = faceHandleOverlays[i];
|
|
Overlays.editOverlay(overlay, {
|
|
visible: isVisible && (isShowAll || overlay === overlayID),
|
|
color: color,
|
|
alpha: alpha
|
|
});
|
|
}
|
|
}
|
|
|
|
function clear() {
|
|
var i;
|
|
|
|
isVisible = false;
|
|
|
|
Overlays.editOverlay(boundingBoxOverlay, { visible: false });
|
|
for (i = 0; i < NUM_CORNER_HANDLES; i += 1) {
|
|
Overlays.editOverlay(cornerHandleOverlays[i], { visible: false });
|
|
}
|
|
for (i = 0; i < NUM_FACE_HANDLES; i += 1) {
|
|
Overlays.editOverlay(faceHandleOverlays[i], { visible: false });
|
|
}
|
|
}
|
|
|
|
function destroy() {
|
|
clear();
|
|
Overlays.deleteOverlay(boundingBoxOverlay);
|
|
}
|
|
|
|
if (!this instanceof Handles) {
|
|
return new Handles(side);
|
|
}
|
|
|
|
return {
|
|
display: display,
|
|
isHandle: isHandle,
|
|
scalingAxis: scalingAxis,
|
|
scalingDirections: scalingDirections,
|
|
startScaling: startScaling,
|
|
scale: scale,
|
|
finishScaling: finishScaling,
|
|
hover: hover,
|
|
grab: grab,
|
|
clear: clear,
|
|
destroy: destroy
|
|
};
|
|
};
|
|
|
|
|
|
Selection = function (side) {
|
|
// Manages set of selected entities. Currently supports just one set of linked entities.
|
|
var selection = [],
|
|
selectedEntityID = null,
|
|
rootEntityID = null,
|
|
rootPosition,
|
|
rootOrientation,
|
|
scaleCenter,
|
|
scaleRootOffset,
|
|
scaleRootOrientation,
|
|
ENTITY_TYPE = "entity";
|
|
|
|
function traverseEntityTree(id, result) {
|
|
// Recursively traverses tree of entities and their children, gather IDs and properties.
|
|
var children,
|
|
properties,
|
|
SELECTION_PROPERTIES = ["position", "registrationPoint", "rotation", "dimensions", "localPosition",
|
|
"dynamic", "collisionless"],
|
|
i,
|
|
length;
|
|
|
|
properties = Entities.getEntityProperties(id, SELECTION_PROPERTIES);
|
|
result.push({
|
|
id: id,
|
|
position: properties.position,
|
|
localPosition: properties.localPosition,
|
|
registrationPoint: properties.registrationPoint,
|
|
rotation: properties.rotation,
|
|
dimensions: properties.dimensions,
|
|
dynamic: properties.dynamic,
|
|
collisionless: properties.collisionless
|
|
});
|
|
|
|
children = Entities.getChildrenIDs(id);
|
|
for (i = 0, length = children.length; i < length; i += 1) {
|
|
if (Entities.getNestableType(children[i]) === ENTITY_TYPE) {
|
|
traverseEntityTree(children[i], result);
|
|
}
|
|
}
|
|
}
|
|
|
|
function select(entityID) {
|
|
var entityProperties,
|
|
PARENT_PROPERTIES = ["parentID", "position", "rotation", "dymamic", "collisionless"];
|
|
|
|
// Find root parent.
|
|
rootEntityID = Entities.rootOf(entityID);
|
|
|
|
// Selection position and orientation is that of the root entity.
|
|
entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES);
|
|
rootPosition = entityProperties.position;
|
|
rootOrientation = entityProperties.rotation;
|
|
|
|
// Find all children.
|
|
selection = [];
|
|
traverseEntityTree(rootEntityID, selection);
|
|
|
|
selectedEntityID = entityID;
|
|
}
|
|
|
|
function getRootEntityID() {
|
|
return rootEntityID;
|
|
}
|
|
|
|
function getSelection() {
|
|
return selection;
|
|
}
|
|
|
|
function count() {
|
|
return selection.length;
|
|
}
|
|
|
|
function getBoundingBox() {
|
|
var center,
|
|
localCenter,
|
|
orientation,
|
|
inverseOrientation,
|
|
dimensions,
|
|
min,
|
|
max,
|
|
i,
|
|
j,
|
|
length,
|
|
registration,
|
|
position,
|
|
rotation,
|
|
corners = [],
|
|
NUM_CORNERS = 8;
|
|
|
|
if (selection.length === 1) {
|
|
if (Vec3.equal(selection[0].registrationPoint, Vec3.HALF)) {
|
|
center = rootPosition;
|
|
} else {
|
|
center = Vec3.sum(rootPosition,
|
|
Vec3.multiplyQbyV(rootOrientation,
|
|
Vec3.multiplyVbyV(selection[0].dimensions,
|
|
Vec3.subtract(Vec3.HALF, selection[0].registrationPoint))));
|
|
}
|
|
localCenter = Vec3.multiplyQbyV(Quat.inverse(rootOrientation), Vec3.subtract(center, rootPosition));
|
|
orientation = rootOrientation;
|
|
dimensions = selection[0].dimensions;
|
|
} else if (selection.length > 1) {
|
|
// Find min & max x, y, z values of entities' dimension box corners in root entity coordinate system.
|
|
// Note: Don't use entities' bounding boxes because they're in world coordinates and may make the calculated
|
|
// bounding box be larger than necessary.
|
|
min = Vec3.multiplyVbyV(Vec3.subtract(Vec3.ZERO, selection[0].registrationPoint), selection[0].dimensions);
|
|
max = Vec3.multiplyVbyV(Vec3.subtract(Vec3.ONE, selection[0].registrationPoint), selection[0].dimensions);
|
|
inverseOrientation = Quat.inverse(rootOrientation);
|
|
for (i = 1, length = selection.length; i < length; i += 1) {
|
|
|
|
registration = selection[i].registrationPoint;
|
|
corners[0] = { x: -registration.x, y: -registration.y, z: -registration.z };
|
|
corners[1] = { x: -registration.x, y: -registration.y, z: 1.0 - registration.z };
|
|
corners[2] = { x: -registration.x, y: 1.0 - registration.y, z: -registration.z };
|
|
corners[3] = { x: -registration.x, y: 1.0 - registration.y, z: 1.0 - registration.z };
|
|
corners[4] = { x: 1.0 - registration.x, y: -registration.y, z: -registration.z };
|
|
corners[5] = { x: 1.0 - registration.x, y: -registration.y, z: 1.0 - registration.z };
|
|
corners[6] = { x: 1.0 - registration.x, y: 1.0 - registration.y, z: -registration.z };
|
|
corners[7] = { x: 1.0 - registration.x, y: 1.0 - registration.y, z: 1.0 - registration.z };
|
|
|
|
position = selection[i].position;
|
|
rotation = selection[i].rotation;
|
|
dimensions = selection[i].dimensions;
|
|
|
|
for (j = 0; j < NUM_CORNERS; j += 1) {
|
|
// Corner position in world coordinates.
|
|
corners[j] = Vec3.sum(position, Vec3.multiplyQbyV(rotation, Vec3.multiplyVbyV(corners[j], dimensions)));
|
|
// Corner position in root entity coordinates.
|
|
corners[j] = Vec3.multiplyQbyV(inverseOrientation, Vec3.subtract(corners[j], rootPosition));
|
|
// Update min & max.
|
|
min = Vec3.min(corners[j], min);
|
|
max = Vec3.max(corners[j], max);
|
|
}
|
|
}
|
|
|
|
// Calculate bounding box.
|
|
center = Vec3.sum(rootPosition,
|
|
Vec3.multiplyQbyV(rootOrientation, Vec3.multiply(0.5, Vec3.sum(min, max))));
|
|
localCenter = Vec3.multiply(0.5, Vec3.sum(min, max));
|
|
orientation = rootOrientation;
|
|
dimensions = Vec3.subtract(max, min);
|
|
}
|
|
|
|
return {
|
|
center: center,
|
|
localCenter: localCenter,
|
|
orientation: orientation,
|
|
dimensions: dimensions
|
|
};
|
|
}
|
|
|
|
function startEditing() {
|
|
var count,
|
|
i;
|
|
|
|
// Disable entity set's physics.
|
|
for (i = 0, count = selection.length; i < count; i += 1) {
|
|
Entities.editEntity(selection[i].id, {
|
|
dynamic: false, // So that gravity doesn't fight with us trying to hold the entity in place.
|
|
collisionless: true // So that entity doesn't bump us about as we resize the entity.
|
|
});
|
|
}
|
|
}
|
|
|
|
function finishEditing() {
|
|
var firstDynamicEntityID = null,
|
|
properties,
|
|
VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD
|
|
VELOCITY_KICK = { x: 0, y: 0.02, z: 0 },
|
|
count,
|
|
i;
|
|
|
|
// Restore entity set's physics.
|
|
for (i = 0, count = selection.length; i < count; i += 1) {
|
|
if (firstDynamicEntityID === null && selection[i].dynamic) {
|
|
firstDynamicEntityID = selection[i].id;
|
|
}
|
|
Entities.editEntity(selection[i].id, {
|
|
dynamic: selection[i].dynamic,
|
|
collisionless: selection[i].collisionless
|
|
});
|
|
}
|
|
|
|
// If dynamic with gravity, and velocity is zero, give the entity set a little kick to set off physics.
|
|
if (firstDynamicEntityID) {
|
|
properties = Entities.getEntityProperties(firstDynamicEntityID, ["velocity", "gravity"]);
|
|
if (Vec3.length(properties.gravity) > 0 && Vec3.length(properties.velocity) < VELOCITY_THRESHOLD) {
|
|
Entities.editEntity(firstDynamicEntityID, { velocity: VELOCITY_KICK });
|
|
}
|
|
}
|
|
}
|
|
|
|
function getPositionAndOrientation() {
|
|
// Position and orientation of root entity.
|
|
return {
|
|
position: rootPosition,
|
|
orientation: rootOrientation
|
|
};
|
|
}
|
|
|
|
function setPositionAndOrientation(position, orientation) {
|
|
// Position and orientation of root entity.
|
|
rootPosition = position;
|
|
rootOrientation = orientation;
|
|
Entities.editEntity(rootEntityID, {
|
|
position: position,
|
|
rotation: orientation
|
|
});
|
|
}
|
|
|
|
function startDirectScaling(center) {
|
|
// Save initial position and orientation so that can scale relative to these without accumulating float errors.
|
|
scaleCenter = center;
|
|
scaleRootOffset = Vec3.subtract(rootPosition, center);
|
|
scaleRootOrientation = rootOrientation;
|
|
}
|
|
|
|
function directScale(factor, rotation, center) {
|
|
// Scale, position, and rotate selection.
|
|
var i,
|
|
length;
|
|
|
|
// Scale, position, and orient root.
|
|
rootPosition = Vec3.sum(center, Vec3.multiply(factor, Vec3.multiplyQbyV(rotation, scaleRootOffset)));
|
|
rootOrientation = Quat.multiply(rotation, scaleRootOrientation);
|
|
Entities.editEntity(selection[0].id, {
|
|
dimensions: Vec3.multiply(factor, selection[0].dimensions),
|
|
position: rootPosition,
|
|
rotation: rootOrientation
|
|
});
|
|
|
|
// Scale and position children.
|
|
for (i = 1, length = selection.length; i < length; i += 1) {
|
|
Entities.editEntity(selection[i].id, {
|
|
dimensions: Vec3.multiply(factor, selection[i].dimensions),
|
|
localPosition: Vec3.multiply(factor, selection[i].localPosition)
|
|
});
|
|
}
|
|
}
|
|
|
|
function finishDirectScaling() {
|
|
select(selectedEntityID); // Refresh.
|
|
}
|
|
|
|
function startHandleScaling() {
|
|
// Nothing to do.
|
|
}
|
|
|
|
function handleScale(factor, position, orientation) {
|
|
// Scale and reposition and orient selection.
|
|
var i,
|
|
length;
|
|
|
|
// Scale and position root.
|
|
rootPosition = position;
|
|
rootOrientation = orientation;
|
|
Entities.editEntity(selection[0].id, {
|
|
dimensions: Vec3.multiplyVbyV(factor, selection[0].dimensions),
|
|
position: rootPosition,
|
|
rotation: rootOrientation
|
|
});
|
|
|
|
// Scale and position children.
|
|
// Only corner handles are used for scaling multiple entities so scale factor is the same in all dimensions.
|
|
// Therefore don't need to take into account orientation relative to parent when scaling local position.
|
|
for (i = 1, length = selection.length; i < length; i += 1) {
|
|
Entities.editEntity(selection[i].id, {
|
|
dimensions: Vec3.multiplyVbyV(factor, selection[i].dimensions),
|
|
localPosition: Vec3.multiplyVbyV(factor, selection[i].localPosition)
|
|
});
|
|
}
|
|
}
|
|
|
|
function finishHandleScaling() {
|
|
select(selectedEntityID); // Refresh.
|
|
}
|
|
|
|
function clear() {
|
|
selection = [];
|
|
selectedEntityID = null;
|
|
rootEntityID = null;
|
|
}
|
|
|
|
function destroy() {
|
|
clear();
|
|
}
|
|
|
|
if (!this instanceof Selection) {
|
|
return new Selection(side);
|
|
}
|
|
|
|
return {
|
|
select: select,
|
|
selection: getSelection,
|
|
count: count,
|
|
rootEntityID: getRootEntityID,
|
|
boundingBox: getBoundingBox,
|
|
getPositionAndOrientation: getPositionAndOrientation,
|
|
setPositionAndOrientation: setPositionAndOrientation,
|
|
startEditing: startEditing,
|
|
startDirectScaling: startDirectScaling,
|
|
directScale: directScale,
|
|
finishDirectScaling: finishDirectScaling,
|
|
startHandleScaling: startHandleScaling,
|
|
handleScale: handleScale,
|
|
finishHandleScaling: finishHandleScaling,
|
|
finishEditing: finishEditing,
|
|
clear: clear,
|
|
destroy: destroy
|
|
};
|
|
};
|
|
|
|
|
|
Laser = function (side) {
|
|
// Draws hand lasers.
|
|
// May intersect with entities or bounding box of other hand's selection.
|
|
|
|
var isLaserEnabled = true,
|
|
isLaserOn = false,
|
|
|
|
laserLine = null,
|
|
laserSphere = null,
|
|
|
|
searchDistance = 0.0,
|
|
|
|
SEARCH_SPHERE_SIZE = 0.013, // Per handControllerGrab.js multiplied by 1.2 per handControllerGrab.js.
|
|
SEARCH_SPHERE_FOLLOW_RATE = 0.5, // Per handControllerGrab.js.
|
|
COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { red: 10, green: 10, blue: 255 }, // Per handControllgerGrab.js.
|
|
COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }, // Per handControllgerGrab.js.
|
|
COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT,
|
|
COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT,
|
|
BRIGHT_POW = 0.06, // Per handControllgerGrab.js.
|
|
|
|
GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js.
|
|
|
|
PICK_MAX_DISTANCE = 500, // Per handControllerGrab.js.
|
|
PRECISION_PICKING = true,
|
|
NO_INCLUDE_IDS = [],
|
|
NO_EXCLUDE_IDS = [],
|
|
VISIBLE_ONLY = true,
|
|
|
|
laserLength,
|
|
specifiedLaserLength = null,
|
|
|
|
intersection;
|
|
|
|
function colorPow(color, power) { // Per handControllerGrab.js.
|
|
return {
|
|
red: Math.pow(color.red / 255, power) * 255,
|
|
green: Math.pow(color.green / 255, power) * 255,
|
|
blue: Math.pow(color.blue / 255, power) * 255
|
|
};
|
|
}
|
|
|
|
COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT = colorPow(COLORS_GRAB_SEARCHING_HALF_SQUEEZE, BRIGHT_POW);
|
|
COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT = colorPow(COLORS_GRAB_SEARCHING_FULL_SQUEEZE, BRIGHT_POW);
|
|
|
|
if (side === LEFT_HAND) {
|
|
GRAB_POINT_SPHERE_OFFSET.x = -GRAB_POINT_SPHERE_OFFSET.x;
|
|
}
|
|
|
|
laserLine = Overlays.addOverlay("line3d", {
|
|
lineWidth: 5,
|
|
alpha: 1.0,
|
|
glow: 1.0,
|
|
ignoreRayIntersection: true,
|
|
drawInFront: true,
|
|
parentID: AVATAR_SELF_ID,
|
|
parentJointIndex: MyAvatar.getJointIndex(side === LEFT_HAND
|
|
? "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"
|
|
: "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"),
|
|
visible: false
|
|
});
|
|
laserSphere = Overlays.addOverlay("circle3d", {
|
|
innerAlpha: 1.0,
|
|
outerAlpha: 0.0,
|
|
solid: true,
|
|
ignoreRayIntersection: true,
|
|
drawInFront: true,
|
|
visible: false
|
|
});
|
|
|
|
function updateLine(start, end, color) {
|
|
Overlays.editOverlay(laserLine, {
|
|
start: start,
|
|
end: end,
|
|
color: color,
|
|
visible: true
|
|
});
|
|
}
|
|
|
|
function updateSphere(location, size, color, brightColor) {
|
|
var rotation;
|
|
|
|
rotation = Quat.lookAt(location, Camera.getPosition(), Vec3.UP);
|
|
|
|
Overlays.editOverlay(laserSphere, {
|
|
position: location,
|
|
rotation: rotation,
|
|
innerColor: brightColor,
|
|
outerColor: color,
|
|
outerRadius: size,
|
|
visible: true
|
|
});
|
|
}
|
|
|
|
function display(origin, direction, distance, isClicked) {
|
|
var searchTarget,
|
|
sphereSize,
|
|
color,
|
|
brightColor;
|
|
|
|
searchDistance = SEARCH_SPHERE_FOLLOW_RATE * searchDistance + (1.0 - SEARCH_SPHERE_FOLLOW_RATE) * distance;
|
|
searchTarget = Vec3.sum(origin, Vec3.multiply(searchDistance, direction));
|
|
sphereSize = SEARCH_SPHERE_SIZE * searchDistance;
|
|
color = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE : COLORS_GRAB_SEARCHING_HALF_SQUEEZE;
|
|
brightColor = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT : COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT;
|
|
|
|
updateLine(origin, searchTarget, color);
|
|
updateSphere(searchTarget, sphereSize, color, brightColor);
|
|
}
|
|
|
|
function hide() {
|
|
Overlays.editOverlay(laserLine, { visible: false });
|
|
Overlays.editOverlay(laserSphere, { visible: false });
|
|
}
|
|
|
|
function update(hand) {
|
|
var handPosition,
|
|
handOrientation,
|
|
deltaOrigin,
|
|
pickRay;
|
|
|
|
if (!isLaserEnabled) {
|
|
return;
|
|
}
|
|
|
|
if (!hand.intersection().intersects && hand.triggerPressed()) {
|
|
handPosition = hand.position();
|
|
handOrientation = hand.orientation();
|
|
deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET);
|
|
pickRay = {
|
|
origin: Vec3.sum(handPosition, deltaOrigin),
|
|
direction: Quat.getUp(handOrientation),
|
|
length: PICK_MAX_DISTANCE
|
|
};
|
|
|
|
intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS,
|
|
VISIBLE_ONLY);
|
|
if (!intersection.intersects) {
|
|
intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS,
|
|
VISIBLE_ONLY);
|
|
if (intersection.intersects && !isEditableRoot(intersection.entityID)) {
|
|
intersection.intersects = false;
|
|
intersection.entityID = null;
|
|
}
|
|
}
|
|
intersection.laserIntersected = true;
|
|
laserLength = (specifiedLaserLength !== null)
|
|
? specifiedLaserLength
|
|
: (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE);
|
|
|
|
isLaserOn = true;
|
|
display(pickRay.origin, pickRay.direction, laserLength, hand.triggerClicked());
|
|
} else {
|
|
intersection = {
|
|
intersects: false
|
|
};
|
|
if (isLaserOn) {
|
|
isLaserOn = false;
|
|
hide();
|
|
}
|
|
}
|
|
}
|
|
|
|
function getIntersection() {
|
|
return intersection;
|
|
}
|
|
|
|
function setLength(length) {
|
|
specifiedLaserLength = length;
|
|
laserLength = length;
|
|
}
|
|
|
|
function clearLength() {
|
|
specifiedLaserLength = null;
|
|
}
|
|
|
|
function getLength() {
|
|
return laserLength;
|
|
}
|
|
|
|
function handOffset() {
|
|
return GRAB_POINT_SPHERE_OFFSET;
|
|
}
|
|
|
|
function clear() {
|
|
isLaserOn = false;
|
|
hide();
|
|
}
|
|
|
|
function enable() {
|
|
isLaserEnabled = true;
|
|
}
|
|
|
|
function disable() {
|
|
isLaserEnabled = false;
|
|
if (isLaserOn) {
|
|
hide();
|
|
}
|
|
isLaserOn = false;
|
|
}
|
|
|
|
function destroy() {
|
|
Overlays.deleteOverlay(laserLine);
|
|
Overlays.deleteOverlay(laserSphere);
|
|
}
|
|
|
|
if (!this instanceof Laser) {
|
|
return new Laser(side);
|
|
}
|
|
|
|
return {
|
|
update: update,
|
|
intersection: getIntersection,
|
|
setLength: setLength,
|
|
clearLength: clearLength,
|
|
length: getLength,
|
|
enable: enable,
|
|
disable: disable,
|
|
handOffset: handOffset,
|
|
clear: clear,
|
|
destroy: destroy
|
|
};
|
|
};
|
|
|
|
|
|
Hand = function (side, gripPressedCallback) {
|
|
// Hand controller input.
|
|
var handController, // ####### Rename to "controller".
|
|
controllerTrigger,
|
|
controllerTriggerClicked,
|
|
controllerGrip,
|
|
|
|
isGripPressed = false,
|
|
GRIP_ON_VALUE = 0.99,
|
|
GRIP_OFF_VALUE = 0.95,
|
|
|
|
isTriggerPressed,
|
|
isTriggerClicked,
|
|
TRIGGER_ON_VALUE = 0.15, // Per handControllerGrab.js.
|
|
TRIGGER_OFF_VALUE = 0.1, // Per handControllerGrab.js.
|
|
|
|
NEAR_GRAB_RADIUS = 0.1, // Per handControllerGrab.js.
|
|
NEAR_HOVER_RADIUS = 0.025,
|
|
|
|
handPose,
|
|
handPosition,
|
|
handOrientation,
|
|
|
|
intersection = {};
|
|
|
|
if (side === LEFT_HAND) {
|
|
handController = Controller.Standard.LeftHand;
|
|
controllerTrigger = Controller.Standard.LT;
|
|
controllerTriggerClicked = Controller.Standard.LTClick;
|
|
controllerGrip = Controller.Standard.LeftGrip;
|
|
} else {
|
|
handController = Controller.Standard.RightHand;
|
|
controllerTrigger = Controller.Standard.RT;
|
|
controllerTriggerClicked = Controller.Standard.RTClick;
|
|
controllerGrip = Controller.Standard.RightGrip;
|
|
}
|
|
|
|
function valid() {
|
|
return handPose.valid;
|
|
}
|
|
|
|
function position() {
|
|
return handPosition;
|
|
}
|
|
|
|
function orientation() {
|
|
return handOrientation;
|
|
}
|
|
|
|
function triggerPressed() {
|
|
return isTriggerPressed;
|
|
}
|
|
|
|
function triggerClicked() {
|
|
return isTriggerClicked;
|
|
}
|
|
|
|
function getIntersection() {
|
|
return intersection;
|
|
}
|
|
|
|
function update() {
|
|
var gripValue,
|
|
palmPosition,
|
|
overlayID,
|
|
overlayIDs,
|
|
overlayDistance,
|
|
distance,
|
|
entityID,
|
|
entityIDs,
|
|
entitySize,
|
|
size,
|
|
i,
|
|
length;
|
|
|
|
|
|
// Hand pose.
|
|
handPose = Controller.getPoseValue(handController);
|
|
if (!handPose.valid) {
|
|
intersection = {};
|
|
return;
|
|
}
|
|
handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position);
|
|
handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation);
|
|
|
|
// Controller trigger.
|
|
isTriggerPressed = Controller.getValue(controllerTrigger) > (isTriggerPressed
|
|
? TRIGGER_OFF_VALUE : TRIGGER_ON_VALUE);
|
|
isTriggerClicked = Controller.getValue(controllerTriggerClicked);
|
|
|
|
// Controller grip.
|
|
gripValue = Controller.getValue(controllerGrip);
|
|
if (isGripPressed) {
|
|
isGripPressed = gripValue > GRIP_OFF_VALUE;
|
|
} else {
|
|
isGripPressed = gripValue > GRIP_ON_VALUE;
|
|
if (isGripPressed) {
|
|
gripPressedCallback();
|
|
}
|
|
}
|
|
|
|
// Hand-overlay intersection, if any.
|
|
overlayID = null;
|
|
palmPosition = side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition();
|
|
overlayIDs = Overlays.findOverlays(palmPosition, NEAR_HOVER_RADIUS);
|
|
if (overlayIDs.length > 0) {
|
|
// Typically, there will be only one overlay; optimize for that case.
|
|
overlayID = overlayIDs[0];
|
|
if (overlayIDs.length > 1) {
|
|
// Find closest overlay.
|
|
overlayDistance = Vec3.length(Vec3.subtract(Overlays.getProperty(overlayID, "position"), palmPosition));
|
|
for (i = 1, length = overlayIDs.length; i < length; i += 1) {
|
|
distance =
|
|
Vec3.length(Vec3.subtract(Overlays.getProperty(overlayIDs[i], "position"), palmPosition));
|
|
if (distance > overlayDistance) {
|
|
overlayID = overlayIDs[i];
|
|
overlayDistance = distance;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hand-entity intersection, if any, if overlay not intersected.
|
|
entityID = null;
|
|
if (overlayID === null) {
|
|
// palmPosition is set above.
|
|
entityIDs = Entities.findEntities(palmPosition, NEAR_GRAB_RADIUS);
|
|
if (entityIDs.length > 0) {
|
|
// Typically, there will be only one entity; optimize for that case.
|
|
if (isEditableRoot(entityIDs[0])) {
|
|
entityID = entityIDs[0];
|
|
}
|
|
if (entityIDs.length > 1) {
|
|
// Find smallest, editable entity.
|
|
entitySize = HALF_TREE_SCALE;
|
|
for (i = 0, length = entityIDs.length; i < length; i += 1) {
|
|
if (isEditableRoot(entityIDs[i])) {
|
|
size = Vec3.length(Entities.getEntityProperties(entityIDs[i], "dimensions").dimensions);
|
|
if (size < entitySize) {
|
|
entityID = entityIDs[i];
|
|
entitySize = size;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
intersection = {
|
|
intersects: overlayID !== null || entityID !== null,
|
|
overlayID: overlayID,
|
|
entityID: entityID,
|
|
handIntersected: true
|
|
};
|
|
}
|
|
|
|
function clear() {
|
|
// Nothing to do.
|
|
}
|
|
|
|
function destroy() {
|
|
// Nothing to do.
|
|
}
|
|
|
|
if (!this instanceof Hand) {
|
|
return new Hand(side);
|
|
}
|
|
|
|
return {
|
|
valid: valid,
|
|
position: position,
|
|
orientation: orientation,
|
|
triggerPressed: triggerPressed,
|
|
triggerClicked: triggerClicked,
|
|
intersection: getIntersection,
|
|
update: update,
|
|
clear: clear,
|
|
destroy: destroy
|
|
};
|
|
};
|
|
|
|
|
|
Editor = function (side, gripPressedCallback) {
|
|
// Each controller has a hand, laser, an entity selection, entity highlighter, and entity handles.
|
|
|
|
var otherEditor, // Other hand's Editor object.
|
|
|
|
// Editor states.
|
|
EDITOR_IDLE = 0,
|
|
EDITOR_SEARCHING = 1,
|
|
EDITOR_HIGHLIGHTING = 2, // Highlighting an entity (not hovering a handle).
|
|
EDITOR_GRABBING = 3,
|
|
EDITOR_DIRECT_SCALING = 4, // Scaling data are sent to other editor's EDITOR_GRABBING state.
|
|
EDITOR_HANDLE_SCALING = 5, // ""
|
|
editorState = EDITOR_IDLE,
|
|
EDITOR_STATE_STRINGS = ["EDITOR_IDLE", "EDITOR_SEARCHING", "EDITOR_HIGHLIGHTING", "EDITOR_GRABBING",
|
|
"EDITOR_DIRECT_SCALING", "EDITOR_HANDLE_SCALING"],
|
|
|
|
// State machine.
|
|
STATE_MACHINE,
|
|
highlightedEntityID = null,
|
|
wasAppScaleWithHandles = false,
|
|
isOtherEditorEditingEntityID = false,
|
|
hoveredOverlayID = null,
|
|
|
|
// Primary objects.
|
|
hand,
|
|
laser,
|
|
selection,
|
|
highlights,
|
|
handles,
|
|
|
|
// Position values.
|
|
initialHandOrientationInverse,
|
|
initialHandToSelectionVector,
|
|
initialSelectionOrientation,
|
|
|
|
// Scaling values.
|
|
isScalingWithHand = false,
|
|
isDirectScaling = false, // Modifies EDITOR_GRABBING state.
|
|
isHandleScaling = false, // ""
|
|
initialTargetsSeparation,
|
|
initialtargetsDirection,
|
|
initialTargetToBoundingBoxCenter,
|
|
otherTargetPosition,
|
|
handleUnitScaleAxis,
|
|
handleScaleDirections,
|
|
handleHandOffset,
|
|
initialHandleDistance,
|
|
initialHandleOrientationInverse,
|
|
initialHandleRegistrationOffset,
|
|
initialSelectionOrientationInverse,
|
|
laserOffset,
|
|
MIN_SCALE = 0.001,
|
|
|
|
intersection;
|
|
|
|
hand = new Hand(side, gripPressedCallback);
|
|
laser = new Laser(side);
|
|
selection = new Selection(side);
|
|
highlights = new Highlights(side);
|
|
handles = new Handles(side);
|
|
|
|
laserOffset = laser.handOffset();
|
|
|
|
function setOtherEditor(editor) {
|
|
otherEditor = editor;
|
|
}
|
|
|
|
function hoverHandle(overlayID) {
|
|
// Highlights handle if overlayID is a handle, otherwise unhighlights currently highlighted handle if any.
|
|
handles.hover(overlayID);
|
|
}
|
|
|
|
function isHandle(overlayID) {
|
|
return handles.isHandle(overlayID);
|
|
}
|
|
|
|
function isEditing(rootEntityID) {
|
|
// rootEntityID is an optional parameter.
|
|
return editorState > EDITOR_HIGHLIGHTING
|
|
&& (rootEntityID === undefined || rootEntityID === selection.rootEntityID());
|
|
}
|
|
|
|
function isScaling() {
|
|
return editorState === EDITOR_DIRECT_SCALING || editorState === EDITOR_HANDLE_SCALING;
|
|
}
|
|
|
|
function rootEntityID() {
|
|
return selection.rootEntityID();
|
|
}
|
|
|
|
|
|
function startEditing() {
|
|
var selectionPositionAndOrientation;
|
|
|
|
initialHandOrientationInverse = Quat.inverse(hand.orientation());
|
|
selectionPositionAndOrientation = selection.getPositionAndOrientation();
|
|
initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, hand.position());
|
|
initialSelectionOrientation = selectionPositionAndOrientation.orientation;
|
|
|
|
selection.startEditing();
|
|
}
|
|
|
|
function stopEditing() {
|
|
selection.finishEditing();
|
|
}
|
|
|
|
|
|
function getScaleTargetPosition() {
|
|
if (isScalingWithHand) {
|
|
return side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition();
|
|
}
|
|
return Vec3.sum(Vec3.sum(hand.position(), Vec3.multiplyQbyV(hand.orientation(), laserOffset)),
|
|
Vec3.multiply(laser.length(), Quat.getUp(hand.orientation())));
|
|
}
|
|
|
|
function startDirectScaling(targetPosition) {
|
|
// Called on grabbing hand by scaling hand.
|
|
var initialTargetPosition,
|
|
initialTargetsCenter;
|
|
|
|
isScalingWithHand = intersection.handIntersected;
|
|
|
|
otherTargetPosition = targetPosition;
|
|
initialTargetPosition = getScaleTargetPosition();
|
|
initialTargetsCenter = Vec3.multiply(0.5, Vec3.sum(initialTargetPosition, otherTargetPosition));
|
|
initialTargetsSeparation = Vec3.distance(initialTargetPosition, otherTargetPosition);
|
|
initialtargetsDirection = Vec3.subtract(otherTargetPosition, initialTargetPosition);
|
|
|
|
selection.startDirectScaling(initialTargetsCenter);
|
|
isDirectScaling = true;
|
|
}
|
|
|
|
function updateDirectScaling(targetPosition) {
|
|
// Called on grabbing hand by scaling hand.
|
|
otherTargetPosition = targetPosition;
|
|
}
|
|
|
|
function stopDirectScaling() {
|
|
// Called on grabbing hand by scaling hand.
|
|
selection.finishDirectScaling();
|
|
isDirectScaling = false;
|
|
}
|
|
|
|
function startHandleScaling(targetPosition, overlayID) {
|
|
// Called on grabbing hand by scaling hand.
|
|
var initialTargetPosition,
|
|
boundingBox,
|
|
selectionPositionAndOrientation,
|
|
scaleAxis,
|
|
handDistance;
|
|
|
|
isScalingWithHand = intersection.handIntersected;
|
|
|
|
otherTargetPosition = targetPosition;
|
|
|
|
// Keep grabbed handle highlighted and hide other handles.
|
|
handles.grab(overlayID);
|
|
|
|
// Vector from target to bounding box center.
|
|
initialTargetPosition = getScaleTargetPosition();
|
|
boundingBox = selection.boundingBox();
|
|
initialTargetToBoundingBoxCenter = Vec3.subtract(boundingBox.center, initialTargetPosition);
|
|
|
|
// Selection information.
|
|
selectionPositionAndOrientation = selection.getPositionAndOrientation();
|
|
initialSelectionOrientationInverse = Quat.inverse(selectionPositionAndOrientation.orientation);
|
|
|
|
// Handle information.
|
|
initialHandleOrientationInverse = Quat.inverse(hand.orientation());
|
|
handleUnitScaleAxis = handles.scalingAxis(overlayID); // Unit vector in direction of scaling.
|
|
handleScaleDirections = handles.scalingDirections(overlayID); // Which axes to scale the selection on.
|
|
initialHandleDistance = Vec3.length(Vec3.multiplyVbyV(boundingBox.dimensions, handleScaleDirections)) / 2;
|
|
initialHandleRegistrationOffset = Vec3.multiplyQbyV(initialSelectionOrientationInverse,
|
|
Vec3.subtract(selectionPositionAndOrientation.position, boundingBox.center));
|
|
|
|
// Distance from hand to handle in direction of handle.
|
|
scaleAxis = Vec3.multiplyQbyV(selectionPositionAndOrientation.orientation, handleUnitScaleAxis);
|
|
handDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, boundingBox.center), scaleAxis));
|
|
handleHandOffset = handDistance - initialHandleDistance;
|
|
|
|
selection.startHandleScaling();
|
|
handles.startScaling();
|
|
isHandleScaling = true;
|
|
}
|
|
|
|
function updateHandleScaling(targetPosition) {
|
|
// Called on grabbing hand by scaling hand.
|
|
otherTargetPosition = targetPosition;
|
|
}
|
|
|
|
function stopHandleScaling() {
|
|
// Called on grabbing hand by scaling hand.
|
|
handles.finishScaling();
|
|
selection.finishHandleScaling();
|
|
handles.grab(null); // Stop highlighting grabbed handle and resume displaying all handles.
|
|
isHandleScaling = false;
|
|
}
|
|
|
|
|
|
function applyGrab() {
|
|
// Sets position and orientation of selection per grabbing hand.
|
|
var deltaOrientation,
|
|
selectionPosition,
|
|
selectionOrientation;
|
|
|
|
deltaOrientation = Quat.multiply(hand.orientation(), initialHandOrientationInverse);
|
|
selectionPosition = Vec3.sum(hand.position(), Vec3.multiplyQbyV(deltaOrientation, initialHandToSelectionVector));
|
|
selectionOrientation = Quat.multiply(deltaOrientation, initialSelectionOrientation);
|
|
|
|
selection.setPositionAndOrientation(selectionPosition, selectionOrientation);
|
|
}
|
|
|
|
function applyDirectScale() {
|
|
// Scales, rotates, and positions selection per changing length, orientation, and position of vector between hands.
|
|
var targetPosition,
|
|
targetsSeparation,
|
|
scale,
|
|
rotation,
|
|
center,
|
|
selectionPositionAndOrientation;
|
|
|
|
// Scale selection.
|
|
targetPosition = getScaleTargetPosition();
|
|
targetsSeparation = Vec3.distance(targetPosition, otherTargetPosition);
|
|
scale = targetsSeparation / initialTargetsSeparation;
|
|
scale = Math.max(scale, MIN_SCALE);
|
|
|
|
rotation = Quat.rotationBetween(initialtargetsDirection, Vec3.subtract(otherTargetPosition, targetPosition));
|
|
center = Vec3.multiply(0.5, Vec3.sum(targetPosition, otherTargetPosition));
|
|
selection.directScale(scale, rotation, center);
|
|
|
|
// Update grab offset.
|
|
selectionPositionAndOrientation = selection.getPositionAndOrientation();
|
|
initialHandOrientationInverse = Quat.inverse(hand.orientation());
|
|
initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, hand.position());
|
|
initialSelectionOrientation = selectionPositionAndOrientation.orientation;
|
|
}
|
|
|
|
function applyHandleScale() {
|
|
// Scales selection per changing position of scaling hand; positions and orients per grabbing hand.
|
|
var targetPosition,
|
|
deltaHandOrientation,
|
|
deltaHandleOrientation,
|
|
selectionPosition,
|
|
selectionOrientation,
|
|
boundingBoxCenter,
|
|
scaleAxis,
|
|
handleDistance,
|
|
scale,
|
|
scale3D,
|
|
selectionPositionAndOrientation;
|
|
|
|
// Orient selection per grabbing hand.
|
|
deltaHandOrientation = Quat.multiply(hand.orientation(), initialHandOrientationInverse);
|
|
selectionOrientation = Quat.multiply(deltaHandOrientation, initialSelectionOrientation);
|
|
|
|
// Desired distance of handle from center of bounding box.
|
|
targetPosition = getScaleTargetPosition();
|
|
deltaHandleOrientation = Quat.multiply(hand.orientation(), initialHandleOrientationInverse);
|
|
boundingBoxCenter = Vec3.sum(targetPosition,
|
|
Vec3.multiplyQbyV(deltaHandleOrientation, initialTargetToBoundingBoxCenter));
|
|
scaleAxis = Vec3.multiplyQbyV(selection.getPositionAndOrientation().orientation, handleUnitScaleAxis);
|
|
handleDistance = Math.abs(Vec3.dot(Vec3.subtract(otherTargetPosition, boundingBoxCenter), scaleAxis));
|
|
handleDistance -= handleHandOffset;
|
|
handleDistance = Math.max(handleDistance, MIN_SCALE);
|
|
|
|
// Scale selection relative to initial dimensions.
|
|
scale = handleDistance / initialHandleDistance;
|
|
scale3D = Vec3.multiply(scale, handleScaleDirections);
|
|
scale3D = {
|
|
x: handleScaleDirections.x !== 0 ? scale3D.x : 1,
|
|
y: handleScaleDirections.y !== 0 ? scale3D.y : 1,
|
|
z: handleScaleDirections.z !== 0 ? scale3D.z : 1
|
|
};
|
|
|
|
// Reposition selection per scale.
|
|
selectionPosition = Vec3.sum(boundingBoxCenter,
|
|
Vec3.multiplyQbyV(selectionOrientation, Vec3.multiplyVbyV(scale3D, initialHandleRegistrationOffset)));
|
|
|
|
// Scale.
|
|
handles.scale(scale3D);
|
|
selection.handleScale(scale3D, selectionPosition, selectionOrientation);
|
|
|
|
// Update grab offset.
|
|
selectionPositionAndOrientation = selection.getPositionAndOrientation();
|
|
initialHandOrientationInverse = Quat.inverse(hand.orientation());
|
|
initialHandToSelectionVector = Vec3.subtract(selectionPositionAndOrientation.position, hand.position());
|
|
initialSelectionOrientation = selectionPositionAndOrientation.orientation;
|
|
}
|
|
|
|
|
|
function enterEditorIdle() {
|
|
laser.clear();
|
|
selection.clear();
|
|
}
|
|
|
|
function exitEditorIdle() {
|
|
// Nothing to do.
|
|
}
|
|
|
|
function enterEditorSearching() {
|
|
selection.clear();
|
|
hoveredOverlayID = intersection.overlayID;
|
|
otherEditor.hoverHandle(hoveredOverlayID);
|
|
}
|
|
|
|
function updateEditorSearching() {
|
|
if (isAppScaleWithHandles && intersection.overlayID !== hoveredOverlayID && otherEditor.isEditing()) {
|
|
hoveredOverlayID = intersection.overlayID;
|
|
otherEditor.hoverHandle(hoveredOverlayID);
|
|
}
|
|
}
|
|
|
|
function exitEditorSearching() {
|
|
otherEditor.hoverHandle(null);
|
|
}
|
|
|
|
function enterEditorHighlighting() {
|
|
selection.select(highlightedEntityID);
|
|
if (!isAppScaleWithHandles || !otherEditor.isEditing(highlightedEntityID)) {
|
|
highlights.display(intersection.handIntersected, selection.selection(),
|
|
isAppScaleWithHandles || otherEditor.isEditing(highlightedEntityID));
|
|
}
|
|
isOtherEditorEditingEntityID = otherEditor.isEditing(highlightedEntityID);
|
|
wasAppScaleWithHandles = isAppScaleWithHandles;
|
|
}
|
|
|
|
function updateEditorHighlighting() {
|
|
selection.select(highlightedEntityID);
|
|
if (!isAppScaleWithHandles || !otherEditor.isEditing(highlightedEntityID)) {
|
|
highlights.display(intersection.handIntersected, selection.selection(),
|
|
isAppScaleWithHandles || otherEditor.isEditing(highlightedEntityID));
|
|
} else {
|
|
highlights.clear();
|
|
}
|
|
isOtherEditorEditingEntityID = !isOtherEditorEditingEntityID;
|
|
}
|
|
|
|
function exitEditorHighlighting() {
|
|
highlights.clear();
|
|
isOtherEditorEditingEntityID = false;
|
|
}
|
|
|
|
function enterEditorGrabbing() {
|
|
selection.select(highlightedEntityID); // For when transitioning from EDITOR_SEARCHING.
|
|
if (intersection.laserIntersected) {
|
|
laser.setLength(laser.length());
|
|
} else {
|
|
laser.disable();
|
|
}
|
|
if (isAppScaleWithHandles) {
|
|
handles.display(highlightedEntityID, selection.boundingBox(), selection.count() > 1);
|
|
}
|
|
startEditing();
|
|
wasAppScaleWithHandles = isAppScaleWithHandles;
|
|
}
|
|
|
|
function updateEditorGrabbing() {
|
|
selection.select(highlightedEntityID);
|
|
if (isAppScaleWithHandles) {
|
|
handles.display(highlightedEntityID, selection.boundingBox(), selection.count() > 1);
|
|
} else {
|
|
handles.clear();
|
|
}
|
|
}
|
|
|
|
function exitEditorGrabbing() {
|
|
stopEditing();
|
|
handles.clear();
|
|
laser.clearLength();
|
|
laser.enable();
|
|
}
|
|
|
|
function enterEditorDirectScaling() {
|
|
selection.select(highlightedEntityID); // In case need to transition to EDITOR_GRABBING.
|
|
isScalingWithHand = intersection.handIntersected;
|
|
if (intersection.laserIntersected) {
|
|
laser.setLength(laser.length());
|
|
} else {
|
|
laser.disable();
|
|
}
|
|
otherEditor.startDirectScaling(getScaleTargetPosition());
|
|
}
|
|
|
|
function updateEditorDirectScaling() {
|
|
otherEditor.updateDirectScaling(getScaleTargetPosition());
|
|
}
|
|
|
|
function exitEditorDirectScaling() {
|
|
otherEditor.stopDirectScaling();
|
|
laser.clearLength();
|
|
laser.enable();
|
|
}
|
|
|
|
function enterEditorHandleScaling() {
|
|
selection.select(highlightedEntityID); // In case need to transition to EDITOR_GRABBING.
|
|
isScalingWithHand = intersection.handIntersected;
|
|
if (intersection.laserIntersected) {
|
|
laser.setLength(laser.length());
|
|
} else {
|
|
laser.disable();
|
|
}
|
|
otherEditor.startHandleScaling(getScaleTargetPosition(), intersection.overlayID);
|
|
}
|
|
|
|
function updateEditorHandleScaling() {
|
|
otherEditor.updateHandleScaling(getScaleTargetPosition());
|
|
}
|
|
|
|
function exitEditorHandleScaling() {
|
|
otherEditor.stopHandleScaling();
|
|
laser.clearLength();
|
|
laser.enable();
|
|
}
|
|
|
|
STATE_MACHINE = {
|
|
EDITOR_IDLE: {
|
|
enter: enterEditorIdle,
|
|
update: null,
|
|
exit: exitEditorIdle
|
|
},
|
|
EDITOR_SEARCHING: {
|
|
enter: enterEditorSearching,
|
|
update: updateEditorSearching,
|
|
exit: exitEditorSearching
|
|
},
|
|
EDITOR_HIGHLIGHTING: {
|
|
enter: enterEditorHighlighting,
|
|
update: updateEditorHighlighting,
|
|
exit: exitEditorHighlighting
|
|
},
|
|
EDITOR_GRABBING: {
|
|
enter: enterEditorGrabbing,
|
|
update: updateEditorGrabbing,
|
|
exit: exitEditorGrabbing
|
|
},
|
|
EDITOR_DIRECT_SCALING: {
|
|
enter: enterEditorDirectScaling,
|
|
update: updateEditorDirectScaling,
|
|
exit: exitEditorDirectScaling
|
|
},
|
|
EDITOR_HANDLE_SCALING: {
|
|
enter: enterEditorHandleScaling,
|
|
update: updateEditorHandleScaling,
|
|
exit: exitEditorHandleScaling
|
|
}
|
|
};
|
|
|
|
function setState(state) {
|
|
if (state !== editorState) {
|
|
STATE_MACHINE[EDITOR_STATE_STRINGS[editorState]].exit();
|
|
STATE_MACHINE[EDITOR_STATE_STRINGS[state]].enter();
|
|
editorState = state;
|
|
} else if (DEBUG) {
|
|
log("ERROR: Null state transition: " + state + "!");
|
|
}
|
|
}
|
|
|
|
function updateState() {
|
|
STATE_MACHINE[EDITOR_STATE_STRINGS[editorState]].update();
|
|
}
|
|
|
|
|
|
function update() {
|
|
var previousState = editorState,
|
|
doUpdateState;
|
|
|
|
// Hand update.
|
|
hand.update();
|
|
intersection = hand.intersection();
|
|
|
|
// Laser update.
|
|
// Displays laser if hand has no intersection and trigger is pressed.
|
|
if (hand.valid()) {
|
|
laser.update(hand);
|
|
if (!intersection.intersects) {
|
|
intersection = laser.intersection();
|
|
}
|
|
}
|
|
|
|
// State update.
|
|
switch (editorState) {
|
|
case EDITOR_IDLE:
|
|
if (!hand.valid()) {
|
|
// No transition.
|
|
break;
|
|
}
|
|
setState(EDITOR_SEARCHING);
|
|
break;
|
|
case EDITOR_SEARCHING:
|
|
if (hand.valid() && !intersection.entityID
|
|
&& !(intersection.overlayID && hand.triggerClicked() && otherEditor.isHandle(intersection.overlayID))) {
|
|
// No transition.
|
|
updateState();
|
|
break;
|
|
}
|
|
if (!hand.valid()) {
|
|
setState(EDITOR_IDLE);
|
|
} else if (intersection.overlayID && hand.triggerClicked()
|
|
&& otherEditor.isHandle(intersection.overlayID)) {
|
|
highlightedEntityID = otherEditor.rootEntityID();
|
|
setState(EDITOR_HANDLE_SCALING);
|
|
} else if (intersection.entityID && !hand.triggerClicked()) {
|
|
highlightedEntityID = Entities.rootOf(intersection.entityID);
|
|
setState(EDITOR_HIGHLIGHTING);
|
|
} else if (intersection.entityID && hand.triggerClicked()) {
|
|
highlightedEntityID = Entities.rootOf(intersection.entityID);
|
|
if (otherEditor.isEditing(highlightedEntityID)) {
|
|
if (!isAppScaleWithHandles) {
|
|
setState(EDITOR_DIRECT_SCALING);
|
|
}
|
|
} else {
|
|
setState(EDITOR_GRABBING);
|
|
}
|
|
} else {
|
|
debug(side, "ERROR: Unexpected condition in EDITOR_SEARCHING!");
|
|
}
|
|
break;
|
|
case EDITOR_HIGHLIGHTING:
|
|
if (hand.valid()
|
|
&& intersection.entityID
|
|
&& !(hand.triggerClicked() && (!otherEditor.isEditing(highlightedEntityID) || !isAppScaleWithHandles))
|
|
&& !(hand.triggerClicked() && intersection.overlayID && otherEditor.isHandle(intersection.overlayID))) {
|
|
// No transition.
|
|
doUpdateState = false;
|
|
if (otherEditor.isEditing(highlightedEntityID) !== isOtherEditorEditingEntityID) {
|
|
doUpdateState = true;
|
|
}
|
|
if (Entities.rootOf(intersection.entityID) !== highlightedEntityID) {
|
|
highlightedEntityID = Entities.rootOf(intersection.entityID);
|
|
doUpdateState = true;
|
|
}
|
|
if (isAppScaleWithHandles !== wasAppScaleWithHandles) {
|
|
wasAppScaleWithHandles = isAppScaleWithHandles;
|
|
doUpdateState = true;
|
|
}
|
|
if (doUpdateState) {
|
|
updateState();
|
|
}
|
|
break;
|
|
}
|
|
if (!hand.valid()) {
|
|
setState(EDITOR_IDLE);
|
|
} else if (intersection.overlayID && hand.triggerClicked()
|
|
&& otherEditor.isHandle(intersection.overlayID)) {
|
|
highlightedEntityID = otherEditor.rootEntityID();
|
|
setState(EDITOR_HANDLE_SCALING);
|
|
} else if (intersection.entityID && hand.triggerClicked()) {
|
|
highlightedEntityID = Entities.rootOf(intersection.entityID); // May be a different entityID.
|
|
if (otherEditor.isEditing(highlightedEntityID)) {
|
|
if (!isAppScaleWithHandles) {
|
|
setState(EDITOR_DIRECT_SCALING);
|
|
} else {
|
|
debug(side, "ERROR: Unexpected condition in EDITOR_HIGHLIGHTING! A");
|
|
}
|
|
} else {
|
|
setState(EDITOR_GRABBING);
|
|
}
|
|
} else if (!intersection.entityID) {
|
|
setState(EDITOR_SEARCHING);
|
|
} else {
|
|
debug(side, "ERROR: Unexpected condition in EDITOR_HIGHLIGHTING! B");
|
|
}
|
|
break;
|
|
case EDITOR_GRABBING:
|
|
if (hand.valid() && hand.triggerClicked()) {
|
|
// Don't test for intersection.intersected because when scaling with handles intersection may lag behind.
|
|
// No transition.
|
|
if (isAppScaleWithHandles !== wasAppScaleWithHandles) {
|
|
updateState();
|
|
wasAppScaleWithHandles = isAppScaleWithHandles;
|
|
}
|
|
break;
|
|
}
|
|
if (!hand.valid()) {
|
|
setState(EDITOR_IDLE);
|
|
} else if (!hand.triggerClicked()) {
|
|
if (intersection.entityID) {
|
|
highlightedEntityID = Entities.rootOf(intersection.entityID);
|
|
setState(EDITOR_HIGHLIGHTING);
|
|
} else {
|
|
setState(EDITOR_SEARCHING);
|
|
}
|
|
} else {
|
|
debug(side, "ERROR: Unexpected condition in EDITOR_GRABBING!");
|
|
}
|
|
break;
|
|
case EDITOR_DIRECT_SCALING:
|
|
if (hand.valid() && hand.triggerClicked()
|
|
&& (otherEditor.isEditing(highlightedEntityID) || otherEditor.isHandle(intersection.overlayID))) {
|
|
// Don't test for intersection.intersected because when scaling with handles intersection may lag behind.
|
|
// Don't test isAppScaleWithHandles because this will eventually be a UI element and so not able to be
|
|
// changed while scaling with two hands.
|
|
// No transition.
|
|
updateState();
|
|
break;
|
|
}
|
|
if (!hand.valid()) {
|
|
setState(EDITOR_IDLE);
|
|
} else if (!hand.triggerClicked()) {
|
|
if (!intersection.entityID) {
|
|
setState(EDITOR_SEARCHING);
|
|
} else {
|
|
highlightedEntityID = Entities.rootOf(intersection.entityID);
|
|
setState(EDITOR_HIGHLIGHTING);
|
|
}
|
|
} else if (!otherEditor.isEditing(highlightedEntityID)) {
|
|
// Grab highlightEntityID that was scaling and has already been set.
|
|
setState(EDITOR_GRABBING);
|
|
}
|
|
break;
|
|
case EDITOR_HANDLE_SCALING:
|
|
if (hand.valid() && hand.triggerClicked() && otherEditor.isEditing(highlightedEntityID)) {
|
|
// Don't test for intersection.intersected because when scaling with handles intersection may lag behind.
|
|
// Don't test isAppScaleWithHandles because this will eventually be a UI element and so not able to be
|
|
// changed while scaling with two hands.
|
|
// No transition.
|
|
updateState();
|
|
break;
|
|
}
|
|
if (!hand.valid()) {
|
|
setState(EDITOR_IDLE);
|
|
} else if (!hand.triggerClicked()) {
|
|
if (!intersection.entityID) {
|
|
setState(EDITOR_SEARCHING);
|
|
} else {
|
|
highlightedEntityID = Entities.rootOf(intersection.entityID);
|
|
setState(EDITOR_HIGHLIGHTING);
|
|
}
|
|
} else if (!otherEditor.isEditing(highlightedEntityID)) {
|
|
// Grab highlightEntityID that was scaling and has already been set.
|
|
setState(EDITOR_GRABBING);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (DEBUG && editorState !== previousState) {
|
|
debug(side, EDITOR_STATE_STRINGS[editorState]);
|
|
}
|
|
}
|
|
|
|
function apply() {
|
|
switch (editorState) {
|
|
case EDITOR_GRABBING:
|
|
if (isDirectScaling) {
|
|
applyDirectScale();
|
|
} else if (isHandleScaling) {
|
|
applyHandleScale();
|
|
} else {
|
|
applyGrab();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
function clear() {
|
|
setState(EDITOR_IDLE);
|
|
|
|
hand.clear();
|
|
laser.clear();
|
|
selection.clear();
|
|
highlights.clear();
|
|
handles.clear();
|
|
}
|
|
|
|
function destroy() {
|
|
if (hand) {
|
|
hand.destroy();
|
|
hand = null;
|
|
}
|
|
if (laser) {
|
|
laser.destroy();
|
|
laser = null;
|
|
}
|
|
if (selection) {
|
|
selection.destroy();
|
|
selection = null;
|
|
}
|
|
if (highlights) {
|
|
highlights.destroy();
|
|
highlights = null;
|
|
}
|
|
if (handles) {
|
|
handles.destroy();
|
|
handles = null;
|
|
}
|
|
}
|
|
|
|
if (!this instanceof Editor) {
|
|
return new Editor();
|
|
}
|
|
|
|
return {
|
|
setOtherEditor: setOtherEditor,
|
|
hoverHandle: hoverHandle,
|
|
isHandle: isHandle,
|
|
isEditing: isEditing,
|
|
isScaling: isScaling,
|
|
rootEntityID: rootEntityID,
|
|
startDirectScaling: startDirectScaling,
|
|
updateDirectScaling: updateDirectScaling,
|
|
stopDirectScaling: stopDirectScaling,
|
|
startHandleScaling: startHandleScaling,
|
|
updateHandleScaling: updateHandleScaling,
|
|
stopHandleScaling: stopHandleScaling,
|
|
update: update,
|
|
apply: apply,
|
|
clear: clear,
|
|
destroy: destroy
|
|
};
|
|
};
|
|
|
|
|
|
function update() {
|
|
// Main update loop.
|
|
updateTimer = null;
|
|
|
|
// Each hand's action depends on the state of the other hand, so update the states first then apply actions.
|
|
editors[LEFT_HAND].update();
|
|
editors[RIGHT_HAND].update();
|
|
editors[LEFT_HAND].apply();
|
|
editors[RIGHT_HAND].apply();
|
|
|
|
updateTimer = Script.setTimeout(update, UPDATE_LOOP_TIMEOUT);
|
|
}
|
|
|
|
function updateHandControllerGrab() {
|
|
// Communicate app status to handControllerGrab.js.
|
|
Settings.setValue(VR_EDIT_SETTING, isAppActive);
|
|
}
|
|
|
|
function onAppButtonClicked() {
|
|
// Application tablet/toolbar button clicked.
|
|
isAppActive = !isAppActive;
|
|
updateHandControllerGrab();
|
|
button.editProperties({ isActive: isAppActive });
|
|
|
|
if (isAppActive) {
|
|
update();
|
|
} else {
|
|
Script.clearTimeout(updateTimer);
|
|
updateTimer = null;
|
|
editors[LEFT_HAND].clear();
|
|
editors[RIGHT_HAND].clear();
|
|
}
|
|
}
|
|
|
|
function onGripClicked() {
|
|
// Do not change scale mode if are currently scaling.
|
|
if (!editors[LEFT_HAND].isScaling() && !editors[RIGHT_HAND].isScaling()) {
|
|
isAppScaleWithHandles = !isAppScaleWithHandles;
|
|
}
|
|
}
|
|
|
|
|
|
function setUp() {
|
|
updateHandControllerGrab();
|
|
|
|
tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
|
if (!tablet) {
|
|
return;
|
|
}
|
|
|
|
// Tablet/toolbar button.
|
|
button = tablet.addButton({
|
|
icon: APP_ICON_INACTIVE,
|
|
activeIcon: APP_ICON_ACTIVE,
|
|
text: APP_NAME,
|
|
isActive: isAppActive
|
|
});
|
|
if (button) {
|
|
button.clicked.connect(onAppButtonClicked);
|
|
}
|
|
|
|
// Hands, each with a laser, selection, etc.
|
|
editors[LEFT_HAND] = new Editor(LEFT_HAND, onGripClicked);
|
|
editors[RIGHT_HAND] = new Editor(RIGHT_HAND, onGripClicked);
|
|
editors[LEFT_HAND].setOtherEditor(editors[RIGHT_HAND]);
|
|
editors[RIGHT_HAND].setOtherEditor(editors[LEFT_HAND]);
|
|
|
|
if (isAppActive) {
|
|
update();
|
|
}
|
|
}
|
|
|
|
function tearDown() {
|
|
if (updateTimer) {
|
|
Script.clearTimeout(updateTimer);
|
|
}
|
|
|
|
isAppActive = false;
|
|
updateHandControllerGrab();
|
|
|
|
if (!tablet) {
|
|
return;
|
|
}
|
|
|
|
if (button) {
|
|
button.clicked.disconnect(onAppButtonClicked);
|
|
tablet.removeButton(button);
|
|
button = null;
|
|
}
|
|
|
|
if (editors[LEFT_HAND]) {
|
|
editors[LEFT_HAND].destroy();
|
|
editors[LEFT_HAND] = null;
|
|
}
|
|
if (editors[RIGHT_HAND]) {
|
|
editors[RIGHT_HAND].destroy();
|
|
editors[RIGHT_HAND] = null;
|
|
}
|
|
|
|
tablet = null;
|
|
}
|
|
|
|
setUp();
|
|
Script.scriptEnding.connect(tearDown);
|
|
}());
|