// // 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); }());