diff --git a/applications/flow/flow-a.svg b/applications/flow/flow-a.svg
new file mode 100644
index 0000000..e304f36
--- /dev/null
+++ b/applications/flow/flow-a.svg
@@ -0,0 +1,55 @@
+
+
+
+
\ No newline at end of file
diff --git a/applications/flow/flow-i.svg b/applications/flow/flow-i.svg
new file mode 100644
index 0000000..cbb2fe4
--- /dev/null
+++ b/applications/flow/flow-i.svg
@@ -0,0 +1,58 @@
+
+
+
+
\ No newline at end of file
diff --git a/applications/flow/flowAppCpp.html b/applications/flow/flowAppCpp.html
new file mode 100644
index 0000000..27781e9
--- /dev/null
+++ b/applications/flow/flowAppCpp.html
@@ -0,0 +1,393 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/applications/flow/flowAppCpp.js b/applications/flow/flowAppCpp.js
new file mode 100644
index 0000000..bfca970
--- /dev/null
+++ b/applications/flow/flowAppCpp.js
@@ -0,0 +1,495 @@
+//
+// Created by Luis Cuenca on 3/8/19
+// Copyright 2018 High Fidelity, Inc.
+// Copyright 2023 Overte e.V.
+//
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+/* jslint bitwise: true */
+/* global Script, MyAvatar, Tablet
+*/
+
+(function () {
+ // Adding some additional time before load fix an issue when updating the debugging overlays (local entities)
+ var MS_AFTER_LOADING = 500;
+ Script.setTimeout(function() {
+ Script.registerValue("FLOWAPP", true);
+
+ var TABLET_BUTTON_NAME = "FLOW";
+ var HTML_URL = Script.resolvePath("./flowAppCpp.html");
+
+ var SHOW_AVATAR = true;
+ var SHOW_DEBUG_SHAPES = false;
+ var SHOW_SOLID_SHAPES = false;
+ var POLYLINE_TEXTURE = Script.resolvePath("./polylineTexture.png");
+
+ var USE_COLLISIONS = false;
+ var IS_ACTIVE = false;
+
+ var MSG_DOCUMENT_LOADED = 0;
+ var MSG_JOINT_INPUT_DATA = 1;
+ var MSG_COLLISION_DATA = 2;
+ var MSG_COLLISION_INPUT_DATA = 3;
+ var MSG_DISPLAY_DATA = 4;
+ var MSG_CREATE = 5;
+
+ var avatarScale = MyAvatar.getSensorToWorldScale();
+
+ var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
+ var tabletButton = tablet.addButton({
+ text: TABLET_BUTTON_NAME,
+ icon: Script.resolvePath("./flow-i.svg"),
+ activeIcon: Script.resolvePath("./flow-a.svg")
+ });
+
+ var FlowDebug = function() {
+ var self = this;
+ this.debugLines = {};
+ this.debugSpheres = {};
+ this.showDebugShapes = false;
+ this.showSolidShapes = "lines"; // wireframe
+
+ this.setDebugLine = function(lineName, norms, startPosition, endPosition, shapeColor, width, forceRendering) {
+ var doRender = self.showDebugShapes || forceRendering;
+ if (!doRender) {
+ return;
+ }
+ var start = startPosition ? startPosition : {x: 0, y: 0, z: 0};
+ var end = endPosition ? endPosition : {x: 0, y: 1, z: 0};
+ var color = shapeColor ? shapeColor : { red: 0, green: 255, blue: 255 };
+ if (self.debugLines[lineName] !== undefined) {
+ Entities.editEntity(self.debugLines[lineName], {
+ color: color,
+ linePoints: [start, end],
+ strokeWidths: [width, width],
+ });
+ } else {
+ self.debugLines[lineName] = Entities.addEntity({
+ type: "PolyLine",
+ textures: POLYLINE_TEXTURE,
+ color: color,
+ normals: norms,
+ linePoints: [start, end],
+ visible: true,
+ strokeWidths: [width, width],
+ collisionless: true
+ }, "local");
+ }
+ };
+
+ this.setDebugSphere = function(sphereName, pos, diameter, shapeColor, forceRendering) {
+ var doRender = self.showDebugShapes || forceRendering;
+ if (!doRender) {
+ return;
+ }
+ var DEFAULT_SPHERE_DIAMETER = 0.02;
+ var scale = diameter ? diameter : DEFAULT_SPHERE_DIAMETER;
+ var color = shapeColor ? shapeColor : { red: 255, green: 0, blue: 255 };
+ if (self.debugSpheres[sphereName] !== undefined) {
+ Entities.editEntity(self.debugSpheres[sphereName], {
+ color: color,
+ position: pos,
+ dimensions: {x: scale, y: scale, z: scale},
+ primitiveMode: self.showSolidShapes,
+ });
+ } else {
+ self.debugSpheres[sphereName] = Entities.addEntity({
+ type: "Sphere",
+ color: color,
+ position: pos,
+ dimensions: {x: scale, y: scale, z: scale},
+ primitiveMode: self.showSolidShapes,
+ visible: true,
+ collisionless: true
+ }, "local");
+ }
+ };
+
+ this.deleteSphere = function(name) {
+ Entities.deleteEntity(self.debugSpheres[name]);
+ self.debugSpheres[name] = undefined;
+ };
+
+ this.deleteLine = function(name) {
+ Entities.deleteEntity(self.debugLines[name]);
+ self.debugLines[name] = undefined;
+ };
+
+ this.cleanup = function() {
+ for (var lineName in self.debugLines) {
+ if (lineName !== undefined) {
+ self.deleteLine(lineName);
+ }
+ }
+ for (var sphereName in self.debugSpheres) {
+ if (sphereName !== undefined) {
+ self.deleteSphere(sphereName);
+ }
+ }
+ self.debugLines = {};
+ self.debugSpheres = {};
+ };
+
+ this.setVisible = function(isVisible) {
+ self.showDebugShapes = isVisible;
+ for (var lineName in self.debugLines) {
+ if (lineName !== undefined) {
+ Entities.editEntity(self.debugLines[lineName], {
+ visible: isVisible
+ });
+ }
+ }
+ for (var sphereName in self.debugSpheres) {
+ if (sphereName !== undefined) {
+ Entities.editEntity(self.debugSpheres[sphereName], {
+ visible: isVisible
+ });
+ }
+ }
+ };
+
+ this.setSolid = function(isSolid) {
+ if(isSolid) {
+ self.showSolidShapes = "solid"
+ } else {
+ self.showSolidShapes = "lines"
+ }
+ for (var lineName in self.debugLines) {
+ if (lineName !== undefined) {
+ Entities.editEntity(self.debugLines[lineName], {
+ primitiveMode: self.showSolidShapes
+ });
+ }
+ }
+ for (var sphereName in self.debugSpheres) {
+ if (sphereName !== undefined) {
+ Entities.editEntity(self.debugSpheres[sphereName], {
+ primitiveMode: self.showSolidShapes
+ });
+ }
+ }
+ };
+
+ };
+
+ var flowData, initActive, initColliding, initDebugging;
+ updateFlowData(true);
+ var collisionDebug = new FlowDebug();
+ var jointDebug = new FlowDebug();
+
+ collisionDebug.setVisible(SHOW_DEBUG_SHAPES);
+ collisionDebug.setSolid(SHOW_SOLID_SHAPES);
+
+ MyAvatar.setEnableMeshVisible(SHOW_AVATAR);
+ jointDebug.setVisible(SHOW_DEBUG_SHAPES);
+ jointDebug.setSolid(SHOW_SOLID_SHAPES);
+
+ var shown = false;
+
+ function manageClick() {
+ if (shown) {
+ MyAvatar.useFlow(initActive, initColliding);
+ initDebugging = SHOW_DEBUG_SHAPES;
+ if (SHOW_DEBUG_SHAPES) {
+ toggleDebugShapes();
+ }
+ tablet.gotoHomeScreen();
+ } else {
+ updateFlowData();
+ tablet.gotoWebScreen(HTML_URL);
+ }
+ }
+
+ tabletButton.clicked.connect(manageClick);
+
+ function onScreenChanged(type, url) {
+ console.log("Screen changed");
+ if (type === "Web" && url === HTML_URL) {
+ tabletButton.editProperties({isActive: true});
+ if (!shown) {
+ // hook up to event bridge
+ tablet.webEventReceived.connect(onWebEventReceived);
+ }
+ shown = true;
+ } else {
+ tabletButton.editProperties({isActive: false});
+ if (shown) {
+ // disconnect from event bridge
+ tablet.webEventReceived.disconnect(onWebEventReceived);
+ }
+ shown = false;
+ }
+ }
+
+ var toggleAvatarVisible = function() {
+ SHOW_AVATAR = !SHOW_AVATAR;
+ MyAvatar.setEnableMeshVisible(SHOW_AVATAR);
+ };
+
+ var toggleDebugShapes = function() {
+ SHOW_DEBUG_SHAPES = !SHOW_DEBUG_SHAPES;
+ if (USE_COLLISIONS) {
+ collisionDebug.setVisible(SHOW_DEBUG_SHAPES);
+ }
+ jointDebug.setVisible(SHOW_DEBUG_SHAPES);
+ };
+
+ var toggleSolidShapes = function() {
+ SHOW_SOLID_SHAPES = !SHOW_SOLID_SHAPES;
+ collisionDebug.setSolid(SHOW_SOLID_SHAPES);
+ jointDebug.setSolid(SHOW_SOLID_SHAPES);
+ };
+
+ var toggleCollisions = function() {
+ USE_COLLISIONS = !USE_COLLISIONS;
+ if (USE_COLLISIONS && SHOW_DEBUG_SHAPES) {
+ collisionDebug.setVisible(true);
+ } else {
+ collisionDebug.setVisible(false);
+ }
+ MyAvatar.useFlow(IS_ACTIVE, USE_COLLISIONS);
+ };
+
+ var getDisplayData = function() {
+ return {"avatar": SHOW_AVATAR,
+ "collisions": USE_COLLISIONS,
+ "debug": SHOW_DEBUG_SHAPES,
+ "solid": SHOW_SOLID_SHAPES};
+ };
+
+ var jointNames = MyAvatar.getJointNames();
+
+ function roundFloat(number, decimals) {
+ var DECIMALS_MULTIPLIER = 10;
+ var multiplier = Math.pow(DECIMALS_MULTIPLIER, decimals);
+ var rounded = Math.round(number * multiplier);
+ return rounded / multiplier;
+ }
+
+ function roundVector(vector, decimals) {
+ return {x: roundFloat(vector.x, decimals), y: roundFloat(vector.y, decimals), z: roundFloat(vector.z, decimals)};
+ }
+
+ function roundDataValues(decimals) {
+ var collisions = flowData.collisions;
+ var physics = flowData.physics;
+ Object.keys(collisions).forEach(function(key) {
+ var data = collisions[key];
+ data.radius = roundFloat(data.radius, decimals);
+ data.offset = roundVector(data.offset, decimals);
+ });
+ Object.keys(physics).forEach(function(key) {
+ var data = physics[key];
+ data.damping = roundFloat(data.damping, decimals);
+ data.delta = roundFloat(data.delta, decimals);
+ data.gravity = roundFloat(data.gravity, decimals);
+ data.inertia = roundFloat(data.inertia, decimals);
+ data.radius = roundFloat(data.radius, decimals);
+ data.stiffness = roundFloat(data.stiffness, decimals);
+ });
+ }
+
+ function updateFlowData(catchInitValues) {
+ flowData = MyAvatar.getFlowData();
+ if (typeof(flowData) !== "object" || typeof(flowData.collisions) !== "object") {
+ return;
+ }
+ var ROUND_DECIMALS = 4;
+ var collisionJoints = Object.keys(flowData.collisions);
+ var inverseScale = 1.0 / avatarScale;
+ for (var i = 0; i < collisionJoints.length; i++) {
+ var collision = flowData.collisions[collisionJoints[i]];
+ collision.radius *= inverseScale;
+ collision.offset = Vec3.multiply(collision.offset, inverseScale);
+ }
+ roundDataValues(ROUND_DECIMALS);
+ IS_ACTIVE = flowData.active;
+ USE_COLLISIONS = flowData.colliding;
+ if (catchInitValues) {
+ initActive = flowData.active;
+ initColliding = flowData.colliding;
+ }
+ }
+
+ function onWebEventReceived(msg) {
+ var message = JSON.parse(msg);
+ switch (message.type) {
+ case MSG_JOINT_INPUT_DATA: {
+ flowData.physics[message.group][message.name] = message.value;
+ MyAvatar.useFlow(IS_ACTIVE, USE_COLLISIONS, flowData.physics, flowData.collisions);
+ break;
+ }
+ case MSG_COLLISION_INPUT_DATA: {
+ var value = message.name === "offset" ? {x: 0.0, y: message.value, z: 0.0} : message.value;
+ flowData.collisions[message.group][message.name] = value;
+ MyAvatar.useFlow(IS_ACTIVE, USE_COLLISIONS, flowData.physics, flowData.collisions);
+ break;
+ }
+ case MSG_DISPLAY_DATA: {
+ switch (message.name) {
+ case "collisions":
+ toggleCollisions();
+ break;
+ case "debug":
+ toggleDebugShapes();
+ break;
+ case "solid":
+ toggleSolidShapes();
+ break;
+ case "avatar":
+ toggleAvatarVisible();
+ break;
+ }
+ break;
+ }
+ case MSG_DOCUMENT_LOADED: {
+ MyAvatar.useFlow(true, true);
+ updateFlowData();
+ if (initDebugging && !SHOW_DEBUG_SHAPES) {
+ toggleDebugShapes();
+ }
+ createHTMLMenu();
+ break;
+ }
+ case MSG_COLLISION_DATA: {
+ switch (message.name) {
+ case "add":
+ var collisionData = {"type": "sphere", "radius": 0.05, "offset": {"x": 0.0, "y": 0.0, "z": 0.0}};
+ flowData.collisions[message.value] = collisionData;
+ MyAvatar.useFlow(IS_ACTIVE, USE_COLLISIONS, flowData.physics, flowData.collisions);
+ updateFlowData();
+ tablet.emitScriptEvent(JSON.stringify({
+ "type": MSG_COLLISION_DATA,
+ "name": message.value,
+ "data": collisionData
+ }));
+ break;
+ case "remove":
+ var jointName = message.value;
+ collisionDebug.deleteSphere(jointName + "_col");
+ if (flowData.collisions[jointName] !== undefined) {
+ delete flowData.collisions[jointName];
+ MyAvatar.useFlow(IS_ACTIVE, USE_COLLISIONS, flowData.physics, flowData.collisions);
+ updateFlowData();
+ }
+ break;
+ }
+ break;
+ }
+ }
+ }
+
+ tablet.screenChanged.connect(onScreenChanged);
+
+ function createHTMLMenu() {
+ jointNames = MyAvatar.getJointNames();
+ tablet.emitScriptEvent(JSON.stringify(
+ {
+ "type": MSG_CREATE,
+ "data": {
+ "display": getDisplayData(),
+ "group": flowData.physics,
+ "collisions": flowData.collisions,
+ "joints": jointNames
+ }
+ }
+ ));
+ }
+
+ function shutdownTabletApp() {
+ MyAvatar.useFlow(initActive, initColliding);
+ tablet.removeButton(tabletButton);
+ if (shown) {
+ tablet.webEventReceived.disconnect(onWebEventReceived);
+ tablet.gotoHomeScreen();
+ }
+ tablet.screenChanged.disconnect(onScreenChanged);
+ }
+
+ MyAvatar.skeletonChanged.connect(function() {
+ var MS_AFTER_AVATAR_UPDATE = 200;
+ collisionDebug.cleanup();
+ jointDebug.cleanup();
+ Script.setTimeout(function() {
+ jointNames = MyAvatar.getJointNames();
+ avatarScale = MyAvatar.getSensorToWorldScale();
+ updateFlowData(true);
+ if (shown) {
+ manageClick();
+ }
+ }, MS_AFTER_AVATAR_UPDATE);
+ });
+
+ MyAvatar.scaleChanged.connect(function() {
+ avatarScale = MyAvatar.getSensorToWorldScale();
+ });
+
+ Script.update.connect(function() {
+ if (IS_ACTIVE) {
+ var groupData = flowData.physics;
+ var collisionData = flowData.collisions;
+ var threads = flowData.threads;
+ var groups = Object.keys(groupData);
+ var flowPositions = Array(jointNames.length);
+ var flowCollisionColors = Array(jointNames.length);
+ var collidingJoints = MyAvatar.getCollidingFlowJoints();
+ for (var i = 0; i < groups.length; i++) {
+ var group = groups[i];
+ var data = groupData[group];
+ for (var j = 0; j < data.jointIndices.length; j++) {
+ var index = data.jointIndices[j];
+ var name = jointNames[index];
+ var position = MyAvatar.getJointPosition(index);
+ flowPositions[index] = position;
+ var colliding = collidingJoints.indexOf(index) > -1;
+ var color = { red: 255, green: 255, blue: 0 };
+ if (colliding) {
+ color.green = 0;
+ }
+ flowCollisionColors[index] = color;
+ var radius = 2.0 * avatarScale * data.radius;
+ jointDebug.setDebugSphere(name + "_flow", position, radius, color);
+ }
+ }
+ var names = Object.keys(collisionData);
+ for (i = 0; i < names.length; i++) {
+ name = names[i];
+ index = collisionData[name].jointIndex;
+
+ var offset = Vec3.multiply(collisionData[name].offset, avatarScale);
+ radius = avatarScale * collisionData[name].radius;
+ position = MyAvatar.jointToWorldPoint(offset, index);
+ collisionDebug.setDebugSphere(name + "_col", position, 2 * radius, {red: 200, green: 10, blue: 50});
+ }
+ var threadKeys = Object.keys(threads);
+ for (i = 0; i < threadKeys.length; i++) {
+ var thread = threads[threadKeys[i]];
+ for (j = 1; j < thread.length; j++) {
+ var index1 = thread[j-1];
+ var index2 = thread[j];
+ if (flowPositions[index1] !== undefined && flowPositions[index2] !== undefined) {
+ var lineName = jointNames[index1] + "_line";
+ color = flowCollisionColors[index1];
+ var DEFAULT_LINE_WIDTH = 0.004;
+ var lineWidth = DEFAULT_LINE_WIDTH * avatarScale;
+ // We are creating two PolyLines with different normals, to make them more visible from the sides.
+ var LINE_NORMALS_1 = [{ x: 0, y: 0, z: 1 }, { x: 0, y: 0, z: 1 }];
+ var LINE_NORMALS_2 = [{ x: 1, y: 0, z: 0 }, { x: 1, y: 0, z: 0 }];
+ jointDebug.setDebugLine(lineName + "_1", LINE_NORMALS_1, flowPositions[index1], flowPositions[index2], color, lineWidth);
+ jointDebug.setDebugLine(lineName + "_2", LINE_NORMALS_2, flowPositions[index1], flowPositions[index2], color, lineWidth);
+ }
+ }
+ }
+ }
+ });
+
+ Script.scriptEnding.connect(function () {
+ collisionDebug.cleanup();
+ jointDebug.cleanup();
+ shutdownTabletApp();
+ });
+ }, MS_AFTER_LOADING);
+
+}());
diff --git a/applications/flow/polylineTexture.png b/applications/flow/polylineTexture.png
new file mode 100644
index 0000000..e7996c0
Binary files /dev/null and b/applications/flow/polylineTexture.png differ
diff --git a/applications/metadata.js b/applications/metadata.js
index 2398a11..9cf9be5 100644
--- a/applications/metadata.js
+++ b/applications/metadata.js
@@ -341,6 +341,15 @@ var metadata = { "applications":
"jsfile": "hmd3rdPerson/app-hmd3rdPerson.js",
"icon": "hmd3rdPerson/icon_inactive_white.png",
"caption": "3rd PERS"
+ },
+ {
+ "isActive": true,
+ "directory": "flow",
+ "name": "Flow Bones",
+ "description": "Effortlessly tweak and tune avatar flow bones.",
+ "jsfile": "flow/flowAppCpp.js",
+ "icon": "flow/flow-i.svg",
+ "caption": "FLOW"
}
]
};
\ No newline at end of file