diff --git a/interface/resources/icons/tablet-icons/finger-paint-a.svg b/interface/resources/icons/tablet-icons/finger-paint-a.svg
new file mode 100644
index 0000000000..acc93608d9
--- /dev/null
+++ b/interface/resources/icons/tablet-icons/finger-paint-a.svg
@@ -0,0 +1,66 @@
+
+
+
+
\ No newline at end of file
diff --git a/interface/resources/icons/tablet-icons/finger-paint-i.svg b/interface/resources/icons/tablet-icons/finger-paint-i.svg
new file mode 100644
index 0000000000..b295727d8a
--- /dev/null
+++ b/interface/resources/icons/tablet-icons/finger-paint-i.svg
@@ -0,0 +1,30 @@
+
+
+
diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js
index 6bebbf0498..f8a336a017 100644
--- a/scripts/system/controllers/handControllerPointer.js
+++ b/scripts/system/controllers/handControllerPointer.js
@@ -480,6 +480,10 @@ var LASER_SEARCH_COLOR_XYZW = {x: 10 / 255, y: 10 / 255, z: 255 / 255, w: LASER_
var LASER_TRIGGER_COLOR_XYZW = {x: 250 / 255, y: 10 / 255, z: 10 / 255, w: LASER_ALPHA};
var SYSTEM_LASER_DIRECTION = {x: 0, y: 0, z: -1};
var systemLaserOn = false;
+
+var HIFI_POINTER_DISABLE_MESSAGE_CHANNEL = "Hifi-Pointer-Disable";
+var isPointerEnabled = true;
+
function clearSystemLaser() {
if (!systemLaserOn) {
return;
@@ -542,9 +546,8 @@ function update() {
return off();
}
-
// If there's a HUD element at the (newly moved) reticle, just make it visible and bail.
- if (isPointingAtOverlay(hudPoint2d)) {
+ if (isPointingAtOverlay(hudPoint2d) && isPointerEnabled) {
if (HMD.active) {
Reticle.depth = hudReticleDistance();
@@ -579,9 +582,25 @@ function checkSettings() {
}
checkSettings();
+// Enable/disable pointer.
+function handleMessages(channel, message, sender) {
+ if (sender === MyAvatar.sessionUUID && channel === HIFI_POINTER_DISABLE_MESSAGE_CHANNEL) {
+ var data = JSON.parse(message);
+ if (data.pointerEnabled !== undefined) {
+ print("pointerEnabled: " + data.pointerEnabled);
+ isPointerEnabled = data.pointerEnabled;
+ }
+ }
+}
+
+Messages.subscribe(HIFI_POINTER_DISABLE_MESSAGE_CHANNEL);
+Messages.messageReceived.connect(handleMessages);
+
var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL);
Script.update.connect(update);
Script.scriptEnding.connect(function () {
+ Messages.unsubscribe(HIFI_POINTER_DISABLE_MESSAGE_CHANNEL);
+ Messages.messageReceived.disconnect(handleMessages);
Script.clearInterval(settingsChecker);
Script.update.disconnect(update);
OffscreenFlags.navigationFocusDisabled = false;
diff --git a/scripts/system/controllers/squeezeHands.js b/scripts/system/controllers/squeezeHands.js
index 1e94c29521..3f1d21b46c 100644
--- a/scripts/system/controllers/squeezeHands.js
+++ b/scripts/system/controllers/squeezeHands.js
@@ -25,6 +25,11 @@ var OVERLAY_RAMP_RATE = 8.0;
var animStateHandlerID;
+var isPointingIndex = false;
+var HIFI_POINT_INDEX_MESSAGE_CHANNEL = "Hifi-Point-Index";
+
+var indexfingerJointNames = ["LeftHandIndex1", "LeftHandIndex2", "LeftHandIndex3", "RightHandIndex1", "RightHandIndex2", "RightHandIndex3"];
+
function clamp(val, min, max) {
return Math.min(Math.max(val, min), max);
}
@@ -43,6 +48,8 @@ function init() {
animStateHandler,
["leftHandOverlayAlpha", "rightHandOverlayAlpha", "leftHandGraspAlpha", "rightHandGraspAlpha"]
);
+ Messages.subscribe(HIFI_POINT_INDEX_MESSAGE_CHANNEL);
+ Messages.messageReceived.connect(handleMessages);
}
function animStateHandler(props) {
@@ -76,11 +83,37 @@ function update(dt) {
} else {
rightHandOverlayAlpha = clamp(rightHandOverlayAlpha - OVERLAY_RAMP_RATE * dt, 0, 1);
}
+
+ // Point index finger.
+ if (isPointingIndex) {
+ var zeroRotation = { x: 0, y: 0, z: 0, w: 1 };
+ for (var i = 0; i < indexfingerJointNames.length; i++) {
+ MyAvatar.setJointRotation(indexfingerJointNames[i], zeroRotation);
+ }
+ }
+}
+
+function handleMessages(channel, message, sender) {
+ if (sender === MyAvatar.sessionUUID && channel === HIFI_POINT_INDEX_MESSAGE_CHANNEL) {
+ var data = JSON.parse(message);
+ if (data.pointIndex !== undefined) {
+ print("pointIndex: " + data.pointIndex);
+ isPointingIndex = data.pointIndex;
+
+ if (!isPointingIndex) {
+ for (var i = 0; i < indexfingerJointNames.length; i++) {
+ MyAvatar.clearJointData(indexfingerJointNames[i]);
+ }
+ }
+ }
+ }
}
function shutdown() {
Script.update.disconnect(update);
MyAvatar.removeAnimationStateHandler(animStateHandlerID);
+ Messages.unsubscribe(HIFI_POINT_INDEX_MESSAGE_CHANNEL);
+ Messages.messageReceived.disconnect(handleMessages);
}
Script.scriptEnding.connect(shutdown);
diff --git a/scripts/system/fingerPaint.js b/scripts/system/fingerPaint.js
new file mode 100644
index 0000000000..959f594212
--- /dev/null
+++ b/scripts/system/fingerPaint.js
@@ -0,0 +1,433 @@
+//
+// 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,
+ 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";
+
+ 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);
+ Messages.sendMessage(HIFI_POINT_INDEX_MESSAGE_CHANNEL, JSON.stringify({
+ pointIndex: !enabled
+ }), 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);
+ Controller.enableMapping(CONTROLLER_MAPPING_NAME);
+
+ // 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);
+ }
+
+ setUp();
+ Script.scriptEnding.connect(tearDown);
+}());
\ No newline at end of file