mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-05 21:22:07 +02:00
465 lines
16 KiB
JavaScript
465 lines
16 KiB
JavaScript
//
|
|
// fingerPaint.js
|
|
//
|
|
// Created by David Rowe on 15 Feb 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 tablet,
|
|
button,
|
|
BUTTON_NAME = "PAINT",
|
|
isFingerPainting = false,
|
|
shouldPointFingers = false,
|
|
leftHand = null,
|
|
rightHand = null,
|
|
leftBrush = null,
|
|
rightBrush = null,
|
|
CONTROLLER_MAPPING_NAME = "com.highfidelity.fingerPaint",
|
|
isTabletDisplayed = false,
|
|
HIFI_POINT_INDEX_MESSAGE_CHANNEL = "Hifi-Point-Index",
|
|
HIFI_GRAB_DISABLE_MESSAGE_CHANNEL = "Hifi-Grab-Disable",
|
|
HIFI_POINTER_DISABLE_MESSAGE_CHANNEL = "Hifi-Pointer-Disable";
|
|
HOW_TO_EXIT_MESSAGE = "Press B on your controller to exit FingerPainting mode";
|
|
|
|
function paintBrush(name) {
|
|
// Paints in 3D.
|
|
var brushName = name,
|
|
STROKE_COLOR = { red: 250, green: 0, blue: 0 },
|
|
ERASE_SEARCH_RADIUS = 0.1, // m
|
|
isDrawingLine = false,
|
|
entityID,
|
|
basePosition,
|
|
strokePoints,
|
|
strokeNormals,
|
|
strokeWidths,
|
|
timeOfLastPoint,
|
|
MIN_STROKE_LENGTH = 0.005, // m
|
|
MIN_STROKE_INTERVAL = 66, // ms
|
|
MAX_POINTS_PER_LINE = 70; // Hard-coded limit in PolyLineEntityItem.h.
|
|
|
|
function strokeNormal() {
|
|
return Vec3.multiplyQbyV(Camera.getOrientation(), Vec3.UNIT_NEG_Z);
|
|
}
|
|
|
|
function startLine(position, width) {
|
|
// Start drawing a polyline.
|
|
|
|
if (isDrawingLine) {
|
|
print("ERROR: startLine() called when already drawing line");
|
|
// Nevertheless, continue on and start a new line.
|
|
}
|
|
|
|
basePosition = position;
|
|
|
|
strokePoints = [Vec3.ZERO];
|
|
strokeNormals = [strokeNormal()];
|
|
strokeWidths = [width];
|
|
timeOfLastPoint = Date.now();
|
|
|
|
entityID = Entities.addEntity({
|
|
type: "PolyLine",
|
|
name: "fingerPainting",
|
|
color: STROKE_COLOR,
|
|
position: position,
|
|
linePoints: strokePoints,
|
|
normals: strokeNormals,
|
|
strokeWidths: strokeWidths,
|
|
dimensions: { x: 10, y: 10, z: 10 }
|
|
});
|
|
|
|
isDrawingLine = true;
|
|
}
|
|
|
|
function drawLine(position, width) {
|
|
// Add a stroke to the polyline if stroke is a sufficient length.
|
|
var localPosition,
|
|
distanceToPrevious,
|
|
MAX_DISTANCE_TO_PREVIOUS = 1.0;
|
|
|
|
if (!isDrawingLine) {
|
|
print("ERROR: drawLine() called when not drawing line");
|
|
return;
|
|
}
|
|
|
|
localPosition = Vec3.subtract(position, basePosition);
|
|
distanceToPrevious = Vec3.distance(localPosition, strokePoints[strokePoints.length - 1]);
|
|
|
|
if (distanceToPrevious > MAX_DISTANCE_TO_PREVIOUS) {
|
|
// Ignore occasional spurious finger tip positions.
|
|
return;
|
|
}
|
|
|
|
if (distanceToPrevious >= MIN_STROKE_LENGTH
|
|
&& (Date.now() - timeOfLastPoint) >= MIN_STROKE_INTERVAL
|
|
&& strokePoints.length < MAX_POINTS_PER_LINE) {
|
|
strokePoints.push(localPosition);
|
|
strokeNormals.push(strokeNormal());
|
|
strokeWidths.push(width);
|
|
timeOfLastPoint = Date.now();
|
|
|
|
Entities.editEntity(entityID, {
|
|
linePoints: strokePoints,
|
|
normals: strokeNormals,
|
|
strokeWidths: strokeWidths
|
|
});
|
|
}
|
|
}
|
|
|
|
function finishLine(position, width) {
|
|
// Finish drawing polyline; delete if it has only 1 point.
|
|
|
|
if (!isDrawingLine) {
|
|
print("ERROR: finishLine() called when not drawing line");
|
|
return;
|
|
}
|
|
|
|
if (strokePoints.length === 1) {
|
|
// Delete "empty" line.
|
|
Entities.deleteEntity(entityID);
|
|
}
|
|
|
|
isDrawingLine = false;
|
|
}
|
|
|
|
function cancelLine() {
|
|
// Cancel any line being drawn.
|
|
if (isDrawingLine) {
|
|
Entities.deleteEntity(entityID);
|
|
isDrawingLine = false;
|
|
}
|
|
}
|
|
|
|
function eraseClosestLine(position) {
|
|
// Erase closest line that is within search radius of finger tip.
|
|
var entities,
|
|
entitiesLength,
|
|
properties,
|
|
i,
|
|
pointsLength,
|
|
j,
|
|
distance,
|
|
found = false,
|
|
foundID,
|
|
foundDistance = ERASE_SEARCH_RADIUS;
|
|
|
|
// Find entities with bounding box within search radius.
|
|
entities = Entities.findEntities(position, ERASE_SEARCH_RADIUS);
|
|
|
|
// Fine polyline entity with closest point within search radius.
|
|
for (i = 0, entitiesLength = entities.length; i < entitiesLength; i += 1) {
|
|
properties = Entities.getEntityProperties(entities[i], ["type", "position", "linePoints"]);
|
|
if (properties.type === "PolyLine") {
|
|
basePosition = properties.position;
|
|
for (j = 0, pointsLength = properties.linePoints.length; j < pointsLength; j += 1) {
|
|
distance = Vec3.distance(position, Vec3.sum(basePosition, properties.linePoints[j]));
|
|
if (distance <= foundDistance) {
|
|
found = true;
|
|
foundID = entities[i];
|
|
foundDistance = distance;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete found entity.
|
|
if (found) {
|
|
Entities.deleteEntity(foundID);
|
|
}
|
|
}
|
|
|
|
function tearDown() {
|
|
cancelLine();
|
|
}
|
|
|
|
return {
|
|
startLine: startLine,
|
|
drawLine: drawLine,
|
|
finishLine: finishLine,
|
|
cancelLine: cancelLine,
|
|
eraseClosestLine: eraseClosestLine,
|
|
tearDown: tearDown
|
|
};
|
|
}
|
|
|
|
function handController(name) {
|
|
// Translates controller data into application events.
|
|
var handName = name,
|
|
|
|
triggerPressedCallback,
|
|
triggerPressingCallback,
|
|
triggerReleasedCallback,
|
|
gripPressedCallback,
|
|
|
|
rawTriggerValue = 0.0,
|
|
triggerValue = 0.0,
|
|
isTriggerPressed = false,
|
|
TRIGGER_SMOOTH_RATIO = 0.1,
|
|
TRIGGER_OFF = 0.05,
|
|
TRIGGER_ON = 0.1,
|
|
TRIGGER_START_WIDTH_RAMP = 0.15,
|
|
TRIGGER_FINISH_WIDTH_RAMP = 1.0,
|
|
TRIGGER_RAMP_WIDTH = TRIGGER_FINISH_WIDTH_RAMP - TRIGGER_START_WIDTH_RAMP,
|
|
MIN_LINE_WIDTH = 0.005,
|
|
MAX_LINE_WIDTH = 0.03,
|
|
RAMP_LINE_WIDTH = MAX_LINE_WIDTH - MIN_LINE_WIDTH,
|
|
|
|
rawGripValue = 0.0,
|
|
gripValue = 0.0,
|
|
isGripPressed = false,
|
|
GRIP_SMOOTH_RATIO = 0.1,
|
|
GRIP_OFF = 0.05,
|
|
GRIP_ON = 0.1;
|
|
|
|
function onTriggerPress(value) {
|
|
// Controller values are only updated when they change so store latest for use in update.
|
|
rawTriggerValue = value;
|
|
}
|
|
|
|
function updateTriggerPress(value) {
|
|
var wasTriggerPressed,
|
|
fingerTipPosition,
|
|
lineWidth;
|
|
|
|
triggerValue = triggerValue * TRIGGER_SMOOTH_RATIO + rawTriggerValue * (1.0 - TRIGGER_SMOOTH_RATIO);
|
|
|
|
wasTriggerPressed = isTriggerPressed;
|
|
if (isTriggerPressed) {
|
|
isTriggerPressed = triggerValue > TRIGGER_OFF;
|
|
} else {
|
|
isTriggerPressed = triggerValue > TRIGGER_ON;
|
|
}
|
|
|
|
if (wasTriggerPressed || isTriggerPressed) {
|
|
fingerTipPosition = MyAvatar.getJointPosition(handName === "left" ? "LeftHandIndex4" : "RightHandIndex4");
|
|
if (triggerValue < TRIGGER_START_WIDTH_RAMP) {
|
|
lineWidth = MIN_LINE_WIDTH;
|
|
} else {
|
|
lineWidth = MIN_LINE_WIDTH
|
|
+ (triggerValue - TRIGGER_START_WIDTH_RAMP) / TRIGGER_RAMP_WIDTH * RAMP_LINE_WIDTH;
|
|
}
|
|
|
|
if (!wasTriggerPressed && isTriggerPressed) {
|
|
triggerPressedCallback(fingerTipPosition, lineWidth);
|
|
} else if (wasTriggerPressed && isTriggerPressed) {
|
|
triggerPressingCallback(fingerTipPosition, lineWidth);
|
|
} else {
|
|
triggerReleasedCallback(fingerTipPosition, lineWidth);
|
|
}
|
|
}
|
|
}
|
|
|
|
function onGripPress(value) {
|
|
// Controller values are only updated when they change so store latest for use in update.
|
|
rawGripValue = value;
|
|
}
|
|
|
|
function updateGripPress() {
|
|
var fingerTipPosition;
|
|
|
|
gripValue = gripValue * GRIP_SMOOTH_RATIO + rawGripValue * (1.0 - GRIP_SMOOTH_RATIO);
|
|
|
|
if (isGripPressed) {
|
|
isGripPressed = gripValue > GRIP_OFF;
|
|
} else {
|
|
isGripPressed = gripValue > GRIP_ON;
|
|
if (isGripPressed) {
|
|
fingerTipPosition = MyAvatar.getJointPosition(handName === "left" ? "LeftHandIndex4" : "RightHandIndex4");
|
|
gripPressedCallback(fingerTipPosition);
|
|
}
|
|
}
|
|
}
|
|
|
|
function onUpdate() {
|
|
updateTriggerPress();
|
|
updateGripPress();
|
|
}
|
|
|
|
function setUp(onTriggerPressed, onTriggerPressing, onTriggerReleased, onGripPressed) {
|
|
triggerPressedCallback = onTriggerPressed;
|
|
triggerPressingCallback = onTriggerPressing;
|
|
triggerReleasedCallback = onTriggerReleased;
|
|
gripPressedCallback = onGripPressed;
|
|
}
|
|
|
|
function tearDown() {
|
|
// Nothing to do.
|
|
}
|
|
|
|
return {
|
|
onTriggerPress: onTriggerPress,
|
|
onGripPress: onGripPress,
|
|
onUpdate: onUpdate,
|
|
setUp: setUp,
|
|
tearDown: tearDown
|
|
};
|
|
}
|
|
|
|
function updateHandFunctions() {
|
|
// Update other scripts' hand functions.
|
|
var enabled = !isFingerPainting || isTabletDisplayed;
|
|
|
|
Messages.sendMessage(HIFI_GRAB_DISABLE_MESSAGE_CHANNEL, JSON.stringify({
|
|
holdEnabled: enabled,
|
|
nearGrabEnabled: enabled,
|
|
farGrabEnabled: enabled
|
|
}), true);
|
|
Messages.sendMessage(HIFI_POINTER_DISABLE_MESSAGE_CHANNEL, JSON.stringify({
|
|
pointerEnabled: enabled
|
|
}), true);
|
|
|
|
var newShouldPointFingers = !enabled;
|
|
if (newShouldPointFingers !== shouldPointFingers) {
|
|
Messages.sendMessage(HIFI_POINT_INDEX_MESSAGE_CHANNEL, JSON.stringify({
|
|
pointIndex: newShouldPointFingers
|
|
}), true);
|
|
shouldPointFingers = newShouldPointFingers;
|
|
}
|
|
}
|
|
|
|
function howToExitTutorial() {
|
|
HMD.requestShowHandControllers();
|
|
setControllerPartLayer('button_b', 'highlight');
|
|
messageWindow = Window.alert(HOW_TO_EXIT_MESSAGE);
|
|
setControllerPartLayer('button_b', 'blank');
|
|
HMD.requestHideHandControllers();
|
|
Settings.setValue("FingerPaintTutorialComplete", true);
|
|
}
|
|
|
|
function enableProcessing() {
|
|
// Connect controller API to handController objects.
|
|
leftHand = handController("left");
|
|
rightHand = handController("right");
|
|
var controllerMapping = Controller.newMapping(CONTROLLER_MAPPING_NAME);
|
|
controllerMapping.from(Controller.Standard.LT).to(leftHand.onTriggerPress);
|
|
controllerMapping.from(Controller.Standard.LeftGrip).to(leftHand.onGripPress);
|
|
controllerMapping.from(Controller.Standard.RT).to(rightHand.onTriggerPress);
|
|
controllerMapping.from(Controller.Standard.RightGrip).to(rightHand.onGripPress);
|
|
controllerMapping.from(Controller.Standard.B).to(onButtonClicked);
|
|
Controller.enableMapping(CONTROLLER_MAPPING_NAME);
|
|
|
|
if (!Settings.getValue("FingerPaintTutorialComplete")) {
|
|
howToExitTutorial();
|
|
}
|
|
|
|
// Connect handController outputs to paintBrush objects.
|
|
leftBrush = paintBrush("left");
|
|
leftHand.setUp(leftBrush.startLine, leftBrush.drawLine, leftBrush.finishLine, leftBrush.eraseClosestLine);
|
|
rightBrush = paintBrush("right");
|
|
rightHand.setUp(rightBrush.startLine, rightBrush.drawLine, rightBrush.finishLine, rightBrush.eraseClosestLine);
|
|
|
|
// Messages channels for enabling/disabling other scripts' functions.
|
|
Messages.subscribe(HIFI_POINT_INDEX_MESSAGE_CHANNEL);
|
|
Messages.subscribe(HIFI_GRAB_DISABLE_MESSAGE_CHANNEL);
|
|
Messages.subscribe(HIFI_POINTER_DISABLE_MESSAGE_CHANNEL);
|
|
|
|
// Update hand controls.
|
|
Script.update.connect(leftHand.onUpdate);
|
|
Script.update.connect(rightHand.onUpdate);
|
|
}
|
|
|
|
function disableProcessing() {
|
|
Script.update.disconnect(leftHand.onUpdate);
|
|
Script.update.disconnect(rightHand.onUpdate);
|
|
|
|
Controller.disableMapping(CONTROLLER_MAPPING_NAME);
|
|
|
|
leftBrush.tearDown();
|
|
leftBrush = null;
|
|
leftHand.tearDown();
|
|
leftHand = null;
|
|
|
|
rightBrush.tearDown();
|
|
rightBrush = null;
|
|
rightHand.tearDown();
|
|
rightHand = null;
|
|
|
|
Messages.unsubscribe(HIFI_POINT_INDEX_MESSAGE_CHANNEL);
|
|
Messages.unsubscribe(HIFI_GRAB_DISABLE_MESSAGE_CHANNEL);
|
|
Messages.unsubscribe(HIFI_POINTER_DISABLE_MESSAGE_CHANNEL);
|
|
}
|
|
|
|
function onButtonClicked() {
|
|
var wasFingerPainting = isFingerPainting;
|
|
|
|
isFingerPainting = !isFingerPainting;
|
|
button.editProperties({ isActive: isFingerPainting });
|
|
|
|
print("Finger painting: " + isFingerPainting ? "on" : "off");
|
|
|
|
if (wasFingerPainting) {
|
|
leftBrush.cancelLine();
|
|
rightBrush.cancelLine();
|
|
}
|
|
|
|
if (isFingerPainting) {
|
|
enableProcessing();
|
|
}
|
|
|
|
updateHandFunctions();
|
|
|
|
if (!isFingerPainting) {
|
|
disableProcessing();
|
|
}
|
|
}
|
|
|
|
function onTabletScreenChanged(type, url) {
|
|
var TABLET_SCREEN_CLOSED = "Closed";
|
|
|
|
isTabletDisplayed = type !== TABLET_SCREEN_CLOSED;
|
|
updateHandFunctions();
|
|
}
|
|
|
|
function setUp() {
|
|
tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
|
if (!tablet) {
|
|
return;
|
|
}
|
|
|
|
// Tablet button.
|
|
button = tablet.addButton({
|
|
icon: "icons/tablet-icons/finger-paint-i.svg",
|
|
activeIcon: "icons/tablet-icons/finger-paint-a.svg",
|
|
text: BUTTON_NAME,
|
|
isActive: isFingerPainting
|
|
});
|
|
button.clicked.connect(onButtonClicked);
|
|
|
|
// Track whether tablet is displayed or not.
|
|
tablet.screenChanged.connect(onTabletScreenChanged);
|
|
}
|
|
|
|
function tearDown() {
|
|
if (!tablet) {
|
|
return;
|
|
}
|
|
|
|
if (isFingerPainting) {
|
|
isFingerPainting = false;
|
|
updateHandFunctions();
|
|
disableProcessing();
|
|
}
|
|
|
|
tablet.screenChanged.disconnect(onTabletScreenChanged);
|
|
|
|
button.clicked.disconnect(onButtonClicked);
|
|
tablet.removeButton(button);
|
|
}
|
|
|
|
/**
|
|
* A controller is made up of parts, and each part can have multiple "layers,"
|
|
* which are really just different texures. For example, the "trigger" part
|
|
* has "normal" and "highlight" layers.
|
|
*/
|
|
function setControllerPartLayer(part, layer) {
|
|
data = {};
|
|
data[part] = layer;
|
|
Messages.sendLocalMessage('Controller-Set-Part-Layer', JSON.stringify(data));
|
|
}
|
|
|
|
setUp();
|
|
Script.scriptEnding.connect(tearDown);
|
|
}());
|