diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..282c969 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +################################################################################ +# This .gitignore file was automatically created by Microsoft(R) Visual Studio. +################################################################################ + +/.vs/NeuroCave/FileContentIndex/27839cb6-f38a-4914-a88d-ac8ef9d3e8ec.vsidx +/.vs/NeuroCave/FileContentIndex/42c85120-e77b-4fde-8880-d78adcb8b13f.vsidx +/.vs/NeuroCave/FileContentIndex +/.vs/NeuroCave/v17 +/.vs/NeuroCave/config +/.vs diff --git a/index.html b/index.html index f010084..c66640f 100644 --- a/index.html +++ b/index.html @@ -4,8 +4,8 @@ NeuroCave - - + + - \ No newline at end of file + diff --git a/js/GUI.js b/js/GUI.js index 69a9f4a..99dc3e3 100644 --- a/js/GUI.js +++ b/js/GUI.js @@ -2,19 +2,37 @@ * Created by giorgioconte on 31/01/15. */ // this file contains functions that create\delete GUI controlling elements +//import {labelLUT, dataFiles, atlas, folder, setDataFile, setAtlas} from "../globals"; +//import {dataFiles, atlas, folder, setDataFile, setAtlas} from "../globals"; -var SHORTEST_DISTANCE = 0, NUMBER_HOPS = 1; //enums +const SHORTEST_DISTANCE = 0, NUMBER_HOPS = 1; //enums var shortestPathVisMethod = SHORTEST_DISTANCE; var thresholdMultiplier = 1.0; // 100.0 for fMRI data of values (-1.0->1.0) and 1.0 if values > 1.0 +var lockLegend = true; +var enableLeftDimLock = true; +var enableRightDimLock = true; +var enableSphereDimLock = true; +var enableBoxDimLock = true; +var rightSearching = false; +var leftSearching = false; // initialize subject selection drop down menus -initSubjectMenu = function (side) { +import {getDataFile,setDataFile,atlas} from "./globals.js"; +import { changeSceneToSubject, changeActiveGeometry, changeColorGroup, updateScenes, redrawEdges, updateOpacity, updateNodesVisiblity, getSpt, getNodesSelected, previewAreaLeft, previewAreaRight, setThresholdModality, getEnableIpsi, getEnableContra, enableIpsilaterality, enableContralaterality, enableEdgeBundling } from './drawing' +import {modelLeft,modelRight} from './model' +import {setDimensionFactorLeftSphere,setDimensionFactorRightSphere,setDimensionFactorLeftBox,setDimensionFactorRightBox} from './graphicsUtils.js' +import { scaleColorGroup } from "./utils/scale"; +import { PreviewArea } from './previewArea.js'; +import { forEach } from "./external-libraries/gl-matrix/vec3.js"; +//import * as math from 'mathjs' + +var initSubjectMenu = function (side) { var select = document.getElementById("subjectMenu" + side); - for (var i = 0; i < dataFiles.length; ++i) { + for (var i = 0; i < getDataFile().length; ++i) { var el = document.createElement("option"); - el.textContent = dataFiles[i].subjectID; - el.value = dataFiles[i].subjectID; + el.textContent = getDataFile()[i].subjectID; + el.value = getDataFile()[i].subjectID; el.selected = (i==0); select.appendChild(el); } @@ -33,31 +51,171 @@ initSubjectMenu = function (side) { }; /* Node stuff at nodeInfoPanel */ -// adds a slider to control glyphs size -addDimensionFactorSlider = function () { - var panel = d3.select("#nodeInfoPanel"); +// adds a slider to control Left of Right Sphere glyphs size +var addDimensionFactorSliderLeft = function (side) { + var panel = d3.select("#nodeInfoPanel"+side); - panel.append("input") + console.log("#nodeInfoPanel"+side); + //console.log(side); + + if(side == 'Left') { + panel.append("input") + .attr("type", "range") + .attr("value", "1") + .attr("id", "dimensionSliderLeft"+side) + .attr("min","0.2") + .attr("max", "4") + .attr("step","0.1") + .on("change", function () { + setDimensionFactorLeftSphere(this.value); + if (enableLeftDimLock) { + setDimensionFactorLeftBox(this.value); + document.getElementById("dimensionSliderRightLeft").value = this.value; + } + if (enableSphereDimLock) { + setDimensionFactorRightSphere(this.value); + document.getElementById("dimensionSliderLeftRight").value = this.value; + } + if ((enableBoxDimLock && enableLeftDimLock) || + (enableSphereDimLock && enableRightDimLock)) { + setDimensionFactorRightBox(this.value); + document.getElementById("dimensionSliderRightRight").value = this.value; + } + }); + } else { + panel.append("label") + .attr("for", "enableSphereDimLock") + .attr("id", "enableSphereDimLockLabel") + .text('\u0020\uD83D\uDD12'); + + panel.append("input") + .attr("type", "checkbox") + .attr("checked", false) + .attr("id", "enableSphereDimLock") + .on("change", function () { enableSphereDimLock = this.checked }); + + panel.append("input") + .attr("type", "range") + .attr("value", "1") + .attr("id", "dimensionSliderLeft"+side) + .attr("min","0.2") + .attr("max", "4") + .attr("step","0.1") + .on("change", function () { + setDimensionFactorRightSphere(this.value); + if (enableRightDimLock) { + setDimensionFactorRightBox(this.value); + document.getElementById("dimensionSliderRightRight").value = this.value; + } + if (enableSphereDimLock) { + setDimensionFactorLeftSphere(this.value); + document.getElementById("dimensionSliderLeftLeft").value = this.value; + } + if ((enableBoxDimLock && enableRightDimLock) || + (enableSphereDimLock && enableLeftDimLock)) { + setDimensionFactorLeftBox(this.value); + document.getElementById("dimensionSliderRightLeft").value = this.value; + } + }); + } + + panel.append("label") + .attr("for", "dimensionSlider") + .attr("id", "dimensionSliderLabel"+side) + .text(side+" Sphere Size"); + + panel.append("br"); +}; + +/* Node stuff at nodeInfoPanel */ +// adds a slider to control Left or Right Box glyphs size +var addDimensionFactorSliderRight = function (side) { + var panel = d3.select("#nodeInfoPanel"+side); + + console.log("#nodeInfoPanel"+side); + + if(side == 'Left') { + panel.append("input") + .attr("type", "range") + .attr("value", "1") + .attr("id", "dimensionSliderRight"+side) + .attr("min","0.2") + .attr("max", "4") + .attr("step","0.1") + .on("change", function () { + setDimensionFactorLeftBox(this.value); + if (enableLeftDimLock) { + setDimensionFactorLeftSphere(this.value); + document.getElementById("dimensionSliderLeftLeft").value = this.value; + } + if (enableBoxDimLock) { + setDimensionFactorRightBox(this.value); + document.getElementById("dimensionSliderRightRight").value = this.value; + } + if ((enableBoxDimLock && enableRightDimLock) || + (enableSphereDimLock && enableLeftDimLock)) { + setDimensionFactorRightSphere(this.value); + document.getElementById("dimensionSliderLeftRight").value = this.value; + } + }); + } else { + panel.append("label") + .attr("for", "enableBoxDimLock") + .attr("id", "enableBoxDimLockLabel") + .text('\u0020\uD83D\uDD12'); + + panel.append("input") + .attr("type", "checkbox") + .attr("checked", false) + .attr("id", "enableBoxDimLock") + .on("change", function () { enableBoxDimLock = this.checked }); + + panel.append("input") .attr("type", "range") .attr("value", "1") - .attr("id", "dimensionSlider") + .attr("id", "dimensionSliderRight"+side) .attr("min","0.2") .attr("max", "4") .attr("step","0.1") .on("change", function () { - setDimensionFactor(this.value); + setDimensionFactorRightBox(this.value); + if (enableRightDimLock) { + setDimensionFactorRightSphere(this.value); + document.getElementById("dimensionSliderLeftRight").value = this.value; + } + if (enableBoxDimLock) { + setDimensionFactorLeftBox(this.value); + document.getElementById("dimensionSliderRightLeft").value = this.value; + } + if ((enableBoxDimLock && enableLeftDimLock) || + (enableSphereDimLock && enableRightDimLock)) { + setDimensionFactorLeftSphere(this.value); + document.getElementById("dimensionSliderLeftLeft").value = this.value; + } + }); + } panel.append("label") .attr("for", "dimensionSlider") - .attr("id", "dimensionSliderLabel") - .text("Glyph Size"); + .attr("id", "dimensionSliderLabel"+side) + .text(side + " Box Size "); + panel.append("label") + .attr("for", "enable"+side+"DimLock") + .attr("id", "enable"+side+"DimLockLabel") + .text('\u0020\uD83D\uDD12'); + + panel.append("input") + .attr("type", "checkbox") + .attr("checked", false) + .attr("id", "enable"+side+"DimLock") + .on("change", (side == 'Left') ? function () { enableLeftDimLock = this.checked } : function () { enableRightDimLock = this.checked }); panel.append("br"); }; // adds a button to toggle skybox visibility -addSkyboxButton = function () { +var addSkyboxButton = function () { var menu = d3.select("#nodeInfoPanel"); menu.append("button") @@ -77,7 +235,7 @@ addSkyboxButton = function () { }; // adds a text label showing: label - region name - nodal strength -setNodeInfoPanel = function (region, index) { +var setNodeInfoPanel = function (region, index) { var panel = d3.select('#nodeInfoPanel'); @@ -94,7 +252,7 @@ setNodeInfoPanel = function (region, index) { /* Edges stuff at edgeInfoPanel */ // add a slider to threshold edges at specific values -addThresholdSlider = function () { +var addThresholdSlider = function () { var max = Math.max(modelLeft.getMaximumWeight(), modelRight.getMaximumWeight()); var min = Math.min(modelLeft.getMinimumWeight(), modelRight.getMinimumWeight()); @@ -123,8 +281,39 @@ addThresholdSlider = function () { modelRight.setThreshold(max/2/thresholdMultiplier); }; +/* Edges stuff at edgeInfoPanel */ +// add a slider to threshold Contralateral edges at specific values +var addConThresholdSlider = function () { + + var max = Math.max(modelLeft.getMaximumWeight(), modelRight.getMaximumWeight()); + var min = Math.min(modelLeft.getMinimumWeight(), modelRight.getMinimumWeight()); + max = Math.max(Math.abs(max), Math.abs(min)); + thresholdMultiplier = (max < 1.0) ? 100.0 : 1.0; + max *= thresholdMultiplier; + var menu = d3.select("#edgeInfoPanel"); + menu.append("input") + .attr("type", "range") + .attr("value", max / 2) + .attr("id", "conThresholdSlider") + .attr("min", 0.) + .attr("max", max) + .attr("step", max / 20) + .on("change", function () { + modelLeft.setConThreshold(this.value / thresholdMultiplier); + modelRight.setConThreshold(this.value / thresholdMultiplier); + redrawEdges(); + document.getElementById("conThresholdSliderLabel").innerHTML = "Contra Threshold @ " + this.value / thresholdMultiplier; + }); + menu.append("label") + .attr("for", "conThresholdSlider") + .attr("id", "conThresholdSliderLabel") + .text("Contra Threshold @ " + max / 2 / thresholdMultiplier); + modelLeft.setConThreshold(max / 2 / thresholdMultiplier); + modelRight.setConThreshold(max / 2 / thresholdMultiplier); +}; + // add opacity slider 0 to 1 -addOpacitySlider = function () { +var addOpacitySlider = function () { var menu = d3.select("#edgeInfoPanel"); menu.append("label") .attr("for", "opacitySlider") @@ -143,7 +332,7 @@ addOpacitySlider = function () { }); }; -addEdgeBundlingCheck = function () { +var addEdgeBundlingCheck = function () { var menu = d3.select("#edgeInfoPanel"); menu.append("br"); menu.append("label") @@ -160,20 +349,89 @@ addEdgeBundlingCheck = function () { menu.append("br"); }; +// add laterality checkboxes +var addLateralityCheck = function () { + var menu = d3.select("#edgeInfoPanel"); + menu.append("br"); + menu.append("label") + .attr("for", "enableIpsiCheck") + .attr("id", "enableIpsiCheckLabel") + .text("Ipsilateral"); + menu.append("input") + .attr("type", "checkbox") + .attr("checked", false) + .attr("id", "enableIpsiCheck") + .on("change", function () { + enableIpsilaterality(this.checked); + var input = $('#changeModalityBtn'); + var modChecked = input.data("checked"); + var elem = document.getElementById('conThresholdSlider'); + + if (this.checked && getEnableContra() && !elem && modChecked) { + addConThresholdSlider(); + } else { + removeConThresholdSlider(); + } + updateScenes(); + }); + menu.append("br"); + menu.append("label") + .attr("for", "enableContraCheck") + .attr("id", "enableContraCheckLabel") + .text("Contralateral"); + menu.append("input") + .attr("type", "checkbox") + .attr("checked", false) + .attr("id", "enableContraCheck") + .on("change", function () { + enableContralaterality(this.checked); + var input = $('#changeModalityBtn'); + var modChecked = input.data("checked"); + var elem = document.getElementById('conThresholdSlider'); + + if (this.checked && getEnableIpsi() && !elem &&modChecked) { + addConThresholdSlider(); + } else { + removeConThresholdSlider(); + } + updateScenes(); + + }); + //menu.append("br"); + //menu.append("label") + // .attr("for", "enableLateralityCheck") + //.attr("id", "enableLateralityCheckLabel") + //.text("laterality"); +}; + // remove threshold slider and its labels -removeThresholdSlider = function () { +var removeThresholdSlider = function () { var elem = document.getElementById('thresholdSlider'); - if(elem) { + if (elem) { elem.parentNode.removeChild(elem); } elem = document.getElementById('thresholdSliderLabel'); - if(elem) { + if (elem) { + elem.parentNode.removeChild(elem); + } + removeConThresholdSlider(); +} + +// remove threshold slider and its labels +var removeConThresholdSlider = function () { + + var elem = document.getElementById('conThresholdSlider'); + if (elem) { + elem.parentNode.removeChild(elem); + } + elem = document.getElementById('conThresholdSliderLabel'); + if (elem) { elem.parentNode.removeChild(elem); } }; // add slider to filter the top N edges in terms of value -addTopNSlider = function () { +var addTopNSlider = function () { var menu = d3.select("#edgeInfoPanel"); menu.append("input") @@ -196,7 +454,7 @@ addTopNSlider = function () { }; // remove top N edges slider and its labels -removeTopNSlider= function () { +var removeTopNSlider= function () { var elem = document.getElementById('topNThresholdSlider'); if(elem) { elem.parentNode.removeChild(elem); @@ -205,17 +463,25 @@ removeTopNSlider= function () { if(elem) { elem.parentNode.removeChild(elem); } + var elem = document.getElementById('conTopNThresholdSlider'); + if (elem) { + elem.parentNode.removeChild(elem); + } + elem = document.getElementById('conTopNThresholdSliderLabel'); + if (elem) { + elem.parentNode.removeChild(elem); + } }; // remove all DOM elements from the edgeInfoPanel -removeElementsFromEdgePanel = function () { +var removeElementsFromEdgePanel = function () { removeThresholdSlider(); removeTopNSlider(); }; // add "Change Modality" button to toggle between: // edge threshold and top N edges -addModalityButton = function () { +var addModalityButton = function () { var menu = d3.select("#edgeInfoPanel"); menu.append("button") @@ -235,14 +501,20 @@ addModalityButton = function () { }; // change modality callback -changeModality = function (modality) { - thresholdModality = modality; +var changeModality = function (modality) { + if (modality !== undefined) { + setThresholdModality(modality); + } if(modality){ //if it is thresholdModality removeTopNSlider(); addThresholdSlider(); + var elem = document.getElementById('conThresholdSlider'); + if (getEnableIpsi() && getEnableContra() && !elem) { + addConThresholdSlider(); + } } else{ //top N modality removeThresholdSlider(); @@ -253,14 +525,29 @@ changeModality = function (modality) { /* Edges legend */ // create legend panel containing different groups // the state of each group can be either: active, transparent or inactive -createLegend = function(model) { - var legendMenu = document.getElementById("legend"); +var createLegend = function(model,side) { + //var legendMenu = document.getElementById("legend"); + var legendMenu; + + if (side === "Left") { + legendMenu = document.getElementById("legendLeft"); + } else { + legendMenu = document.getElementById("legend"); + } + + //if (model && modelLeft) { legendMenu = (model.getName() === "Left") ? document.getElementById("legendLeft") : document.getElementById("legend"); } - while(legendMenu.hasChildNodes()){ + console.log(side, legendMenu, model.getName()) + + while (legendMenu.hasChildNodes()) { legendMenu.removeChild(legendMenu.childNodes[0]); } - legendMenu = d3.select("#legend"); + var legendDocElmt = legendMenu; + legendMenu = (side === "Left") ? d3.select("#legendLeft") : d3.select("#legend"); + //if (model && modelLeft) { legendMenu = (model.getName() === "Left") ? d3.select("#legendLeft") : d3.select("#legend"); } + + console.log(side, legendMenu, model.name) if(model.getActiveGroupName() != 4) { var activeGroup = model.getActiveGroup(); @@ -271,12 +558,14 @@ createLegend = function(model) { } var l = activeGroup.length; - document.getElementById("legend").style.height = 25*l+"px"; + //document.getElementById("legend").style.height = 25*l+"px"; + legendDocElmt.style.height = 25 * l + "px"; - for(var i=0; i < l; i++){ + for (var i = 0; i < l; i++) { var opacity; - switch (modelLeft.getRegionState(activeGroup[i])){ + //switch (modelLeft.getRegionState(activeGroup[i])) { + switch (model.getRegionState(activeGroup[i])) { case 'active': opacity = 1; break; @@ -288,19 +577,38 @@ createLegend = function(model) { break; } - var elementGroup = legendMenu.append("g") - .attr("transform","translate(10,"+i*25+")") - .attr("id",activeGroup[i]) - .style("cursor","pointer") - .on("click", function(){ - modelLeft.toggleRegion(this.id); - modelRight.toggleRegion(this.id); - if (modelLeft.getRegionState(this.id) == 'transparent') - updateNodesVisiblity(); - else - updateScenes(); - }); + var elementGroup; + if ((side === "Left")) { // || (model && modelLeft && (model.getName() === "Left")) ){ + elementGroup = legendMenu.append("g") + .attr("transform", "translate(10," + i * 25 + ")") + .attr("id", activeGroup[i]) + .style("cursor", "pointer") + .on("click", function () { + modelLeft.toggleRegion(this.id);//,"Left"); + console.log("LEFTmodel:"+side + model.getName()); + //model.toggleRegion(this.id); + if (model.getRegionState(this.id) == 'transparent') + updateNodesVisiblity("Left"); + else + updateScenes("Left"); + }); + } else { // if { (side === "Right") { + elementGroup = legendMenu.append("g") + .attr("transform", "translate(10," + i * 25 + ")") + .attr("id", activeGroup[i]) + .style("cursor", "pointer") + .on("click", function () { + if (lockLegend) { modelLeft.toggleRegion(this.id); } + console.log("RIGHTmodel:" + side + model.getName()); + modelRight.toggleRegion(this.id);//,"Right"); + if (model.getRegionState(this.id) == 'transparent') + updateNodesVisiblity(lockLegend?"Both":"Right"); + else + updateScenes(lockLegend?"Both":"Right"); + }); + } + if(typeof(activeGroup[i]) != 'number' && activeGroup[i].indexOf("right") > -1){ elementGroup.append("rect") .attr("x",-5) @@ -320,7 +628,8 @@ createLegend = function(model) { //choose color of the text var textColor; - if(modelLeft.getRegionActivation(activeGroup[i])){ + //if (modelLeft.getRegionActivation(activeGroup[i])) { + if (model.getRegionActivation(activeGroup[i])) { textColor = "rgb(191,191,191)"; } else{ textColor = "rgb(0,0,0)"; @@ -345,7 +654,8 @@ createLegend = function(model) { console.log("custom group color"); l = quantiles.length+1; - document.getElementById("legend").style.height =30*l+"px"; + //document.getElementById("legend").style.height = 30 * l + "px"; + legendDocElmt.style.height = 30 * l + "px"; for(i = 0; i < quantiles.length + 1 ; i++){ var elementGroup = legendMenu.append("g") @@ -394,9 +704,9 @@ createLegend = function(model) { }; -/* Color coding area at upload */ +/* Color coding area for Right Viewport at upload */ // add "Color Coding" radio button group containing: Anatomy, Embeddedness ... -addColorGroupList = function() { +var addColorGroupList = function() { var select = document.getElementById("colorCodingMenu"); var names = atlas.getGroupsNames(); @@ -411,7 +721,8 @@ addColorGroupList = function() { var hierarchicalClusteringExist = false; if (modelLeft.hasClusteringData() && modelRight.hasClusteringData()) { - var clusterNames = modelLeft.getClusteringTopologiesNames(); + //var clusterNames = modelLeft.getClusteringTopologiesNames(); + var clusterNames = modelRight.getClusteringTopologiesNames(); for (var i = 0; i < clusterNames.length; ++i) { var name = clusterNames[i]; @@ -435,7 +746,7 @@ addColorGroupList = function() { setColorClusteringSliderVisibility("hidden"); break; } - changeColorGroup(selection); + changeColorGroup(selection,lockLegend?"Both":"Right"); }; if (hierarchicalClusteringExist) @@ -443,9 +754,123 @@ addColorGroupList = function() { } setColorClusteringSliderVisibility("hidden"); + + + //document.getElementById("syncColorRight").hidden = true; //.onclick = function () { + document.getElementById("syncColorRight").onclick = function () { + //previewAreaRight.syncCameraWith(previewAreaLeft.getCamera()); + if (this.innerHTML === "Unlock") { + document.getElementById("colorCodingLeft").hidden = false; + document.getElementById("colorCodingMenu").label = "Right ColorCoding:"; + document.getElementById("legendLeft").hidden = false; + var selection = document.getElementById("colorCodingMenu"); + var target = document.getElementById("colorCodingMenuLeft"); + //modelRight.getActiveGroup(); + target.value = selection.value; + this.value = 'Unlocked'; + this.innerHTML = "Lock"; + lockLegend = false; + } else { + document.getElementById("colorCodingLeft").hidden = true; + document.getElementById("colorCodingMenu").label = "ColorCoding:"; + document.getElementById("legendLeft").hidden = true; + this.value = 'Locked'; + this.innerHTML = "Unlock"; + var selection = document.getElementById("colorCodingMenu"); + var target = document.getElementById("colorCodingMenuLeft"); + //modelRight.getActiveGroup(); + target.value = selection.value; + changeColorGroup(selection.value, "Left"); + modelLeft.setCurrentRegionsInformation(modelRight.getCurrentRegionsInformation()); + updateNodesVisiblity("Left"); + updateScenes("Left"); + lockLegend = true; + } + }; +}; + + +/* Color coding area for Left Viewport at upload */ +// add "Color Coding" radio button group containing: Anatomy, Embeddedness ... +var addColorGroupListLeft = function () { + + var select = document.getElementById("colorCodingMenuLeft"); + var names = atlas.getGroupsNames(); + + for (var i = 0; i < names.length; ++i) { + var el = document.createElement("option"); + el.textContent = names[i]; + el.value = names[i]; + el.selected = (i == 0); + select.appendChild(el); + } + + var hierarchicalClusteringExist = false; + if (modelLeft.hasClusteringData() && modelRight.hasClusteringData()) { + var clusterNames = modelLeft.getClusteringTopologiesNames(); + + for (var i = 0; i < clusterNames.length; ++i) { + var name = clusterNames[i]; + var isHierarchical = false; // name == "PLACE" || name == "PACE"; // Todo: Enable later + hierarchicalClusteringExist |= isHierarchical; + + var el = document.createElement("option"); + el.textContent = name; + el.value = name; + select.appendChild(el); + } + + select.onchange = function () { + var selection = this.options[this.selectedIndex].value; + switch (selection) { + case ("PLACE"): + case ("PACE"): + //setColorClusteringSliderVisibility("visible"); // Todo: Let Main Color Menu Control Slider for now. Enable Later + break; + default: + setColorClusteringSliderVisibility("hidden"); + break; + } + changeColorGroup(selection, "Left"); + }; + + if (false && hierarchicalClusteringExist) // Todo: Let Main Color Menu Control Slider for now. Enable Later + addColorClusteringSlider(); + } + + setColorClusteringSliderVisibility("hidden"); + + // Get the button, and when the user clicks on it, execute myFunction + document.getElementById("syncColorLeft").onclick = function () { + //previewAreaLeft.syncCameraWith(previewAreaRight.getCamera()); + var selection = document.getElementById("colorCodingMenu"); + var target = document.getElementById("colorCodingMenuLeft"); + //modelRight.getActiveGroup(); + target.value = selection.value; + changeColorGroup(selection.value, "Left"); + + modelLeft.setCurrentRegionsInformation(modelRight.getCurrentRegionsInformation()); + updateNodesVisiblity("Left"); + updateScenes("Left"); + /* + var activeGroup = model.getActiveGroup(); + + var l = activeGroup.length; + + for (var i = 0; i < l; i++) { + var opacity; + + //switch (modelLeft.getRegionState(activeGroup[i])) { + var state = modelRight.getRegionState(activeGroup[i]); + + modelLeft.toggleRegion(modelRight.getRegionByIndex[i], state); + */ + + }; }; -addColorClusteringSlider = function () { + +var addColorClusteringSlider = function () { var menu = d3.select("#colorCoding"); menu.append("br"); menu.append("label") @@ -467,7 +892,7 @@ addColorClusteringSlider = function () { }); }; -setColorClusteringSliderVisibility = function (value) { +var setColorClusteringSliderVisibility = function (value) { var elem = document.getElementById('colorClusteringSlider'); if (elem) elem.style.visibility = value; @@ -479,7 +904,7 @@ setColorClusteringSliderVisibility = function (value) { /* Topology options at viewLeft and viewRight */ // add "Topological Spaces" menu for scene containing: // Isomap, MDS, tSNE and anatomy spaces -addTopologyMenu = function (model, side) { +var addTopologyMenu = function (model, side) { var topologies = model.getTopologies(); var hierarchicalClusteringExist = false; @@ -518,12 +943,12 @@ addTopologyMenu = function (model, side) { }; // remove geometry buttons -removeGeometryButtons = function (side) { +var removeGeometryButtons = function (side) { document.getElementById("topologyMenu" + side).innerHTML = ""; }; // add clustering level slider -addClusteringSlider = function (model, side) { +var addClusteringSlider = function (model, side) { var menu = d3.select("#topology" + side); menu.append("br"); @@ -546,7 +971,7 @@ addClusteringSlider = function (model, side) { }; // control clustering level slider visibility -setClusteringSliderVisibility = function (side, value) { +var setClusteringSliderVisibility = function (side, value) { var elem = document.getElementById('clusteringSlider' + side); if (elem) elem.style.visibility = value; @@ -558,7 +983,7 @@ setClusteringSliderVisibility = function (side, value) { /*Shortest path stuff at shortestPathLeft and shortestPathRight */ // add filter to shortest path by percentage -addDistanceSlider = function () { +var addDistanceSlider = function () { var menu = d3.select("#shortestPath"); menu.append("br"); @@ -587,14 +1012,14 @@ addDistanceSlider = function () { }; // remove shortest path percentage filter -enableDistanceSlider = function (status) { +var enableDistanceSlider = function (status) { var elem = document.getElementById('distanceThresholdSlider'); if(elem) elem.disabled = !status; }; // add a slider that filters shortest paths by the number of hops -addShortestPathHopsSlider = function () { +var addShortestPathHopsSlider = function () { var menu = d3.select('#shortestPath'); menu.append("br"); @@ -619,13 +1044,13 @@ addShortestPathHopsSlider = function () { }; // remove the shortest path number of hops filter -enableShortestPathHopsSlider = function (status) { +var enableShortestPathHopsSlider = function (status) { var elem = document.getElementById('numberOfHopsSlider'); if(elem) elem.disabled = !status; }; -addShortestPathFilterButton = function () { +var addShortestPathFilterButton = function () { var menu = d3.select("#shortestPath"); menu.append('button') .attr("id", "sptFilterBtn") @@ -646,12 +1071,12 @@ addShortestPathFilterButton = function () { document.getElementById("sptFilterBtn").innerHTML = "Number of Hops"; break; } - if (spt) + if (getSpt()) updateScenes(); }); }; -enableShortestPathFilterButton = function (status) { +var enableShortestPathFilterButton = function (status) { var elem = document.getElementById('sptFilterBtn'); if(elem) elem.disabled = !status; @@ -661,7 +1086,7 @@ enableShortestPathFilterButton = function (status) { enableShortestPathHopsSlider(status && shortestPathVisMethod == NUMBER_HOPS); }; -enableThresholdControls = function (status) { +var enableThresholdControls = function (status) { var elem = document.getElementById('changeModalityBtn'); if(elem) elem.disabled = !status; @@ -673,14 +1098,15 @@ enableThresholdControls = function (status) { elem.disabled = !status; }; -hideVRMaximizeButtons = function () { - document.getElementById("magicWindowLeft").style.visibility = "hidden"; - document.getElementById("magicWindowRight").style.visibility = "hidden"; -}; +// todo: this hides the Enter VR buttons I guess but not sure what its for or if required in webXR +// var hideVRMaximizeButtons = function () { +// document.getElementById("magicWindowLeft").style.visibility = "hidden"; +// document.getElementById("magicWindowRight").style.visibility = "hidden"; +// }; // add labels check boxes, appear/disappear on right click -addFslRadioButton = function() { +var addFslRadioButton = function() { var rightMenu = d3.select("#rightFslLabels"); var leftMenu = d3.select("#leftFslLabels"); @@ -718,7 +1144,7 @@ addFslRadioButton = function() { }; // add search nodes by index panel, appear/disappear on right click -addSearchPanel = function(){ +var addSearchPanel = function(){ var menu = d3.select("#search"); menu.append("text") @@ -730,37 +1156,108 @@ addSearchPanel = function(){ .attr("type", "text") .attr("id", "nodeSearch") .attr("name","nodeid"); + menu.append("br"); + menu.append("label") + .attr("for", "enableSearchLeftCheck") + .attr("id", "enableSearchLeftCheckLabel") + .attr("hidden", true) + .html("←"); + menu.append("input") + .attr("type", "checkbox") + .attr("checked", false) + .attr("hidden", true) + .attr("id", "enableSearchLeftCheck") + .on("change", function () { + enableLeftSearching(this.checked); + }); menu.append("button") .text("Search") .on("click",function(){ var text = document.getElementById("nodeSearch"); - searchElement(text.value); + if (leftSearching) { searchElement(text.value, 'Left'); } + if (rightSearching) { searchElement(text.value, 'Right'); } + if (!leftSearching && !rightSearching) { searchElement(text.value); } + }); + menu.append("input") + .attr("type", "checkbox") + .attr("checked", false) + .attr("hidden", true) + .attr("id", "enableSearchRightCheck") + .on("change", function () { + enableRightSearching(this.checked); }); + menu.append("label") + .attr("for", "enableSearchRightCheck") + .attr("id", "enableSearchRightCheckLabel") + .attr("hidden", true) + .html("→"); }; +var enableLeftSearching = function (value) { + leftSearching = value; +} + +var enableRightSearching = function (value) { + rightSearching = value; +} + // search by index callback -searchElement = function(index) { - index = parseInt(index); - console.log(index); - if(typeof(index) != 'number' || isNaN(index)){ - alert("The value inserted is not a number"); +var searchElement = function(intext,side) { + var index = -1; + + console.log("Search Text" + intext); + + if ((typeof (parseInt(intext)) != 'number') || isNaN(parseInt(intext))) { + // alert("The value inserted is not a number"); + + + // if search field is text search regions for match and continue with index + var glyphCountLeft = previewAreaLeft.getGlyphCount(); + var glyphCountRight = previewAreaRight.getGlyphCount(); + //max(previewAreaLeft.getGlyphCount(), previewAreaRight.getGlyphCount())); + var glyphCount = ((side === 'Left') ? glyphCountLeft : (side === 'Right') ? glyphCountRight : + (glyphCountLeft > glyphCountRight) ? glyphCountLeft : glyphCountRight); + //math.max((glyphCountLeft, glyphCountRight)); + for (var i = 0; i < glyphCount; i++) { + //if (intext === modelRight.getRegionByIndex(i)) { index = i; break; } + var teststriLeft = modelLeft.getRegionByIndex(i); + var teststriRight = modelRight.getRegionByIndex(i); + if ((side !== 'Left') && teststriRight && teststriRight.name.includes(intext) && !getNodesSelected().includes(i)) { index = i; break; } + if ((side !== 'Right') && teststriLeft && teststriLeft.name.includes(intext) && !getNodesSelected().includes(i)) { index = i; break; } + } + } else { // It is a number + index = parseInt(intext)-1; } - if(index < 0 || index > glyphs.length){ - alert("Node not found"); + //if (index < 0 || index > glyphs.length) { + if (index < 0 || index > glyphCount) { + //((side === 'Left') ? previewAreaLeft.getGlyphCount() : + //(side === 'Right') ? previewAreaRight.getGlyphCount() : + // max(previewAreaLeft.getGlyphCount() , previewAreaRight.getGlyphCount()) )) { + alert("Node not found"); } - drawSelectedNode(index, glyphs[index]); + //drawSelectedNode(index, glyphs[index]); + if (side !== 'Right' || leftSearching) { + previewAreaLeft.drawSelectedNode(index, previewAreaLeft.getGlyph[index]); + } + if (side !== 'Left' || rightSearching) { + previewAreaRight.drawSelectedNode(index, previewAreaRight.getGlyph[index]); + } + redrawEdges(); + + var teststri = (side !== 'Left') ? modelRight.getRegionByIndex(index) : modelLeft.getRegionByIndex(index) ; + setNodeInfoPanel(teststri,index); }; // toggle labels check boxes on right click -toggleMenus = function (e) { +var toggleMenus = function (e) { $('#shortestPath').toggle(); $('#viewLeft').toggle(); $('#viewRight').toggle(); $('#legend').toggle(); - $('#nodeInfoPanel').toggle(); + // $('#nodeInfoPanel').toggle(); $('#colorCoding').toggle(); $('#edgeInfoPanel').toggle(); $('#search').toggle(); @@ -768,4 +1265,8 @@ toggleMenus = function (e) { $('#leftFslLabels').toggle(); $('#vrLeft').toggle(); $('#vrRight').toggle(); -}; \ No newline at end of file +}; + +var getShortestPathVisMethod = function () { return shortestPathVisMethod } + +export { toggleMenus, initSubjectMenu, removeGeometryButtons, addOpacitySlider, addModalityButton, addThresholdSlider, addLateralityCheck, addColorGroupList, addColorGroupListLeft, addTopologyMenu, addShortestPathFilterButton, addDistanceSlider, addShortestPathHopsSlider, enableShortestPathFilterButton, addDimensionFactorSliderLeft, addEdgeBundlingCheck, addDimensionFactorSliderRight, addSearchPanel, getShortestPathVisMethod, SHORTEST_DISTANCE, NUMBER_HOPS, setNodeInfoPanel, enableThresholdControls,createLegend} //hideVRMaximizeButtons diff --git a/js/atlas.js b/js/atlas.js index ce1d82a..3e23eaa 100644 --- a/js/atlas.js +++ b/js/atlas.js @@ -14,6 +14,8 @@ place: embeddness (optional) rich_club: rich club affiliation: region name vs non-RichClub (optional) */ +import {sphereResolution,getSphereResolution,setSphereResolution} from "./graphicsUtils"; + function Atlas(data) { var lut = {}; @@ -31,10 +33,15 @@ function Atlas(data) { } colorCodingGroups = fields; // take out label, region_name and hemisphere - for (var i = colorCodingGroups.length-1; i > -1; --i) { - if (colorCodingGroups[i] == "label" || colorCodingGroups[i] == "region_name" || colorCodingGroups[i] == "hemisphere") - colorCodingGroups.remove(i); - } + // for (var i = colorCodingGroups.length-1; i > -1; --i) { + // if (colorCodingGroups[i] == "label" || colorCodingGroups[i] == "region_name" || colorCodingGroups[i] == "hemisphere") + // colorCodingGroups.remove(i); + // } + + colorCodingGroups = colorCodingGroups.filter(val => val !== "label"); + colorCodingGroups = colorCodingGroups.filter(val => val !== "region_name"); + colorCodingGroups = colorCodingGroups.filter(val => val !== "hemisphere"); + // store data var el; for (var i = 0; i < d.data.length; ++i) { @@ -42,7 +49,7 @@ function Atlas(data) { el.visibility = true; lut[d.data[i].label] = el; } - sphereResolution = (d.data.length < 1000) ? 12 : (d.data.length < 2000) ? 8 : 4; + setSphereResolution((d.data.length < 1000) ? 12 : (d.data.length < 2000) ? 8 : 4); }; @@ -77,3 +84,5 @@ function Atlas(data) { // constructor call setLut(data); } + +export {Atlas} \ No newline at end of file diff --git a/js/drawing.js b/js/drawing.js index 87350ac..65f4d25 100644 --- a/js/drawing.js +++ b/js/drawing.js @@ -2,6 +2,7 @@ * Created by Johnson on 2/15/2017. */ + var previewAreaLeft, previewAreaRight; var glyphNodeDictionary ={}; /// Object that stores uuid of left and right glyphs @@ -16,7 +17,9 @@ var pointedObject; // node object under mouse var root; // the index of the root node = start point of shortest path computation var thresholdModality = true; -var enableEB = true; +var enableEB = false; +var enableIpsi = true; +var enableContra = true; var vr = false; // enable VR var spt = false; // enabling shortest path @@ -24,6 +27,36 @@ var click = false; var hoverTimeout = false; var oldNodeIndex = -1; +import * as THREE from 'three' +import {isLoaded, dataFiles,mobile} from "./globals"; +import { + addEdgeBundlingCheck, + addModalityButton, + addLateralityCheck, + removeGeometryButtons, + addOpacitySlider, + addThresholdSlider, + addColorGroupList, addColorGroupListLeft, + addTopologyMenu, + addShortestPathFilterButton, + addDistanceSlider, + addShortestPathHopsSlider, + enableShortestPathFilterButton, + addDimensionFactorSlider, + addDimensionFactorSliderLeft, + addDimensionFactorSliderRight, + createLegend, + //hideVRMaximizeButtons, + toggleMenus +} from './GUI.js'; +import {queue} from "./external-libraries/queue"; +import {scanFolder, loadLookUpTable, loadSubjectNetwork, loadSubjectTopology} from "./utils/parsingData"; +import {modelLeft,modelRight} from './model'; +import {PreviewArea} from "./previewArea"; +import {setUpdateNeeded} from './utils/Dijkstra'; +import { setNodeInfoPanel, enableThresholdControls, addSearchPanel } from './GUI' +import {setColorGroupScale} from './utils/scale' + // callback on mouse moving, expected action: node beneath pointer are drawn bigger function onDocumentMouseMove(model, event) { // the following line would stop any other event handler from firing @@ -35,7 +68,7 @@ function onDocumentMouseMove(model, event) { } -updateNodeMoveOver = function (model, intersectedObject) { +var updateNodeMoveOver = function (model, intersectedObject) { var nodeIdx, region, nodeRegion; if ( intersectedObject ) { nodeIdx = glyphNodeDictionary[intersectedObject.object.uuid]; @@ -47,10 +80,10 @@ updateNodeMoveOver = function (model, intersectedObject) { // update node information label if ( nodeExistAndVisible ) { setNodeInfoPanel(region, nodeIdx); - if (vr) { - previewAreaLeft.updateNodeLabel(region.name, nodeIdx); - previewAreaRight.updateNodeLabel(region.name, nodeIdx); - } + // if (vr) { //todo: this can be used outside of VR to help get node label info next to the node itself, not in the screen corner + // previewAreaLeft.updateNodeLabel(region.name, nodeIdx); + // previewAreaRight.updateNodeLabel(region.name, nodeIdx); + // } } if ( nodeExistAndVisible && (nodesSelected.indexOf(nodeIdx) == -1)) { // not selected @@ -108,8 +141,8 @@ function onMiddleClick(event) { previewAreaRight.computeShortestPathForNode(nodeIndex); } updateScenes(); - enableShortestPathFilterButton(spt); - enableThresholdControls(!spt); + enableShortestPathFilterButton(getSpt()); + enableThresholdControls(!getSpt()); } } @@ -122,7 +155,7 @@ function onLeftClick(model, event) { updateNodeSelection(model, objectIntersected, isLeft); } -updateNodeSelection = function (model, objectIntersected, isLeft) { +var updateNodeSelection = function (model, objectIntersected, isLeft) { var nodeIndex; if ( objectIntersected ) { nodeIndex = glyphNodeDictionary[objectIntersected.object.uuid]; @@ -199,77 +232,82 @@ function onMouseUp(model, event) { } function onKeyPress(event) { - if (event.key === 'v' || event.keyCode === 118) { - if (!previewAreaLeft.isVRAvailable()) { - alert("No VR Hardware found!!!"); - return; - } - updateVRStatus('enable'); - console.log("Enter VR mode"); - } - if (vr && (event.key === 's' || event.keyCode === 115)) { - updateVRStatus('left'); - console.log("VR Active for left preview area"); - } - if (vr && (event.key === 'd' || event.keyCode === 100)) { - updateVRStatus('right'); - console.log("VR Active for right preview area"); - } - if (event.key === 'e' || event.keyCode === 101) { - updateVRStatus('disable'); - console.log("Exit VR mode"); - } + // todo: this is now a stub. no move keyboard activated VR } - + // if (event.key === 'v' || event.keyCode === 118) { + // if (!previewAreaLeft.isVRAvailable()) { + // alert("No VR Hardware found!!!"); + // return; + // } + // updateVRStatus('enable'); + // console.log("Enter VR mode"); + // } + // if (vr && (event.key === 's' || event.keyCode === 115)) { + // updateVRStatus('left'); + // console.log("VR Active for left preview area"); + // } + // if (vr && (event.key === 'd' || event.keyCode === 100)) { + // updateVRStatus('right'); + // console.log("VR Active for right preview area"); + // } + // if (event.key === 'e' || event.keyCode === 101) { + // updateVRStatus('disable'); + // console.log("Exit VR mode"); + // } +//} + +// todo: this is probably not needed in WebXR // update VR status for desktop -updateVRStatus = function (status) { - switch (status) - { - case 'enable': - activeVR = 'none'; - vr = true; - break; - case 'left': - activeVR = 'left'; - previewAreaLeft.activateVR(false); - previewAreaRight.activateVR(false); - // VR allows only one canvas to perform the rendering - previewAreaLeft.enableRender(true); - previewAreaRight.enableRender(false); - setTimeout(function () { previewAreaLeft.activateVR(true); }, 500); - break; - case 'right': - activeVR = 'right'; - previewAreaLeft.activateVR(false); - previewAreaRight.activateVR(false); - // VR allows only one canvas to perform the rendering - previewAreaLeft.enableRender(false); - previewAreaRight.enableRender(true); - setTimeout(function () { previewAreaRight.activateVR(true); }, 500); - break; - case 'disable': - activeVR = 'none'; - previewAreaLeft.activateVR(false); - previewAreaRight.activateVR(false); - vr = false; - previewAreaLeft.resetCamera(); - previewAreaRight.resetCamera(); - previewAreaLeft.resetBrainPosition(); - previewAreaRight.resetBrainPosition(); - previewAreaLeft.enableRender(true); - previewAreaRight.enableRender(true); - break; - } -}; +// var updateVRStatus = function (status) { +// switch (status) +// { +// case 'enable': +// activeVR = 'none'; +// vr = true; +// break; +// case 'left': +// activeVR = 'left'; +// previewAreaLeft.activateVR(false); +// previewAreaRight.activateVR(false); +// // VR allows only one canvas to perform the rendering +// previewAreaLeft.enableRender(true); +// previewAreaRight.enableRender(false); +// setTimeout(function () { previewAreaLeft.activateVR(true); }, 500); +// break; +// case 'right': +// activeVR = 'right'; +// previewAreaLeft.activateVR(false); +// previewAreaRight.activateVR(false); +// // VR allows only one canvas to perform the rendering +// previewAreaLeft.enableRender(false); +// previewAreaRight.enableRender(true); +// setTimeout(function () { previewAreaRight.activateVR(true); }, 500); +// break; +// case 'disable': +// activeVR = 'none'; +// previewAreaLeft.activateVR(false); +// previewAreaRight.activateVR(false); +// vr = false; +// previewAreaLeft.resetCamera(); +// previewAreaRight.resetCamera(); +// previewAreaLeft.resetBrainPosition(); +// previewAreaRight.resetBrainPosition(); +// previewAreaLeft.enableRender(true); +// previewAreaRight.enableRender(true); +// break; +// } +// }; // init the GUI controls -initControls = function () { +var initControls = function () { // add controls addOpacitySlider(); - addEdgeBundlingCheck(); + // addEdgeBundlingCheck(); addModalityButton(); addThresholdSlider(); + addLateralityCheck(); addColorGroupList(); + addColorGroupListLeft(); addTopologyMenu(modelLeft, 'Left'); addTopologyMenu(modelRight, 'Right'); @@ -279,24 +317,27 @@ initControls = function () { enableShortestPathFilterButton(false); // addSkyboxButton(); - addDimensionFactorSlider(); + addDimensionFactorSliderLeft('Left'); + addDimensionFactorSliderRight('Left'); + addDimensionFactorSliderLeft('Right'); + addDimensionFactorSliderRight('Right'); // addFslRadioButton(); - // addSearchPanel(); + addSearchPanel(); modelLeft.setAllRegionsActivated(); modelRight.setAllRegionsActivated(); createLegend(modelLeft); - if (mobile) { - console.log("Mobile VR requested"); - } else { - hideVRMaximizeButtons(); - } + // if (mobile) { // todo: probably not required for webXR + // console.log("Mobile VR requested"); + // } else { + // hideVRMaximizeButtons(); + // } }; // init the canvas where we render the brain -initCanvas = function () { +var initCanvas = function () { glyphNodeDictionary = {}; visibleNodes = new Array(modelLeft.getConnectionMatrixDimension()).fill(true); @@ -324,25 +365,68 @@ initCanvas = function () { previewAreaRight.resizeScene(); }); - window.addEventListener('vrdisplaypresentchange', function(e){ - //e.preventDefault(); - console.log("on resize event"); - previewAreaLeft.resizeScene(); - previewAreaRight.resizeScene();} - , true); + // todo: Not sure how this will be handled in WebXR, adding or removing a headset or controller in mid-session + // window.addEventListener('vrdisplaypresentchange', function(e){ + // //e.preventDefault(); + // console.log("on resize event"); + // previewAreaLeft.resizeScene(); + // previewAreaRight.resizeScene();} + // , true); previewAreaLeft.requestAnimate(); previewAreaRight.requestAnimate(); }; // set the threshold for both models -setThreshold = function(value) { +var setThreshold = function (value) { modelLeft.setThreshold(value); modelRight.setThreshold(value); }; +// set the threshold for both models +var setConThreshold = function (value) { + modelLeft.setConThreshold(value); + modelRight.setConThreshold(value); +}; + +//enable Ipsilaterality +var enableIpsilaterality = function (enable) { + //if (!enableIpsi && enable) {} + enableIpsi = enable; + + console.log("IPSI:"+enable); + + modelLeft.computeEdgesForTopology(modelLeft.getActiveTopology()); + modelRight.computeEdgesForTopology(modelRight.getActiveTopology()); + + previewAreaLeft.removeEdgesFromScene(); + previewAreaRight.removeEdgesFromScene(); + + previewAreaLeft.drawConnections(); + previewAreaRight.drawConnections(); + +} + +//enable Contralaterality +var enableContralaterality = function (enable) { + enableContra = enable; + + console.log("CONTRA:"+enable); + + modelLeft.computeEdgesForTopology(modelLeft.getActiveTopology()); + modelRight.computeEdgesForTopology(modelRight.getActiveTopology()); + + previewAreaLeft.removeEdgesFromScene(); + previewAreaRight.removeEdgesFromScene(); + + previewAreaLeft.drawConnections(); + previewAreaRight.drawConnections(); + +} + + // enable edge bundling -enableEdgeBundling = function (enable) { +var enableEdgeBundling = function (enable) { if (enableEB == enable) return; @@ -359,30 +443,40 @@ enableEdgeBundling = function (enable) { }; // updating scenes: redrawing glyphs and displayed edges -updateScenes = function () { - console.log("Scene update"); - previewAreaLeft.updateScene(); - previewAreaRight.updateScene(); - createLegend(modelLeft); +var updateScenes = function (side) { + console.log("Scene update "+side); + if (side !== "Right") { + previewAreaLeft.updateScene(); + createLegend(modelLeft,"Left"); + } + if (side !== "Left") { + previewAreaRight.updateScene(); + createLegend(modelRight,"Right"); + } }; -updateNodesVisiblity = function () { - previewAreaLeft.updateNodesVisibility(); - previewAreaRight.updateNodesVisibility(); - createLegend(modelLeft); +var updateNodesVisiblity = function (side) { + if (side !== "Right") { + previewAreaLeft.updateNodesVisibility(); + createLegend(modelLeft,"Left"); + } + if (side !== "Left") { + previewAreaRight.updateNodesVisibility(); + createLegend(modelRight,"Right"); + } }; -redrawEdges = function () { +var redrawEdges = function () { previewAreaLeft.redrawEdges(); previewAreaRight.redrawEdges(); }; -updateOpacity = function (opacity) { +var updateOpacity = function (opacity) { previewAreaLeft.updateEdgeOpacity(opacity); previewAreaRight.updateEdgeOpacity(opacity); }; -removeEdgesGivenNodeFromScenes = function(nodeIndex) { +var removeEdgesGivenNodeFromScenes = function(nodeIndex) { previewAreaLeft.removeEdgesGivenNode(nodeIndex); previewAreaRight.removeEdgesGivenNode(nodeIndex); @@ -393,7 +487,7 @@ removeEdgesGivenNodeFromScenes = function(nodeIndex) { // get intersected object beneath the mouse pointer // detects which scene: left or right // return undefined if no object was found -getIntersectedObject = function(event) { +var getIntersectedObject = function(event) { var isLeft = event.clientX < window.innerWidth/2; @@ -406,24 +500,44 @@ getIntersectedObject = function(event) { return isLeft ? previewAreaLeft.getIntersectedObject(vector) : previewAreaRight.getIntersectedObject(vector); }; -changeColorGroup = function (name) { +// This now only changes the Right color group +var changeColorGroup = function (name, side) { + if (side !== "Right") { modelLeft.setActiveGroup(name); } + if (side !== "Left") { modelRight.setActiveGroup(name); } + + if (side !== "Right") { modelLeft.setAllRegionsActivated(); } + if (side !== "Left") { modelRight.setAllRegionsActivated(); } + setColorGroupScale(side); + + if (side !== "Right") { previewAreaLeft.updateNodesVisibility(); } + if (side !== "Left") { previewAreaRight.updateNodesVisibility(); } + if (side !== "Right") { previewAreaLeft.updateNodesColor(); } + if (side !== "Left") { previewAreaRight.updateNodesColor(); } + redrawEdges(); + if (side !== "Right") { createLegend(modelLeft,"Left"); } + if (side !== "Left") { createLegend(modelRight,"Right"); } +}; + +/* Instead of two functions just add an arguement to original one +// This One now Does the Left Color Group +var changeColorGroupLeft = function (name) { modelLeft.setActiveGroup(name); - modelRight.setActiveGroup(name); + //modelRight.setActiveGroup(name); modelLeft.setAllRegionsActivated(); - modelRight.setAllRegionsActivated(); + //modelRight.setAllRegionsActivated(); setColorGroupScale(); previewAreaLeft.updateNodesVisibility(); - previewAreaRight.updateNodesVisibility(); + //previewAreaRight.updateNodesVisibility(); previewAreaLeft.updateNodesColor(); - previewAreaRight.updateNodesColor(); + //previewAreaRight.updateNodesColor(); redrawEdges(); createLegend(modelLeft); -}; +};*/ -redrawScene = function (side) { - updateNeeded = true; +var redrawScene = function (side) { + setUpdateNeeded(true); switch(side) { case 'Left': case 'left': @@ -437,14 +551,14 @@ redrawScene = function (side) { }; // change the active geometry -changeActiveGeometry = function (model, side, type) { +var changeActiveGeometry = function (model, side, type) { console.log("Change Active Geometry to: ", type); model.setActiveTopology(type); redrawScene(side); }; // draw shortest path for the left and right scenes = prepare the edges and plot them -updateShortestPathEdges = function (side) { +var updateShortestPathEdges = function (side) { if (!spt) return; switch (side) { @@ -462,7 +576,7 @@ updateShortestPathEdges = function (side) { }; // change the subject in a specific scene -changeSceneToSubject = function (subjectId, model, previewArea, side) { +var changeSceneToSubject = function (subjectId, model, previewArea, side) { var fileNames = dataFiles[subjectId]; removeGeometryButtons(side); var info = model.getCurrentRegionsInformation(); @@ -491,4 +605,46 @@ changeSceneToSubject = function (subjectId, model, previewArea, side) { }) ; }); -}; \ No newline at end of file +}; + +var setRoot = function (rootNode) { + root = rootNode; +} + +var getRoot = function () { + return root; +} + +var getSpt = function () { + return spt; +} + +var getNodesSelected = function () { + return nodesSelected; +} + +var clrNodesSelected = function () { + nodesSelected = []; +} + +var setNodesSelected = function (arrIndex, newNodeVal) { + nodesSelected[arrIndex] = newNodeVal; +} + +var getEnableEB = function () { return enableEB }; + +var getEnableIpsi = function () { return enableIpsi }; + +var getEnableContra = function () { return enableContra }; + +var getVisibleNodesLength = function (arrIndex) { return visibleNodes.length } + +var getVisibleNodes = function (arrIndex) { return visibleNodes[arrIndex] } + +var setVisibleNodes = function (arrIndex, arrValue) { visibleNodes[arrIndex] = arrValue } + +var getThresholdModality = function () { return thresholdModality } + +var setThresholdModality = function (modality) { thresholdModality = modality } + +export { changeSceneToSubject, initControls, initCanvas, changeActiveGeometry, changeColorGroup, setRoot, getRoot, getSpt, updateScenes, updateNodesVisiblity, redrawEdges, updateOpacity, glyphNodeDictionary, previewAreaLeft, previewAreaRight, getNodesSelected, setNodesSelected, clrNodesSelected, getVisibleNodes, getVisibleNodesLength, setVisibleNodes, enableEdgeBundling, getEnableEB, getEnableIpsi, getEnableContra, enableIpsilaterality, enableContralaterality, getThresholdModality, setThresholdModality }; diff --git a/js/external-libraries/TrackBallControlsOLD.js b/js/external-libraries/TrackBallControlsOLD.js new file mode 100644 index 0000000..26ccbdf --- /dev/null +++ b/js/external-libraries/TrackBallControlsOLD.js @@ -0,0 +1,615 @@ +/** + * Created by giorgioconte on 26/01/15. + */ +/** + * @author Eberhard Graether / http://egraether.com/ + * @author Mark Lundin / http://mark-lundin.com + */ + +THREE.TrackballControls = function ( object, domElement ) { + + var _this = this; + var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; + + this.object = object; + this.domElement = ( domElement !== undefined ) ? domElement : document; + + // API + + this.enabled = true; + + this.screen = { left: 0, top: 0, width: 0, height: 0 }; + + this.rotateSpeed = 1.0; + this.zoomSpeed = 1.2; + this.panSpeed = 0.3; + + this.noRotate = false; + this.noZoom = false; + this.noPan = false; + this.noRoll = false; + + this.staticMoving = false; + this.dynamicDampingFactor = 0.2; + + this.minDistance = 0; + this.maxDistance = Infinity; + + this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ]; + + // internals + + this.target = new THREE.Vector3(); + + var EPS = 0.000001; + + var lastPosition = new THREE.Vector3(); + + var _state = STATE.NONE, + _prevState = STATE.NONE, + + _eye = new THREE.Vector3(), + + _rotateStart = new THREE.Vector3(), + _rotateEnd = new THREE.Vector3(), + + _zoomStart = new THREE.Vector2(), + _zoomEnd = new THREE.Vector2(), + + _touchZoomDistanceStart = 0, + _touchZoomDistanceEnd = 0, + + _panStart = new THREE.Vector2(), + _panEnd = new THREE.Vector2(); + + // for reset + + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + this.up0 = this.object.up.clone(); + + // events + + var changeEvent = { type: 'change' }; + var startEvent = { type: 'start'}; + var endEvent = { type: 'end'}; + + + // methods + + this.handleResize = function () { + + if ( this.domElement === document ) { + + this.screen.left = 0; + this.screen.top = 0; + this.screen.width = window.innerWidth; + this.screen.height = window.innerHeight; + + } else { + + var box = this.domElement.getBoundingClientRect(); + // adjustments come from similar code in the jquery offset() function + var d = this.domElement.ownerDocument.documentElement; + this.screen.left = box.left + window.pageXOffset - d.clientLeft; + this.screen.top = box.top + window.pageYOffset - d.clientTop; + this.screen.width = box.width; + this.screen.height = box.height; + + } + + }; + + this.handleEvent = function ( event ) { + + if ( typeof this[ event.type ] == 'function' ) { + + this[ event.type ]( event ); + + } + + }; + + var getMouseOnScreen = ( function () { + + var vector = new THREE.Vector2(); + + return function ( pageX, pageY ) { + + vector.set( + ( pageX - _this.screen.left ) / _this.screen.width, + ( pageY - _this.screen.top ) / _this.screen.height + ); + + return vector; + + }; + + }() ); + + var getMouseProjectionOnBall = ( function () { + + var vector = new THREE.Vector3(); + var objectUp = new THREE.Vector3(); + var mouseOnBall = new THREE.Vector3(); + + return function ( pageX, pageY ) { + + mouseOnBall.set( + ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / (_this.screen.width*.5), + ( _this.screen.height * 0.5 + _this.screen.top - pageY ) / (_this.screen.height*.5), + 0.0 + ); + + var length = mouseOnBall.length(); + + if ( _this.noRoll ) { + + if ( length < Math.SQRT1_2 ) { + + mouseOnBall.z = Math.sqrt( 1.0 - length*length ); + + } else { + + mouseOnBall.z = .5 / length; + + } + + } else if ( length > 1.0 ) { + + mouseOnBall.normalize(); + + } else { + + mouseOnBall.z = Math.sqrt( 1.0 - length * length ); + + } + + _eye.copy( _this.object.position ).sub( _this.target ); + + vector.copy( _this.object.up ).setLength( mouseOnBall.y ) + vector.add( objectUp.copy( _this.object.up ).cross( _eye ).setLength( mouseOnBall.x ) ); + vector.add( _eye.setLength( mouseOnBall.z ) ); + + return vector; + + }; + + }() ); + + this.rotateCamera = (function(){ + + var axis = new THREE.Vector3(), + quaternion = new THREE.Quaternion(); + + + return function () { + + var angle = Math.acos( _rotateStart.dot( _rotateEnd ) / _rotateStart.length() / _rotateEnd.length() ); + + if ( angle ) { + + axis.crossVectors( _rotateStart, _rotateEnd ).normalize(); + + angle *= _this.rotateSpeed; + + quaternion.setFromAxisAngle( axis, -angle ); + + _eye.applyQuaternion( quaternion ); + _this.object.up.applyQuaternion( quaternion ); + + _rotateEnd.applyQuaternion( quaternion ); + + if ( _this.staticMoving ) { + + _rotateStart.copy( _rotateEnd ); + + } else { + + quaternion.setFromAxisAngle( axis, angle * ( _this.dynamicDampingFactor - 1.0 ) ); + _rotateStart.applyQuaternion( quaternion ); + + } + + } + } + + }()); + + this.zoomCamera = function () { + + if ( _state === STATE.TOUCH_ZOOM_PAN ) { + + var factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; + _touchZoomDistanceStart = _touchZoomDistanceEnd; + _eye.multiplyScalar( factor ); + + } else { + + var factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed; + + if ( factor !== 1.0 && factor > 0.0 ) { + + _eye.multiplyScalar( factor ); + + if ( _this.staticMoving ) { + + _zoomStart.copy( _zoomEnd ); + + } else { + + _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; + + } + + } + + } + + }; + + this.panCamera = (function(){ + + var mouseChange = new THREE.Vector2(), + objectUp = new THREE.Vector3(), + pan = new THREE.Vector3(); + + return function () { + + mouseChange.copy( _panEnd ).sub( _panStart ); + + if ( mouseChange.lengthSq() ) { + + mouseChange.multiplyScalar( _eye.length() * _this.panSpeed ); + + pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x ); + pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) ); + + _this.object.position.add( pan ); + _this.target.add( pan ); + + if ( _this.staticMoving ) { + + _panStart.copy( _panEnd ); + + } else { + + _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) ); + + } + + } + } + + }()); + + this.checkDistances = function () { + + if ( !_this.noZoom || !_this.noPan ) { + + if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) { + + _this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) ); + + } + + if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) { + + _this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) ); + + } + + } + + }; + + this.update = function () { + + _eye.subVectors( _this.object.position, _this.target ); + + if ( !_this.noRotate ) { + + _this.rotateCamera(); + + } + + if ( !_this.noZoom ) { + + _this.zoomCamera(); + + } + + if ( !_this.noPan ) { + + _this.panCamera(); + + } + + _this.object.position.addVectors( _this.target, _eye ); + + _this.checkDistances(); + + _this.object.lookAt( _this.target ); + + if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) { + + _this.dispatchEvent( changeEvent ); + + lastPosition.copy( _this.object.position ); + + } + + }; + + this.reset = function () { + + _state = STATE.NONE; + _prevState = STATE.NONE; + + _this.target.copy( _this.target0 ); + _this.object.position.copy( _this.position0 ); + _this.object.up.copy( _this.up0 ); + + _eye.subVectors( _this.object.position, _this.target ); + + _this.object.lookAt( _this.target ); + + _this.dispatchEvent( changeEvent ); + + lastPosition.copy( _this.object.position ); + + }; + + // listeners + + function keydown( event ) { + + if ( _this.enabled === false ) return; + + window.removeEventListener( 'keydown', keydown ); + + _prevState = _state; + + if ( _state !== STATE.NONE ) { + + return; + + } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && !_this.noRotate ) { + + _state = STATE.ROTATE; + + } else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && !_this.noZoom ) { + + _state = STATE.ZOOM; + + } else if ( event.keyCode === _this.keys[ STATE.PAN ] && !_this.noPan ) { + + _state = STATE.PAN; + + } + + } + + function keyup( event ) { + + if ( _this.enabled === false ) return; + + _state = _prevState; + + window.addEventListener( 'keydown', keydown, false ); + + } + + function mousedown( event ) { + + if ( _this.enabled === false ) return; + + event.preventDefault(); + event.stopPropagation(); + + if ( _state === STATE.NONE ) { + + _state = event.button; + + } + + if ( _state === STATE.ROTATE && !_this.noRotate ) { + + _rotateStart.copy( getMouseProjectionOnBall( event.pageX, event.pageY ) ); + _rotateEnd.copy( _rotateStart ); + + } else if ( _state === STATE.ZOOM && !_this.noZoom ) { + + _zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); + _zoomEnd.copy(_zoomStart); + + } else if ( _state === STATE.PAN && !_this.noPan ) { + + _panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); + _panEnd.copy(_panStart) + + } + + document.addEventListener( 'mousemove', mousemove, false ); + document.addEventListener( 'mouseup', mouseup, false ); + + _this.dispatchEvent( startEvent ); + + } + + function mousemove( event ) { + + if ( _this.enabled === false ) return; + + event.preventDefault(); + event.stopPropagation(); + + if ( _state === STATE.ROTATE && !_this.noRotate ) { + + _rotateEnd.copy( getMouseProjectionOnBall( event.pageX, event.pageY ) ); + + } else if ( _state === STATE.ZOOM && !_this.noZoom ) { + + _zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); + + } else if ( _state === STATE.PAN && !_this.noPan ) { + + _panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); + + } + + } + + function mouseup( event ) { + + if ( _this.enabled === false ) return; + + event.preventDefault(); + event.stopPropagation(); + + _state = STATE.NONE; + + document.removeEventListener( 'mousemove', mousemove ); + document.removeEventListener( 'mouseup', mouseup ); + _this.dispatchEvent( endEvent ); + + } + + function mousewheel( event ) { + + if ( _this.enabled === false ) return; + + event.preventDefault(); + event.stopPropagation(); + + var delta = 0; + + if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9 + + delta = event.wheelDelta / 40; + + } else if ( event.detail ) { // Firefox + + delta = - event.detail / 3; + + } + + _zoomStart.y += delta * 0.01; + _this.dispatchEvent( startEvent ); + _this.dispatchEvent( endEvent ); + + } + + function touchstart( event ) { + + if ( _this.enabled === false ) return; + + switch ( event.touches.length ) { + + case 1: + _state = STATE.TOUCH_ROTATE; + _rotateStart.copy( getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); + _rotateEnd.copy( _rotateStart ); + break; + + case 2: + _state = STATE.TOUCH_ZOOM_PAN; + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); + + var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; + var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; + _panStart.copy( getMouseOnScreen( x, y ) ); + _panEnd.copy( _panStart ); + break; + + default: + _state = STATE.NONE; + + } + _this.dispatchEvent( startEvent ); + + + } + + function touchmove( event ) { + + if ( _this.enabled === false ) return; + + event.preventDefault(); + event.stopPropagation(); + + switch ( event.touches.length ) { + + case 1: + _rotateEnd.copy( getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); + break; + + case 2: + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ); + + var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; + var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; + _panEnd.copy( getMouseOnScreen( x, y ) ); + break; + + default: + _state = STATE.NONE; + + } + + } + + function touchend( event ) { + + if ( _this.enabled === false ) return; + + switch ( event.touches.length ) { + + case 1: + _rotateEnd.copy( getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); + _rotateStart.copy( _rotateEnd ); + break; + + case 2: + _touchZoomDistanceStart = _touchZoomDistanceEnd = 0; + + var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; + var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; + _panEnd.copy( getMouseOnScreen( x, y ) ); + _panStart.copy( _panEnd ); + break; + + } + + _state = STATE.NONE; + _this.dispatchEvent( endEvent ); + + } + + this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); + + this.domElement.addEventListener( 'mousedown', mousedown, false ); + + this.domElement.addEventListener( 'mousewheel', mousewheel, false ); + this.domElement.addEventListener( 'DOMMouseScroll', mousewheel, false ); // firefox + + this.domElement.addEventListener( 'touchstart', touchstart, false ); + this.domElement.addEventListener( 'touchend', touchend, false ); + this.domElement.addEventListener( 'touchmove', touchmove, false ); + + window.addEventListener( 'keydown', keydown, false ); + window.addEventListener( 'keyup', keyup, false ); + + this.handleResize(); + + // force an update at start + this.update(); + +}; + +THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype ); +THREE.TrackballControls.prototype.constructor = THREE.TrackballControls; \ No newline at end of file diff --git a/js/external-libraries/TrackballControls.js b/js/external-libraries/TrackballControls.js index 26ccbdf..c4208b6 100644 --- a/js/external-libraries/TrackballControls.js +++ b/js/external-libraries/TrackballControls.js @@ -6,7 +6,10 @@ * @author Mark Lundin / http://mark-lundin.com */ -THREE.TrackballControls = function ( object, domElement ) { +import * as THREE from 'three' + +//THREE.TrackballControls = function ( object, domElement ) { +var TrackballControls = function ( object, domElement ) { var _this = this; var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; @@ -611,5 +614,6 @@ THREE.TrackballControls = function ( object, domElement ) { }; -THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype ); -THREE.TrackballControls.prototype.constructor = THREE.TrackballControls; \ No newline at end of file +//THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype ); +//THREE.TrackballControls.prototype.constructor = THREE.TrackballControls; +export {TrackballControls} \ No newline at end of file diff --git a/js/external-libraries/gl-matrix/common.js b/js/external-libraries/gl-matrix/common.js new file mode 100644 index 0000000..ff6ea0c --- /dev/null +++ b/js/external-libraries/gl-matrix/common.js @@ -0,0 +1,42 @@ +/** + * Common utilities + * @module glMatrix + */ + +// Configuration Constants +export const EPSILON = 0.000001; +export let ARRAY_TYPE = (typeof Float32Array !== 'undefined') ? Float32Array : Array; +export const RANDOM = Math.random; + +/** + * Sets the type of array used when creating new vectors and matrices + * + * @param {Type} type Array type, such as Float32Array or Array + */ +export function setMatrixArrayType(type) { + ARRAY_TYPE = type; +} + +const degree = Math.PI / 180; + +/** + * Convert Degree To Radian + * + * @param {Number} a Angle in Degrees + */ +export function toRadian(a) { + return a * degree; +} + +/** + * Tests whether or not the arguments have approximately the same value, within an absolute + * or relative tolerance of glMatrix.EPSILON (an absolute tolerance is used for values less + * than or equal to 1.0, and a relative tolerance is used for larger values) + * + * @param {Number} a The first number to test. + * @param {Number} b The second number to test. + * @returns {Boolean} True if the numbers are approximately equal, false otherwise. + */ +export function equals(a, b) { + return Math.abs(a - b) <= EPSILON*Math.max(1.0, Math.abs(a), Math.abs(b)); +} diff --git a/js/external-libraries/gl-matrix/mat3.js b/js/external-libraries/gl-matrix/mat3.js new file mode 100644 index 0000000..9ea1d99 --- /dev/null +++ b/js/external-libraries/gl-matrix/mat3.js @@ -0,0 +1,747 @@ +import * as glMatrix from "./common.js"; + +/** + * 3x3 Matrix + * @module mat3 + */ + +/** + * Creates a new identity mat3 + * + * @returns {mat3} a new 3x3 matrix + */ +export function create() { + let out = new glMatrix.ARRAY_TYPE(9); + if(glMatrix.ARRAY_TYPE != Float32Array) { + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[5] = 0; + out[6] = 0; + out[7] = 0; + } + out[0] = 1; + out[4] = 1; + out[8] = 1; + return out; +} + +/** + * Copies the upper-left 3x3 values into the given mat3. + * + * @param {mat3} out the receiving 3x3 matrix + * @param {mat4} a the source 4x4 matrix + * @returns {mat3} out + */ +export function fromMat4(out, a) { + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[4]; + out[4] = a[5]; + out[5] = a[6]; + out[6] = a[8]; + out[7] = a[9]; + out[8] = a[10]; + return out; +} + +/** + * Creates a new mat3 initialized with values from an existing matrix + * + * @param {mat3} a matrix to clone + * @returns {mat3} a new 3x3 matrix + */ +export function clone(a) { + let out = new glMatrix.ARRAY_TYPE(9); + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + out[4] = a[4]; + out[5] = a[5]; + out[6] = a[6]; + out[7] = a[7]; + out[8] = a[8]; + return out; +} + +/** + * Copy the values from one mat3 to another + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the source matrix + * @returns {mat3} out + */ +export function copy(out, a) { + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + out[4] = a[4]; + out[5] = a[5]; + out[6] = a[6]; + out[7] = a[7]; + out[8] = a[8]; + return out; +} + +/** + * Create a new mat3 with the given values + * + * @param {Number} m00 Component in column 0, row 0 position (index 0) + * @param {Number} m01 Component in column 0, row 1 position (index 1) + * @param {Number} m02 Component in column 0, row 2 position (index 2) + * @param {Number} m10 Component in column 1, row 0 position (index 3) + * @param {Number} m11 Component in column 1, row 1 position (index 4) + * @param {Number} m12 Component in column 1, row 2 position (index 5) + * @param {Number} m20 Component in column 2, row 0 position (index 6) + * @param {Number} m21 Component in column 2, row 1 position (index 7) + * @param {Number} m22 Component in column 2, row 2 position (index 8) + * @returns {mat3} A new mat3 + */ +export function fromValues(m00, m01, m02, m10, m11, m12, m20, m21, m22) { + let out = new glMatrix.ARRAY_TYPE(9); + out[0] = m00; + out[1] = m01; + out[2] = m02; + out[3] = m10; + out[4] = m11; + out[5] = m12; + out[6] = m20; + out[7] = m21; + out[8] = m22; + return out; +} + +/** + * Set the components of a mat3 to the given values + * + * @param {mat3} out the receiving matrix + * @param {Number} m00 Component in column 0, row 0 position (index 0) + * @param {Number} m01 Component in column 0, row 1 position (index 1) + * @param {Number} m02 Component in column 0, row 2 position (index 2) + * @param {Number} m10 Component in column 1, row 0 position (index 3) + * @param {Number} m11 Component in column 1, row 1 position (index 4) + * @param {Number} m12 Component in column 1, row 2 position (index 5) + * @param {Number} m20 Component in column 2, row 0 position (index 6) + * @param {Number} m21 Component in column 2, row 1 position (index 7) + * @param {Number} m22 Component in column 2, row 2 position (index 8) + * @returns {mat3} out + */ +export function set(out, m00, m01, m02, m10, m11, m12, m20, m21, m22) { + out[0] = m00; + out[1] = m01; + out[2] = m02; + out[3] = m10; + out[4] = m11; + out[5] = m12; + out[6] = m20; + out[7] = m21; + out[8] = m22; + return out; +} + +/** + * Set a mat3 to the identity matrix + * + * @param {mat3} out the receiving matrix + * @returns {mat3} out + */ +export function identity(out) { + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 1; + out[5] = 0; + out[6] = 0; + out[7] = 0; + out[8] = 1; + return out; +} + +/** + * Transpose the values of a mat3 + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the source matrix + * @returns {mat3} out + */ +export function transpose(out, a) { + // If we are transposing ourselves we can skip a few steps but have to cache some values + if (out === a) { + let a01 = a[1], a02 = a[2], a12 = a[5]; + out[1] = a[3]; + out[2] = a[6]; + out[3] = a01; + out[5] = a[7]; + out[6] = a02; + out[7] = a12; + } else { + out[0] = a[0]; + out[1] = a[3]; + out[2] = a[6]; + out[3] = a[1]; + out[4] = a[4]; + out[5] = a[7]; + out[6] = a[2]; + out[7] = a[5]; + out[8] = a[8]; + } + + return out; +} + +/** + * Inverts a mat3 + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the source matrix + * @returns {mat3} out + */ +export function invert(out, a) { + let a00 = a[0], a01 = a[1], a02 = a[2]; + let a10 = a[3], a11 = a[4], a12 = a[5]; + let a20 = a[6], a21 = a[7], a22 = a[8]; + + let b01 = a22 * a11 - a12 * a21; + let b11 = -a22 * a10 + a12 * a20; + let b21 = a21 * a10 - a11 * a20; + + // Calculate the determinant + let det = a00 * b01 + a01 * b11 + a02 * b21; + + if (!det) { + return null; + } + det = 1.0 / det; + + out[0] = b01 * det; + out[1] = (-a22 * a01 + a02 * a21) * det; + out[2] = (a12 * a01 - a02 * a11) * det; + out[3] = b11 * det; + out[4] = (a22 * a00 - a02 * a20) * det; + out[5] = (-a12 * a00 + a02 * a10) * det; + out[6] = b21 * det; + out[7] = (-a21 * a00 + a01 * a20) * det; + out[8] = (a11 * a00 - a01 * a10) * det; + return out; +} + +/** + * Calculates the adjugate of a mat3 + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the source matrix + * @returns {mat3} out + */ +export function adjoint(out, a) { + let a00 = a[0], a01 = a[1], a02 = a[2]; + let a10 = a[3], a11 = a[4], a12 = a[5]; + let a20 = a[6], a21 = a[7], a22 = a[8]; + + out[0] = (a11 * a22 - a12 * a21); + out[1] = (a02 * a21 - a01 * a22); + out[2] = (a01 * a12 - a02 * a11); + out[3] = (a12 * a20 - a10 * a22); + out[4] = (a00 * a22 - a02 * a20); + out[5] = (a02 * a10 - a00 * a12); + out[6] = (a10 * a21 - a11 * a20); + out[7] = (a01 * a20 - a00 * a21); + out[8] = (a00 * a11 - a01 * a10); + return out; +} + +/** + * Calculates the determinant of a mat3 + * + * @param {mat3} a the source matrix + * @returns {Number} determinant of a + */ +export function determinant(a) { + let a00 = a[0], a01 = a[1], a02 = a[2]; + let a10 = a[3], a11 = a[4], a12 = a[5]; + let a20 = a[6], a21 = a[7], a22 = a[8]; + + return a00 * (a22 * a11 - a12 * a21) + a01 * (-a22 * a10 + a12 * a20) + a02 * (a21 * a10 - a11 * a20); +} + +/** + * Multiplies two mat3's + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the first operand + * @param {mat3} b the second operand + * @returns {mat3} out + */ +export function multiply(out, a, b) { + let a00 = a[0], a01 = a[1], a02 = a[2]; + let a10 = a[3], a11 = a[4], a12 = a[5]; + let a20 = a[6], a21 = a[7], a22 = a[8]; + + let b00 = b[0], b01 = b[1], b02 = b[2]; + let b10 = b[3], b11 = b[4], b12 = b[5]; + let b20 = b[6], b21 = b[7], b22 = b[8]; + + out[0] = b00 * a00 + b01 * a10 + b02 * a20; + out[1] = b00 * a01 + b01 * a11 + b02 * a21; + out[2] = b00 * a02 + b01 * a12 + b02 * a22; + + out[3] = b10 * a00 + b11 * a10 + b12 * a20; + out[4] = b10 * a01 + b11 * a11 + b12 * a21; + out[5] = b10 * a02 + b11 * a12 + b12 * a22; + + out[6] = b20 * a00 + b21 * a10 + b22 * a20; + out[7] = b20 * a01 + b21 * a11 + b22 * a21; + out[8] = b20 * a02 + b21 * a12 + b22 * a22; + return out; +} + +/** + * Translate a mat3 by the given vector + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the matrix to translate + * @param {vec2} v vector to translate by + * @returns {mat3} out + */ +export function translate(out, a, v) { + let a00 = a[0], a01 = a[1], a02 = a[2], + a10 = a[3], a11 = a[4], a12 = a[5], + a20 = a[6], a21 = a[7], a22 = a[8], + x = v[0], y = v[1]; + + out[0] = a00; + out[1] = a01; + out[2] = a02; + + out[3] = a10; + out[4] = a11; + out[5] = a12; + + out[6] = x * a00 + y * a10 + a20; + out[7] = x * a01 + y * a11 + a21; + out[8] = x * a02 + y * a12 + a22; + return out; +} + +/** + * Rotates a mat3 by the given angle + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the matrix to rotate + * @param {Number} rad the angle to rotate the matrix by + * @returns {mat3} out + */ +export function rotate(out, a, rad) { + let a00 = a[0], a01 = a[1], a02 = a[2], + a10 = a[3], a11 = a[4], a12 = a[5], + a20 = a[6], a21 = a[7], a22 = a[8], + + s = Math.sin(rad), + c = Math.cos(rad); + + out[0] = c * a00 + s * a10; + out[1] = c * a01 + s * a11; + out[2] = c * a02 + s * a12; + + out[3] = c * a10 - s * a00; + out[4] = c * a11 - s * a01; + out[5] = c * a12 - s * a02; + + out[6] = a20; + out[7] = a21; + out[8] = a22; + return out; +}; + +/** + * Scales the mat3 by the dimensions in the given vec2 + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the matrix to rotate + * @param {vec2} v the vec2 to scale the matrix by + * @returns {mat3} out + **/ +export function scale(out, a, v) { + let x = v[0], y = v[1]; + + out[0] = x * a[0]; + out[1] = x * a[1]; + out[2] = x * a[2]; + + out[3] = y * a[3]; + out[4] = y * a[4]; + out[5] = y * a[5]; + + out[6] = a[6]; + out[7] = a[7]; + out[8] = a[8]; + return out; +} + +/** + * Creates a matrix from a vector translation + * This is equivalent to (but much faster than): + * + * mat3.identity(dest); + * mat3.translate(dest, dest, vec); + * + * @param {mat3} out mat3 receiving operation result + * @param {vec2} v Translation vector + * @returns {mat3} out + */ +export function fromTranslation(out, v) { + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 1; + out[5] = 0; + out[6] = v[0]; + out[7] = v[1]; + out[8] = 1; + return out; +} + +/** + * Creates a matrix from a given angle + * This is equivalent to (but much faster than): + * + * mat3.identity(dest); + * mat3.rotate(dest, dest, rad); + * + * @param {mat3} out mat3 receiving operation result + * @param {Number} rad the angle to rotate the matrix by + * @returns {mat3} out + */ +export function fromRotation(out, rad) { + let s = Math.sin(rad), c = Math.cos(rad); + + out[0] = c; + out[1] = s; + out[2] = 0; + + out[3] = -s; + out[4] = c; + out[5] = 0; + + out[6] = 0; + out[7] = 0; + out[8] = 1; + return out; +} + +/** + * Creates a matrix from a vector scaling + * This is equivalent to (but much faster than): + * + * mat3.identity(dest); + * mat3.scale(dest, dest, vec); + * + * @param {mat3} out mat3 receiving operation result + * @param {vec2} v Scaling vector + * @returns {mat3} out + */ +export function fromScaling(out, v) { + out[0] = v[0]; + out[1] = 0; + out[2] = 0; + + out[3] = 0; + out[4] = v[1]; + out[5] = 0; + + out[6] = 0; + out[7] = 0; + out[8] = 1; + return out; +} + +/** + * Copies the values from a mat2d into a mat3 + * + * @param {mat3} out the receiving matrix + * @param {mat2d} a the matrix to copy + * @returns {mat3} out + **/ +export function fromMat2d(out, a) { + out[0] = a[0]; + out[1] = a[1]; + out[2] = 0; + + out[3] = a[2]; + out[4] = a[3]; + out[5] = 0; + + out[6] = a[4]; + out[7] = a[5]; + out[8] = 1; + return out; +} + +/** + * Calculates a 3x3 matrix from the given quaternion + * + * @param {mat3} out mat3 receiving operation result + * @param {quat} q Quaternion to create matrix from + * + * @returns {mat3} out + */ +export function fromQuat(out, q) { + let x = q[0], y = q[1], z = q[2], w = q[3]; + let x2 = x + x; + let y2 = y + y; + let z2 = z + z; + + let xx = x * x2; + let yx = y * x2; + let yy = y * y2; + let zx = z * x2; + let zy = z * y2; + let zz = z * z2; + let wx = w * x2; + let wy = w * y2; + let wz = w * z2; + + out[0] = 1 - yy - zz; + out[3] = yx - wz; + out[6] = zx + wy; + + out[1] = yx + wz; + out[4] = 1 - xx - zz; + out[7] = zy - wx; + + out[2] = zx - wy; + out[5] = zy + wx; + out[8] = 1 - xx - yy; + + return out; +} + +/** + * Calculates a 3x3 normal matrix (transpose inverse) from the 4x4 matrix + * + * @param {mat3} out mat3 receiving operation result + * @param {mat4} a Mat4 to derive the normal matrix from + * + * @returns {mat3} out + */ +export function normalFromMat4(out, a) { + let a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]; + let a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7]; + let a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; + let a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; + + let b00 = a00 * a11 - a01 * a10; + let b01 = a00 * a12 - a02 * a10; + let b02 = a00 * a13 - a03 * a10; + let b03 = a01 * a12 - a02 * a11; + let b04 = a01 * a13 - a03 * a11; + let b05 = a02 * a13 - a03 * a12; + let b06 = a20 * a31 - a21 * a30; + let b07 = a20 * a32 - a22 * a30; + let b08 = a20 * a33 - a23 * a30; + let b09 = a21 * a32 - a22 * a31; + let b10 = a21 * a33 - a23 * a31; + let b11 = a22 * a33 - a23 * a32; + + // Calculate the determinant + let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; + + if (!det) { + return null; + } + det = 1.0 / det; + + out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; + out[1] = (a12 * b08 - a10 * b11 - a13 * b07) * det; + out[2] = (a10 * b10 - a11 * b08 + a13 * b06) * det; + + out[3] = (a02 * b10 - a01 * b11 - a03 * b09) * det; + out[4] = (a00 * b11 - a02 * b08 + a03 * b07) * det; + out[5] = (a01 * b08 - a00 * b10 - a03 * b06) * det; + + out[6] = (a31 * b05 - a32 * b04 + a33 * b03) * det; + out[7] = (a32 * b02 - a30 * b05 - a33 * b01) * det; + out[8] = (a30 * b04 - a31 * b02 + a33 * b00) * det; + + return out; +} + +/** + * Generates a 2D projection matrix with the given bounds + * + * @param {mat3} out mat3 frustum matrix will be written into + * @param {number} width Width of your gl context + * @param {number} height Height of gl context + * @returns {mat3} out + */ +export function projection(out, width, height) { + out[0] = 2 / width; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = -2 / height; + out[5] = 0; + out[6] = -1; + out[7] = 1; + out[8] = 1; + return out; +} + +/** + * Returns a string representation of a mat3 + * + * @param {mat3} a matrix to represent as a string + * @returns {String} string representation of the matrix + */ +export function str(a) { + return 'mat3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + + a[3] + ', ' + a[4] + ', ' + a[5] + ', ' + + a[6] + ', ' + a[7] + ', ' + a[8] + ')'; +} + +/** + * Returns Frobenius norm of a mat3 + * + * @param {mat3} a the matrix to calculate Frobenius norm of + * @returns {Number} Frobenius norm + */ +export function frob(a) { + return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + Math.pow(a[6], 2) + Math.pow(a[7], 2) + Math.pow(a[8], 2))) +} + +/** + * Adds two mat3's + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the first operand + * @param {mat3} b the second operand + * @returns {mat3} out + */ +export function add(out, a, b) { + out[0] = a[0] + b[0]; + out[1] = a[1] + b[1]; + out[2] = a[2] + b[2]; + out[3] = a[3] + b[3]; + out[4] = a[4] + b[4]; + out[5] = a[5] + b[5]; + out[6] = a[6] + b[6]; + out[7] = a[7] + b[7]; + out[8] = a[8] + b[8]; + return out; +} + +/** + * Subtracts matrix b from matrix a + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the first operand + * @param {mat3} b the second operand + * @returns {mat3} out + */ +export function subtract(out, a, b) { + out[0] = a[0] - b[0]; + out[1] = a[1] - b[1]; + out[2] = a[2] - b[2]; + out[3] = a[3] - b[3]; + out[4] = a[4] - b[4]; + out[5] = a[5] - b[5]; + out[6] = a[6] - b[6]; + out[7] = a[7] - b[7]; + out[8] = a[8] - b[8]; + return out; +} + + + +/** + * Multiply each element of the matrix by a scalar. + * + * @param {mat3} out the receiving matrix + * @param {mat3} a the matrix to scale + * @param {Number} b amount to scale the matrix's elements by + * @returns {mat3} out + */ +export function multiplyScalar(out, a, b) { + out[0] = a[0] * b; + out[1] = a[1] * b; + out[2] = a[2] * b; + out[3] = a[3] * b; + out[4] = a[4] * b; + out[5] = a[5] * b; + out[6] = a[6] * b; + out[7] = a[7] * b; + out[8] = a[8] * b; + return out; +} + +/** + * Adds two mat3's after multiplying each element of the second operand by a scalar value. + * + * @param {mat3} out the receiving vector + * @param {mat3} a the first operand + * @param {mat3} b the second operand + * @param {Number} scale the amount to scale b's elements by before adding + * @returns {mat3} out + */ +export function multiplyScalarAndAdd(out, a, b, scale) { + out[0] = a[0] + (b[0] * scale); + out[1] = a[1] + (b[1] * scale); + out[2] = a[2] + (b[2] * scale); + out[3] = a[3] + (b[3] * scale); + out[4] = a[4] + (b[4] * scale); + out[5] = a[5] + (b[5] * scale); + out[6] = a[6] + (b[6] * scale); + out[7] = a[7] + (b[7] * scale); + out[8] = a[8] + (b[8] * scale); + return out; +} + +/** + * Returns whether or not the matrices have exactly the same elements in the same position (when compared with ===) + * + * @param {mat3} a The first matrix. + * @param {mat3} b The second matrix. + * @returns {Boolean} True if the matrices are equal, false otherwise. + */ +export function exactEquals(a, b) { + return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && + a[3] === b[3] && a[4] === b[4] && a[5] === b[5] && + a[6] === b[6] && a[7] === b[7] && a[8] === b[8]; +} + +/** + * Returns whether or not the matrices have approximately the same elements in the same position. + * + * @param {mat3} a The first matrix. + * @param {mat3} b The second matrix. + * @returns {Boolean} True if the matrices are equal, false otherwise. + */ +export function equals(a, b) { + let a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5], a6 = a[6], a7 = a[7], a8 = a[8]; + let b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3], b4 = b[4], b5 = b[5], b6 = b[6], b7 = b[7], b8 = b[8]; + return (Math.abs(a0 - b0) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a0), Math.abs(b0)) && + Math.abs(a1 - b1) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a1), Math.abs(b1)) && + Math.abs(a2 - b2) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a2), Math.abs(b2)) && + Math.abs(a3 - b3) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a3), Math.abs(b3)) && + Math.abs(a4 - b4) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a4), Math.abs(b4)) && + Math.abs(a5 - b5) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a5), Math.abs(b5)) && + Math.abs(a6 - b6) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a6), Math.abs(b6)) && + Math.abs(a7 - b7) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a7), Math.abs(b7)) && + Math.abs(a8 - b8) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a8), Math.abs(b8))); +} + +/** + * Alias for {@link mat3.multiply} + * @function + */ +export const mul = multiply; + +/** + * Alias for {@link mat3.subtract} + * @function + */ +export const sub = subtract; \ No newline at end of file diff --git a/js/external-libraries/gl-matrix/quat.js b/js/external-libraries/gl-matrix/quat.js new file mode 100644 index 0000000..4221ce0 --- /dev/null +++ b/js/external-libraries/gl-matrix/quat.js @@ -0,0 +1,631 @@ +import * as glMatrix from "./common.js" +import * as mat3 from "./mat3.js" +import * as vec3 from "./vec3.js" +import * as vec4 from "./vec4.js" + +/** + * Quaternion + * @module quat + */ + +/** + * Creates a new identity quat + * + * @returns {quat} a new quaternion + */ +export function create() { + let out = new glMatrix.ARRAY_TYPE(4); + if(glMatrix.ARRAY_TYPE != Float32Array) { + out[0] = 0; + out[1] = 0; + out[2] = 0; + } + out[3] = 1; + return out; +} + +/** + * Set a quat to the identity quaternion + * + * @param {quat} out the receiving quaternion + * @returns {quat} out + */ +export function identity(out) { + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 1; + return out; +} + +/** + * Sets a quat from the given angle and rotation axis, + * then returns it. + * + * @param {quat} out the receiving quaternion + * @param {vec3} axis the axis around which to rotate + * @param {Number} rad the angle in radians + * @returns {quat} out + **/ +export function setAxisAngle(out, axis, rad) { + rad = rad * 0.5; + let s = Math.sin(rad); + out[0] = s * axis[0]; + out[1] = s * axis[1]; + out[2] = s * axis[2]; + out[3] = Math.cos(rad); + return out; +} + +/** + * Gets the rotation axis and angle for a given + * quaternion. If a quaternion is created with + * setAxisAngle, this method will return the same + * values as providied in the original parameter list + * OR functionally equivalent values. + * Example: The quaternion formed by axis [0, 0, 1] and + * angle -90 is the same as the quaternion formed by + * [0, 0, 1] and 270. This method favors the latter. + * @param {vec3} out_axis Vector receiving the axis of rotation + * @param {quat} q Quaternion to be decomposed + * @return {Number} Angle, in radians, of the rotation + */ +export function getAxisAngle(out_axis, q) { + let rad = Math.acos(q[3]) * 2.0; + let s = Math.sin(rad / 2.0); + if (s > glMatrix.EPSILON) { + out_axis[0] = q[0] / s; + out_axis[1] = q[1] / s; + out_axis[2] = q[2] / s; + } else { + // If s is zero, return any axis (no rotation - axis does not matter) + out_axis[0] = 1; + out_axis[1] = 0; + out_axis[2] = 0; + } + return rad; +} + +/** + * Multiplies two quat's + * + * @param {quat} out the receiving quaternion + * @param {quat} a the first operand + * @param {quat} b the second operand + * @returns {quat} out + */ +export function multiply(out, a, b) { + let ax = a[0], ay = a[1], az = a[2], aw = a[3]; + let bx = b[0], by = b[1], bz = b[2], bw = b[3]; + + out[0] = ax * bw + aw * bx + ay * bz - az * by; + out[1] = ay * bw + aw * by + az * bx - ax * bz; + out[2] = az * bw + aw * bz + ax * by - ay * bx; + out[3] = aw * bw - ax * bx - ay * by - az * bz; + return out; +} + +/** + * Rotates a quaternion by the given angle about the X axis + * + * @param {quat} out quat receiving operation result + * @param {quat} a quat to rotate + * @param {number} rad angle (in radians) to rotate + * @returns {quat} out + */ +export function rotateX(out, a, rad) { + rad *= 0.5; + + let ax = a[0], ay = a[1], az = a[2], aw = a[3]; + let bx = Math.sin(rad), bw = Math.cos(rad); + + out[0] = ax * bw + aw * bx; + out[1] = ay * bw + az * bx; + out[2] = az * bw - ay * bx; + out[3] = aw * bw - ax * bx; + return out; +} + +/** + * Rotates a quaternion by the given angle about the Y axis + * + * @param {quat} out quat receiving operation result + * @param {quat} a quat to rotate + * @param {number} rad angle (in radians) to rotate + * @returns {quat} out + */ +export function rotateY(out, a, rad) { + rad *= 0.5; + + let ax = a[0], ay = a[1], az = a[2], aw = a[3]; + let by = Math.sin(rad), bw = Math.cos(rad); + + out[0] = ax * bw - az * by; + out[1] = ay * bw + aw * by; + out[2] = az * bw + ax * by; + out[3] = aw * bw - ay * by; + return out; +} + +/** + * Rotates a quaternion by the given angle about the Z axis + * + * @param {quat} out quat receiving operation result + * @param {quat} a quat to rotate + * @param {number} rad angle (in radians) to rotate + * @returns {quat} out + */ +export function rotateZ(out, a, rad) { + rad *= 0.5; + + let ax = a[0], ay = a[1], az = a[2], aw = a[3]; + let bz = Math.sin(rad), bw = Math.cos(rad); + + out[0] = ax * bw + ay * bz; + out[1] = ay * bw - ax * bz; + out[2] = az * bw + aw * bz; + out[3] = aw * bw - az * bz; + return out; +} + +/** + * Calculates the W component of a quat from the X, Y, and Z components. + * Assumes that quaternion is 1 unit in length. + * Any existing W component will be ignored. + * + * @param {quat} out the receiving quaternion + * @param {quat} a quat to calculate W component of + * @returns {quat} out + */ +export function calculateW(out, a) { + let x = a[0], y = a[1], z = a[2]; + + out[0] = x; + out[1] = y; + out[2] = z; + out[3] = Math.sqrt(Math.abs(1.0 - x * x - y * y - z * z)); + return out; +} + +/** + * Performs a spherical linear interpolation between two quat + * + * @param {quat} out the receiving quaternion + * @param {quat} a the first operand + * @param {quat} b the second operand + * @param {Number} t interpolation amount, in the range [0-1], between the two inputs + * @returns {quat} out + */ +export function slerp(out, a, b, t) { + // benchmarks: + // http://jsperf.com/quaternion-slerp-implementations + let ax = a[0], ay = a[1], az = a[2], aw = a[3]; + let bx = b[0], by = b[1], bz = b[2], bw = b[3]; + + let omega, cosom, sinom, scale0, scale1; + + // calc cosine + cosom = ax * bx + ay * by + az * bz + aw * bw; + // adjust signs (if necessary) + if ( cosom < 0.0 ) { + cosom = -cosom; + bx = - bx; + by = - by; + bz = - bz; + bw = - bw; + } + // calculate coefficients + if ( (1.0 - cosom) > glMatrix.EPSILON ) { + // standard case (slerp) + omega = Math.acos(cosom); + sinom = Math.sin(omega); + scale0 = Math.sin((1.0 - t) * omega) / sinom; + scale1 = Math.sin(t * omega) / sinom; + } else { + // "from" and "to" quaternions are very close + // ... so we can do a linear interpolation + scale0 = 1.0 - t; + scale1 = t; + } + // calculate final values + out[0] = scale0 * ax + scale1 * bx; + out[1] = scale0 * ay + scale1 * by; + out[2] = scale0 * az + scale1 * bz; + out[3] = scale0 * aw + scale1 * bw; + + return out; +} + +/** + * Generates a random quaternion + * + * @param {quat} out the receiving quaternion + * @returns {quat} out + */ +export function random(out) { + // Implementation of http://planning.cs.uiuc.edu/node198.html + // TODO: Calling random 3 times is probably not the fastest solution + let u1 = glMatrix.RANDOM(); + let u2 = glMatrix.RANDOM(); + let u3 = glMatrix.RANDOM(); + + let sqrt1MinusU1 = Math.sqrt(1 - u1); + let sqrtU1 = Math.sqrt(u1); + + out[0] = sqrt1MinusU1 * Math.sin(2.0 * Math.PI * u2); + out[1] = sqrt1MinusU1 * Math.cos(2.0 * Math.PI * u2); + out[2] = sqrtU1 * Math.sin(2.0 * Math.PI * u3); + out[3] = sqrtU1 * Math.cos(2.0 * Math.PI * u3); + return out; +} + +/** + * Calculates the inverse of a quat + * + * @param {quat} out the receiving quaternion + * @param {quat} a quat to calculate inverse of + * @returns {quat} out + */ +export function invert(out, a) { + let a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3]; + let dot = a0*a0 + a1*a1 + a2*a2 + a3*a3; + let invDot = dot ? 1.0/dot : 0; + + // TODO: Would be faster to return [0,0,0,0] immediately if dot == 0 + + out[0] = -a0*invDot; + out[1] = -a1*invDot; + out[2] = -a2*invDot; + out[3] = a3*invDot; + return out; +} + +/** + * Calculates the conjugate of a quat + * If the quaternion is normalized, this function is faster than quat.inverse and produces the same result. + * + * @param {quat} out the receiving quaternion + * @param {quat} a quat to calculate conjugate of + * @returns {quat} out + */ +export function conjugate(out, a) { + out[0] = -a[0]; + out[1] = -a[1]; + out[2] = -a[2]; + out[3] = a[3]; + return out; +} + +/** + * Creates a quaternion from the given 3x3 rotation matrix. + * + * NOTE: The resultant quaternion is not normalized, so you should be sure + * to renormalize the quaternion yourself where necessary. + * + * @param {quat} out the receiving quaternion + * @param {mat3} m rotation matrix + * @returns {quat} out + * @function + */ +export function fromMat3(out, m) { + // Algorithm in Ken Shoemake's article in 1987 SIGGRAPH course notes + // article "Quaternion Calculus and Fast Animation". + let fTrace = m[0] + m[4] + m[8]; + let fRoot; + + if ( fTrace > 0.0 ) { + // |w| > 1/2, may as well choose w > 1/2 + fRoot = Math.sqrt(fTrace + 1.0); // 2w + out[3] = 0.5 * fRoot; + fRoot = 0.5/fRoot; // 1/(4w) + out[0] = (m[5]-m[7])*fRoot; + out[1] = (m[6]-m[2])*fRoot; + out[2] = (m[1]-m[3])*fRoot; + } else { + // |w| <= 1/2 + let i = 0; + if ( m[4] > m[0] ) + i = 1; + if ( m[8] > m[i*3+i] ) + i = 2; + let j = (i+1)%3; + let k = (i+2)%3; + + fRoot = Math.sqrt(m[i*3+i]-m[j*3+j]-m[k*3+k] + 1.0); + out[i] = 0.5 * fRoot; + fRoot = 0.5 / fRoot; + out[3] = (m[j*3+k] - m[k*3+j]) * fRoot; + out[j] = (m[j*3+i] + m[i*3+j]) * fRoot; + out[k] = (m[k*3+i] + m[i*3+k]) * fRoot; + } + + return out; +} + +/** + * Creates a quaternion from the given euler angle x, y, z. + * + * @param {quat} out the receiving quaternion + * @param {x} Angle to rotate around X axis in degrees. + * @param {y} Angle to rotate around Y axis in degrees. + * @param {z} Angle to rotate around Z axis in degrees. + * @returns {quat} out + * @function + */ +export function fromEuler(out, x, y, z) { + let halfToRad = 0.5 * Math.PI / 180.0; + x *= halfToRad; + y *= halfToRad; + z *= halfToRad; + + let sx = Math.sin(x); + let cx = Math.cos(x); + let sy = Math.sin(y); + let cy = Math.cos(y); + let sz = Math.sin(z); + let cz = Math.cos(z); + + out[0] = sx * cy * cz - cx * sy * sz; + out[1] = cx * sy * cz + sx * cy * sz; + out[2] = cx * cy * sz - sx * sy * cz; + out[3] = cx * cy * cz + sx * sy * sz; + + return out; +} + +/** + * Returns a string representation of a quatenion + * + * @param {quat} a vector to represent as a string + * @returns {String} string representation of the vector + */ +export function str(a) { + return 'quat(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')'; +} + +/** + * Creates a new quat initialized with values from an existing quaternion + * + * @param {quat} a quaternion to clone + * @returns {quat} a new quaternion + * @function + */ +export const clone = vec4.clone; + +/** + * Creates a new quat initialized with the given values + * + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @param {Number} w W component + * @returns {quat} a new quaternion + * @function + */ +export const fromValues = vec4.fromValues; + +/** + * Copy the values from one quat to another + * + * @param {quat} out the receiving quaternion + * @param {quat} a the source quaternion + * @returns {quat} out + * @function + */ +export const copy = vec4.copy; + +/** + * Set the components of a quat to the given values + * + * @param {quat} out the receiving quaternion + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @param {Number} w W component + * @returns {quat} out + * @function + */ +export const set = vec4.set; + +/** + * Adds two quat's + * + * @param {quat} out the receiving quaternion + * @param {quat} a the first operand + * @param {quat} b the second operand + * @returns {quat} out + * @function + */ +export const add = vec4.add; + +/** + * Alias for {@link quat.multiply} + * @function + */ +export const mul = multiply; + +/** + * Scales a quat by a scalar number + * + * @param {quat} out the receiving vector + * @param {quat} a the vector to scale + * @param {Number} b amount to scale the vector by + * @returns {quat} out + * @function + */ +export const scale = vec4.scale; + +/** + * Calculates the dot product of two quat's + * + * @param {quat} a the first operand + * @param {quat} b the second operand + * @returns {Number} dot product of a and b + * @function + */ +export const dot = vec4.dot; + +/** + * Performs a linear interpolation between two quat's + * + * @param {quat} out the receiving quaternion + * @param {quat} a the first operand + * @param {quat} b the second operand + * @param {Number} t interpolation amount, in the range [0-1], between the two inputs + * @returns {quat} out + * @function + */ +export const lerp = vec4.lerp; + +/** + * Calculates the length of a quat + * + * @param {quat} a vector to calculate length of + * @returns {Number} length of a + */ +export const length = vec4.length; + +/** + * Alias for {@link quat.length} + * @function + */ +export const len = length; + +/** + * Calculates the squared length of a quat + * + * @param {quat} a vector to calculate squared length of + * @returns {Number} squared length of a + * @function + */ +export const squaredLength = vec4.squaredLength; + +/** + * Alias for {@link quat.squaredLength} + * @function + */ +export const sqrLen = squaredLength; + +/** + * Normalize a quat + * + * @param {quat} out the receiving quaternion + * @param {quat} a quaternion to normalize + * @returns {quat} out + * @function + */ +export const normalize = vec4.normalize; + +/** + * Returns whether or not the quaternions have exactly the same elements in the same position (when compared with ===) + * + * @param {quat} a The first quaternion. + * @param {quat} b The second quaternion. + * @returns {Boolean} True if the vectors are equal, false otherwise. + */ +export const exactEquals = vec4.exactEquals; + +/** + * Returns whether or not the quaternions have approximately the same elements in the same position. + * + * @param {quat} a The first vector. + * @param {quat} b The second vector. + * @returns {Boolean} True if the vectors are equal, false otherwise. + */ +export const equals = vec4.equals; + +/** + * Sets a quaternion to represent the shortest rotation from one + * vector to another. + * + * Both vectors are assumed to be unit length. + * + * @param {quat} out the receiving quaternion. + * @param {vec3} a the initial vector + * @param {vec3} b the destination vector + * @returns {quat} out + */ +export const rotationTo = (function() { + let tmpvec3 = vec3.create(); + let xUnitVec3 = vec3.fromValues(1,0,0); + let yUnitVec3 = vec3.fromValues(0,1,0); + + return function(out, a, b) { + let dot = vec3.dot(a, b); + if (dot < -0.999999) { + vec3.cross(tmpvec3, xUnitVec3, a); + if (vec3.len(tmpvec3) < 0.000001) + vec3.cross(tmpvec3, yUnitVec3, a); + vec3.normalize(tmpvec3, tmpvec3); + setAxisAngle(out, tmpvec3, Math.PI); + return out; + } else if (dot > 0.999999) { + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 1; + return out; + } else { + vec3.cross(tmpvec3, a, b); + out[0] = tmpvec3[0]; + out[1] = tmpvec3[1]; + out[2] = tmpvec3[2]; + out[3] = 1 + dot; + return normalize(out, out); + } + }; +})(); + +/** + * Performs a spherical linear interpolation with two control points + * + * @param {quat} out the receiving quaternion + * @param {quat} a the first operand + * @param {quat} b the second operand + * @param {quat} c the third operand + * @param {quat} d the fourth operand + * @param {Number} t interpolation amount, in the range [0-1], between the two inputs + * @returns {quat} out + */ +export const sqlerp = (function () { + let temp1 = create(); + let temp2 = create(); + + return function (out, a, b, c, d, t) { + slerp(temp1, a, d, t); + slerp(temp2, b, c, t); + slerp(out, temp1, temp2, 2 * t * (1 - t)); + + return out; + }; +}()); + +/** + * Sets the specified quaternion with values corresponding to the given + * axes. Each axis is a vec3 and is expected to be unit length and + * perpendicular to all other specified axes. + * + * @param {vec3} view the vector representing the viewing direction + * @param {vec3} right the vector representing the local "right" direction + * @param {vec3} up the vector representing the local "up" direction + * @returns {quat} out + */ +export const setAxes = (function() { + let matr = mat3.create(); + + return function(out, view, right, up) { + matr[0] = right[0]; + matr[3] = right[1]; + matr[6] = right[2]; + + matr[1] = up[0]; + matr[4] = up[1]; + matr[7] = up[2]; + + matr[2] = -view[0]; + matr[5] = -view[1]; + matr[8] = -view[2]; + + return normalize(out, fromMat3(out, matr)); + }; +})(); \ No newline at end of file diff --git a/js/external-libraries/gl-matrix/vec3.js b/js/external-libraries/gl-matrix/vec3.js new file mode 100644 index 0000000..a3332b8 --- /dev/null +++ b/js/external-libraries/gl-matrix/vec3.js @@ -0,0 +1,769 @@ +import * as glMatrix from "./common.js"; + +/** + * 3 Dimensional Vector + * @module vec3 + */ + +/** + * Creates a new, empty vec3 + * + * @returns {vec3} a new 3D vector + */ +export function create() { + let out = new glMatrix.ARRAY_TYPE(3); + if(glMatrix.ARRAY_TYPE != Float32Array) { + out[0] = 0; + out[1] = 0; + out[2] = 0; + } + return out; +} + +/** + * Creates a new vec3 initialized with values from an existing vector + * + * @param {vec3} a vector to clone + * @returns {vec3} a new 3D vector + */ +export function clone(a) { + var out = new glMatrix.ARRAY_TYPE(3); + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + return out; +} + +/** + * Calculates the length of a vec3 + * + * @param {vec3} a vector to calculate length of + * @returns {Number} length of a + */ +export function length(a) { + let x = a[0]; + let y = a[1]; + let z = a[2]; + return Math.sqrt(x*x + y*y + z*z); +} + +/** + * Creates a new vec3 initialized with the given values + * + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @returns {vec3} a new 3D vector + */ +export function fromValues(x, y, z) { + let out = new glMatrix.ARRAY_TYPE(3); + out[0] = x; + out[1] = y; + out[2] = z; + return out; +} + +/** + * Copy the values from one vec3 to another + * + * @param {vec3} out the receiving vector + * @param {vec3} a the source vector + * @returns {vec3} out + */ +export function copy(out, a) { + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + return out; +} + +/** + * Set the components of a vec3 to the given values + * + * @param {vec3} out the receiving vector + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @returns {vec3} out + */ +export function set(out, x, y, z) { + out[0] = x; + out[1] = y; + out[2] = z; + return out; +} + +/** + * Adds two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ +export function add(out, a, b) { + out[0] = a[0] + b[0]; + out[1] = a[1] + b[1]; + out[2] = a[2] + b[2]; + return out; +} + +/** + * Subtracts vector b from vector a + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ +export function subtract(out, a, b) { + out[0] = a[0] - b[0]; + out[1] = a[1] - b[1]; + out[2] = a[2] - b[2]; + return out; +} + +/** + * Multiplies two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ +export function multiply(out, a, b) { + out[0] = a[0] * b[0]; + out[1] = a[1] * b[1]; + out[2] = a[2] * b[2]; + return out; +} + +/** + * Divides two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ +export function divide(out, a, b) { + out[0] = a[0] / b[0]; + out[1] = a[1] / b[1]; + out[2] = a[2] / b[2]; + return out; +} + +/** + * Math.ceil the components of a vec3 + * + * @param {vec3} out the receiving vector + * @param {vec3} a vector to ceil + * @returns {vec3} out + */ +export function ceil(out, a) { + out[0] = Math.ceil(a[0]); + out[1] = Math.ceil(a[1]); + out[2] = Math.ceil(a[2]); + return out; +} + +/** + * Math.floor the components of a vec3 + * + * @param {vec3} out the receiving vector + * @param {vec3} a vector to floor + * @returns {vec3} out + */ +export function floor(out, a) { + out[0] = Math.floor(a[0]); + out[1] = Math.floor(a[1]); + out[2] = Math.floor(a[2]); + return out; +} + +/** + * Returns the minimum of two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ +export function min(out, a, b) { + out[0] = Math.min(a[0], b[0]); + out[1] = Math.min(a[1], b[1]); + out[2] = Math.min(a[2], b[2]); + return out; +} + +/** + * Returns the maximum of two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ +export function max(out, a, b) { + out[0] = Math.max(a[0], b[0]); + out[1] = Math.max(a[1], b[1]); + out[2] = Math.max(a[2], b[2]); + return out; +} + +/** + * Math.round the components of a vec3 + * + * @param {vec3} out the receiving vector + * @param {vec3} a vector to round + * @returns {vec3} out + */ +export function round(out, a) { + out[0] = Math.round(a[0]); + out[1] = Math.round(a[1]); + out[2] = Math.round(a[2]); + return out; +} + +/** + * Scales a vec3 by a scalar number + * + * @param {vec3} out the receiving vector + * @param {vec3} a the vector to scale + * @param {Number} b amount to scale the vector by + * @returns {vec3} out + */ +export function scale(out, a, b) { + out[0] = a[0] * b; + out[1] = a[1] * b; + out[2] = a[2] * b; + return out; +} + +/** + * Adds two vec3's after scaling the second operand by a scalar value + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @param {Number} scale the amount to scale b by before adding + * @returns {vec3} out + */ +export function scaleAndAdd(out, a, b, scale) { + out[0] = a[0] + (b[0] * scale); + out[1] = a[1] + (b[1] * scale); + out[2] = a[2] + (b[2] * scale); + return out; +} + +/** + * Calculates the euclidian distance between two vec3's + * + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {Number} distance between a and b + */ +export function distance(a, b) { + let x = b[0] - a[0]; + let y = b[1] - a[1]; + let z = b[2] - a[2]; + return Math.sqrt(x*x + y*y + z*z); +} + +/** + * Calculates the squared euclidian distance between two vec3's + * + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {Number} squared distance between a and b + */ +export function squaredDistance(a, b) { + let x = b[0] - a[0]; + let y = b[1] - a[1]; + let z = b[2] - a[2]; + return x*x + y*y + z*z; +} + +/** + * Calculates the squared length of a vec3 + * + * @param {vec3} a vector to calculate squared length of + * @returns {Number} squared length of a + */ +export function squaredLength(a) { + let x = a[0]; + let y = a[1]; + let z = a[2]; + return x*x + y*y + z*z; +} + +/** + * Negates the components of a vec3 + * + * @param {vec3} out the receiving vector + * @param {vec3} a vector to negate + * @returns {vec3} out + */ +export function negate(out, a) { + out[0] = -a[0]; + out[1] = -a[1]; + out[2] = -a[2]; + return out; +} + +/** + * Returns the inverse of the components of a vec3 + * + * @param {vec3} out the receiving vector + * @param {vec3} a vector to invert + * @returns {vec3} out + */ +export function inverse(out, a) { + out[0] = 1.0 / a[0]; + out[1] = 1.0 / a[1]; + out[2] = 1.0 / a[2]; + return out; +} + +/** + * Normalize a vec3 + * + * @param {vec3} out the receiving vector + * @param {vec3} a vector to normalize + * @returns {vec3} out + */ +export function normalize(out, a) { + let x = a[0]; + let y = a[1]; + let z = a[2]; + let len = x*x + y*y + z*z; + if (len > 0) { + //TODO: evaluate use of glm_invsqrt here? + len = 1 / Math.sqrt(len); + out[0] = a[0] * len; + out[1] = a[1] * len; + out[2] = a[2] * len; + } + return out; +} + +/** + * Calculates the dot product of two vec3's + * + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {Number} dot product of a and b + */ +export function dot(a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} + +/** + * Computes the cross product of two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ +export function cross(out, a, b) { + let ax = a[0], ay = a[1], az = a[2]; + let bx = b[0], by = b[1], bz = b[2]; + + out[0] = ay * bz - az * by; + out[1] = az * bx - ax * bz; + out[2] = ax * by - ay * bx; + return out; +} + +/** + * Performs a linear interpolation between two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @param {Number} t interpolation amount, in the range [0-1], between the two inputs + * @returns {vec3} out + */ +export function lerp(out, a, b, t) { + let ax = a[0]; + let ay = a[1]; + let az = a[2]; + out[0] = ax + t * (b[0] - ax); + out[1] = ay + t * (b[1] - ay); + out[2] = az + t * (b[2] - az); + return out; +} + +/** + * Performs a hermite interpolation with two control points + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @param {vec3} c the third operand + * @param {vec3} d the fourth operand + * @param {Number} t interpolation amount, in the range [0-1], between the two inputs + * @returns {vec3} out + */ +export function hermite(out, a, b, c, d, t) { + let factorTimes2 = t * t; + let factor1 = factorTimes2 * (2 * t - 3) + 1; + let factor2 = factorTimes2 * (t - 2) + t; + let factor3 = factorTimes2 * (t - 1); + let factor4 = factorTimes2 * (3 - 2 * t); + + out[0] = a[0] * factor1 + b[0] * factor2 + c[0] * factor3 + d[0] * factor4; + out[1] = a[1] * factor1 + b[1] * factor2 + c[1] * factor3 + d[1] * factor4; + out[2] = a[2] * factor1 + b[2] * factor2 + c[2] * factor3 + d[2] * factor4; + + return out; +} + +/** + * Performs a bezier interpolation with two control points + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @param {vec3} c the third operand + * @param {vec3} d the fourth operand + * @param {Number} t interpolation amount, in the range [0-1], between the two inputs + * @returns {vec3} out + */ +export function bezier(out, a, b, c, d, t) { + let inverseFactor = 1 - t; + let inverseFactorTimesTwo = inverseFactor * inverseFactor; + let factorTimes2 = t * t; + let factor1 = inverseFactorTimesTwo * inverseFactor; + let factor2 = 3 * t * inverseFactorTimesTwo; + let factor3 = 3 * factorTimes2 * inverseFactor; + let factor4 = factorTimes2 * t; + + out[0] = a[0] * factor1 + b[0] * factor2 + c[0] * factor3 + d[0] * factor4; + out[1] = a[1] * factor1 + b[1] * factor2 + c[1] * factor3 + d[1] * factor4; + out[2] = a[2] * factor1 + b[2] * factor2 + c[2] * factor3 + d[2] * factor4; + + return out; +} + +/** + * Generates a random vector with the given scale + * + * @param {vec3} out the receiving vector + * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned + * @returns {vec3} out + */ +export function random(out, scale) { + scale = scale || 1.0; + + let r = glMatrix.RANDOM() * 2.0 * Math.PI; + let z = (glMatrix.RANDOM() * 2.0) - 1.0; + let zScale = Math.sqrt(1.0-z*z) * scale; + + out[0] = Math.cos(r) * zScale; + out[1] = Math.sin(r) * zScale; + out[2] = z * scale; + return out; +} + +/** + * Transforms the vec3 with a mat4. + * 4th vector component is implicitly '1' + * + * @param {vec3} out the receiving vector + * @param {vec3} a the vector to transform + * @param {mat4} m matrix to transform with + * @returns {vec3} out + */ +export function transformMat4(out, a, m) { + let x = a[0], y = a[1], z = a[2]; + let w = m[3] * x + m[7] * y + m[11] * z + m[15]; + w = w || 1.0; + out[0] = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w; + out[1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w; + out[2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w; + return out; +} + +/** + * Transforms the vec3 with a mat3. + * + * @param {vec3} out the receiving vector + * @param {vec3} a the vector to transform + * @param {mat3} m the 3x3 matrix to transform with + * @returns {vec3} out + */ +export function transformMat3(out, a, m) { + let x = a[0], y = a[1], z = a[2]; + out[0] = x * m[0] + y * m[3] + z * m[6]; + out[1] = x * m[1] + y * m[4] + z * m[7]; + out[2] = x * m[2] + y * m[5] + z * m[8]; + return out; +} + +/** + * Transforms the vec3 with a quat + * Can also be used for dual quaternions. (Multiply it with the real part) + * + * @param {vec3} out the receiving vector + * @param {vec3} a the vector to transform + * @param {quat} q quaternion to transform with + * @returns {vec3} out + */ +export function transformQuat(out, a, q) { + // benchmarks: https://jsperf.com/quaternion-transform-vec3-implementations-fixed + let qx = q[0], qy = q[1], qz = q[2], qw = q[3]; + let x = a[0], y = a[1], z = a[2]; + // var qvec = [qx, qy, qz]; + // var uv = vec3.cross([], qvec, a); + let uvx = qy * z - qz * y, + uvy = qz * x - qx * z, + uvz = qx * y - qy * x; + // var uuv = vec3.cross([], qvec, uv); + let uuvx = qy * uvz - qz * uvy, + uuvy = qz * uvx - qx * uvz, + uuvz = qx * uvy - qy * uvx; + // vec3.scale(uv, uv, 2 * w); + let w2 = qw * 2; + uvx *= w2; + uvy *= w2; + uvz *= w2; + // vec3.scale(uuv, uuv, 2); + uuvx *= 2; + uuvy *= 2; + uuvz *= 2; + // return vec3.add(out, a, vec3.add(out, uv, uuv)); + out[0] = x + uvx + uuvx; + out[1] = y + uvy + uuvy; + out[2] = z + uvz + uuvz; + return out; +} + +/** + * Rotate a 3D vector around the x-axis + * @param {vec3} out The receiving vec3 + * @param {vec3} a The vec3 point to rotate + * @param {vec3} b The origin of the rotation + * @param {Number} c The angle of rotation + * @returns {vec3} out + */ +export function rotateX(out, a, b, c){ + let p = [], r=[]; + //Translate point to the origin + p[0] = a[0] - b[0]; + p[1] = a[1] - b[1]; + p[2] = a[2] - b[2]; + + //perform rotation + r[0] = p[0]; + r[1] = p[1]*Math.cos(c) - p[2]*Math.sin(c); + r[2] = p[1]*Math.sin(c) + p[2]*Math.cos(c); + + //translate to correct position + out[0] = r[0] + b[0]; + out[1] = r[1] + b[1]; + out[2] = r[2] + b[2]; + + return out; +} + +/** + * Rotate a 3D vector around the y-axis + * @param {vec3} out The receiving vec3 + * @param {vec3} a The vec3 point to rotate + * @param {vec3} b The origin of the rotation + * @param {Number} c The angle of rotation + * @returns {vec3} out + */ +export function rotateY(out, a, b, c){ + let p = [], r=[]; + //Translate point to the origin + p[0] = a[0] - b[0]; + p[1] = a[1] - b[1]; + p[2] = a[2] - b[2]; + + //perform rotation + r[0] = p[2]*Math.sin(c) + p[0]*Math.cos(c); + r[1] = p[1]; + r[2] = p[2]*Math.cos(c) - p[0]*Math.sin(c); + + //translate to correct position + out[0] = r[0] + b[0]; + out[1] = r[1] + b[1]; + out[2] = r[2] + b[2]; + + return out; +} + +/** + * Rotate a 3D vector around the z-axis + * @param {vec3} out The receiving vec3 + * @param {vec3} a The vec3 point to rotate + * @param {vec3} b The origin of the rotation + * @param {Number} c The angle of rotation + * @returns {vec3} out + */ +export function rotateZ(out, a, b, c){ + let p = [], r=[]; + //Translate point to the origin + p[0] = a[0] - b[0]; + p[1] = a[1] - b[1]; + p[2] = a[2] - b[2]; + + //perform rotation + r[0] = p[0]*Math.cos(c) - p[1]*Math.sin(c); + r[1] = p[0]*Math.sin(c) + p[1]*Math.cos(c); + r[2] = p[2]; + + //translate to correct position + out[0] = r[0] + b[0]; + out[1] = r[1] + b[1]; + out[2] = r[2] + b[2]; + + return out; +} + +/** + * Get the angle between two 3D vectors + * @param {vec3} a The first operand + * @param {vec3} b The second operand + * @returns {Number} The angle in radians + */ +export function angle(a, b) { + let tempA = fromValues(a[0], a[1], a[2]); + let tempB = fromValues(b[0], b[1], b[2]); + + normalize(tempA, tempA); + normalize(tempB, tempB); + + let cosine = dot(tempA, tempB); + + if(cosine > 1.0) { + return 0; + } + else if(cosine < -1.0) { + return Math.PI; + } else { + return Math.acos(cosine); + } +} + +/** + * Returns a string representation of a vector + * + * @param {vec3} a vector to represent as a string + * @returns {String} string representation of the vector + */ +export function str(a) { + return 'vec3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ')'; +} + +/** + * Returns whether or not the vectors have exactly the same elements in the same position (when compared with ===) + * + * @param {vec3} a The first vector. + * @param {vec3} b The second vector. + * @returns {Boolean} True if the vectors are equal, false otherwise. + */ +export function exactEquals(a, b) { + return a[0] === b[0] && a[1] === b[1] && a[2] === b[2]; +} + +/** + * Returns whether or not the vectors have approximately the same elements in the same position. + * + * @param {vec3} a The first vector. + * @param {vec3} b The second vector. + * @returns {Boolean} True if the vectors are equal, false otherwise. + */ +export function equals(a, b) { + let a0 = a[0], a1 = a[1], a2 = a[2]; + let b0 = b[0], b1 = b[1], b2 = b[2]; + return (Math.abs(a0 - b0) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a0), Math.abs(b0)) && + Math.abs(a1 - b1) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a1), Math.abs(b1)) && + Math.abs(a2 - b2) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a2), Math.abs(b2))); +} + +/** + * Alias for {@link vec3.subtract} + * @function + */ +export const sub = subtract; + +/** + * Alias for {@link vec3.multiply} + * @function + */ +export const mul = multiply; + +/** + * Alias for {@link vec3.divide} + * @function + */ +export const div = divide; + +/** + * Alias for {@link vec3.distance} + * @function + */ +export const dist = distance; + +/** + * Alias for {@link vec3.squaredDistance} + * @function + */ +export const sqrDist = squaredDistance; + +/** + * Alias for {@link vec3.length} + * @function + */ +export const len = length; + +/** + * Alias for {@link vec3.squaredLength} + * @function + */ +export const sqrLen = squaredLength; + +/** + * Perform some operation over an array of vec3s. + * + * @param {Array} a the array of vectors to iterate over + * @param {Number} stride Number of elements between the start of each vec3. If 0 assumes tightly packed + * @param {Number} offset Number of elements to skip at the beginning of the array + * @param {Number} count Number of vec3s to iterate over. If 0 iterates over entire array + * @param {Function} fn Function to call for each vector in the array + * @param {Object} [arg] additional argument to pass to fn + * @returns {Array} a + * @function + */ +export const forEach = (function() { + let vec = create(); + + return function(a, stride, offset, count, fn, arg) { + let i, l; + if(!stride) { + stride = 3; + } + + if(!offset) { + offset = 0; + } + + if(count) { + l = Math.min((count * stride) + offset, a.length); + } else { + l = a.length; + } + + for(i = offset; i < l; i += stride) { + vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2]; + fn(vec, vec, arg); + a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2]; + } + + return a; + }; +})(); \ No newline at end of file diff --git a/js/external-libraries/gl-matrix/vec4.js b/js/external-libraries/gl-matrix/vec4.js new file mode 100644 index 0000000..3f6932e --- /dev/null +++ b/js/external-libraries/gl-matrix/vec4.js @@ -0,0 +1,602 @@ +import * as glMatrix from "./common.js"; + +/** + * 4 Dimensional Vector + * @module vec4 + */ + +/** + * Creates a new, empty vec4 + * + * @returns {vec4} a new 4D vector + */ +export function create() { + let out = new glMatrix.ARRAY_TYPE(4); + if(glMatrix.ARRAY_TYPE != Float32Array) { + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 0; + } + return out; +} + +/** + * Creates a new vec4 initialized with values from an existing vector + * + * @param {vec4} a vector to clone + * @returns {vec4} a new 4D vector + */ +export function clone(a) { + let out = new glMatrix.ARRAY_TYPE(4); + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + return out; +} + +/** + * Creates a new vec4 initialized with the given values + * + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @param {Number} w W component + * @returns {vec4} a new 4D vector + */ +export function fromValues(x, y, z, w) { + let out = new glMatrix.ARRAY_TYPE(4); + out[0] = x; + out[1] = y; + out[2] = z; + out[3] = w; + return out; +} + +/** + * Copy the values from one vec4 to another + * + * @param {vec4} out the receiving vector + * @param {vec4} a the source vector + * @returns {vec4} out + */ +export function copy(out, a) { + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + return out; +} + +/** + * Set the components of a vec4 to the given values + * + * @param {vec4} out the receiving vector + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @param {Number} w W component + * @returns {vec4} out + */ +export function set(out, x, y, z, w) { + out[0] = x; + out[1] = y; + out[2] = z; + out[3] = w; + return out; +} + +/** + * Adds two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ +export function add(out, a, b) { + out[0] = a[0] + b[0]; + out[1] = a[1] + b[1]; + out[2] = a[2] + b[2]; + out[3] = a[3] + b[3]; + return out; +} + +/** + * Subtracts vector b from vector a + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ +export function subtract(out, a, b) { + out[0] = a[0] - b[0]; + out[1] = a[1] - b[1]; + out[2] = a[2] - b[2]; + out[3] = a[3] - b[3]; + return out; +} + +/** + * Multiplies two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ +export function multiply(out, a, b) { + out[0] = a[0] * b[0]; + out[1] = a[1] * b[1]; + out[2] = a[2] * b[2]; + out[3] = a[3] * b[3]; + return out; +} + +/** + * Divides two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ +export function divide(out, a, b) { + out[0] = a[0] / b[0]; + out[1] = a[1] / b[1]; + out[2] = a[2] / b[2]; + out[3] = a[3] / b[3]; + return out; +} + +/** + * Math.ceil the components of a vec4 + * + * @param {vec4} out the receiving vector + * @param {vec4} a vector to ceil + * @returns {vec4} out + */ +export function ceil(out, a) { + out[0] = Math.ceil(a[0]); + out[1] = Math.ceil(a[1]); + out[2] = Math.ceil(a[2]); + out[3] = Math.ceil(a[3]); + return out; +} + +/** + * Math.floor the components of a vec4 + * + * @param {vec4} out the receiving vector + * @param {vec4} a vector to floor + * @returns {vec4} out + */ +export function floor(out, a) { + out[0] = Math.floor(a[0]); + out[1] = Math.floor(a[1]); + out[2] = Math.floor(a[2]); + out[3] = Math.floor(a[3]); + return out; +} + +/** + * Returns the minimum of two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ +export function min(out, a, b) { + out[0] = Math.min(a[0], b[0]); + out[1] = Math.min(a[1], b[1]); + out[2] = Math.min(a[2], b[2]); + out[3] = Math.min(a[3], b[3]); + return out; +} + +/** + * Returns the maximum of two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ +export function max(out, a, b) { + out[0] = Math.max(a[0], b[0]); + out[1] = Math.max(a[1], b[1]); + out[2] = Math.max(a[2], b[2]); + out[3] = Math.max(a[3], b[3]); + return out; +} + +/** + * Math.round the components of a vec4 + * + * @param {vec4} out the receiving vector + * @param {vec4} a vector to round + * @returns {vec4} out + */ +export function round(out, a) { + out[0] = Math.round(a[0]); + out[1] = Math.round(a[1]); + out[2] = Math.round(a[2]); + out[3] = Math.round(a[3]); + return out; +} + +/** + * Scales a vec4 by a scalar number + * + * @param {vec4} out the receiving vector + * @param {vec4} a the vector to scale + * @param {Number} b amount to scale the vector by + * @returns {vec4} out + */ +export function scale(out, a, b) { + out[0] = a[0] * b; + out[1] = a[1] * b; + out[2] = a[2] * b; + out[3] = a[3] * b; + return out; +} + +/** + * Adds two vec4's after scaling the second operand by a scalar value + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @param {Number} scale the amount to scale b by before adding + * @returns {vec4} out + */ +export function scaleAndAdd(out, a, b, scale) { + out[0] = a[0] + (b[0] * scale); + out[1] = a[1] + (b[1] * scale); + out[2] = a[2] + (b[2] * scale); + out[3] = a[3] + (b[3] * scale); + return out; +} + +/** + * Calculates the euclidian distance between two vec4's + * + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {Number} distance between a and b + */ +export function distance(a, b) { + let x = b[0] - a[0]; + let y = b[1] - a[1]; + let z = b[2] - a[2]; + let w = b[3] - a[3]; + return Math.sqrt(x*x + y*y + z*z + w*w); +} + +/** + * Calculates the squared euclidian distance between two vec4's + * + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {Number} squared distance between a and b + */ +export function squaredDistance(a, b) { + let x = b[0] - a[0]; + let y = b[1] - a[1]; + let z = b[2] - a[2]; + let w = b[3] - a[3]; + return x*x + y*y + z*z + w*w; +} + +/** + * Calculates the length of a vec4 + * + * @param {vec4} a vector to calculate length of + * @returns {Number} length of a + */ +export function length(a) { + let x = a[0]; + let y = a[1]; + let z = a[2]; + let w = a[3]; + return Math.sqrt(x*x + y*y + z*z + w*w); +} + +/** + * Calculates the squared length of a vec4 + * + * @param {vec4} a vector to calculate squared length of + * @returns {Number} squared length of a + */ +export function squaredLength(a) { + let x = a[0]; + let y = a[1]; + let z = a[2]; + let w = a[3]; + return x*x + y*y + z*z + w*w; +} + +/** + * Negates the components of a vec4 + * + * @param {vec4} out the receiving vector + * @param {vec4} a vector to negate + * @returns {vec4} out + */ +export function negate(out, a) { + out[0] = -a[0]; + out[1] = -a[1]; + out[2] = -a[2]; + out[3] = -a[3]; + return out; +} + +/** + * Returns the inverse of the components of a vec4 + * + * @param {vec4} out the receiving vector + * @param {vec4} a vector to invert + * @returns {vec4} out + */ +export function inverse(out, a) { + out[0] = 1.0 / a[0]; + out[1] = 1.0 / a[1]; + out[2] = 1.0 / a[2]; + out[3] = 1.0 / a[3]; + return out; +} + +/** + * Normalize a vec4 + * + * @param {vec4} out the receiving vector + * @param {vec4} a vector to normalize + * @returns {vec4} out + */ +export function normalize(out, a) { + let x = a[0]; + let y = a[1]; + let z = a[2]; + let w = a[3]; + let len = x*x + y*y + z*z + w*w; + if (len > 0) { + len = 1 / Math.sqrt(len); + out[0] = x * len; + out[1] = y * len; + out[2] = z * len; + out[3] = w * len; + } + return out; +} + +/** + * Calculates the dot product of two vec4's + * + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {Number} dot product of a and b + */ +export function dot(a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]; +} + +/** + * Performs a linear interpolation between two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @param {Number} t interpolation amount, in the range [0-1], between the two inputs + * @returns {vec4} out + */ +export function lerp(out, a, b, t) { + let ax = a[0]; + let ay = a[1]; + let az = a[2]; + let aw = a[3]; + out[0] = ax + t * (b[0] - ax); + out[1] = ay + t * (b[1] - ay); + out[2] = az + t * (b[2] - az); + out[3] = aw + t * (b[3] - aw); + return out; +} + +/** + * Generates a random vector with the given scale + * + * @param {vec4} out the receiving vector + * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned + * @returns {vec4} out + */ +export function random(out, scale) { + scale = scale || 1.0; + + // Marsaglia, George. Choosing a Point from the Surface of a + // Sphere. Ann. Math. Statist. 43 (1972), no. 2, 645--646. + // http://projecteuclid.org/euclid.aoms/1177692644; + var v1, v2, v3, v4; + var s1, s2; + do { + v1 = glMatrix.RANDOM() * 2 - 1; + v2 = glMatrix.RANDOM() * 2 - 1; + s1 = v1 * v1 + v2 * v2; + } while (s1 >= 1); + do { + v3 = glMatrix.RANDOM() * 2 - 1; + v4 = glMatrix.RANDOM() * 2 - 1; + s2 = v3 * v3 + v4 * v4; + } while (s2 >= 1); + + var d = Math.sqrt((1 - s1) / s2); + out[0] = scale * v1; + out[1] = scale * v2; + out[2] = scale * v3 * d; + out[3] = scale * v4 * d; + return out; +} + +/** + * Transforms the vec4 with a mat4. + * + * @param {vec4} out the receiving vector + * @param {vec4} a the vector to transform + * @param {mat4} m matrix to transform with + * @returns {vec4} out + */ +export function transformMat4(out, a, m) { + let x = a[0], y = a[1], z = a[2], w = a[3]; + out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w; + out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w; + out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w; + out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w; + return out; +} + +/** + * Transforms the vec4 with a quat + * + * @param {vec4} out the receiving vector + * @param {vec4} a the vector to transform + * @param {quat} q quaternion to transform with + * @returns {vec4} out + */ +export function transformQuat(out, a, q) { + let x = a[0], y = a[1], z = a[2]; + let qx = q[0], qy = q[1], qz = q[2], qw = q[3]; + + // calculate quat * vec + let ix = qw * x + qy * z - qz * y; + let iy = qw * y + qz * x - qx * z; + let iz = qw * z + qx * y - qy * x; + let iw = -qx * x - qy * y - qz * z; + + // calculate result * inverse quat + out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy; + out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz; + out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx; + out[3] = a[3]; + return out; +} + +/** + * Returns a string representation of a vector + * + * @param {vec4} a vector to represent as a string + * @returns {String} string representation of the vector + */ +export function str(a) { + return 'vec4(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')'; +} + +/** + * Returns whether or not the vectors have exactly the same elements in the same position (when compared with ===) + * + * @param {vec4} a The first vector. + * @param {vec4} b The second vector. + * @returns {Boolean} True if the vectors are equal, false otherwise. + */ +export function exactEquals(a, b) { + return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]; +} + +/** + * Returns whether or not the vectors have approximately the same elements in the same position. + * + * @param {vec4} a The first vector. + * @param {vec4} b The second vector. + * @returns {Boolean} True if the vectors are equal, false otherwise. + */ +export function equals(a, b) { + let a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3]; + let b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; + return (Math.abs(a0 - b0) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a0), Math.abs(b0)) && + Math.abs(a1 - b1) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a1), Math.abs(b1)) && + Math.abs(a2 - b2) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a2), Math.abs(b2)) && + Math.abs(a3 - b3) <= glMatrix.EPSILON*Math.max(1.0, Math.abs(a3), Math.abs(b3))); +} + +/** + * Alias for {@link vec4.subtract} + * @function + */ +export const sub = subtract; + +/** + * Alias for {@link vec4.multiply} + * @function + */ +export const mul = multiply; + +/** + * Alias for {@link vec4.divide} + * @function + */ +export const div = divide; + +/** + * Alias for {@link vec4.distance} + * @function + */ +export const dist = distance; + +/** + * Alias for {@link vec4.squaredDistance} + * @function + */ +export const sqrDist = squaredDistance; + +/** + * Alias for {@link vec4.length} + * @function + */ +export const len = length; + +/** + * Alias for {@link vec4.squaredLength} + * @function + */ +export const sqrLen = squaredLength; + +/** + * Perform some operation over an array of vec4s. + * + * @param {Array} a the array of vectors to iterate over + * @param {Number} stride Number of elements between the start of each vec4. If 0 assumes tightly packed + * @param {Number} offset Number of elements to skip at the beginning of the array + * @param {Number} count Number of vec4s to iterate over. If 0 iterates over entire array + * @param {Function} fn Function to call for each vector in the array + * @param {Object} [arg] additional argument to pass to fn + * @returns {Array} a + * @function + */ +export const forEach = (function() { + let vec = create(); + + return function(a, stride, offset, count, fn, arg) { + let i, l; + if(!stride) { + stride = 4; + } + + if(!offset) { + offset = 0; + } + + if(count) { + l = Math.min((count * stride) + offset, a.length); + } else { + l = a.length; + } + + for(i = offset; i < l; i += stride) { + vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2]; vec[3] = a[i+3]; + fn(vec, vec, arg); + a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2]; a[i+3] = vec[3]; + } + + return a; + }; +})(); \ No newline at end of file diff --git a/js/external-libraries/libs/motion-controllers.module.js b/js/external-libraries/libs/motion-controllers.module.js new file mode 100644 index 0000000..21baaa1 --- /dev/null +++ b/js/external-libraries/libs/motion-controllers.module.js @@ -0,0 +1,397 @@ +/** + * @webxr-input-profiles/motion-controllers 1.0.0 https://github.com/immersive-web/webxr-input-profiles + */ + +const Constants = { + Handedness: Object.freeze({ + NONE: 'none', + LEFT: 'left', + RIGHT: 'right' + }), + + ComponentState: Object.freeze({ + DEFAULT: 'default', + TOUCHED: 'touched', + PRESSED: 'pressed' + }), + + ComponentProperty: Object.freeze({ + BUTTON: 'button', + X_AXIS: 'xAxis', + Y_AXIS: 'yAxis', + STATE: 'state' + }), + + ComponentType: Object.freeze({ + TRIGGER: 'trigger', + SQUEEZE: 'squeeze', + TOUCHPAD: 'touchpad', + THUMBSTICK: 'thumbstick', + BUTTON: 'button' + }), + + ButtonTouchThreshold: 0.05, + + AxisTouchThreshold: 0.1, + + VisualResponseProperty: Object.freeze({ + TRANSFORM: 'transform', + VISIBILITY: 'visibility' + }) +}; + +/** + * @description Static helper function to fetch a JSON file and turn it into a JS object + * @param {string} path - Path to JSON file to be fetched + */ +async function fetchJsonFile(path) { + const response = await fetch(path); + if (!response.ok) { + throw new Error(response.statusText); + } else { + return response.json(); + } +} + +async function fetchProfilesList(basePath) { + if (!basePath) { + throw new Error('No basePath supplied'); + } + + const profileListFileName = 'profilesList.json'; + const profilesList = await fetchJsonFile(`${basePath}/${profileListFileName}`); + return profilesList; +} + +async function fetchProfile(xrInputSource, basePath, defaultProfile = null, getAssetPath = true) { + if (!xrInputSource) { + throw new Error('No xrInputSource supplied'); + } + + if (!basePath) { + throw new Error('No basePath supplied'); + } + + // Get the list of profiles + const supportedProfilesList = await fetchProfilesList(basePath); + + // Find the relative path to the first requested profile that is recognized + let match; + xrInputSource.profiles.some((profileId) => { + const supportedProfile = supportedProfilesList[profileId]; + if (supportedProfile) { + match = { + profileId, + profilePath: `${basePath}/${supportedProfile.path}`, + deprecated: !!supportedProfile.deprecated + }; + } + return !!match; + }); + + if (!match) { + if (!defaultProfile) { + throw new Error('No matching profile name found'); + } + + const supportedProfile = supportedProfilesList[defaultProfile]; + if (!supportedProfile) { + throw new Error(`No matching profile name found and default profile "${defaultProfile}" missing.`); + } + + match = { + profileId: defaultProfile, + profilePath: `${basePath}/${supportedProfile.path}`, + deprecated: !!supportedProfile.deprecated + }; + } + + const profile = await fetchJsonFile(match.profilePath); + + let assetPath; + if (getAssetPath) { + let layout; + if (xrInputSource.handedness === 'any') { + layout = profile.layouts[Object.keys(profile.layouts)[0]]; + } else { + layout = profile.layouts[xrInputSource.handedness]; + } + if (!layout) { + throw new Error( + `No matching handedness, ${xrInputSource.handedness}, in profile ${match.profileId}` + ); + } + + if (layout.assetPath) { + assetPath = match.profilePath.replace('profile.json', layout.assetPath); + } + } + + return { profile, assetPath }; +} + +/** @constant {Object} */ +const defaultComponentValues = { + xAxis: 0, + yAxis: 0, + button: 0, + state: Constants.ComponentState.DEFAULT +}; + +/** + * @description Converts an X, Y coordinate from the range -1 to 1 (as reported by the Gamepad + * API) to the range 0 to 1 (for interpolation). Also caps the X, Y values to be bounded within + * a circle. This ensures that thumbsticks are not animated outside the bounds of their physical + * range of motion and touchpads do not report touch locations off their physical bounds. + * @param {number} x The original x coordinate in the range -1 to 1 + * @param {number} y The original y coordinate in the range -1 to 1 + */ +function normalizeAxes(x = 0, y = 0) { + let xAxis = x; + let yAxis = y; + + // Determine if the point is outside the bounds of the circle + // and, if so, place it on the edge of the circle + const hypotenuse = Math.sqrt((x * x) + (y * y)); + if (hypotenuse > 1) { + const theta = Math.atan2(y, x); + xAxis = Math.cos(theta); + yAxis = Math.sin(theta); + } + + // Scale and move the circle so values are in the interpolation range. The circle's origin moves + // from (0, 0) to (0.5, 0.5). The circle's radius scales from 1 to be 0.5. + const result = { + normalizedXAxis: (xAxis * 0.5) + 0.5, + normalizedYAxis: (yAxis * 0.5) + 0.5 + }; + return result; +} + +/** + * Contains the description of how the 3D model should visually respond to a specific user input. + * This is accomplished by initializing the object with the name of a node in the 3D model and + * property that need to be modified in response to user input, the name of the nodes representing + * the allowable range of motion, and the name of the input which triggers the change. In response + * to the named input changing, this object computes the appropriate weighting to use for + * interpolating between the range of motion nodes. + */ +class VisualResponse { + constructor(visualResponseDescription) { + this.componentProperty = visualResponseDescription.componentProperty; + this.states = visualResponseDescription.states; + this.valueNodeName = visualResponseDescription.valueNodeName; + this.valueNodeProperty = visualResponseDescription.valueNodeProperty; + + if (this.valueNodeProperty === Constants.VisualResponseProperty.TRANSFORM) { + this.minNodeName = visualResponseDescription.minNodeName; + this.maxNodeName = visualResponseDescription.maxNodeName; + } + + // Initializes the response's current value based on default data + this.value = 0; + this.updateFromComponent(defaultComponentValues); + } + + /** + * Computes the visual response's interpolation weight based on component state + * @param {Object} componentValues - The component from which to update + * @param {number} xAxis - The reported X axis value of the component + * @param {number} yAxis - The reported Y axis value of the component + * @param {number} button - The reported value of the component's button + * @param {string} state - The component's active state + */ + updateFromComponent({ + xAxis, yAxis, button, state + }) { + const { normalizedXAxis, normalizedYAxis } = normalizeAxes(xAxis, yAxis); + switch (this.componentProperty) { + case Constants.ComponentProperty.X_AXIS: + this.value = (this.states.includes(state)) ? normalizedXAxis : 0.5; + break; + case Constants.ComponentProperty.Y_AXIS: + this.value = (this.states.includes(state)) ? normalizedYAxis : 0.5; + break; + case Constants.ComponentProperty.BUTTON: + this.value = (this.states.includes(state)) ? button : 0; + break; + case Constants.ComponentProperty.STATE: + if (this.valueNodeProperty === Constants.VisualResponseProperty.VISIBILITY) { + this.value = (this.states.includes(state)); + } else { + this.value = this.states.includes(state) ? 1.0 : 0.0; + } + break; + default: + throw new Error(`Unexpected visualResponse componentProperty ${this.componentProperty}`); + } + } +} + +class Component { + /** + * @param {Object} componentId - Id of the component + * @param {Object} componentDescription - Description of the component to be created + */ + constructor(componentId, componentDescription) { + if (!componentId + || !componentDescription + || !componentDescription.visualResponses + || !componentDescription.gamepadIndices + || Object.keys(componentDescription.gamepadIndices).length === 0) { + throw new Error('Invalid arguments supplied'); + } + + this.id = componentId; + this.type = componentDescription.type; + this.rootNodeName = componentDescription.rootNodeName; + this.touchPointNodeName = componentDescription.touchPointNodeName; + + // Build all the visual responses for this component + this.visualResponses = {}; + Object.keys(componentDescription.visualResponses).forEach((responseName) => { + const visualResponse = new VisualResponse(componentDescription.visualResponses[responseName]); + this.visualResponses[responseName] = visualResponse; + }); + + // Set default values + this.gamepadIndices = Object.assign({}, componentDescription.gamepadIndices); + + this.values = { + state: Constants.ComponentState.DEFAULT, + button: (this.gamepadIndices.button !== undefined) ? 0 : undefined, + xAxis: (this.gamepadIndices.xAxis !== undefined) ? 0 : undefined, + yAxis: (this.gamepadIndices.yAxis !== undefined) ? 0 : undefined + }; + } + + get data() { + const data = { id: this.id, ...this.values }; + return data; + } + + /** + * @description Poll for updated data based on current gamepad state + * @param {Object} gamepad - The gamepad object from which the component data should be polled + */ + updateFromGamepad(gamepad) { + // Set the state to default before processing other data sources + this.values.state = Constants.ComponentState.DEFAULT; + + // Get and normalize button + if (this.gamepadIndices.button !== undefined + && gamepad.buttons.length > this.gamepadIndices.button) { + const gamepadButton = gamepad.buttons[this.gamepadIndices.button]; + this.values.button = gamepadButton.value; + this.values.button = (this.values.button < 0) ? 0 : this.values.button; + this.values.button = (this.values.button > 1) ? 1 : this.values.button; + + // Set the state based on the button + if (gamepadButton.pressed || this.values.button === 1) { + this.values.state = Constants.ComponentState.PRESSED; + } else if (gamepadButton.touched || this.values.button > Constants.ButtonTouchThreshold) { + this.values.state = Constants.ComponentState.TOUCHED; + } + } + + // Get and normalize x axis value + if (this.gamepadIndices.xAxis !== undefined + && gamepad.axes.length > this.gamepadIndices.xAxis) { + this.values.xAxis = gamepad.axes[this.gamepadIndices.xAxis]; + this.values.xAxis = (this.values.xAxis < -1) ? -1 : this.values.xAxis; + this.values.xAxis = (this.values.xAxis > 1) ? 1 : this.values.xAxis; + + // If the state is still default, check if the xAxis makes it touched + if (this.values.state === Constants.ComponentState.DEFAULT + && Math.abs(this.values.xAxis) > Constants.AxisTouchThreshold) { + this.values.state = Constants.ComponentState.TOUCHED; + } + } + + // Get and normalize Y axis value + if (this.gamepadIndices.yAxis !== undefined + && gamepad.axes.length > this.gamepadIndices.yAxis) { + this.values.yAxis = gamepad.axes[this.gamepadIndices.yAxis]; + this.values.yAxis = (this.values.yAxis < -1) ? -1 : this.values.yAxis; + this.values.yAxis = (this.values.yAxis > 1) ? 1 : this.values.yAxis; + + // If the state is still default, check if the yAxis makes it touched + if (this.values.state === Constants.ComponentState.DEFAULT + && Math.abs(this.values.yAxis) > Constants.AxisTouchThreshold) { + this.values.state = Constants.ComponentState.TOUCHED; + } + } + + // Update the visual response weights based on the current component data + Object.values(this.visualResponses).forEach((visualResponse) => { + visualResponse.updateFromComponent(this.values); + }); + } +} + +/** + * @description Builds a motion controller with components and visual responses based on the + * supplied profile description. Data is polled from the xrInputSource's gamepad. + * @author Nell Waliczek / https://github.com/NellWaliczek + */ +class MotionController { + /** + * @param {Object} xrInputSource - The XRInputSource to build the MotionController around + * @param {Object} profile - The best matched profile description for the supplied xrInputSource + * @param {Object} assetUrl + */ + constructor(xrInputSource, profile, assetUrl) { + if (!xrInputSource) { + throw new Error('No xrInputSource supplied'); + } + + if (!profile) { + throw new Error('No profile supplied'); + } + + this.xrInputSource = xrInputSource; + this.assetUrl = assetUrl; + this.id = profile.profileId; + + // Build child components as described in the profile description + this.layoutDescription = profile.layouts[xrInputSource.handedness]; + this.components = {}; + Object.keys(this.layoutDescription.components).forEach((componentId) => { + const componentDescription = this.layoutDescription.components[componentId]; + this.components[componentId] = new Component(componentId, componentDescription); + }); + + // Initialize components based on current gamepad state + this.updateFromGamepad(); + } + + get gripSpace() { + return this.xrInputSource.gripSpace; + } + + get targetRaySpace() { + return this.xrInputSource.targetRaySpace; + } + + /** + * @description Returns a subset of component data for simplified debugging + */ + get data() { + const data = []; + Object.values(this.components).forEach((component) => { + data.push(component.data); + }); + return data; + } + + /** + * @description Poll for updated data based on current gamepad state + */ + updateFromGamepad() { + Object.values(this.components).forEach((component) => { + component.updateFromGamepad(this.xrInputSource.gamepad); + }); + } +} + +export { Constants, MotionController, fetchProfile, fetchProfilesList }; \ No newline at end of file diff --git a/js/external-libraries/loaders/GLTFLoader.js b/js/external-libraries/loaders/GLTFLoader.js new file mode 100644 index 0000000..7a5f55c --- /dev/null +++ b/js/external-libraries/loaders/GLTFLoader.js @@ -0,0 +1,4724 @@ +import { + AnimationClip, + Bone, + Box3, + BufferAttribute, + BufferGeometry, + ClampToEdgeWrapping, + Color, + DirectionalLight, + DoubleSide, + FileLoader, + FrontSide, + Group, + ImageBitmapLoader, + InstancedMesh, + InterleavedBuffer, + InterleavedBufferAttribute, + Interpolant, + InterpolateDiscrete, + InterpolateLinear, + Line, + LineBasicMaterial, + LineLoop, + LineSegments, + LinearFilter, + LinearMipmapLinearFilter, + LinearMipmapNearestFilter, + Loader, + LoaderUtils, + Material, + MathUtils, + Matrix4, + Mesh, + MeshBasicMaterial, + MeshPhysicalMaterial, + MeshStandardMaterial, + MirroredRepeatWrapping, + NearestFilter, + NearestMipmapLinearFilter, + NearestMipmapNearestFilter, + NumberKeyframeTrack, + Object3D, + OrthographicCamera, + PerspectiveCamera, + PointLight, + Points, + PointsMaterial, + PropertyBinding, + Quaternion, + QuaternionKeyframeTrack, + RepeatWrapping, + Skeleton, + SkinnedMesh, + Sphere, + SpotLight, + TangentSpaceNormalMap, + Texture, + TextureLoader, + TriangleFanDrawMode, + TriangleStripDrawMode, + Vector2, + Vector3, + VectorKeyframeTrack, + sRGBEncoding +} from 'three'; + +class GLTFLoader extends Loader { + + constructor( manager ) { + + super( manager ); + + this.dracoLoader = null; + this.ktx2Loader = null; + this.meshoptDecoder = null; + + this.pluginCallbacks = []; + + this.register( function ( parser ) { + + return new GLTFMaterialsClearcoatExtension( parser ); + + } ); + + this.register( function ( parser ) { + + return new GLTFTextureBasisUExtension( parser ); + + } ); + + this.register( function ( parser ) { + + return new GLTFTextureWebPExtension( parser ); + + } ); + + this.register( function ( parser ) { + + return new GLTFMaterialsSheenExtension( parser ); + + } ); + + this.register( function ( parser ) { + + return new GLTFMaterialsTransmissionExtension( parser ); + + } ); + + this.register( function ( parser ) { + + return new GLTFMaterialsVolumeExtension( parser ); + + } ); + + this.register( function ( parser ) { + + return new GLTFMaterialsIorExtension( parser ); + + } ); + + this.register( function ( parser ) { + + return new GLTFMaterialsEmissiveStrengthExtension( parser ); + + } ); + + this.register( function ( parser ) { + + return new GLTFMaterialsSpecularExtension( parser ); + + } ); + + this.register( function ( parser ) { + + return new GLTFMaterialsIridescenceExtension( parser ); + + } ); + + this.register( function ( parser ) { + + return new GLTFLightsExtension( parser ); + + } ); + + this.register( function ( parser ) { + + return new GLTFMeshoptCompression( parser ); + + } ); + + this.register( function ( parser ) { + + return new GLTFMeshGpuInstancing( parser ); + + } ); + + } + + load( url, onLoad, onProgress, onError ) { + + const scope = this; + + let resourcePath; + + if ( this.resourcePath !== '' ) { + + resourcePath = this.resourcePath; + + } else if ( this.path !== '' ) { + + resourcePath = this.path; + + } else { + + resourcePath = LoaderUtils.extractUrlBase( url ); + + } + + // Tells the LoadingManager to track an extra item, which resolves after + // the model is fully loaded. This means the count of items loaded will + // be incorrect, but ensures manager.onLoad() does not fire early. + this.manager.itemStart( url ); + + const _onError = function ( e ) { + + if ( onError ) { + + onError( e ); + + } else { + + console.error( e ); + + } + + scope.manager.itemError( url ); + scope.manager.itemEnd( url ); + + }; + + const loader = new FileLoader( this.manager ); + + loader.setPath( this.path ); + loader.setResponseType( 'arraybuffer' ); + loader.setRequestHeader( this.requestHeader ); + loader.setWithCredentials( this.withCredentials ); + + loader.load( url, function ( data ) { + + try { + + scope.parse( data, resourcePath, function ( gltf ) { + + onLoad( gltf ); + + scope.manager.itemEnd( url ); + + }, _onError ); + + } catch ( e ) { + + _onError( e ); + + } + + }, onProgress, _onError ); + + } + + setDRACOLoader( dracoLoader ) { + + this.dracoLoader = dracoLoader; + return this; + + } + + setDDSLoader() { + + throw new Error( + + 'THREE.GLTFLoader: "MSFT_texture_dds" no longer supported. Please update to "KHR_texture_basisu".' + + ); + + } + + setKTX2Loader( ktx2Loader ) { + + this.ktx2Loader = ktx2Loader; + return this; + + } + + setMeshoptDecoder( meshoptDecoder ) { + + this.meshoptDecoder = meshoptDecoder; + return this; + + } + + register( callback ) { + + if ( this.pluginCallbacks.indexOf( callback ) === - 1 ) { + + this.pluginCallbacks.push( callback ); + + } + + return this; + + } + + unregister( callback ) { + + if ( this.pluginCallbacks.indexOf( callback ) !== - 1 ) { + + this.pluginCallbacks.splice( this.pluginCallbacks.indexOf( callback ), 1 ); + + } + + return this; + + } + + parse( data, path, onLoad, onError ) { + + let json; + const extensions = {}; + const plugins = {}; + + if ( typeof data === 'string' ) { + + json = JSON.parse( data ); + + } else if ( data instanceof ArrayBuffer ) { + + const magic = LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) ); + + if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) { + + try { + + extensions[ EXTENSIONS.KHR_BINARY_GLTF ] = new GLTFBinaryExtension( data ); + + } catch ( error ) { + + if ( onError ) onError( error ); + return; + + } + + json = JSON.parse( extensions[ EXTENSIONS.KHR_BINARY_GLTF ].content ); + + } else { + + json = JSON.parse( LoaderUtils.decodeText( new Uint8Array( data ) ) ); + + } + + } else { + + json = data; + + } + + if ( json.asset === undefined || json.asset.version[ 0 ] < 2 ) { + + if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported.' ) ); + return; + + } + + const parser = new GLTFParser( json, { + + path: path || this.resourcePath || '', + crossOrigin: this.crossOrigin, + requestHeader: this.requestHeader, + manager: this.manager, + ktx2Loader: this.ktx2Loader, + meshoptDecoder: this.meshoptDecoder + + } ); + + parser.fileLoader.setRequestHeader( this.requestHeader ); + + for ( let i = 0; i < this.pluginCallbacks.length; i ++ ) { + + const plugin = this.pluginCallbacks[ i ]( parser ); + plugins[ plugin.name ] = plugin; + + // Workaround to avoid determining as unknown extension + // in addUnknownExtensionsToUserData(). + // Remove this workaround if we move all the existing + // extension handlers to plugin system + extensions[ plugin.name ] = true; + + } + + if ( json.extensionsUsed ) { + + for ( let i = 0; i < json.extensionsUsed.length; ++ i ) { + + const extensionName = json.extensionsUsed[ i ]; + const extensionsRequired = json.extensionsRequired || []; + + switch ( extensionName ) { + + case EXTENSIONS.KHR_MATERIALS_UNLIT: + extensions[ extensionName ] = new GLTFMaterialsUnlitExtension(); + break; + + case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: + extensions[ extensionName ] = new GLTFMaterialsPbrSpecularGlossinessExtension(); + break; + + case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION: + extensions[ extensionName ] = new GLTFDracoMeshCompressionExtension( json, this.dracoLoader ); + break; + + case EXTENSIONS.KHR_TEXTURE_TRANSFORM: + extensions[ extensionName ] = new GLTFTextureTransformExtension(); + break; + + case EXTENSIONS.KHR_MESH_QUANTIZATION: + extensions[ extensionName ] = new GLTFMeshQuantizationExtension(); + break; + + default: + + if ( extensionsRequired.indexOf( extensionName ) >= 0 && plugins[ extensionName ] === undefined ) { + + console.warn( 'THREE.GLTFLoader: Unknown extension "' + extensionName + '".' ); + + } + + } + + } + + } + + parser.setExtensions( extensions ); + parser.setPlugins( plugins ); + parser.parse( onLoad, onError ); + + } + + parseAsync( data, path ) { + + const scope = this; + + return new Promise( function ( resolve, reject ) { + + scope.parse( data, path, resolve, reject ); + + } ); + + } + +} + +/* GLTFREGISTRY */ + +function GLTFRegistry() { + + let objects = {}; + + return { + + get: function ( key ) { + + return objects[ key ]; + + }, + + add: function ( key, object ) { + + objects[ key ] = object; + + }, + + remove: function ( key ) { + + delete objects[ key ]; + + }, + + removeAll: function () { + + objects = {}; + + } + + }; + +} + +/*********************************/ +/********** EXTENSIONS ***********/ +/*********************************/ + +const EXTENSIONS = { + KHR_BINARY_GLTF: 'KHR_binary_glTF', + KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression', + KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual', + KHR_MATERIALS_CLEARCOAT: 'KHR_materials_clearcoat', + KHR_MATERIALS_IOR: 'KHR_materials_ior', + KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness', + KHR_MATERIALS_SHEEN: 'KHR_materials_sheen', + KHR_MATERIALS_SPECULAR: 'KHR_materials_specular', + KHR_MATERIALS_TRANSMISSION: 'KHR_materials_transmission', + KHR_MATERIALS_IRIDESCENCE: 'KHR_materials_iridescence', + KHR_MATERIALS_UNLIT: 'KHR_materials_unlit', + KHR_MATERIALS_VOLUME: 'KHR_materials_volume', + KHR_TEXTURE_BASISU: 'KHR_texture_basisu', + KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform', + KHR_MESH_QUANTIZATION: 'KHR_mesh_quantization', + KHR_MATERIALS_EMISSIVE_STRENGTH: 'KHR_materials_emissive_strength', + EXT_TEXTURE_WEBP: 'EXT_texture_webp', + EXT_MESHOPT_COMPRESSION: 'EXT_meshopt_compression', + EXT_MESH_GPU_INSTANCING: 'EXT_mesh_gpu_instancing' +}; + +/** + * Punctual Lights Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual + */ +class GLTFLightsExtension { + + constructor( parser ) { + + this.parser = parser; + this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL; + + // Object3D instance caches + this.cache = { refs: {}, uses: {} }; + + } + + _markDefs() { + + const parser = this.parser; + const nodeDefs = this.parser.json.nodes || []; + + for ( let nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) { + + const nodeDef = nodeDefs[ nodeIndex ]; + + if ( nodeDef.extensions + && nodeDef.extensions[ this.name ] + && nodeDef.extensions[ this.name ].light !== undefined ) { + + parser._addNodeRef( this.cache, nodeDef.extensions[ this.name ].light ); + + } + + } + + } + + _loadLight( lightIndex ) { + + const parser = this.parser; + const cacheKey = 'light:' + lightIndex; + let dependency = parser.cache.get( cacheKey ); + + if ( dependency ) return dependency; + + const json = parser.json; + const extensions = ( json.extensions && json.extensions[ this.name ] ) || {}; + const lightDefs = extensions.lights || []; + const lightDef = lightDefs[ lightIndex ]; + let lightNode; + + const color = new Color( 0xffffff ); + + if ( lightDef.color !== undefined ) color.fromArray( lightDef.color ); + + const range = lightDef.range !== undefined ? lightDef.range : 0; + + switch ( lightDef.type ) { + + case 'directional': + lightNode = new DirectionalLight( color ); + lightNode.target.position.set( 0, 0, - 1 ); + lightNode.add( lightNode.target ); + break; + + case 'point': + lightNode = new PointLight( color ); + lightNode.distance = range; + break; + + case 'spot': + lightNode = new SpotLight( color ); + lightNode.distance = range; + // Handle spotlight properties. + lightDef.spot = lightDef.spot || {}; + lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0; + lightDef.spot.outerConeAngle = lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0; + lightNode.angle = lightDef.spot.outerConeAngle; + lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle; + lightNode.target.position.set( 0, 0, - 1 ); + lightNode.add( lightNode.target ); + break; + + default: + throw new Error( 'THREE.GLTFLoader: Unexpected light type: ' + lightDef.type ); + + } + + // Some lights (e.g. spot) default to a position other than the origin. Reset the position + // here, because node-level parsing will only override position if explicitly specified. + lightNode.position.set( 0, 0, 0 ); + + lightNode.decay = 2; + + if ( lightDef.intensity !== undefined ) lightNode.intensity = lightDef.intensity; + + lightNode.name = parser.createUniqueName( lightDef.name || ( 'light_' + lightIndex ) ); + + dependency = Promise.resolve( lightNode ); + + parser.cache.add( cacheKey, dependency ); + + return dependency; + + } + + createNodeAttachment( nodeIndex ) { + + const self = this; + const parser = this.parser; + const json = parser.json; + const nodeDef = json.nodes[ nodeIndex ]; + const lightDef = ( nodeDef.extensions && nodeDef.extensions[ this.name ] ) || {}; + const lightIndex = lightDef.light; + + if ( lightIndex === undefined ) return null; + + return this._loadLight( lightIndex ).then( function ( light ) { + + return parser._getNodeRef( self.cache, lightIndex, light ); + + } ); + + } + +} + +/** + * Unlit Materials Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit + */ +class GLTFMaterialsUnlitExtension { + + constructor() { + + this.name = EXTENSIONS.KHR_MATERIALS_UNLIT; + + } + + getMaterialType() { + + return MeshBasicMaterial; + + } + + extendParams( materialParams, materialDef, parser ) { + + const pending = []; + + materialParams.color = new Color( 1.0, 1.0, 1.0 ); + materialParams.opacity = 1.0; + + const metallicRoughness = materialDef.pbrMetallicRoughness; + + if ( metallicRoughness ) { + + if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { + + const array = metallicRoughness.baseColorFactor; + + materialParams.color.fromArray( array ); + materialParams.opacity = array[ 3 ]; + + } + + if ( metallicRoughness.baseColorTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture, sRGBEncoding ) ); + + } + + } + + return Promise.all( pending ); + + } + +} + +/** + * Materials Emissive Strength Extension + * + * Specification: https://github.com/KhronosGroup/glTF/blob/5768b3ce0ef32bc39cdf1bef10b948586635ead3/extensions/2.0/Khronos/KHR_materials_emissive_strength/README.md + */ +class GLTFMaterialsEmissiveStrengthExtension { + + constructor( parser ) { + + this.parser = parser; + this.name = EXTENSIONS.KHR_MATERIALS_EMISSIVE_STRENGTH; + + } + + extendMaterialParams( materialIndex, materialParams ) { + + const parser = this.parser; + const materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { + + return Promise.resolve(); + + } + + const emissiveStrength = materialDef.extensions[ this.name ].emissiveStrength; + + if ( emissiveStrength !== undefined ) { + + materialParams.emissiveIntensity = emissiveStrength; + + } + + return Promise.resolve(); + + } + +} + +/** + * Clearcoat Materials Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat + */ +class GLTFMaterialsClearcoatExtension { + + constructor( parser ) { + + this.parser = parser; + this.name = EXTENSIONS.KHR_MATERIALS_CLEARCOAT; + + } + + getMaterialType( materialIndex ) { + + const parser = this.parser; + const materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; + + return MeshPhysicalMaterial; + + } + + extendMaterialParams( materialIndex, materialParams ) { + + const parser = this.parser; + const materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { + + return Promise.resolve(); + + } + + const pending = []; + + const extension = materialDef.extensions[ this.name ]; + + if ( extension.clearcoatFactor !== undefined ) { + + materialParams.clearcoat = extension.clearcoatFactor; + + } + + if ( extension.clearcoatTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'clearcoatMap', extension.clearcoatTexture ) ); + + } + + if ( extension.clearcoatRoughnessFactor !== undefined ) { + + materialParams.clearcoatRoughness = extension.clearcoatRoughnessFactor; + + } + + if ( extension.clearcoatRoughnessTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'clearcoatRoughnessMap', extension.clearcoatRoughnessTexture ) ); + + } + + if ( extension.clearcoatNormalTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'clearcoatNormalMap', extension.clearcoatNormalTexture ) ); + + if ( extension.clearcoatNormalTexture.scale !== undefined ) { + + const scale = extension.clearcoatNormalTexture.scale; + + materialParams.clearcoatNormalScale = new Vector2( scale, scale ); + + } + + } + + return Promise.all( pending ); + + } + +} + +/** + * Iridescence Materials Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_iridescence + */ +class GLTFMaterialsIridescenceExtension { + + constructor( parser ) { + + this.parser = parser; + this.name = EXTENSIONS.KHR_MATERIALS_IRIDESCENCE; + + } + + getMaterialType( materialIndex ) { + + const parser = this.parser; + const materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; + + return MeshPhysicalMaterial; + + } + + extendMaterialParams( materialIndex, materialParams ) { + + const parser = this.parser; + const materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { + + return Promise.resolve(); + + } + + const pending = []; + + const extension = materialDef.extensions[ this.name ]; + + if ( extension.iridescenceFactor !== undefined ) { + + materialParams.iridescence = extension.iridescenceFactor; + + } + + if ( extension.iridescenceTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'iridescenceMap', extension.iridescenceTexture ) ); + + } + + if ( extension.iridescenceIor !== undefined ) { + + materialParams.iridescenceIOR = extension.iridescenceIor; + + } + + if ( materialParams.iridescenceThicknessRange === undefined ) { + + materialParams.iridescenceThicknessRange = [ 100, 400 ]; + + } + + if ( extension.iridescenceThicknessMinimum !== undefined ) { + + materialParams.iridescenceThicknessRange[ 0 ] = extension.iridescenceThicknessMinimum; + + } + + if ( extension.iridescenceThicknessMaximum !== undefined ) { + + materialParams.iridescenceThicknessRange[ 1 ] = extension.iridescenceThicknessMaximum; + + } + + if ( extension.iridescenceThicknessTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'iridescenceThicknessMap', extension.iridescenceThicknessTexture ) ); + + } + + return Promise.all( pending ); + + } + +} + +/** + * Sheen Materials Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_sheen + */ +class GLTFMaterialsSheenExtension { + + constructor( parser ) { + + this.parser = parser; + this.name = EXTENSIONS.KHR_MATERIALS_SHEEN; + + } + + getMaterialType( materialIndex ) { + + const parser = this.parser; + const materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; + + return MeshPhysicalMaterial; + + } + + extendMaterialParams( materialIndex, materialParams ) { + + const parser = this.parser; + const materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { + + return Promise.resolve(); + + } + + const pending = []; + + materialParams.sheenColor = new Color( 0, 0, 0 ); + materialParams.sheenRoughness = 0; + materialParams.sheen = 1; + + const extension = materialDef.extensions[ this.name ]; + + if ( extension.sheenColorFactor !== undefined ) { + + materialParams.sheenColor.fromArray( extension.sheenColorFactor ); + + } + + if ( extension.sheenRoughnessFactor !== undefined ) { + + materialParams.sheenRoughness = extension.sheenRoughnessFactor; + + } + + if ( extension.sheenColorTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'sheenColorMap', extension.sheenColorTexture, sRGBEncoding ) ); + + } + + if ( extension.sheenRoughnessTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'sheenRoughnessMap', extension.sheenRoughnessTexture ) ); + + } + + return Promise.all( pending ); + + } + +} + +/** + * Transmission Materials Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_transmission + * Draft: https://github.com/KhronosGroup/glTF/pull/1698 + */ +class GLTFMaterialsTransmissionExtension { + + constructor( parser ) { + + this.parser = parser; + this.name = EXTENSIONS.KHR_MATERIALS_TRANSMISSION; + + } + + getMaterialType( materialIndex ) { + + const parser = this.parser; + const materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; + + return MeshPhysicalMaterial; + + } + + extendMaterialParams( materialIndex, materialParams ) { + + const parser = this.parser; + const materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { + + return Promise.resolve(); + + } + + const pending = []; + + const extension = materialDef.extensions[ this.name ]; + + if ( extension.transmissionFactor !== undefined ) { + + materialParams.transmission = extension.transmissionFactor; + + } + + if ( extension.transmissionTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'transmissionMap', extension.transmissionTexture ) ); + + } + + return Promise.all( pending ); + + } + +} + +/** + * Materials Volume Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_volume + */ +class GLTFMaterialsVolumeExtension { + + constructor( parser ) { + + this.parser = parser; + this.name = EXTENSIONS.KHR_MATERIALS_VOLUME; + + } + + getMaterialType( materialIndex ) { + + const parser = this.parser; + const materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; + + return MeshPhysicalMaterial; + + } + + extendMaterialParams( materialIndex, materialParams ) { + + const parser = this.parser; + const materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { + + return Promise.resolve(); + + } + + const pending = []; + + const extension = materialDef.extensions[ this.name ]; + + materialParams.thickness = extension.thicknessFactor !== undefined ? extension.thicknessFactor : 0; + + if ( extension.thicknessTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'thicknessMap', extension.thicknessTexture ) ); + + } + + materialParams.attenuationDistance = extension.attenuationDistance || Infinity; + + const colorArray = extension.attenuationColor || [ 1, 1, 1 ]; + materialParams.attenuationColor = new Color( colorArray[ 0 ], colorArray[ 1 ], colorArray[ 2 ] ); + + return Promise.all( pending ); + + } + +} + +/** + * Materials ior Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_ior + */ +class GLTFMaterialsIorExtension { + + constructor( parser ) { + + this.parser = parser; + this.name = EXTENSIONS.KHR_MATERIALS_IOR; + + } + + getMaterialType( materialIndex ) { + + const parser = this.parser; + const materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; + + return MeshPhysicalMaterial; + + } + + extendMaterialParams( materialIndex, materialParams ) { + + const parser = this.parser; + const materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { + + return Promise.resolve(); + + } + + const extension = materialDef.extensions[ this.name ]; + + materialParams.ior = extension.ior !== undefined ? extension.ior : 1.5; + + return Promise.resolve(); + + } + +} + +/** + * Materials specular Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_specular + */ +class GLTFMaterialsSpecularExtension { + + constructor( parser ) { + + this.parser = parser; + this.name = EXTENSIONS.KHR_MATERIALS_SPECULAR; + + } + + getMaterialType( materialIndex ) { + + const parser = this.parser; + const materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; + + return MeshPhysicalMaterial; + + } + + extendMaterialParams( materialIndex, materialParams ) { + + const parser = this.parser; + const materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { + + return Promise.resolve(); + + } + + const pending = []; + + const extension = materialDef.extensions[ this.name ]; + + materialParams.specularIntensity = extension.specularFactor !== undefined ? extension.specularFactor : 1.0; + + if ( extension.specularTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'specularIntensityMap', extension.specularTexture ) ); + + } + + const colorArray = extension.specularColorFactor || [ 1, 1, 1 ]; + materialParams.specularColor = new Color( colorArray[ 0 ], colorArray[ 1 ], colorArray[ 2 ] ); + + if ( extension.specularColorTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'specularColorMap', extension.specularColorTexture, sRGBEncoding ) ); + + } + + return Promise.all( pending ); + + } + +} + +/** + * BasisU Texture Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_basisu + */ +class GLTFTextureBasisUExtension { + + constructor( parser ) { + + this.parser = parser; + this.name = EXTENSIONS.KHR_TEXTURE_BASISU; + + } + + loadTexture( textureIndex ) { + + const parser = this.parser; + const json = parser.json; + + const textureDef = json.textures[ textureIndex ]; + + if ( ! textureDef.extensions || ! textureDef.extensions[ this.name ] ) { + + return null; + + } + + const extension = textureDef.extensions[ this.name ]; + const loader = parser.options.ktx2Loader; + + if ( ! loader ) { + + if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) { + + throw new Error( 'THREE.GLTFLoader: setKTX2Loader must be called before loading KTX2 textures' ); + + } else { + + // Assumes that the extension is optional and that a fallback texture is present + return null; + + } + + } + + return parser.loadTextureImage( textureIndex, extension.source, loader ); + + } + +} + +/** + * WebP Texture Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_texture_webp + */ +class GLTFTextureWebPExtension { + + constructor( parser ) { + + this.parser = parser; + this.name = EXTENSIONS.EXT_TEXTURE_WEBP; + this.isSupported = null; + + } + + loadTexture( textureIndex ) { + + const name = this.name; + const parser = this.parser; + const json = parser.json; + + const textureDef = json.textures[ textureIndex ]; + + if ( ! textureDef.extensions || ! textureDef.extensions[ name ] ) { + + return null; + + } + + const extension = textureDef.extensions[ name ]; + const source = json.images[ extension.source ]; + + let loader = parser.textureLoader; + if ( source.uri ) { + + const handler = parser.options.manager.getHandler( source.uri ); + if ( handler !== null ) loader = handler; + + } + + return this.detectSupport().then( function ( isSupported ) { + + if ( isSupported ) return parser.loadTextureImage( textureIndex, extension.source, loader ); + + if ( json.extensionsRequired && json.extensionsRequired.indexOf( name ) >= 0 ) { + + throw new Error( 'THREE.GLTFLoader: WebP required by asset but unsupported.' ); + + } + + // Fall back to PNG or JPEG. + return parser.loadTexture( textureIndex ); + + } ); + + } + + detectSupport() { + + if ( ! this.isSupported ) { + + this.isSupported = new Promise( function ( resolve ) { + + const image = new Image(); + + // Lossy test image. Support for lossy images doesn't guarantee support for all + // WebP images, unfortunately. + image.src = 'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA'; + + image.onload = image.onerror = function () { + + resolve( image.height === 1 ); + + }; + + } ); + + } + + return this.isSupported; + + } + +} + +/** + * meshopt BufferView Compression Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_meshopt_compression + */ +class GLTFMeshoptCompression { + + constructor( parser ) { + + this.name = EXTENSIONS.EXT_MESHOPT_COMPRESSION; + this.parser = parser; + + } + + loadBufferView( index ) { + + const json = this.parser.json; + const bufferView = json.bufferViews[ index ]; + + if ( bufferView.extensions && bufferView.extensions[ this.name ] ) { + + const extensionDef = bufferView.extensions[ this.name ]; + + const buffer = this.parser.getDependency( 'buffer', extensionDef.buffer ); + const decoder = this.parser.options.meshoptDecoder; + + if ( ! decoder || ! decoder.supported ) { + + if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) { + + throw new Error( 'THREE.GLTFLoader: setMeshoptDecoder must be called before loading compressed files' ); + + } else { + + // Assumes that the extension is optional and that fallback buffer data is present + return null; + + } + + } + + return buffer.then( function ( res ) { + + const byteOffset = extensionDef.byteOffset || 0; + const byteLength = extensionDef.byteLength || 0; + + const count = extensionDef.count; + const stride = extensionDef.byteStride; + + const source = new Uint8Array( res, byteOffset, byteLength ); + + if ( decoder.decodeGltfBufferAsync ) { + + return decoder.decodeGltfBufferAsync( count, stride, source, extensionDef.mode, extensionDef.filter ).then( function ( res ) { + + return res.buffer; + + } ); + + } else { + + // Support for MeshoptDecoder 0.18 or earlier, without decodeGltfBufferAsync + return decoder.ready.then( function () { + + const result = new ArrayBuffer( count * stride ); + decoder.decodeGltfBuffer( new Uint8Array( result ), count, stride, source, extensionDef.mode, extensionDef.filter ); + return result; + + } ); + + } + + } ); + + } else { + + return null; + + } + + } + +} + +/** + * GPU Instancing Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_mesh_gpu_instancing + * + */ +class GLTFMeshGpuInstancing { + + constructor( parser ) { + + this.name = EXTENSIONS.EXT_MESH_GPU_INSTANCING; + this.parser = parser; + + } + + createNodeMesh( nodeIndex ) { + + const json = this.parser.json; + const nodeDef = json.nodes[ nodeIndex ]; + + if ( ! nodeDef.extensions || ! nodeDef.extensions[ this.name ] || + nodeDef.mesh === undefined ) { + + return null; + + } + + const meshDef = json.meshes[ nodeDef.mesh ]; + + // No Points or Lines + Instancing support yet + + for ( const primitive of meshDef.primitives ) { + + if ( primitive.mode !== WEBGL_CONSTANTS.TRIANGLES && + primitive.mode !== WEBGL_CONSTANTS.TRIANGLE_STRIP && + primitive.mode !== WEBGL_CONSTANTS.TRIANGLE_FAN && + primitive.mode !== undefined ) { + + return null; + + } + + } + + const extensionDef = nodeDef.extensions[ this.name ]; + const attributesDef = extensionDef.attributes; + + // @TODO: Can we support InstancedMesh + SkinnedMesh? + + const pending = []; + const attributes = {}; + + for ( const key in attributesDef ) { + + pending.push( this.parser.getDependency( 'accessor', attributesDef[ key ] ).then( accessor => { + + attributes[ key ] = accessor; + return attributes[ key ]; + + } ) ); + + } + + if ( pending.length < 1 ) { + + return null; + + } + + pending.push( this.parser.createNodeMesh( nodeIndex ) ); + + return Promise.all( pending ).then( results => { + + const nodeObject = results.pop(); + const meshes = nodeObject.isGroup ? nodeObject.children : [ nodeObject ]; + const count = results[ 0 ].count; // All attribute counts should be same + const instancedMeshes = []; + + for ( const mesh of meshes ) { + + // Temporal variables + const m = new Matrix4(); + const p = new Vector3(); + const q = new Quaternion(); + const s = new Vector3( 1, 1, 1 ); + + const instancedMesh = new InstancedMesh( mesh.geometry, mesh.material, count ); + + for ( let i = 0; i < count; i ++ ) { + + if ( attributes.TRANSLATION ) { + + p.fromBufferAttribute( attributes.TRANSLATION, i ); + + } + + if ( attributes.ROTATION ) { + + q.fromBufferAttribute( attributes.ROTATION, i ); + + } + + if ( attributes.SCALE ) { + + s.fromBufferAttribute( attributes.SCALE, i ); + + } + + instancedMesh.setMatrixAt( i, m.compose( p, q, s ) ); + + } + + // Add instance attributes to the geometry, excluding TRS. + for ( const attributeName in attributes ) { + + if ( attributeName !== 'TRANSLATION' && + attributeName !== 'ROTATION' && + attributeName !== 'SCALE' ) { + + mesh.geometry.setAttribute( attributeName, attributes[ attributeName ] ); + + } + + } + + // Just in case + Object3D.prototype.copy.call( instancedMesh, mesh ); + + // https://github.com/mrdoob/three.js/issues/18334 + instancedMesh.frustumCulled = false; + this.parser.assignFinalMaterial( instancedMesh ); + + instancedMeshes.push( instancedMesh ); + + } + + if ( nodeObject.isGroup ) { + + nodeObject.clear(); + + nodeObject.add( ... instancedMeshes ); + + return nodeObject; + + } + + return instancedMeshes[ 0 ]; + + } ); + + } + +} + +/* BINARY EXTENSION */ +const BINARY_EXTENSION_HEADER_MAGIC = 'glTF'; +const BINARY_EXTENSION_HEADER_LENGTH = 12; +const BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 }; + +class GLTFBinaryExtension { + + constructor( data ) { + + this.name = EXTENSIONS.KHR_BINARY_GLTF; + this.content = null; + this.body = null; + + const headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH ); + + this.header = { + magic: LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ), + version: headerView.getUint32( 4, true ), + length: headerView.getUint32( 8, true ) + }; + + if ( this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC ) { + + throw new Error( 'THREE.GLTFLoader: Unsupported glTF-Binary header.' ); + + } else if ( this.header.version < 2.0 ) { + + throw new Error( 'THREE.GLTFLoader: Legacy binary file detected.' ); + + } + + const chunkContentsLength = this.header.length - BINARY_EXTENSION_HEADER_LENGTH; + const chunkView = new DataView( data, BINARY_EXTENSION_HEADER_LENGTH ); + let chunkIndex = 0; + + while ( chunkIndex < chunkContentsLength ) { + + const chunkLength = chunkView.getUint32( chunkIndex, true ); + chunkIndex += 4; + + const chunkType = chunkView.getUint32( chunkIndex, true ); + chunkIndex += 4; + + if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) { + + const contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength ); + this.content = LoaderUtils.decodeText( contentArray ); + + } else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) { + + const byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex; + this.body = data.slice( byteOffset, byteOffset + chunkLength ); + + } + + // Clients must ignore chunks with unknown types. + + chunkIndex += chunkLength; + + } + + if ( this.content === null ) { + + throw new Error( 'THREE.GLTFLoader: JSON content not found.' ); + + } + + } + +} + +/** + * DRACO Mesh Compression Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression + */ +class GLTFDracoMeshCompressionExtension { + + constructor( json, dracoLoader ) { + + if ( ! dracoLoader ) { + + throw new Error( 'THREE.GLTFLoader: No DRACOLoader instance provided.' ); + + } + + this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION; + this.json = json; + this.dracoLoader = dracoLoader; + this.dracoLoader.preload(); + + } + + decodePrimitive( primitive, parser ) { + + const json = this.json; + const dracoLoader = this.dracoLoader; + const bufferViewIndex = primitive.extensions[ this.name ].bufferView; + const gltfAttributeMap = primitive.extensions[ this.name ].attributes; + const threeAttributeMap = {}; + const attributeNormalizedMap = {}; + const attributeTypeMap = {}; + + for ( const attributeName in gltfAttributeMap ) { + + const threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); + + threeAttributeMap[ threeAttributeName ] = gltfAttributeMap[ attributeName ]; + + } + + for ( const attributeName in primitive.attributes ) { + + const threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); + + if ( gltfAttributeMap[ attributeName ] !== undefined ) { + + const accessorDef = json.accessors[ primitive.attributes[ attributeName ] ]; + const componentType = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; + + attributeTypeMap[ threeAttributeName ] = componentType.name; + attributeNormalizedMap[ threeAttributeName ] = accessorDef.normalized === true; + + } + + } + + return parser.getDependency( 'bufferView', bufferViewIndex ).then( function ( bufferView ) { + + return new Promise( function ( resolve ) { + + dracoLoader.decodeDracoFile( bufferView, function ( geometry ) { + + for ( const attributeName in geometry.attributes ) { + + const attribute = geometry.attributes[ attributeName ]; + const normalized = attributeNormalizedMap[ attributeName ]; + + if ( normalized !== undefined ) attribute.normalized = normalized; + + } + + resolve( geometry ); + + }, threeAttributeMap, attributeTypeMap ); + + } ); + + } ); + + } + +} + +/** + * Texture Transform Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform + */ +class GLTFTextureTransformExtension { + + constructor() { + + this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM; + + } + + extendTexture( texture, transform ) { + + if ( transform.texCoord !== undefined ) { + + console.warn( 'THREE.GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.' ); + + } + + if ( transform.offset === undefined && transform.rotation === undefined && transform.scale === undefined ) { + + // See https://github.com/mrdoob/three.js/issues/21819. + return texture; + + } + + texture = texture.clone(); + + if ( transform.offset !== undefined ) { + + texture.offset.fromArray( transform.offset ); + + } + + if ( transform.rotation !== undefined ) { + + texture.rotation = transform.rotation; + + } + + if ( transform.scale !== undefined ) { + + texture.repeat.fromArray( transform.scale ); + + } + + texture.needsUpdate = true; + + return texture; + + } + +} + +/** + * Specular-Glossiness Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Archived/KHR_materials_pbrSpecularGlossiness + */ + +/** + * A sub class of StandardMaterial with some of the functionality + * changed via the `onBeforeCompile` callback + * @pailhead + */ +class GLTFMeshStandardSGMaterial extends MeshStandardMaterial { + + constructor( params ) { + + super(); + + this.isGLTFSpecularGlossinessMaterial = true; + + //various chunks that need replacing + const specularMapParsFragmentChunk = [ + '#ifdef USE_SPECULARMAP', + ' uniform sampler2D specularMap;', + '#endif' + ].join( '\n' ); + + const glossinessMapParsFragmentChunk = [ + '#ifdef USE_GLOSSINESSMAP', + ' uniform sampler2D glossinessMap;', + '#endif' + ].join( '\n' ); + + const specularMapFragmentChunk = [ + 'vec3 specularFactor = specular;', + '#ifdef USE_SPECULARMAP', + ' vec4 texelSpecular = texture2D( specularMap, vUv );', + ' // reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture', + ' specularFactor *= texelSpecular.rgb;', + '#endif' + ].join( '\n' ); + + const glossinessMapFragmentChunk = [ + 'float glossinessFactor = glossiness;', + '#ifdef USE_GLOSSINESSMAP', + ' vec4 texelGlossiness = texture2D( glossinessMap, vUv );', + ' // reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture', + ' glossinessFactor *= texelGlossiness.a;', + '#endif' + ].join( '\n' ); + + const lightPhysicalFragmentChunk = [ + 'PhysicalMaterial material;', + 'material.diffuseColor = diffuseColor.rgb * ( 1. - max( specularFactor.r, max( specularFactor.g, specularFactor.b ) ) );', + 'vec3 dxy = max( abs( dFdx( geometryNormal ) ), abs( dFdy( geometryNormal ) ) );', + 'float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );', + 'material.roughness = max( 1.0 - glossinessFactor, 0.0525 ); // 0.0525 corresponds to the base mip of a 256 cubemap.', + 'material.roughness += geometryRoughness;', + 'material.roughness = min( material.roughness, 1.0 );', + 'material.specularColor = specularFactor;', + ].join( '\n' ); + + const uniforms = { + specular: { value: new Color().setHex( 0xffffff ) }, + glossiness: { value: 1 }, + specularMap: { value: null }, + glossinessMap: { value: null } + }; + + this._extraUniforms = uniforms; + + this.onBeforeCompile = function ( shader ) { + + for ( const uniformName in uniforms ) { + + shader.uniforms[ uniformName ] = uniforms[ uniformName ]; + + } + + shader.fragmentShader = shader.fragmentShader + .replace( 'uniform float roughness;', 'uniform vec3 specular;' ) + .replace( 'uniform float metalness;', 'uniform float glossiness;' ) + .replace( '#include ', specularMapParsFragmentChunk ) + .replace( '#include ', glossinessMapParsFragmentChunk ) + .replace( '#include ', specularMapFragmentChunk ) + .replace( '#include ', glossinessMapFragmentChunk ) + .replace( '#include ', lightPhysicalFragmentChunk ); + + }; + + Object.defineProperties( this, { + + specular: { + get: function () { + + return uniforms.specular.value; + + }, + set: function ( v ) { + + uniforms.specular.value = v; + + } + }, + + specularMap: { + get: function () { + + return uniforms.specularMap.value; + + }, + set: function ( v ) { + + uniforms.specularMap.value = v; + + if ( v ) { + + this.defines.USE_SPECULARMAP = ''; // USE_UV is set by the renderer for specular maps + + } else { + + delete this.defines.USE_SPECULARMAP; + + } + + } + }, + + glossiness: { + get: function () { + + return uniforms.glossiness.value; + + }, + set: function ( v ) { + + uniforms.glossiness.value = v; + + } + }, + + glossinessMap: { + get: function () { + + return uniforms.glossinessMap.value; + + }, + set: function ( v ) { + + uniforms.glossinessMap.value = v; + + if ( v ) { + + this.defines.USE_GLOSSINESSMAP = ''; + this.defines.USE_UV = ''; + + } else { + + delete this.defines.USE_GLOSSINESSMAP; + delete this.defines.USE_UV; + + } + + } + } + + } ); + + delete this.metalness; + delete this.roughness; + delete this.metalnessMap; + delete this.roughnessMap; + + this.setValues( params ); + + } + + copy( source ) { + + super.copy( source ); + + this.specularMap = source.specularMap; + this.specular.copy( source.specular ); + this.glossinessMap = source.glossinessMap; + this.glossiness = source.glossiness; + delete this.metalness; + delete this.roughness; + delete this.metalnessMap; + delete this.roughnessMap; + return this; + + } + +} + + +class GLTFMaterialsPbrSpecularGlossinessExtension { + + constructor() { + + this.name = EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS; + + this.specularGlossinessParams = [ + 'color', + 'map', + 'lightMap', + 'lightMapIntensity', + 'aoMap', + 'aoMapIntensity', + 'emissive', + 'emissiveIntensity', + 'emissiveMap', + 'bumpMap', + 'bumpScale', + 'normalMap', + 'normalMapType', + 'displacementMap', + 'displacementScale', + 'displacementBias', + 'specularMap', + 'specular', + 'glossinessMap', + 'glossiness', + 'alphaMap', + 'envMap', + 'envMapIntensity' + ]; + + } + + getMaterialType() { + + return GLTFMeshStandardSGMaterial; + + } + + extendParams( materialParams, materialDef, parser ) { + + const pbrSpecularGlossiness = materialDef.extensions[ this.name ]; + + materialParams.color = new Color( 1.0, 1.0, 1.0 ); + materialParams.opacity = 1.0; + + const pending = []; + + if ( Array.isArray( pbrSpecularGlossiness.diffuseFactor ) ) { + + const array = pbrSpecularGlossiness.diffuseFactor; + + materialParams.color.fromArray( array ); + materialParams.opacity = array[ 3 ]; + + } + + if ( pbrSpecularGlossiness.diffuseTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'map', pbrSpecularGlossiness.diffuseTexture, sRGBEncoding ) ); + + } + + materialParams.emissive = new Color( 0.0, 0.0, 0.0 ); + materialParams.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0; + materialParams.specular = new Color( 1.0, 1.0, 1.0 ); + + if ( Array.isArray( pbrSpecularGlossiness.specularFactor ) ) { + + materialParams.specular.fromArray( pbrSpecularGlossiness.specularFactor ); + + } + + if ( pbrSpecularGlossiness.specularGlossinessTexture !== undefined ) { + + const specGlossMapDef = pbrSpecularGlossiness.specularGlossinessTexture; + pending.push( parser.assignTexture( materialParams, 'glossinessMap', specGlossMapDef ) ); + pending.push( parser.assignTexture( materialParams, 'specularMap', specGlossMapDef, sRGBEncoding ) ); + + } + + return Promise.all( pending ); + + } + + createMaterial( materialParams ) { + + const material = new GLTFMeshStandardSGMaterial( materialParams ); + material.fog = true; + + material.color = materialParams.color; + + material.map = materialParams.map === undefined ? null : materialParams.map; + + material.lightMap = null; + material.lightMapIntensity = 1.0; + + material.aoMap = materialParams.aoMap === undefined ? null : materialParams.aoMap; + material.aoMapIntensity = 1.0; + + material.emissive = materialParams.emissive; + material.emissiveIntensity = materialParams.emissiveIntensity === undefined ? 1.0 : materialParams.emissiveIntensity; + material.emissiveMap = materialParams.emissiveMap === undefined ? null : materialParams.emissiveMap; + + material.bumpMap = materialParams.bumpMap === undefined ? null : materialParams.bumpMap; + material.bumpScale = 1; + + material.normalMap = materialParams.normalMap === undefined ? null : materialParams.normalMap; + material.normalMapType = TangentSpaceNormalMap; + + if ( materialParams.normalScale ) material.normalScale = materialParams.normalScale; + + material.displacementMap = null; + material.displacementScale = 1; + material.displacementBias = 0; + + material.specularMap = materialParams.specularMap === undefined ? null : materialParams.specularMap; + material.specular = materialParams.specular; + + material.glossinessMap = materialParams.glossinessMap === undefined ? null : materialParams.glossinessMap; + material.glossiness = materialParams.glossiness; + + material.alphaMap = null; + + material.envMap = materialParams.envMap === undefined ? null : materialParams.envMap; + material.envMapIntensity = 1.0; + + return material; + + } + +} + +/** + * Mesh Quantization Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization + */ +class GLTFMeshQuantizationExtension { + + constructor() { + + this.name = EXTENSIONS.KHR_MESH_QUANTIZATION; + + } + +} + +/*********************************/ +/********** INTERPOLATION ********/ +/*********************************/ + +// Spline Interpolation +// Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation +class GLTFCubicSplineInterpolant extends Interpolant { + + constructor( parameterPositions, sampleValues, sampleSize, resultBuffer ) { + + super( parameterPositions, sampleValues, sampleSize, resultBuffer ); + + } + + copySampleValue_( index ) { + + // Copies a sample value to the result buffer. See description of glTF + // CUBICSPLINE values layout in interpolate_() function below. + + const result = this.resultBuffer, + values = this.sampleValues, + valueSize = this.valueSize, + offset = index * valueSize * 3 + valueSize; + + for ( let i = 0; i !== valueSize; i ++ ) { + + result[ i ] = values[ offset + i ]; + + } + + return result; + + } + + interpolate_( i1, t0, t, t1 ) { + + const result = this.resultBuffer; + const values = this.sampleValues; + const stride = this.valueSize; + + const stride2 = stride * 2; + const stride3 = stride * 3; + + const td = t1 - t0; + + const p = ( t - t0 ) / td; + const pp = p * p; + const ppp = pp * p; + + const offset1 = i1 * stride3; + const offset0 = offset1 - stride3; + + const s2 = - 2 * ppp + 3 * pp; + const s3 = ppp - pp; + const s0 = 1 - s2; + const s1 = s3 - pp + p; + + // Layout of keyframe output values for CUBICSPLINE animations: + // [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ] + for ( let i = 0; i !== stride; i ++ ) { + + const p0 = values[ offset0 + i + stride ]; // splineVertex_k + const m0 = values[ offset0 + i + stride2 ] * td; // outTangent_k * (t_k+1 - t_k) + const p1 = values[ offset1 + i + stride ]; // splineVertex_k+1 + const m1 = values[ offset1 + i ] * td; // inTangent_k+1 * (t_k+1 - t_k) + + result[ i ] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1; + + } + + return result; + + } + +} + +const _q = new Quaternion(); + +class GLTFCubicSplineQuaternionInterpolant extends GLTFCubicSplineInterpolant { + + interpolate_( i1, t0, t, t1 ) { + + const result = super.interpolate_( i1, t0, t, t1 ); + + _q.fromArray( result ).normalize().toArray( result ); + + return result; + + } + +} + + +/*********************************/ +/********** INTERNALS ************/ +/*********************************/ + +/* CONSTANTS */ + +const WEBGL_CONSTANTS = { + FLOAT: 5126, + //FLOAT_MAT2: 35674, + FLOAT_MAT3: 35675, + FLOAT_MAT4: 35676, + FLOAT_VEC2: 35664, + FLOAT_VEC3: 35665, + FLOAT_VEC4: 35666, + LINEAR: 9729, + REPEAT: 10497, + SAMPLER_2D: 35678, + POINTS: 0, + LINES: 1, + LINE_LOOP: 2, + LINE_STRIP: 3, + TRIANGLES: 4, + TRIANGLE_STRIP: 5, + TRIANGLE_FAN: 6, + UNSIGNED_BYTE: 5121, + UNSIGNED_SHORT: 5123 +}; + +const WEBGL_COMPONENT_TYPES = { + 5120: Int8Array, + 5121: Uint8Array, + 5122: Int16Array, + 5123: Uint16Array, + 5125: Uint32Array, + 5126: Float32Array +}; + +const WEBGL_FILTERS = { + 9728: NearestFilter, + 9729: LinearFilter, + 9984: NearestMipmapNearestFilter, + 9985: LinearMipmapNearestFilter, + 9986: NearestMipmapLinearFilter, + 9987: LinearMipmapLinearFilter +}; + +const WEBGL_WRAPPINGS = { + 33071: ClampToEdgeWrapping, + 33648: MirroredRepeatWrapping, + 10497: RepeatWrapping +}; + +const WEBGL_TYPE_SIZES = { + 'SCALAR': 1, + 'VEC2': 2, + 'VEC3': 3, + 'VEC4': 4, + 'MAT2': 4, + 'MAT3': 9, + 'MAT4': 16 +}; + +const ATTRIBUTES = { + POSITION: 'position', + NORMAL: 'normal', + TANGENT: 'tangent', + TEXCOORD_0: 'uv', + TEXCOORD_1: 'uv2', + COLOR_0: 'color', + WEIGHTS_0: 'skinWeight', + JOINTS_0: 'skinIndex', +}; + +const PATH_PROPERTIES = { + scale: 'scale', + translation: 'position', + rotation: 'quaternion', + weights: 'morphTargetInfluences' +}; + +const INTERPOLATION = { + CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each + // keyframe track will be initialized with a default interpolation type, then modified. + LINEAR: InterpolateLinear, + STEP: InterpolateDiscrete +}; + +const ALPHA_MODES = { + OPAQUE: 'OPAQUE', + MASK: 'MASK', + BLEND: 'BLEND' +}; + +/** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material + */ +function createDefaultMaterial( cache ) { + + if ( cache[ 'DefaultMaterial' ] === undefined ) { + + cache[ 'DefaultMaterial' ] = new MeshStandardMaterial( { + color: 0xFFFFFF, + emissive: 0x000000, + metalness: 1, + roughness: 1, + transparent: false, + depthTest: true, + side: FrontSide + } ); + + } + + return cache[ 'DefaultMaterial' ]; + +} + +function addUnknownExtensionsToUserData( knownExtensions, object, objectDef ) { + + // Add unknown glTF extensions to an object's userData. + + for ( const name in objectDef.extensions ) { + + if ( knownExtensions[ name ] === undefined ) { + + object.userData.gltfExtensions = object.userData.gltfExtensions || {}; + object.userData.gltfExtensions[ name ] = objectDef.extensions[ name ]; + + } + + } + +} + +/** + * @param {Object3D|Material|BufferGeometry} object + * @param {GLTF.definition} gltfDef + */ +function assignExtrasToUserData( object, gltfDef ) { + + if ( gltfDef.extras !== undefined ) { + + if ( typeof gltfDef.extras === 'object' ) { + + Object.assign( object.userData, gltfDef.extras ); + + } else { + + console.warn( 'THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras ); + + } + + } + +} + +/** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets + * + * @param {BufferGeometry} geometry + * @param {Array} targets + * @param {GLTFParser} parser + * @return {Promise} + */ +function addMorphTargets( geometry, targets, parser ) { + + let hasMorphPosition = false; + let hasMorphNormal = false; + let hasMorphColor = false; + + for ( let i = 0, il = targets.length; i < il; i ++ ) { + + const target = targets[ i ]; + + if ( target.POSITION !== undefined ) hasMorphPosition = true; + if ( target.NORMAL !== undefined ) hasMorphNormal = true; + if ( target.COLOR_0 !== undefined ) hasMorphColor = true; + + if ( hasMorphPosition && hasMorphNormal && hasMorphColor ) break; + + } + + if ( ! hasMorphPosition && ! hasMorphNormal && ! hasMorphColor ) return Promise.resolve( geometry ); + + const pendingPositionAccessors = []; + const pendingNormalAccessors = []; + const pendingColorAccessors = []; + + for ( let i = 0, il = targets.length; i < il; i ++ ) { + + const target = targets[ i ]; + + if ( hasMorphPosition ) { + + const pendingAccessor = target.POSITION !== undefined + ? parser.getDependency( 'accessor', target.POSITION ) + : geometry.attributes.position; + + pendingPositionAccessors.push( pendingAccessor ); + + } + + if ( hasMorphNormal ) { + + const pendingAccessor = target.NORMAL !== undefined + ? parser.getDependency( 'accessor', target.NORMAL ) + : geometry.attributes.normal; + + pendingNormalAccessors.push( pendingAccessor ); + + } + + if ( hasMorphColor ) { + + const pendingAccessor = target.COLOR_0 !== undefined + ? parser.getDependency( 'accessor', target.COLOR_0 ) + : geometry.attributes.color; + + pendingColorAccessors.push( pendingAccessor ); + + } + + } + + return Promise.all( [ + Promise.all( pendingPositionAccessors ), + Promise.all( pendingNormalAccessors ), + Promise.all( pendingColorAccessors ) + ] ).then( function ( accessors ) { + + const morphPositions = accessors[ 0 ]; + const morphNormals = accessors[ 1 ]; + const morphColors = accessors[ 2 ]; + + if ( hasMorphPosition ) geometry.morphAttributes.position = morphPositions; + if ( hasMorphNormal ) geometry.morphAttributes.normal = morphNormals; + if ( hasMorphColor ) geometry.morphAttributes.color = morphColors; + geometry.morphTargetsRelative = true; + + return geometry; + + } ); + +} + +/** + * @param {Mesh} mesh + * @param {GLTF.Mesh} meshDef + */ +function updateMorphTargets( mesh, meshDef ) { + + mesh.updateMorphTargets(); + + if ( meshDef.weights !== undefined ) { + + for ( let i = 0, il = meshDef.weights.length; i < il; i ++ ) { + + mesh.morphTargetInfluences[ i ] = meshDef.weights[ i ]; + + } + + } + + // .extras has user-defined data, so check that .extras.targetNames is an array. + if ( meshDef.extras && Array.isArray( meshDef.extras.targetNames ) ) { + + const targetNames = meshDef.extras.targetNames; + + if ( mesh.morphTargetInfluences.length === targetNames.length ) { + + mesh.morphTargetDictionary = {}; + + for ( let i = 0, il = targetNames.length; i < il; i ++ ) { + + mesh.morphTargetDictionary[ targetNames[ i ] ] = i; + + } + + } else { + + console.warn( 'THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.' ); + + } + + } + +} + +function createPrimitiveKey( primitiveDef ) { + + const dracoExtension = primitiveDef.extensions && primitiveDef.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ]; + let geometryKey; + + if ( dracoExtension ) { + + geometryKey = 'draco:' + dracoExtension.bufferView + + ':' + dracoExtension.indices + + ':' + createAttributesKey( dracoExtension.attributes ); + + } else { + + geometryKey = primitiveDef.indices + ':' + createAttributesKey( primitiveDef.attributes ) + ':' + primitiveDef.mode; + + } + + return geometryKey; + +} + +function createAttributesKey( attributes ) { + + let attributesKey = ''; + + const keys = Object.keys( attributes ).sort(); + + for ( let i = 0, il = keys.length; i < il; i ++ ) { + + attributesKey += keys[ i ] + ':' + attributes[ keys[ i ] ] + ';'; + + } + + return attributesKey; + +} + +function getNormalizedComponentScale( constructor ) { + + // Reference: + // https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization#encoding-quantized-data + + switch ( constructor ) { + + case Int8Array: + return 1 / 127; + + case Uint8Array: + return 1 / 255; + + case Int16Array: + return 1 / 32767; + + case Uint16Array: + return 1 / 65535; + + default: + throw new Error( 'THREE.GLTFLoader: Unsupported normalized accessor component type.' ); + + } + +} + +function getImageURIMimeType( uri ) { + + if ( uri.search( /\.jpe?g($|\?)/i ) > 0 || uri.search( /^data\:image\/jpeg/ ) === 0 ) return 'image/jpeg'; + if ( uri.search( /\.webp($|\?)/i ) > 0 || uri.search( /^data\:image\/webp/ ) === 0 ) return 'image/webp'; + + return 'image/png'; + +} + +/* GLTF PARSER */ + +class GLTFParser { + + constructor( json = {}, options = {} ) { + + this.json = json; + this.extensions = {}; + this.plugins = {}; + this.options = options; + + // loader object cache + this.cache = new GLTFRegistry(); + + // associations between Three.js objects and glTF elements + this.associations = new Map(); + + // BufferGeometry caching + this.primitiveCache = {}; + + // Object3D instance caches + this.meshCache = { refs: {}, uses: {} }; + this.cameraCache = { refs: {}, uses: {} }; + this.lightCache = { refs: {}, uses: {} }; + + this.sourceCache = {}; + this.textureCache = {}; + + // Track node names, to ensure no duplicates + this.nodeNamesUsed = {}; + + // Use an ImageBitmapLoader if imageBitmaps are supported. Moves much of the + // expensive work of uploading a texture to the GPU off the main thread. + + const isSafari = /^((?!chrome|android).)*safari/i.test( navigator.userAgent ) === true; + const isFirefox = navigator.userAgent.indexOf( 'Firefox' ) > - 1; + const firefoxVersion = isFirefox ? navigator.userAgent.match( /Firefox\/([0-9]+)\./ )[ 1 ] : - 1; + + if ( typeof createImageBitmap === 'undefined' || isSafari || ( isFirefox && firefoxVersion < 98 ) ) { + + this.textureLoader = new TextureLoader( this.options.manager ); + + } else { + + this.textureLoader = new ImageBitmapLoader( this.options.manager ); + + } + + this.textureLoader.setCrossOrigin( this.options.crossOrigin ); + this.textureLoader.setRequestHeader( this.options.requestHeader ); + + this.fileLoader = new FileLoader( this.options.manager ); + this.fileLoader.setResponseType( 'arraybuffer' ); + + if ( this.options.crossOrigin === 'use-credentials' ) { + + this.fileLoader.setWithCredentials( true ); + + } + + } + + setExtensions( extensions ) { + + this.extensions = extensions; + + } + + setPlugins( plugins ) { + + this.plugins = plugins; + + } + + parse( onLoad, onError ) { + + const parser = this; + const json = this.json; + const extensions = this.extensions; + + // Clear the loader cache + this.cache.removeAll(); + + // Mark the special nodes/meshes in json for efficient parse + this._invokeAll( function ( ext ) { + + return ext._markDefs && ext._markDefs(); + + } ); + + Promise.all( this._invokeAll( function ( ext ) { + + return ext.beforeRoot && ext.beforeRoot(); + + } ) ).then( function () { + + return Promise.all( [ + + parser.getDependencies( 'scene' ), + parser.getDependencies( 'animation' ), + parser.getDependencies( 'camera' ), + + ] ); + + } ).then( function ( dependencies ) { + + const result = { + scene: dependencies[ 0 ][ json.scene || 0 ], + scenes: dependencies[ 0 ], + animations: dependencies[ 1 ], + cameras: dependencies[ 2 ], + asset: json.asset, + parser: parser, + userData: {} + }; + + addUnknownExtensionsToUserData( extensions, result, json ); + + assignExtrasToUserData( result, json ); + + Promise.all( parser._invokeAll( function ( ext ) { + + return ext.afterRoot && ext.afterRoot( result ); + + } ) ).then( function () { + + onLoad( result ); + + } ); + + } ).catch( onError ); + + } + + /** + * Marks the special nodes/meshes in json for efficient parse. + */ + _markDefs() { + + const nodeDefs = this.json.nodes || []; + const skinDefs = this.json.skins || []; + const meshDefs = this.json.meshes || []; + + // Nothing in the node definition indicates whether it is a Bone or an + // Object3D. Use the skins' joint references to mark bones. + for ( let skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex ++ ) { + + const joints = skinDefs[ skinIndex ].joints; + + for ( let i = 0, il = joints.length; i < il; i ++ ) { + + nodeDefs[ joints[ i ] ].isBone = true; + + } + + } + + // Iterate over all nodes, marking references to shared resources, + // as well as skeleton joints. + for ( let nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) { + + const nodeDef = nodeDefs[ nodeIndex ]; + + if ( nodeDef.mesh !== undefined ) { + + this._addNodeRef( this.meshCache, nodeDef.mesh ); + + // Nothing in the mesh definition indicates whether it is + // a SkinnedMesh or Mesh. Use the node's mesh reference + // to mark SkinnedMesh if node has skin. + if ( nodeDef.skin !== undefined ) { + + meshDefs[ nodeDef.mesh ].isSkinnedMesh = true; + + } + + } + + if ( nodeDef.camera !== undefined ) { + + this._addNodeRef( this.cameraCache, nodeDef.camera ); + + } + + } + + } + + /** + * Counts references to shared node / Object3D resources. These resources + * can be reused, or "instantiated", at multiple nodes in the scene + * hierarchy. Mesh, Camera, and Light instances are instantiated and must + * be marked. Non-scenegraph resources (like Materials, Geometries, and + * Textures) can be reused directly and are not marked here. + * + * Example: CesiumMilkTruck sample model reuses "Wheel" meshes. + */ + _addNodeRef( cache, index ) { + + if ( index === undefined ) return; + + if ( cache.refs[ index ] === undefined ) { + + cache.refs[ index ] = cache.uses[ index ] = 0; + + } + + cache.refs[ index ] ++; + + } + + /** Returns a reference to a shared resource, cloning it if necessary. */ + _getNodeRef( cache, index, object ) { + + if ( cache.refs[ index ] <= 1 ) return object; + + const ref = object.clone(); + + // Propagates mappings to the cloned object, prevents mappings on the + // original object from being lost. + const updateMappings = ( original, clone ) => { + + const mappings = this.associations.get( original ); + if ( mappings != null ) { + + this.associations.set( clone, mappings ); + + } + + for ( const [ i, child ] of original.children.entries() ) { + + updateMappings( child, clone.children[ i ] ); + + } + + }; + + updateMappings( object, ref ); + + ref.name += '_instance_' + ( cache.uses[ index ] ++ ); + + return ref; + + } + + _invokeOne( func ) { + + const extensions = Object.values( this.plugins ); + extensions.push( this ); + + for ( let i = 0; i < extensions.length; i ++ ) { + + const result = func( extensions[ i ] ); + + if ( result ) return result; + + } + + return null; + + } + + _invokeAll( func ) { + + const extensions = Object.values( this.plugins ); + extensions.unshift( this ); + + const pending = []; + + for ( let i = 0; i < extensions.length; i ++ ) { + + const result = func( extensions[ i ] ); + + if ( result ) pending.push( result ); + + } + + return pending; + + } + + /** + * Requests the specified dependency asynchronously, with caching. + * @param {string} type + * @param {number} index + * @return {Promise} + */ + getDependency( type, index ) { + + const cacheKey = type + ':' + index; + let dependency = this.cache.get( cacheKey ); + + if ( ! dependency ) { + + switch ( type ) { + + case 'scene': + dependency = this.loadScene( index ); + break; + + case 'node': + dependency = this.loadNode( index ); + break; + + case 'mesh': + dependency = this._invokeOne( function ( ext ) { + + return ext.loadMesh && ext.loadMesh( index ); + + } ); + break; + + case 'accessor': + dependency = this.loadAccessor( index ); + break; + + case 'bufferView': + dependency = this._invokeOne( function ( ext ) { + + return ext.loadBufferView && ext.loadBufferView( index ); + + } ); + break; + + case 'buffer': + dependency = this.loadBuffer( index ); + break; + + case 'material': + dependency = this._invokeOne( function ( ext ) { + + return ext.loadMaterial && ext.loadMaterial( index ); + + } ); + break; + + case 'texture': + dependency = this._invokeOne( function ( ext ) { + + return ext.loadTexture && ext.loadTexture( index ); + + } ); + break; + + case 'skin': + dependency = this.loadSkin( index ); + break; + + case 'animation': + dependency = this._invokeOne( function ( ext ) { + + return ext.loadAnimation && ext.loadAnimation( index ); + + } ); + break; + + case 'camera': + dependency = this.loadCamera( index ); + break; + + default: + throw new Error( 'Unknown type: ' + type ); + + } + + this.cache.add( cacheKey, dependency ); + + } + + return dependency; + + } + + /** + * Requests all dependencies of the specified type asynchronously, with caching. + * @param {string} type + * @return {Promise>} + */ + getDependencies( type ) { + + let dependencies = this.cache.get( type ); + + if ( ! dependencies ) { + + const parser = this; + const defs = this.json[ type + ( type === 'mesh' ? 'es' : 's' ) ] || []; + + dependencies = Promise.all( defs.map( function ( def, index ) { + + return parser.getDependency( type, index ); + + } ) ); + + this.cache.add( type, dependencies ); + + } + + return dependencies; + + } + + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views + * @param {number} bufferIndex + * @return {Promise} + */ + loadBuffer( bufferIndex ) { + + const bufferDef = this.json.buffers[ bufferIndex ]; + const loader = this.fileLoader; + + if ( bufferDef.type && bufferDef.type !== 'arraybuffer' ) { + + throw new Error( 'THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.' ); + + } + + // If present, GLB container is required to be the first buffer. + if ( bufferDef.uri === undefined && bufferIndex === 0 ) { + + return Promise.resolve( this.extensions[ EXTENSIONS.KHR_BINARY_GLTF ].body ); + + } + + const options = this.options; + + return new Promise( function ( resolve, reject ) { + + loader.load( LoaderUtils.resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () { + + reject( new Error( 'THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ) ); + + } ); + + } ); + + } + + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views + * @param {number} bufferViewIndex + * @return {Promise} + */ + loadBufferView( bufferViewIndex ) { + + const bufferViewDef = this.json.bufferViews[ bufferViewIndex ]; + + return this.getDependency( 'buffer', bufferViewDef.buffer ).then( function ( buffer ) { + + const byteLength = bufferViewDef.byteLength || 0; + const byteOffset = bufferViewDef.byteOffset || 0; + return buffer.slice( byteOffset, byteOffset + byteLength ); + + } ); + + } + + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors + * @param {number} accessorIndex + * @return {Promise} + */ + loadAccessor( accessorIndex ) { + + const parser = this; + const json = this.json; + + const accessorDef = this.json.accessors[ accessorIndex ]; + + if ( accessorDef.bufferView === undefined && accessorDef.sparse === undefined ) { + + // Ignore empty accessors, which may be used to declare runtime + // information about attributes coming from another source (e.g. Draco + // compression extension). + return Promise.resolve( null ); + + } + + const pendingBufferViews = []; + + if ( accessorDef.bufferView !== undefined ) { + + pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.bufferView ) ); + + } else { + + pendingBufferViews.push( null ); + + } + + if ( accessorDef.sparse !== undefined ) { + + pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.indices.bufferView ) ); + pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.values.bufferView ) ); + + } + + return Promise.all( pendingBufferViews ).then( function ( bufferViews ) { + + const bufferView = bufferViews[ 0 ]; + + const itemSize = WEBGL_TYPE_SIZES[ accessorDef.type ]; + const TypedArray = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; + + // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. + const elementBytes = TypedArray.BYTES_PER_ELEMENT; + const itemBytes = elementBytes * itemSize; + const byteOffset = accessorDef.byteOffset || 0; + const byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[ accessorDef.bufferView ].byteStride : undefined; + const normalized = accessorDef.normalized === true; + let array, bufferAttribute; + + // The buffer is not interleaved if the stride is the item size in bytes. + if ( byteStride && byteStride !== itemBytes ) { + + // Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer + // This makes sure that IBA.count reflects accessor.count properly + const ibSlice = Math.floor( byteOffset / byteStride ); + const ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType + ':' + ibSlice + ':' + accessorDef.count; + let ib = parser.cache.get( ibCacheKey ); + + if ( ! ib ) { + + array = new TypedArray( bufferView, ibSlice * byteStride, accessorDef.count * byteStride / elementBytes ); + + // Integer parameters to IB/IBA are in array elements, not bytes. + ib = new InterleavedBuffer( array, byteStride / elementBytes ); + + parser.cache.add( ibCacheKey, ib ); + + } + + bufferAttribute = new InterleavedBufferAttribute( ib, itemSize, ( byteOffset % byteStride ) / elementBytes, normalized ); + + } else { + + if ( bufferView === null ) { + + array = new TypedArray( accessorDef.count * itemSize ); + + } else { + + array = new TypedArray( bufferView, byteOffset, accessorDef.count * itemSize ); + + } + + bufferAttribute = new BufferAttribute( array, itemSize, normalized ); + + } + + // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors + if ( accessorDef.sparse !== undefined ) { + + const itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR; + const TypedArrayIndices = WEBGL_COMPONENT_TYPES[ accessorDef.sparse.indices.componentType ]; + + const byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0; + const byteOffsetValues = accessorDef.sparse.values.byteOffset || 0; + + const sparseIndices = new TypedArrayIndices( bufferViews[ 1 ], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices ); + const sparseValues = new TypedArray( bufferViews[ 2 ], byteOffsetValues, accessorDef.sparse.count * itemSize ); + + if ( bufferView !== null ) { + + // Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes. + bufferAttribute = new BufferAttribute( bufferAttribute.array.slice(), bufferAttribute.itemSize, bufferAttribute.normalized ); + + } + + for ( let i = 0, il = sparseIndices.length; i < il; i ++ ) { + + const index = sparseIndices[ i ]; + + bufferAttribute.setX( index, sparseValues[ i * itemSize ] ); + if ( itemSize >= 2 ) bufferAttribute.setY( index, sparseValues[ i * itemSize + 1 ] ); + if ( itemSize >= 3 ) bufferAttribute.setZ( index, sparseValues[ i * itemSize + 2 ] ); + if ( itemSize >= 4 ) bufferAttribute.setW( index, sparseValues[ i * itemSize + 3 ] ); + if ( itemSize >= 5 ) throw new Error( 'THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.' ); + + } + + } + + return bufferAttribute; + + } ); + + } + + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures + * @param {number} textureIndex + * @return {Promise} + */ + loadTexture( textureIndex ) { + + const json = this.json; + const options = this.options; + const textureDef = json.textures[ textureIndex ]; + const sourceIndex = textureDef.source; + const sourceDef = json.images[ sourceIndex ]; + + let loader = this.textureLoader; + + if ( sourceDef.uri ) { + + const handler = options.manager.getHandler( sourceDef.uri ); + if ( handler !== null ) loader = handler; + + } + + return this.loadTextureImage( textureIndex, sourceIndex, loader ); + + } + + loadTextureImage( textureIndex, sourceIndex, loader ) { + + const parser = this; + const json = this.json; + + const textureDef = json.textures[ textureIndex ]; + const sourceDef = json.images[ sourceIndex ]; + + const cacheKey = ( sourceDef.uri || sourceDef.bufferView ) + ':' + textureDef.sampler; + + if ( this.textureCache[ cacheKey ] ) { + + // See https://github.com/mrdoob/three.js/issues/21559. + return this.textureCache[ cacheKey ]; + + } + + const promise = this.loadImageSource( sourceIndex, loader ).then( function ( texture ) { + + texture.flipY = false; + + texture.name = textureDef.name || sourceDef.name || ''; + + const samplers = json.samplers || {}; + const sampler = samplers[ textureDef.sampler ] || {}; + + texture.magFilter = WEBGL_FILTERS[ sampler.magFilter ] || LinearFilter; + texture.minFilter = WEBGL_FILTERS[ sampler.minFilter ] || LinearMipmapLinearFilter; + texture.wrapS = WEBGL_WRAPPINGS[ sampler.wrapS ] || RepeatWrapping; + texture.wrapT = WEBGL_WRAPPINGS[ sampler.wrapT ] || RepeatWrapping; + + parser.associations.set( texture, { textures: textureIndex } ); + + return texture; + + } ).catch( function () { + + return null; + + } ); + + this.textureCache[ cacheKey ] = promise; + + return promise; + + } + + loadImageSource( sourceIndex, loader ) { + + const parser = this; + const json = this.json; + const options = this.options; + + if ( this.sourceCache[ sourceIndex ] !== undefined ) { + + return this.sourceCache[ sourceIndex ].then( ( texture ) => texture.clone() ); + + } + + const sourceDef = json.images[ sourceIndex ]; + + const URL = self.URL || self.webkitURL; + + let sourceURI = sourceDef.uri || ''; + let isObjectURL = false; + + if ( sourceDef.bufferView !== undefined ) { + + // Load binary image data from bufferView, if provided. + + sourceURI = parser.getDependency( 'bufferView', sourceDef.bufferView ).then( function ( bufferView ) { + + isObjectURL = true; + const blob = new Blob( [ bufferView ], { type: sourceDef.mimeType } ); + sourceURI = URL.createObjectURL( blob ); + return sourceURI; + + } ); + + } else if ( sourceDef.uri === undefined ) { + + throw new Error( 'THREE.GLTFLoader: Image ' + sourceIndex + ' is missing URI and bufferView' ); + + } + + const promise = Promise.resolve( sourceURI ).then( function ( sourceURI ) { + + return new Promise( function ( resolve, reject ) { + + let onLoad = resolve; + + if ( loader.isImageBitmapLoader === true ) { + + onLoad = function ( imageBitmap ) { + + const texture = new Texture( imageBitmap ); + texture.needsUpdate = true; + + resolve( texture ); + + }; + + } + + loader.load( LoaderUtils.resolveURL( sourceURI, options.path ), onLoad, undefined, reject ); + + } ); + + } ).then( function ( texture ) { + + // Clean up resources and configure Texture. + + if ( isObjectURL === true ) { + + URL.revokeObjectURL( sourceURI ); + + } + + texture.userData.mimeType = sourceDef.mimeType || getImageURIMimeType( sourceDef.uri ); + + return texture; + + } ).catch( function ( error ) { + + console.error( 'THREE.GLTFLoader: Couldn\'t load texture', sourceURI ); + throw error; + + } ); + + this.sourceCache[ sourceIndex ] = promise; + return promise; + + } + + /** + * Asynchronously assigns a texture to the given material parameters. + * @param {Object} materialParams + * @param {string} mapName + * @param {Object} mapDef + * @return {Promise} + */ + assignTexture( materialParams, mapName, mapDef, encoding ) { + + const parser = this; + + return this.getDependency( 'texture', mapDef.index ).then( function ( texture ) { + + // Materials sample aoMap from UV set 1 and other maps from UV set 0 - this can't be configured + // However, we will copy UV set 0 to UV set 1 on demand for aoMap + if ( mapDef.texCoord !== undefined && mapDef.texCoord != 0 && ! ( mapName === 'aoMap' && mapDef.texCoord == 1 ) ) { + + console.warn( 'THREE.GLTFLoader: Custom UV set ' + mapDef.texCoord + ' for texture ' + mapName + ' not yet supported.' ); + + } + + if ( parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] ) { + + const transform = mapDef.extensions !== undefined ? mapDef.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] : undefined; + + if ( transform ) { + + const gltfReference = parser.associations.get( texture ); + texture = parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ].extendTexture( texture, transform ); + parser.associations.set( texture, gltfReference ); + + } + + } + + if ( encoding !== undefined ) { + + texture.encoding = encoding; + + } + + materialParams[ mapName ] = texture; + + return texture; + + } ); + + } + + /** + * Assigns final material to a Mesh, Line, or Points instance. The instance + * already has a material (generated from the glTF material options alone) + * but reuse of the same glTF material may require multiple threejs materials + * to accommodate different primitive types, defines, etc. New materials will + * be created if necessary, and reused from a cache. + * @param {Object3D} mesh Mesh, Line, or Points instance. + */ + assignFinalMaterial( mesh ) { + + const geometry = mesh.geometry; + let material = mesh.material; + + const useDerivativeTangents = geometry.attributes.tangent === undefined; + const useVertexColors = geometry.attributes.color !== undefined; + const useFlatShading = geometry.attributes.normal === undefined; + + if ( mesh.isPoints ) { + + const cacheKey = 'PointsMaterial:' + material.uuid; + + let pointsMaterial = this.cache.get( cacheKey ); + + if ( ! pointsMaterial ) { + + pointsMaterial = new PointsMaterial(); + Material.prototype.copy.call( pointsMaterial, material ); + pointsMaterial.color.copy( material.color ); + pointsMaterial.map = material.map; + pointsMaterial.sizeAttenuation = false; // glTF spec says points should be 1px + + this.cache.add( cacheKey, pointsMaterial ); + + } + + material = pointsMaterial; + + } else if ( mesh.isLine ) { + + const cacheKey = 'LineBasicMaterial:' + material.uuid; + + let lineMaterial = this.cache.get( cacheKey ); + + if ( ! lineMaterial ) { + + lineMaterial = new LineBasicMaterial(); + Material.prototype.copy.call( lineMaterial, material ); + lineMaterial.color.copy( material.color ); + + this.cache.add( cacheKey, lineMaterial ); + + } + + material = lineMaterial; + + } + + // Clone the material if it will be modified + if ( useDerivativeTangents || useVertexColors || useFlatShading ) { + + let cacheKey = 'ClonedMaterial:' + material.uuid + ':'; + + if ( material.isGLTFSpecularGlossinessMaterial ) cacheKey += 'specular-glossiness:'; + if ( useDerivativeTangents ) cacheKey += 'derivative-tangents:'; + if ( useVertexColors ) cacheKey += 'vertex-colors:'; + if ( useFlatShading ) cacheKey += 'flat-shading:'; + + let cachedMaterial = this.cache.get( cacheKey ); + + if ( ! cachedMaterial ) { + + cachedMaterial = material.clone(); + + if ( useVertexColors ) cachedMaterial.vertexColors = true; + if ( useFlatShading ) cachedMaterial.flatShading = true; + + if ( useDerivativeTangents ) { + + // https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995 + if ( cachedMaterial.normalScale ) cachedMaterial.normalScale.y *= - 1; + if ( cachedMaterial.clearcoatNormalScale ) cachedMaterial.clearcoatNormalScale.y *= - 1; + + } + + this.cache.add( cacheKey, cachedMaterial ); + + this.associations.set( cachedMaterial, this.associations.get( material ) ); + + } + + material = cachedMaterial; + + } + + // workarounds for mesh and geometry + + if ( material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined ) { + + geometry.setAttribute( 'uv2', geometry.attributes.uv ); + + } + + mesh.material = material; + + } + + getMaterialType( /* materialIndex */ ) { + + return MeshStandardMaterial; + + } + + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials + * @param {number} materialIndex + * @return {Promise} + */ + loadMaterial( materialIndex ) { + + const parser = this; + const json = this.json; + const extensions = this.extensions; + const materialDef = json.materials[ materialIndex ]; + + let materialType; + const materialParams = {}; + const materialExtensions = materialDef.extensions || {}; + + const pending = []; + + if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) { + + const sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ]; + materialType = sgExtension.getMaterialType(); + pending.push( sgExtension.extendParams( materialParams, materialDef, parser ) ); + + } else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] ) { + + const kmuExtension = extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ]; + materialType = kmuExtension.getMaterialType(); + pending.push( kmuExtension.extendParams( materialParams, materialDef, parser ) ); + + } else { + + // Specification: + // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material + + const metallicRoughness = materialDef.pbrMetallicRoughness || {}; + + materialParams.color = new Color( 1.0, 1.0, 1.0 ); + materialParams.opacity = 1.0; + + if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { + + const array = metallicRoughness.baseColorFactor; + + materialParams.color.fromArray( array ); + materialParams.opacity = array[ 3 ]; + + } + + if ( metallicRoughness.baseColorTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture, sRGBEncoding ) ); + + } + + materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0; + materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0; + + if ( metallicRoughness.metallicRoughnessTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'metalnessMap', metallicRoughness.metallicRoughnessTexture ) ); + pending.push( parser.assignTexture( materialParams, 'roughnessMap', metallicRoughness.metallicRoughnessTexture ) ); + + } + + materialType = this._invokeOne( function ( ext ) { + + return ext.getMaterialType && ext.getMaterialType( materialIndex ); + + } ); + + pending.push( Promise.all( this._invokeAll( function ( ext ) { + + return ext.extendMaterialParams && ext.extendMaterialParams( materialIndex, materialParams ); + + } ) ) ); + + } + + if ( materialDef.doubleSided === true ) { + + materialParams.side = DoubleSide; + + } + + const alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE; + + if ( alphaMode === ALPHA_MODES.BLEND ) { + + materialParams.transparent = true; + + // See: https://github.com/mrdoob/three.js/issues/17706 + materialParams.depthWrite = false; + + } else { + + materialParams.transparent = false; + + if ( alphaMode === ALPHA_MODES.MASK ) { + + materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5; + + } + + } + + if ( materialDef.normalTexture !== undefined && materialType !== MeshBasicMaterial ) { + + pending.push( parser.assignTexture( materialParams, 'normalMap', materialDef.normalTexture ) ); + + materialParams.normalScale = new Vector2( 1, 1 ); + + if ( materialDef.normalTexture.scale !== undefined ) { + + const scale = materialDef.normalTexture.scale; + + materialParams.normalScale.set( scale, scale ); + + } + + } + + if ( materialDef.occlusionTexture !== undefined && materialType !== MeshBasicMaterial ) { + + pending.push( parser.assignTexture( materialParams, 'aoMap', materialDef.occlusionTexture ) ); + + if ( materialDef.occlusionTexture.strength !== undefined ) { + + materialParams.aoMapIntensity = materialDef.occlusionTexture.strength; + + } + + } + + if ( materialDef.emissiveFactor !== undefined && materialType !== MeshBasicMaterial ) { + + materialParams.emissive = new Color().fromArray( materialDef.emissiveFactor ); + + } + + if ( materialDef.emissiveTexture !== undefined && materialType !== MeshBasicMaterial ) { + + pending.push( parser.assignTexture( materialParams, 'emissiveMap', materialDef.emissiveTexture, sRGBEncoding ) ); + + } + + return Promise.all( pending ).then( function () { + + let material; + + if ( materialType === GLTFMeshStandardSGMaterial ) { + + material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams ); + + } else { + + material = new materialType( materialParams ); + + } + + if ( materialDef.name ) material.name = materialDef.name; + + assignExtrasToUserData( material, materialDef ); + + parser.associations.set( material, { materials: materialIndex } ); + + if ( materialDef.extensions ) addUnknownExtensionsToUserData( extensions, material, materialDef ); + + return material; + + } ); + + } + + /** When Object3D instances are targeted by animation, they need unique names. */ + createUniqueName( originalName ) { + + const sanitizedName = PropertyBinding.sanitizeNodeName( originalName || '' ); + + let name = sanitizedName; + + for ( let i = 1; this.nodeNamesUsed[ name ]; ++ i ) { + + name = sanitizedName + '_' + i; + + } + + this.nodeNamesUsed[ name ] = true; + + return name; + + } + + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry + * + * Creates BufferGeometries from primitives. + * + * @param {Array} primitives + * @return {Promise>} + */ + loadGeometries( primitives ) { + + const parser = this; + const extensions = this.extensions; + const cache = this.primitiveCache; + + function createDracoPrimitive( primitive ) { + + return extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] + .decodePrimitive( primitive, parser ) + .then( function ( geometry ) { + + return addPrimitiveAttributes( geometry, primitive, parser ); + + } ); + + } + + const pending = []; + + for ( let i = 0, il = primitives.length; i < il; i ++ ) { + + const primitive = primitives[ i ]; + const cacheKey = createPrimitiveKey( primitive ); + + // See if we've already created this geometry + const cached = cache[ cacheKey ]; + + if ( cached ) { + + // Use the cached geometry if it exists + pending.push( cached.promise ); + + } else { + + let geometryPromise; + + if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) { + + // Use DRACO geometry if available + geometryPromise = createDracoPrimitive( primitive ); + + } else { + + // Otherwise create a new geometry + geometryPromise = addPrimitiveAttributes( new BufferGeometry(), primitive, parser ); + + } + + // Cache this geometry + cache[ cacheKey ] = { primitive: primitive, promise: geometryPromise }; + + pending.push( geometryPromise ); + + } + + } + + return Promise.all( pending ); + + } + + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes + * @param {number} meshIndex + * @return {Promise} + */ + loadMesh( meshIndex ) { + + const parser = this; + const json = this.json; + const extensions = this.extensions; + + const meshDef = json.meshes[ meshIndex ]; + const primitives = meshDef.primitives; + + const pending = []; + + for ( let i = 0, il = primitives.length; i < il; i ++ ) { + + const material = primitives[ i ].material === undefined + ? createDefaultMaterial( this.cache ) + : this.getDependency( 'material', primitives[ i ].material ); + + pending.push( material ); + + } + + pending.push( parser.loadGeometries( primitives ) ); + + return Promise.all( pending ).then( function ( results ) { + + const materials = results.slice( 0, results.length - 1 ); + const geometries = results[ results.length - 1 ]; + + const meshes = []; + + for ( let i = 0, il = geometries.length; i < il; i ++ ) { + + const geometry = geometries[ i ]; + const primitive = primitives[ i ]; + + // 1. create Mesh + + let mesh; + + const material = materials[ i ]; + + if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES || + primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP || + primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN || + primitive.mode === undefined ) { + + // .isSkinnedMesh isn't in glTF spec. See ._markDefs() + mesh = meshDef.isSkinnedMesh === true + ? new SkinnedMesh( geometry, material ) + : new Mesh( geometry, material ); + + if ( mesh.isSkinnedMesh === true && ! mesh.geometry.attributes.skinWeight.normalized ) { + + // we normalize floating point skin weight array to fix malformed assets (see #15319) + // it's important to skip this for non-float32 data since normalizeSkinWeights assumes non-normalized inputs + mesh.normalizeSkinWeights(); + + } + + if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) { + + mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleStripDrawMode ); + + } else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) { + + mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleFanDrawMode ); + + } + + } else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) { + + mesh = new LineSegments( geometry, material ); + + } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ) { + + mesh = new Line( geometry, material ); + + } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) { + + mesh = new LineLoop( geometry, material ); + + } else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) { + + mesh = new Points( geometry, material ); + + } else { + + throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode ); + + } + + if ( Object.keys( mesh.geometry.morphAttributes ).length > 0 ) { + + updateMorphTargets( mesh, meshDef ); + + } + + mesh.name = parser.createUniqueName( meshDef.name || ( 'mesh_' + meshIndex ) ); + + assignExtrasToUserData( mesh, meshDef ); + + if ( primitive.extensions ) addUnknownExtensionsToUserData( extensions, mesh, primitive ); + + parser.assignFinalMaterial( mesh ); + + meshes.push( mesh ); + + } + + for ( let i = 0, il = meshes.length; i < il; i ++ ) { + + parser.associations.set( meshes[ i ], { + meshes: meshIndex, + primitives: i + } ); + + } + + if ( meshes.length === 1 ) { + + return meshes[ 0 ]; + + } + + const group = new Group(); + + parser.associations.set( group, { meshes: meshIndex } ); + + for ( let i = 0, il = meshes.length; i < il; i ++ ) { + + group.add( meshes[ i ] ); + + } + + return group; + + } ); + + } + + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras + * @param {number} cameraIndex + * @return {Promise} + */ + loadCamera( cameraIndex ) { + + let camera; + const cameraDef = this.json.cameras[ cameraIndex ]; + const params = cameraDef[ cameraDef.type ]; + + if ( ! params ) { + + console.warn( 'THREE.GLTFLoader: Missing camera parameters.' ); + return; + + } + + if ( cameraDef.type === 'perspective' ) { + + camera = new PerspectiveCamera( MathUtils.radToDeg( params.yfov ), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6 ); + + } else if ( cameraDef.type === 'orthographic' ) { + + camera = new OrthographicCamera( - params.xmag, params.xmag, params.ymag, - params.ymag, params.znear, params.zfar ); + + } + + if ( cameraDef.name ) camera.name = this.createUniqueName( cameraDef.name ); + + assignExtrasToUserData( camera, cameraDef ); + + return Promise.resolve( camera ); + + } + + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins + * @param {number} skinIndex + * @return {Promise} + */ + loadSkin( skinIndex ) { + + const skinDef = this.json.skins[ skinIndex ]; + + const skinEntry = { joints: skinDef.joints }; + + if ( skinDef.inverseBindMatrices === undefined ) { + + return Promise.resolve( skinEntry ); + + } + + return this.getDependency( 'accessor', skinDef.inverseBindMatrices ).then( function ( accessor ) { + + skinEntry.inverseBindMatrices = accessor; + + return skinEntry; + + } ); + + } + + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations + * @param {number} animationIndex + * @return {Promise} + */ + loadAnimation( animationIndex ) { + + const json = this.json; + + const animationDef = json.animations[ animationIndex ]; + + const pendingNodes = []; + const pendingInputAccessors = []; + const pendingOutputAccessors = []; + const pendingSamplers = []; + const pendingTargets = []; + + for ( let i = 0, il = animationDef.channels.length; i < il; i ++ ) { + + const channel = animationDef.channels[ i ]; + const sampler = animationDef.samplers[ channel.sampler ]; + const target = channel.target; + const name = target.node; + const input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input; + const output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output; + + pendingNodes.push( this.getDependency( 'node', name ) ); + pendingInputAccessors.push( this.getDependency( 'accessor', input ) ); + pendingOutputAccessors.push( this.getDependency( 'accessor', output ) ); + pendingSamplers.push( sampler ); + pendingTargets.push( target ); + + } + + return Promise.all( [ + + Promise.all( pendingNodes ), + Promise.all( pendingInputAccessors ), + Promise.all( pendingOutputAccessors ), + Promise.all( pendingSamplers ), + Promise.all( pendingTargets ) + + ] ).then( function ( dependencies ) { + + const nodes = dependencies[ 0 ]; + const inputAccessors = dependencies[ 1 ]; + const outputAccessors = dependencies[ 2 ]; + const samplers = dependencies[ 3 ]; + const targets = dependencies[ 4 ]; + + const tracks = []; + + for ( let i = 0, il = nodes.length; i < il; i ++ ) { + + const node = nodes[ i ]; + const inputAccessor = inputAccessors[ i ]; + const outputAccessor = outputAccessors[ i ]; + const sampler = samplers[ i ]; + const target = targets[ i ]; + + if ( node === undefined ) continue; + + node.updateMatrix(); + + let TypedKeyframeTrack; + + switch ( PATH_PROPERTIES[ target.path ] ) { + + case PATH_PROPERTIES.weights: + + TypedKeyframeTrack = NumberKeyframeTrack; + break; + + case PATH_PROPERTIES.rotation: + + TypedKeyframeTrack = QuaternionKeyframeTrack; + break; + + case PATH_PROPERTIES.position: + case PATH_PROPERTIES.scale: + default: + + TypedKeyframeTrack = VectorKeyframeTrack; + break; + + } + + const targetName = node.name ? node.name : node.uuid; + + const interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : InterpolateLinear; + + const targetNames = []; + + if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) { + + node.traverse( function ( object ) { + + if ( object.morphTargetInfluences ) { + + targetNames.push( object.name ? object.name : object.uuid ); + + } + + } ); + + } else { + + targetNames.push( targetName ); + + } + + let outputArray = outputAccessor.array; + + if ( outputAccessor.normalized ) { + + const scale = getNormalizedComponentScale( outputArray.constructor ); + const scaled = new Float32Array( outputArray.length ); + + for ( let j = 0, jl = outputArray.length; j < jl; j ++ ) { + + scaled[ j ] = outputArray[ j ] * scale; + + } + + outputArray = scaled; + + } + + for ( let j = 0, jl = targetNames.length; j < jl; j ++ ) { + + const track = new TypedKeyframeTrack( + targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ], + inputAccessor.array, + outputArray, + interpolation + ); + + // Override interpolation with custom factory method. + if ( sampler.interpolation === 'CUBICSPLINE' ) { + + track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline( result ) { + + // A CUBICSPLINE keyframe in glTF has three output values for each input value, + // representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize() + // must be divided by three to get the interpolant's sampleSize argument. + + const interpolantType = ( this instanceof QuaternionKeyframeTrack ) ? GLTFCubicSplineQuaternionInterpolant : GLTFCubicSplineInterpolant; + + return new interpolantType( this.times, this.values, this.getValueSize() / 3, result ); + + }; + + // Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants. + track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true; + + } + + tracks.push( track ); + + } + + } + + const name = animationDef.name ? animationDef.name : 'animation_' + animationIndex; + + return new AnimationClip( name, undefined, tracks ); + + } ); + + } + + createNodeMesh( nodeIndex ) { + + const json = this.json; + const parser = this; + const nodeDef = json.nodes[ nodeIndex ]; + + if ( nodeDef.mesh === undefined ) return null; + + return parser.getDependency( 'mesh', nodeDef.mesh ).then( function ( mesh ) { + + const node = parser._getNodeRef( parser.meshCache, nodeDef.mesh, mesh ); + + // if weights are provided on the node, override weights on the mesh. + if ( nodeDef.weights !== undefined ) { + + node.traverse( function ( o ) { + + if ( ! o.isMesh ) return; + + for ( let i = 0, il = nodeDef.weights.length; i < il; i ++ ) { + + o.morphTargetInfluences[ i ] = nodeDef.weights[ i ]; + + } + + } ); + + } + + return node; + + } ); + + } + + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy + * @param {number} nodeIndex + * @return {Promise} + */ + loadNode( nodeIndex ) { + + const json = this.json; + const extensions = this.extensions; + const parser = this; + + const nodeDef = json.nodes[ nodeIndex ]; + + // reserve node's name before its dependencies, so the root has the intended name. + const nodeName = nodeDef.name ? parser.createUniqueName( nodeDef.name ) : ''; + + return ( function () { + + const pending = []; + + const meshPromise = parser._invokeOne( function ( ext ) { + + return ext.createNodeMesh && ext.createNodeMesh( nodeIndex ); + + } ); + + if ( meshPromise ) { + + pending.push( meshPromise ); + + } + + if ( nodeDef.camera !== undefined ) { + + pending.push( parser.getDependency( 'camera', nodeDef.camera ).then( function ( camera ) { + + return parser._getNodeRef( parser.cameraCache, nodeDef.camera, camera ); + + } ) ); + + } + + parser._invokeAll( function ( ext ) { + + return ext.createNodeAttachment && ext.createNodeAttachment( nodeIndex ); + + } ).forEach( function ( promise ) { + + pending.push( promise ); + + } ); + + return Promise.all( pending ); + + }() ).then( function ( objects ) { + + let node; + + // .isBone isn't in glTF spec. See ._markDefs + if ( nodeDef.isBone === true ) { + + node = new Bone(); + + } else if ( objects.length > 1 ) { + + node = new Group(); + + } else if ( objects.length === 1 ) { + + node = objects[ 0 ]; + + } else { + + node = new Object3D(); + + } + + if ( node !== objects[ 0 ] ) { + + for ( let i = 0, il = objects.length; i < il; i ++ ) { + + node.add( objects[ i ] ); + + } + + } + + if ( nodeDef.name ) { + + node.userData.name = nodeDef.name; + node.name = nodeName; + + } + + assignExtrasToUserData( node, nodeDef ); + + if ( nodeDef.extensions ) addUnknownExtensionsToUserData( extensions, node, nodeDef ); + + if ( nodeDef.matrix !== undefined ) { + + const matrix = new Matrix4(); + matrix.fromArray( nodeDef.matrix ); + node.applyMatrix4( matrix ); + + } else { + + if ( nodeDef.translation !== undefined ) { + + node.position.fromArray( nodeDef.translation ); + + } + + if ( nodeDef.rotation !== undefined ) { + + node.quaternion.fromArray( nodeDef.rotation ); + + } + + if ( nodeDef.scale !== undefined ) { + + node.scale.fromArray( nodeDef.scale ); + + } + + } + + if ( ! parser.associations.has( node ) ) { + + parser.associations.set( node, {} ); + + } + + parser.associations.get( node ).nodes = nodeIndex; + + return node; + + } ); + + } + + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes + * @param {number} sceneIndex + * @return {Promise} + */ + loadScene( sceneIndex ) { + + const json = this.json; + const extensions = this.extensions; + const sceneDef = this.json.scenes[ sceneIndex ]; + const parser = this; + + // Loader returns Group, not Scene. + // See: https://github.com/mrdoob/three.js/issues/18342#issuecomment-578981172 + const scene = new Group(); + if ( sceneDef.name ) scene.name = parser.createUniqueName( sceneDef.name ); + + assignExtrasToUserData( scene, sceneDef ); + + if ( sceneDef.extensions ) addUnknownExtensionsToUserData( extensions, scene, sceneDef ); + + const nodeIds = sceneDef.nodes || []; + + const pending = []; + + for ( let i = 0, il = nodeIds.length; i < il; i ++ ) { + + pending.push( buildNodeHierarchy( nodeIds[ i ], scene, json, parser ) ); + + } + + return Promise.all( pending ).then( function () { + + // Removes dangling associations, associations that reference a node that + // didn't make it into the scene. + const reduceAssociations = ( node ) => { + + const reducedAssociations = new Map(); + + for ( const [ key, value ] of parser.associations ) { + + if ( key instanceof Material || key instanceof Texture ) { + + reducedAssociations.set( key, value ); + + } + + } + + node.traverse( ( node ) => { + + const mappings = parser.associations.get( node ); + + if ( mappings != null ) { + + reducedAssociations.set( node, mappings ); + + } + + } ); + + return reducedAssociations; + + }; + + parser.associations = reduceAssociations( scene ); + + return scene; + + } ); + + } + +} + +function buildNodeHierarchy( nodeId, parentObject, json, parser ) { + + const nodeDef = json.nodes[ nodeId ]; + + return parser.getDependency( 'node', nodeId ).then( function ( node ) { + + if ( nodeDef.skin === undefined ) return node; + + // build skeleton here as well + + let skinEntry; + + return parser.getDependency( 'skin', nodeDef.skin ).then( function ( skin ) { + + skinEntry = skin; + + const pendingJoints = []; + + for ( let i = 0, il = skinEntry.joints.length; i < il; i ++ ) { + + pendingJoints.push( parser.getDependency( 'node', skinEntry.joints[ i ] ) ); + + } + + return Promise.all( pendingJoints ); + + } ).then( function ( jointNodes ) { + + node.traverse( function ( mesh ) { + + if ( ! mesh.isMesh ) return; + + const bones = []; + const boneInverses = []; + + for ( let j = 0, jl = jointNodes.length; j < jl; j ++ ) { + + const jointNode = jointNodes[ j ]; + + if ( jointNode ) { + + bones.push( jointNode ); + + const mat = new Matrix4(); + + if ( skinEntry.inverseBindMatrices !== undefined ) { + + mat.fromArray( skinEntry.inverseBindMatrices.array, j * 16 ); + + } + + boneInverses.push( mat ); + + } else { + + console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', skinEntry.joints[ j ] ); + + } + + } + + mesh.bind( new Skeleton( bones, boneInverses ), mesh.matrixWorld ); + + } ); + + return node; + + } ); + + } ).then( function ( node ) { + + // build node hierachy + + parentObject.add( node ); + + const pending = []; + + if ( nodeDef.children ) { + + const children = nodeDef.children; + + for ( let i = 0, il = children.length; i < il; i ++ ) { + + const child = children[ i ]; + pending.push( buildNodeHierarchy( child, node, json, parser ) ); + + } + + } + + return Promise.all( pending ); + + } ); + +} + +/** + * @param {BufferGeometry} geometry + * @param {GLTF.Primitive} primitiveDef + * @param {GLTFParser} parser + */ +function computeBounds( geometry, primitiveDef, parser ) { + + const attributes = primitiveDef.attributes; + + const box = new Box3(); + + if ( attributes.POSITION !== undefined ) { + + const accessor = parser.json.accessors[ attributes.POSITION ]; + + const min = accessor.min; + const max = accessor.max; + + // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. + + if ( min !== undefined && max !== undefined ) { + + box.set( + new Vector3( min[ 0 ], min[ 1 ], min[ 2 ] ), + new Vector3( max[ 0 ], max[ 1 ], max[ 2 ] ) + ); + + if ( accessor.normalized ) { + + const boxScale = getNormalizedComponentScale( WEBGL_COMPONENT_TYPES[ accessor.componentType ] ); + box.min.multiplyScalar( boxScale ); + box.max.multiplyScalar( boxScale ); + + } + + } else { + + console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' ); + + return; + + } + + } else { + + return; + + } + + const targets = primitiveDef.targets; + + if ( targets !== undefined ) { + + const maxDisplacement = new Vector3(); + const vector = new Vector3(); + + for ( let i = 0, il = targets.length; i < il; i ++ ) { + + const target = targets[ i ]; + + if ( target.POSITION !== undefined ) { + + const accessor = parser.json.accessors[ target.POSITION ]; + const min = accessor.min; + const max = accessor.max; + + // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. + + if ( min !== undefined && max !== undefined ) { + + // we need to get max of absolute components because target weight is [-1,1] + vector.setX( Math.max( Math.abs( min[ 0 ] ), Math.abs( max[ 0 ] ) ) ); + vector.setY( Math.max( Math.abs( min[ 1 ] ), Math.abs( max[ 1 ] ) ) ); + vector.setZ( Math.max( Math.abs( min[ 2 ] ), Math.abs( max[ 2 ] ) ) ); + + + if ( accessor.normalized ) { + + const boxScale = getNormalizedComponentScale( WEBGL_COMPONENT_TYPES[ accessor.componentType ] ); + vector.multiplyScalar( boxScale ); + + } + + // Note: this assumes that the sum of all weights is at most 1. This isn't quite correct - it's more conservative + // to assume that each target can have a max weight of 1. However, for some use cases - notably, when morph targets + // are used to implement key-frame animations and as such only two are active at a time - this results in very large + // boxes. So for now we make a box that's sometimes a touch too small but is hopefully mostly of reasonable size. + maxDisplacement.max( vector ); + + } else { + + console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' ); + + } + + } + + } + + // As per comment above this box isn't conservative, but has a reasonable size for a very large number of morph targets. + box.expandByVector( maxDisplacement ); + + } + + geometry.boundingBox = box; + + const sphere = new Sphere(); + + box.getCenter( sphere.center ); + sphere.radius = box.min.distanceTo( box.max ) / 2; + + geometry.boundingSphere = sphere; + +} + +/** + * @param {BufferGeometry} geometry + * @param {GLTF.Primitive} primitiveDef + * @param {GLTFParser} parser + * @return {Promise} + */ +function addPrimitiveAttributes( geometry, primitiveDef, parser ) { + + const attributes = primitiveDef.attributes; + + const pending = []; + + function assignAttributeAccessor( accessorIndex, attributeName ) { + + return parser.getDependency( 'accessor', accessorIndex ) + .then( function ( accessor ) { + + geometry.setAttribute( attributeName, accessor ); + + } ); + + } + + for ( const gltfAttributeName in attributes ) { + + const threeAttributeName = ATTRIBUTES[ gltfAttributeName ] || gltfAttributeName.toLowerCase(); + + // Skip attributes already provided by e.g. Draco extension. + if ( threeAttributeName in geometry.attributes ) continue; + + pending.push( assignAttributeAccessor( attributes[ gltfAttributeName ], threeAttributeName ) ); + + } + + if ( primitiveDef.indices !== undefined && ! geometry.index ) { + + const accessor = parser.getDependency( 'accessor', primitiveDef.indices ).then( function ( accessor ) { + + geometry.setIndex( accessor ); + + } ); + + pending.push( accessor ); + + } + + assignExtrasToUserData( geometry, primitiveDef ); + + computeBounds( geometry, primitiveDef, parser ); + + return Promise.all( pending ).then( function () { + + return primitiveDef.targets !== undefined + ? addMorphTargets( geometry, primitiveDef.targets, parser ) + : geometry; + + } ); + +} + +/** + * @param {BufferGeometry} geometry + * @param {Number} drawMode + * @return {BufferGeometry} + */ +function toTrianglesDrawMode( geometry, drawMode ) { + + let index = geometry.getIndex(); + + // generate index if not present + + if ( index === null ) { + + const indices = []; + + const position = geometry.getAttribute( 'position' ); + + if ( position !== undefined ) { + + for ( let i = 0; i < position.count; i ++ ) { + + indices.push( i ); + + } + + geometry.setIndex( indices ); + index = geometry.getIndex(); + + } else { + + console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' ); + return geometry; + + } + + } + + // + + const numberOfTriangles = index.count - 2; + const newIndices = []; + + if ( drawMode === TriangleFanDrawMode ) { + + // gl.TRIANGLE_FAN + + for ( let i = 1; i <= numberOfTriangles; i ++ ) { + + newIndices.push( index.getX( 0 ) ); + newIndices.push( index.getX( i ) ); + newIndices.push( index.getX( i + 1 ) ); + + } + + } else { + + // gl.TRIANGLE_STRIP + + for ( let i = 0; i < numberOfTriangles; i ++ ) { + + if ( i % 2 === 0 ) { + + newIndices.push( index.getX( i ) ); + newIndices.push( index.getX( i + 1 ) ); + newIndices.push( index.getX( i + 2 ) ); + + + } else { + + newIndices.push( index.getX( i + 2 ) ); + newIndices.push( index.getX( i + 1 ) ); + newIndices.push( index.getX( i ) ); + + } + + } + + } + + if ( ( newIndices.length / 3 ) !== numberOfTriangles ) { + + console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' ); + + } + + // build final geometry + + const newGeometry = geometry.clone(); + newGeometry.setIndex( newIndices ); + + return newGeometry; + +} + +export { GLTFLoader }; \ No newline at end of file diff --git a/js/external-libraries/queue.js b/js/external-libraries/queue.js index f9b3361..e96e1d7 100644 --- a/js/external-libraries/queue.js +++ b/js/external-libraries/queue.js @@ -1,80 +1,82 @@ -(function() { - var slice = [].slice; +// (function() { +var slice = [].slice; - function queue(parallelism) { - var q, - tasks = [], - started = 0, // number of tasks that have been started (and perhaps finished) - active = 0, // number of tasks currently being executed (started but not finished) - remaining = 0, // number of tasks not yet finished - popping, // inside a synchronous task callback? - error = null, - await = noop, - all; +function queue(parallelism) { + var q, + tasks = [], + started = 0, // number of tasks that have been started (and perhaps finished) + active = 0, // number of tasks currently being executed (started but not finished) + remaining = 0, // number of tasks not yet finished + popping, // inside a synchronous task callback? + error = null, + a_wait = noop, + all; - if (!parallelism) parallelism = Infinity; + if (!parallelism) parallelism = Infinity; - function pop() { - while (popping = started < tasks.length && active < parallelism) { - var i = started++, - t = tasks[i], - a = slice.call(t, 1); - a.push(callback(i)); - ++active; - t[0].apply(null, a); - } - } - - function callback(i) { - return function(e, r) { - --active; - if (error != null) return; - if (e != null) { - error = e; // ignore new tasks and squelch active callbacks - started = remaining = NaN; // stop queued tasks from starting - notify(); - } else { - tasks[i] = r; - if (--remaining) popping || pop(); - else notify(); - } - }; - } - - function notify() { - if (error != null) await(error); - else if (all) await(error, tasks); - else await.apply(null, [error].concat(tasks)); + function pop() { + while (popping = started < tasks.length && active < parallelism) { + var i = started++, + t = tasks[i], + a = slice.call(t, 1); + a.push(callback(i)); + ++active; + t[0].apply(null, a); } + } - return q = { - defer: function() { - if (!error) { - tasks.push(arguments); - ++remaining; - pop(); - } - return q; - }, - await: function(f) { - await = f; - all = false; - if (!remaining) notify(); - return q; - }, - awaitAll: function(f) { - await = f; - all = true; - if (!remaining) notify(); - return q; + function callback(i) { + return function (e, r) { + --active; + if (error != null) return; + if (e != null) { + error = e; // ignore new tasks and squelch active callbacks + started = remaining = NaN; // stop queued tasks from starting + notify(); + } else { + tasks[i] = r; + if (--remaining) popping || pop(); + else notify(); } }; } - function noop() {} + function notify() { + if (error != null) a_wait(error); + else if (all) a_wait(error, tasks); + else a_wait.apply(null, [error].concat(tasks)); + } + + return q = { + defer: function () { + if (!error) { + tasks.push(arguments); + ++remaining; + pop(); + } + return q; + }, + await: function (f) { + a_wait = f; + all = false; + if (!remaining) notify(); + return q; + }, + awaitAll: function (f) { + a_wait = f; + all = true; + if (!remaining) notify(); + return q; + } + }; +} +function noop() {} +export {queue} + - queue.version = "1.0.7"; - if (typeof define === "function" && define.amd) define(function() { return queue; }); - else if (typeof module === "object" && module.exports) module.exports = queue; - else this.queue = queue; -})(); +// +// queue.version = "1.0.7"; +// if (typeof define === "function" && define.amd) define(function() { return queue; }); +// else if (typeof module === "object" && module.exports) module.exports = queue; +// else this.queue = queue; +// })(); diff --git a/js/external-libraries/vr/VRButton.js b/js/external-libraries/vr/VRButton.js new file mode 100644 index 0000000..c9e61bc --- /dev/null +++ b/js/external-libraries/vr/VRButton.js @@ -0,0 +1,201 @@ +class VRButton { + + static createButton( renderer ) { + + const button = document.createElement( 'button' ); + + function showEnterVR( /*device*/ ) { + + let currentSession = null; + + async function onSessionStarted( session ) { + + session.addEventListener( 'end', onSessionEnded ); + + await renderer.xr.setSession( session ); + button.textContent = 'EXIT VR'; + + currentSession = session; + + } + + function onSessionEnded( /*event*/ ) { + + currentSession.removeEventListener( 'end', onSessionEnded ); + + button.textContent = 'ENTER VR'; + + currentSession = null; + + } + + // + + button.style.display = ''; + + button.style.cursor = 'pointer'; + button.style.left = 'calc(50% - 50px)'; + button.style.width = '100px'; + + button.textContent = 'ENTER VR'; + + button.onmouseenter = function () { + + button.style.opacity = '1.0'; + + }; + + button.onmouseleave = function () { + + button.style.opacity = '0.5'; + + }; + + button.onclick = function () { + + if ( currentSession === null ) { + + // WebXR's requestReferenceSpace only works if the corresponding feature + // was requested at session creation time. For simplicity, just ask for + // the interesting ones as optional features, but be aware that the + // requestReferenceSpace call will fail if it turns out to be unavailable. + // ('local' is always available for immersive sessions and doesn't need to + // be requested separately.) + + const sessionInit = { optionalFeatures: [ 'local-floor', 'bounded-floor', 'hand-tracking', 'layers' ] }; + navigator.xr.requestSession( 'immersive-vr', sessionInit ).then( onSessionStarted ); + + } else { + + currentSession.end(); + + } + + }; + + } + + function disableButton() { + + button.style.display = ''; + + button.style.cursor = 'auto'; + button.style.left = 'calc(50% - 75px)'; + button.style.width = '150px'; + + button.onmouseenter = null; + button.onmouseleave = null; + + button.onclick = null; + + } + + function showWebXRNotFound() { + + disableButton(); + + button.textContent = 'VR NOT SUPPORTED'; + + } + + function showVRNotAllowed( exception ) { + + disableButton(); + + console.warn( 'Exception when trying to call xr.isSessionSupported', exception ); + + button.textContent = 'VR NOT ALLOWED'; + + } + + function stylizeElement( element ) { + + element.style.position = 'absolute'; + element.style.bottom = '20px'; + element.style.padding = '12px 6px'; + element.style.border = '1px solid #fff'; + element.style.borderRadius = '4px'; + element.style.background = 'rgba(0,0,0,0.1)'; + element.style.color = '#fff'; + element.style.font = 'normal 13px sans-serif'; + element.style.textAlign = 'center'; + element.style.opacity = '0.5'; + element.style.outline = 'none'; + element.style.zIndex = '999'; + + } + + if ( 'xr' in navigator ) { + + button.id = 'VRButton'; + button.style.display = 'none'; + + stylizeElement( button ); + + navigator.xr.isSessionSupported( 'immersive-vr' ).then( function ( supported ) { + + supported ? showEnterVR() : showWebXRNotFound(); + + if ( supported && VRButton.xrSessionIsGranted ) { + + button.click(); + + } + + } ).catch( showVRNotAllowed ); + + return button; + + } else { + + const message = document.createElement( 'a' ); + + if ( window.isSecureContext === false ) { + + message.href = document.location.href.replace( /^http:/, 'https:' ); + message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message + + } else { + + message.href = 'https://immersiveweb.dev/'; + message.innerHTML = 'WEBXR NOT AVAILABLE'; + + } + + message.style.left = 'calc(50% - 90px)'; + message.style.width = '180px'; + message.style.textDecoration = 'none'; + + stylizeElement( message ); + + return message; + + } + + } + + static xrSessionIsGranted = false; + + static registerSessionGrantedListener() { + + if ( 'xr' in navigator ) { + + // WebXRViewer (based on Firefox) has a bug where addEventListener + // throws a silent exception and aborts execution entirely. + if ( /WebXRViewer\//i.test( navigator.userAgent ) ) return; + + navigator.xr.addEventListener( 'sessiongranted', () => { + + VRButton.xrSessionIsGranted = true; + + } ); + + } + + } + +} + +VRButton.registerSessionGrantedListener(); + +export { VRButton }; \ No newline at end of file diff --git a/js/external-libraries/vr/VRControls.js b/js/external-libraries/vr/VRControls.js index 6eecd60..f790eb3 100644 --- a/js/external-libraries/vr/VRControls.js +++ b/js/external-libraries/vr/VRControls.js @@ -1,33 +1,34 @@ -/** + /** * @author dmarcos / https://github.com/dmarcos * @author mrdoob / http://mrdoob.com */ -THREE.VRControls = function ( object, onError ) { +import * as THREE from 'three' - var scope = this; - var vrDisplay, vrDisplays; + var VRControls = function ( object, onError ) { - var standingMatrix = new THREE.Matrix4(); + var scope = this; - var frameData = null; + var vrInput; - if ( 'VRFrameData' in window ) { + var standingMatrix = new THREE.Matrix4(); - frameData = new VRFrameData(); + function gotVRDevices( devices ) { - } + for ( var i = 0; i < devices.length; i ++ ) { - function gotVRDisplays( displays ) { + if ( ( 'VRDisplay' in window && devices[ i ] instanceof VRDisplay ) || + ( 'PositionSensorVRDevice' in window && devices[ i ] instanceof PositionSensorVRDevice ) ) { - vrDisplays = displays; + vrInput = devices[ i ]; + break; // We keep the first we encounter - if ( displays.length > 0 ) { + } - vrDisplay = displays[ 0 ]; + } - } else { + if ( !vrInput ) { if ( onError ) onError( 'VR input not available.' ); @@ -37,11 +38,12 @@ THREE.VRControls = function ( object, onError ) { if ( navigator.getVRDisplays ) { - navigator.getVRDisplays().then( gotVRDisplays ).catch ( function () { + navigator.getVRDisplays().then( gotVRDevices ); - console.warn( 'THREE.VRControls: Unable to get VR Displays' ); + } else if ( navigator.getVRDevices ) { - } ); + // Deprecated API. + navigator.getVRDevices().then( gotVRDevices ); } @@ -59,71 +61,60 @@ THREE.VRControls = function ( object, onError ) { // standing=true but the VRDisplay doesn't provide stageParameters. this.userHeight = 1.6; - this.getVRDisplay = function () { - - return vrDisplay; - - }; - - this.setVRDisplay = function ( value ) { - - vrDisplay = value; - - }; + this.update = function () { - this.getVRDisplays = function () { + if ( vrInput ) { - console.warn( 'THREE.VRControls: getVRDisplays() is being deprecated.' ); - return vrDisplays; + if ( vrInput.getPose ) { - }; + var pose = vrInput.getPose(); - this.getStandingMatrix = function () { + if ( pose.orientation !== null ) { - return standingMatrix; + object.quaternion.fromArray( pose.orientation ); - }; + } - this.update = function () { + if ( pose.position !== null ) { - if ( vrDisplay ) { + object.position.fromArray( pose.position ); - var pose; + } else { - if ( vrDisplay.getFrameData ) { + object.position.set( 0, 0, 0 ); - vrDisplay.getFrameData( frameData ); - pose = frameData.pose; + } - } else if ( vrDisplay.getPose ) { + } else { - pose = vrDisplay.getPose(); + // Deprecated API. + var state = vrInput.getState(); - } + if ( state.orientation !== null ) { - if ( pose.orientation !== null ) { + object.quaternion.copy( state.orientation ); - object.quaternion.fromArray( pose.orientation ); + } - } + if ( state.position !== null ) { - if ( pose.position !== null ) { + object.position.copy( state.position ); - object.position.fromArray( pose.position ); + } else { - } else { + object.position.set( 0, 0, 0 ); - object.position.set( 0, 0, 0 ); + } } if ( this.standing ) { - if ( vrDisplay.stageParameters ) { + if ( vrInput.stageParameters ) { object.updateMatrix(); - standingMatrix.fromArray( vrDisplay.stageParameters.sittingToStandingTransform ); + standingMatrix.fromArray(vrInput.stageParameters.sittingToStandingTransform); object.applyMatrix( standingMatrix ); } else { @@ -142,9 +133,23 @@ THREE.VRControls = function ( object, onError ) { this.resetPose = function () { - if ( vrDisplay ) { + if ( vrInput ) { + + if ( vrInput.resetPose !== undefined ) { + + vrInput.resetPose(); + + } else if ( vrInput.resetSensor !== undefined ) { + + // Deprecated API. + vrInput.resetSensor(); - vrDisplay.resetPose(); + } else if ( vrInput.zeroSensor !== undefined ) { + + // Really deprecated API. + vrInput.zeroSensor(); + + } } @@ -171,3 +176,5 @@ THREE.VRControls = function ( object, onError ) { }; }; + +export {VRControls} \ No newline at end of file diff --git a/js/external-libraries/vr/VRControlsOLD.js b/js/external-libraries/vr/VRControlsOLD.js new file mode 100644 index 0000000..f3b26e9 --- /dev/null +++ b/js/external-libraries/vr/VRControlsOLD.js @@ -0,0 +1,173 @@ +/** + * @author dmarcos / https://github.com/dmarcos + * @author mrdoob / http://mrdoob.com + */ + +THREE.VRControls = function ( object, onError ) { + + var scope = this; + + var vrDisplay, vrDisplays; + + var standingMatrix = new THREE.Matrix4(); + + var frameData = null; + + if ( 'VRFrameData' in window ) { + + frameData = new VRFrameData(); + + } + + function gotVRDisplays( displays ) { + + vrDisplays = displays; + + if ( displays.length > 0 ) { + + vrDisplay = displays[ 0 ]; + + } else { + + if ( onError ) onError( 'VR input not available.' ); + + } + + } + + if ( navigator.getVRDisplays ) { + + navigator.getVRDisplays().then( gotVRDisplays ).catch ( function () { + + console.warn( 'THREE.VRControls: Unable to get VR Displays' ); + + } ); + + } + + // the Rift SDK returns the position in meters + // this scale factor allows the user to define how meters + // are converted to scene units. + + this.scale = 1; + + // If true will use "standing space" coordinate system where y=0 is the + // floor and x=0, z=0 is the center of the room. + this.standing = false; + + // Distance from the users eyes to the floor in meters. Used when + // standing=true but the VRDisplay doesn't provide stageParameters. + this.userHeight = 1.6; + + this.getVRDisplay = function () { + + return vrDisplay; + + }; + + this.setVRDisplay = function ( value ) { + + vrDisplay = value; + + }; + + this.getVRDisplays = function () { + + console.warn( 'THREE.VRControls: getVRDisplays() is being deprecated.' ); + return vrDisplays; + + }; + + this.getStandingMatrix = function () { + + return standingMatrix; + + }; + + this.update = function () { + + if ( vrDisplay ) { + + var pose; + + if ( vrDisplay.getFrameData ) { + + vrDisplay.getFrameData( frameData ); + pose = frameData.pose; + + } else if ( vrDisplay.getPose ) { + + pose = vrDisplay.getPose(); + + } + + if ( pose.orientation !== null ) { + + object.quaternion.fromArray( pose.orientation ); + + } + + if ( pose.position !== null ) { + + object.position.fromArray( pose.position ); + + } else { + + object.position.set( 0, 0, 0 ); + + } + + if ( this.standing ) { + + if ( vrDisplay.stageParameters ) { + + object.updateMatrix(); + + standingMatrix.fromArray( vrDisplay.stageParameters.sittingToStandingTransform ); + object.applyMatrix( standingMatrix ); + + } else { + + object.position.setY( object.position.y + this.userHeight ); + + } + + } + + object.position.multiplyScalar( scope.scale ); + + } + + }; + + this.resetPose = function () { + + if ( vrDisplay ) { + + vrDisplay.resetPose(); + + } + + }; + + this.resetSensor = function () { + + console.warn( 'THREE.VRControls: .resetSensor() is now .resetPose().' ); + this.resetPose(); + + }; + + this.zeroSensor = function () { + + console.warn( 'THREE.VRControls: .zeroSensor() is now .resetPose().' ); + this.resetPose(); + + }; + + this.dispose = function () { + + vrDisplay = null; + + }; + +}; \ No newline at end of file diff --git a/js/external-libraries/vr/VREffect.js b/js/external-libraries/vr/VREffect.js index 5911ea1..b5b8d0f 100644 --- a/js/external-libraries/vr/VREffect.js +++ b/js/external-libraries/vr/VREffect.js @@ -5,34 +5,42 @@ * WebVR Spec: http://mozvr.github.io/webvr-spec/webvr.html * * Firefox: http://mozvr.com/downloads/ - * Chromium: https://webvr.info/get-chrome + * Chromium: https://drive.google.com/folderview?id=0BzudLt22BqGRbW9WTHMtOWMzNjQ&usp=sharing#list * */ +import * as THREE from 'three' +import {exp} from "mathjs"; -THREE.VREffect = function( renderer, onError ) { +var VREffect = function ( renderer, onError ) { - var vrDisplay, vrDisplays; + var vrHMD; + var isDeprecatedAPI = false; var eyeTranslationL = new THREE.Vector3(); var eyeTranslationR = new THREE.Vector3(); var renderRectL, renderRectR; + var eyeFOVL, eyeFOVR; - var frameData = null; + function gotVRDevices( devices ) { - if ( 'VRFrameData' in window ) { + for ( var i = 0; i < devices.length; i ++ ) { - frameData = new window.VRFrameData(); + if ( 'VRDisplay' in window && devices[ i ] instanceof VRDisplay ) { - } + vrHMD = devices[ i ]; + isDeprecatedAPI = false; + break; // We keep the first we encounter - function gotVRDisplays( displays ) { + } else if ( 'HMDVRDevice' in window && devices[ i ] instanceof HMDVRDevice ) { - vrDisplays = displays; + vrHMD = devices[ i ]; + isDeprecatedAPI = true; + break; // We keep the first we encounter - if ( displays.length > 0 ) { + } - vrDisplay = displays[ 0 ]; + } - } else { + if ( vrHMD === undefined ) { if ( onError ) onError( 'HMD not available' ); @@ -42,185 +50,188 @@ THREE.VREffect = function( renderer, onError ) { if ( navigator.getVRDisplays ) { - navigator.getVRDisplays().then( gotVRDisplays ).catch( function() { + navigator.getVRDisplays().then( gotVRDevices ); - console.warn( 'THREE.VREffect: Unable to get VR Displays' ); + } else if ( navigator.getVRDevices ) { - } ); + // Deprecated API. + navigator.getVRDevices().then( gotVRDevices ); } // - this.isPresenting = false; this.scale = 1; - var scope = this; + var isPresenting = false; var rendererSize = renderer.getSize(); - var rendererUpdateStyle = false; var rendererPixelRatio = renderer.getPixelRatio(); - this.getVRDisplay = function() { - - return vrDisplay; - - }; + this.setSize = function ( width, height ) { - this.setVRDisplay = function( value ) { + rendererSize = { width: width, height: height }; - vrDisplay = value; + if ( isPresenting ) { - }; + var eyeParamsL = vrHMD.getEyeParameters( 'left' ); + renderer.setPixelRatio( 1 ); - this.getVRDisplays = function() { + if ( isDeprecatedAPI ) { - console.warn( 'THREE.VREffect: getVRDisplays() is being deprecated.' ); - return vrDisplays; + renderer.setSize( eyeParamsL.renderRect.width * 2, eyeParamsL.renderRect.height, false ); - }; - - this.setSize = function( width, height, updateStyle ) { + } else { - rendererSize = { width: width, height: height }; - rendererUpdateStyle = updateStyle; + renderer.setSize( eyeParamsL.renderWidth * 2, eyeParamsL.renderHeight, false ); - if ( scope.isPresenting ) { + } - var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); - renderer.setPixelRatio( 1 ); - renderer.setSize( eyeParamsL.renderWidth * 2, eyeParamsL.renderHeight, false ); } else { renderer.setPixelRatio( rendererPixelRatio ); - renderer.setSize( width, height, updateStyle ); + renderer.setSize( width, height ); } }; - // VR presentation + // fullscreen var canvas = renderer.domElement; - var defaultLeftBounds = [ 0.0, 0.0, 0.5, 1.0 ]; - var defaultRightBounds = [ 0.5, 0.0, 0.5, 1.0 ]; + var requestFullscreen; + var exitFullscreen; + var fullscreenElement; - function onVRDisplayPresentChange() { + function onFullscreenChange () { - var wasPresenting = scope.isPresenting; - scope.isPresenting = vrDisplay !== undefined && vrDisplay.isPresenting; + var wasPresenting = isPresenting; + isPresenting = vrHMD !== undefined && ( vrHMD.isPresenting || ( isDeprecatedAPI && document[ fullscreenElement ] instanceof window.HTMLElement ) ); - if ( scope.isPresenting ) { + if ( wasPresenting === isPresenting ) { - var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); - var eyeWidth = eyeParamsL.renderWidth; - var eyeHeight = eyeParamsL.renderHeight; + return; - if ( ! wasPresenting ) { + } - rendererPixelRatio = renderer.getPixelRatio(); - rendererSize = renderer.getSize(); + if ( isPresenting ) { - renderer.setPixelRatio( 1 ); - renderer.setSize( eyeWidth * 2, eyeHeight, false ); + rendererPixelRatio = renderer.getPixelRatio(); + rendererSize = renderer.getSize(); - } + var eyeParamsL = vrHMD.getEyeParameters( 'left' ); + var eyeWidth, eyeHeight; - } else if ( wasPresenting ) { + if ( isDeprecatedAPI ) { - renderer.setPixelRatio( rendererPixelRatio ); - renderer.setSize( rendererSize.width, rendererSize.height, rendererUpdateStyle ); + eyeWidth = eyeParamsL.renderRect.width; + eyeHeight = eyeParamsL.renderRect.height; - } + } else { - } + eyeWidth = eyeParamsL.renderWidth; + eyeHeight = eyeParamsL.renderHeight; - window.addEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); + } - this.setFullScreen = function( boolean ) { + renderer.setPixelRatio( 1 ); + renderer.setSize( eyeWidth * 2, eyeHeight, false ); - return new Promise( function( resolve, reject ) { + } else { - if ( vrDisplay === undefined ) { + renderer.setPixelRatio( rendererPixelRatio ); + renderer.setSize( rendererSize.width, rendererSize.height ); - reject( new Error( 'No VR hardware found.' ) ); - return; + } - } + } - if ( scope.isPresenting === boolean ) { + if ( canvas.requestFullscreen ) { - resolve(); - return; + requestFullscreen = 'requestFullscreen'; + fullscreenElement = 'fullscreenElement'; + exitFullscreen = 'exitFullscreen'; + document.addEventListener( 'fullscreenchange', onFullscreenChange, false ); - } + } else if ( canvas.mozRequestFullScreen ) { - if ( boolean ) { + requestFullscreen = 'mozRequestFullScreen'; + fullscreenElement = 'mozFullScreenElement'; + exitFullscreen = 'mozCancelFullScreen'; + document.addEventListener( 'mozfullscreenchange', onFullscreenChange, false ); - resolve( vrDisplay.requestPresent( [ { source: canvas } ] ) ); + } else { - } else { + requestFullscreen = 'webkitRequestFullscreen'; + fullscreenElement = 'webkitFullscreenElement'; + exitFullscreen = 'webkitExitFullscreen'; + document.addEventListener( 'webkitfullscreenchange', onFullscreenChange, false ); - resolve( vrDisplay.exitPresent() ); + } - } + window.addEventListener( 'vrdisplaypresentchange', onFullscreenChange, false ); - } ); + this.setFullScreen = function ( boolean ) { - }; + return new Promise( function ( resolve, reject ) { - this.requestPresent = function() { + if ( vrHMD === undefined ) { - return this.setFullScreen( true ); + reject( new Error( 'No VR hardware found.' ) ); + return; - }; + } + if ( isPresenting === boolean ) { - this.exitPresent = function() { + resolve(); + return; - return this.setFullScreen( false ); + } - }; + if ( ! isDeprecatedAPI ) { - this.requestAnimationFrame = function( f ) { + if ( boolean ) { - if ( vrDisplay !== undefined ) { + resolve( vrHMD.requestPresent( [ { source: canvas } ] ) ); - return vrDisplay.requestAnimationFrame( f ); + } else { - } else { + resolve( vrHMD.exitPresent() ); - return window.requestAnimationFrame( f ); + } - } + } else { - }; + if ( canvas[ requestFullscreen ] ) { - this.cancelAnimationFrame = function( h ) { + canvas[ boolean ? requestFullscreen : exitFullscreen ]( { vrDisplay: vrHMD } ); + resolve(); - if ( vrDisplay !== undefined ) { + } else { - vrDisplay.cancelAnimationFrame( h ); + console.error( 'No compatible requestFullscreen method found.' ); + reject( new Error( 'No compatible requestFullscreen method found.' ) ); - } else { + } - window.cancelAnimationFrame( h ); + } - } + } ); }; - this.submitFrame = function() { + this.requestPresent = function () { - if ( vrDisplay !== undefined && scope.isPresenting ) { + return this.setFullScreen( true ); - vrDisplay.submitFrame(); + }; - } + this.exitPresent = function () { - }; + return this.setFullScreen( false ); - this.autoSubmitFrame = true; + }; // render @@ -230,9 +241,9 @@ THREE.VREffect = function( renderer, onError ) { var cameraR = new THREE.PerspectiveCamera(); cameraR.layers.enable( 2 ); - this.render = function( scene, camera, renderTarget, forceClear ) { + this.render = function ( scene, camera ) { - if ( vrDisplay && scope.isPresenting ) { + if ( vrHMD && isPresenting ) { var autoUpdate = scene.autoUpdate; @@ -243,11 +254,24 @@ THREE.VREffect = function( renderer, onError ) { } - var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); - var eyeParamsR = vrDisplay.getEyeParameters( 'right' ); + var eyeParamsL = vrHMD.getEyeParameters( 'left' ); + var eyeParamsR = vrHMD.getEyeParameters( 'right' ); + + if ( ! isDeprecatedAPI ) { - eyeTranslationL.fromArray( eyeParamsL.offset ); - eyeTranslationR.fromArray( eyeParamsR.offset ); + eyeTranslationL.fromArray( eyeParamsL.offset ); + eyeTranslationR.fromArray( eyeParamsR.offset ); + eyeFOVL = eyeParamsL.fieldOfView; + eyeFOVR = eyeParamsR.fieldOfView; + + } else { + + eyeTranslationL.copy( eyeParamsL.eyeTranslation ); + eyeTranslationR.copy( eyeParamsR.eyeTranslation ); + eyeFOVL = eyeParamsL.recommendedFieldOfView; + eyeFOVR = eyeParamsR.recommendedFieldOfView; + + } if ( Array.isArray( scene ) ) { @@ -259,53 +283,17 @@ THREE.VREffect = function( renderer, onError ) { // When rendering we don't care what the recommended size is, only what the actual size // of the backbuffer is. var size = renderer.getSize(); - var layers = vrDisplay.getLayers(); - var leftBounds; - var rightBounds; - - if ( layers.length ) { - - var layer = layers[ 0 ]; - - leftBounds = layer.leftBounds !== null && layer.leftBounds.length === 4 ? layer.leftBounds : defaultLeftBounds; - rightBounds = layer.rightBounds !== null && layer.rightBounds.length === 4 ? layer.rightBounds : defaultRightBounds; - - } else { - - leftBounds = defaultLeftBounds; - rightBounds = defaultRightBounds; - - } + renderRectL = { x: 0, y: 0, width: size.width / 2, height: size.height }; + renderRectR = { x: size.width / 2, y: 0, width: size.width / 2, height: size.height }; - renderRectL = { - x: Math.round( size.width * leftBounds[ 0 ] ), - y: Math.round( size.height * leftBounds[ 1 ] ), - width: Math.round( size.width * leftBounds[ 2 ] ), - height: Math.round( size.height * leftBounds[ 3 ] ) - }; - renderRectR = { - x: Math.round( size.width * rightBounds[ 0 ] ), - y: Math.round( size.height * rightBounds[ 1 ] ), - width: Math.round( size.width * rightBounds[ 2 ] ), - height: Math.round( size.height * rightBounds[ 3 ] ) - }; - - if ( renderTarget ) { - - renderer.setRenderTarget( renderTarget ); - renderTarget.scissorTest = true; - - } else { - - renderer.setRenderTarget( null ); - renderer.setScissorTest( true ); - - } - - if ( renderer.autoClear || forceClear ) renderer.clear(); + renderer.setScissorTest( true ); + renderer.clear(); if ( camera.parent === null ) camera.updateMatrixWorld(); + cameraL.projectionMatrix = fovToProjection( eyeFOVL, true, camera.near, camera.far ); + cameraR.projectionMatrix = fovToProjection( eyeFOVR, true, camera.near, camera.far ); + camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale ); camera.matrixWorld.decompose( cameraR.position, cameraR.quaternion, cameraR.scale ); @@ -313,64 +301,18 @@ THREE.VREffect = function( renderer, onError ) { cameraL.translateOnAxis( eyeTranslationL, scale ); cameraR.translateOnAxis( eyeTranslationR, scale ); - if ( vrDisplay.getFrameData ) { - - vrDisplay.depthNear = camera.near; - vrDisplay.depthFar = camera.far; - - vrDisplay.getFrameData( frameData ); - - cameraL.projectionMatrix.elements = frameData.leftProjectionMatrix; - cameraR.projectionMatrix.elements = frameData.rightProjectionMatrix; - - } else { - - cameraL.projectionMatrix = fovToProjection( eyeParamsL.fieldOfView, true, camera.near, camera.far ); - cameraR.projectionMatrix = fovToProjection( eyeParamsR.fieldOfView, true, camera.near, camera.far ); - - } // render left eye - if ( renderTarget ) { - - renderTarget.viewport.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); - renderTarget.scissor.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); - - } else { - - renderer.setViewport( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); - renderer.setScissor( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); - - } - renderer.render( scene, cameraL, renderTarget, forceClear ); + renderer.setViewport( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); + renderer.setScissor( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); + renderer.render( scene, cameraL ); // render right eye - if ( renderTarget ) { - - renderTarget.viewport.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); - renderTarget.scissor.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); - - } else { + renderer.setViewport( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); + renderer.setScissor( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); + renderer.render( scene, cameraR ); - renderer.setViewport( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); - renderer.setScissor( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); - - } - renderer.render( scene, cameraR, renderTarget, forceClear ); - - if ( renderTarget ) { - - renderTarget.viewport.set( 0, 0, size.width, size.height ); - renderTarget.scissor.set( 0, 0, size.width, size.height ); - renderTarget.scissorTest = false; - renderer.setRenderTarget( null ); - - } else { - - renderer.setViewport( 0, 0, size.width, size.height ); - renderer.setScissorTest( false ); - - } + renderer.setScissorTest( false ); if ( autoUpdate ) { @@ -378,9 +320,9 @@ THREE.VREffect = function( renderer, onError ) { } - if ( scope.autoSubmitFrame ) { + if ( ! isDeprecatedAPI ) { - scope.submitFrame(); + vrHMD.submitFrame(); } @@ -390,13 +332,7 @@ THREE.VREffect = function( renderer, onError ) { // Regular render mode if not HMD - renderer.render( scene, camera, renderTarget, forceClear ); - - }; - - this.dispose = function() { - - window.removeEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); + renderer.render( scene, camera ); }; @@ -475,3 +411,5 @@ THREE.VREffect = function( renderer, onError ) { } }; + +export {VREffect} \ No newline at end of file diff --git a/js/external-libraries/vr/VREffectOLD.js b/js/external-libraries/vr/VREffectOLD.js new file mode 100644 index 0000000..ffd9908 --- /dev/null +++ b/js/external-libraries/vr/VREffectOLD.js @@ -0,0 +1,477 @@ +/** + * @author dmarcos / https://github.com/dmarcos + * @author mrdoob / http://mrdoob.com + * + * WebVR Spec: http://mozvr.github.io/webvr-spec/webvr.html + * + * Firefox: http://mozvr.com/downloads/ + * Chromium: https://webvr.info/get-chrome + * + */ + +THREE.VREffect = function( renderer, onError ) { + + var vrDisplay, vrDisplays; + var eyeTranslationL = new THREE.Vector3(); + var eyeTranslationR = new THREE.Vector3(); + var renderRectL, renderRectR; + + var frameData = null; + + if ( 'VRFrameData' in window ) { + + frameData = new window.VRFrameData(); + + } + + function gotVRDisplays( displays ) { + + vrDisplays = displays; + + if ( displays.length > 0 ) { + + vrDisplay = displays[ 0 ]; + + } else { + + if ( onError ) onError( 'HMD not available' ); + + } + + } + + if ( navigator.getVRDisplays ) { + + navigator.getVRDisplays().then( gotVRDisplays ).catch( function() { + + console.warn( 'THREE.VREffect: Unable to get VR Displays' ); + + } ); + + } + + // + + this.isPresenting = false; + this.scale = 1; + + var scope = this; + + var rendererSize = renderer.getSize(); + var rendererUpdateStyle = false; + var rendererPixelRatio = renderer.getPixelRatio(); + + this.getVRDisplay = function() { + + return vrDisplay; + + }; + + this.setVRDisplay = function( value ) { + + vrDisplay = value; + + }; + + this.getVRDisplays = function() { + + console.warn( 'THREE.VREffect: getVRDisplays() is being deprecated.' ); + return vrDisplays; + + }; + + this.setSize = function( width, height, updateStyle ) { + + rendererSize = { width: width, height: height }; + rendererUpdateStyle = updateStyle; + + if ( scope.isPresenting ) { + + var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); + renderer.setPixelRatio( 1 ); + renderer.setSize( eyeParamsL.renderWidth * 2, eyeParamsL.renderHeight, false ); + + } else { + + renderer.setPixelRatio( rendererPixelRatio ); + renderer.setSize( width, height, updateStyle ); + + } + + }; + + // VR presentation + + var canvas = renderer.domElement; + var defaultLeftBounds = [ 0.0, 0.0, 0.5, 1.0 ]; + var defaultRightBounds = [ 0.5, 0.0, 0.5, 1.0 ]; + + function onVRDisplayPresentChange() { + + var wasPresenting = scope.isPresenting; + scope.isPresenting = vrDisplay !== undefined && vrDisplay.isPresenting; + + if ( scope.isPresenting ) { + + var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); + var eyeWidth = eyeParamsL.renderWidth; + var eyeHeight = eyeParamsL.renderHeight; + + if ( ! wasPresenting ) { + + rendererPixelRatio = renderer.getPixelRatio(); + rendererSize = renderer.getSize(); + + renderer.setPixelRatio( 1 ); + renderer.setSize( eyeWidth * 2, eyeHeight, false ); + + } + + } else if ( wasPresenting ) { + + renderer.setPixelRatio( rendererPixelRatio ); + renderer.setSize( rendererSize.width, rendererSize.height, rendererUpdateStyle ); + + } + + } + + window.addEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); + + this.setFullScreen = function( boolean ) { + + return new Promise( function( resolve, reject ) { + + if ( vrDisplay === undefined ) { + + reject( new Error( 'No VR hardware found.' ) ); + return; + + } + + if ( scope.isPresenting === boolean ) { + + resolve(); + return; + + } + + if ( boolean ) { + + resolve( vrDisplay.requestPresent( [ { source: canvas } ] ) ); + + } else { + + resolve( vrDisplay.exitPresent() ); + + } + + } ); + + }; + + this.requestPresent = function() { + + return this.setFullScreen( true ); + + }; + + this.exitPresent = function() { + + return this.setFullScreen( false ); + + }; + + this.requestAnimationFrame = function( f ) { + + if ( vrDisplay !== undefined ) { + + return vrDisplay.requestAnimationFrame( f ); + + } else { + + return window.requestAnimationFrame( f ); + + } + + }; + + this.cancelAnimationFrame = function( h ) { + + if ( vrDisplay !== undefined ) { + + vrDisplay.cancelAnimationFrame( h ); + + } else { + + window.cancelAnimationFrame( h ); + + } + + }; + + this.submitFrame = function() { + + if ( vrDisplay !== undefined && scope.isPresenting ) { + + vrDisplay.submitFrame(); + + } + + }; + + this.autoSubmitFrame = true; + + // render + + var cameraL = new THREE.PerspectiveCamera(); + cameraL.layers.enable( 1 ); + + var cameraR = new THREE.PerspectiveCamera(); + cameraR.layers.enable( 2 ); + + this.render = function( scene, camera, renderTarget, forceClear ) { + + if ( vrDisplay && scope.isPresenting ) { + + var autoUpdate = scene.autoUpdate; + + if ( autoUpdate ) { + + scene.updateMatrixWorld(); + scene.autoUpdate = false; + + } + + var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); + var eyeParamsR = vrDisplay.getEyeParameters( 'right' ); + + eyeTranslationL.fromArray( eyeParamsL.offset ); + eyeTranslationR.fromArray( eyeParamsR.offset ); + + if ( Array.isArray( scene ) ) { + + console.warn( 'THREE.VREffect.render() no longer supports arrays. Use object.layers instead.' ); + scene = scene[ 0 ]; + + } + + // When rendering we don't care what the recommended size is, only what the actual size + // of the backbuffer is. + var size = renderer.getSize(); + var layers = vrDisplay.getLayers(); + var leftBounds; + var rightBounds; + + if ( layers.length ) { + + var layer = layers[ 0 ]; + + leftBounds = layer.leftBounds !== null && layer.leftBounds.length === 4 ? layer.leftBounds : defaultLeftBounds; + rightBounds = layer.rightBounds !== null && layer.rightBounds.length === 4 ? layer.rightBounds : defaultRightBounds; + + } else { + + leftBounds = defaultLeftBounds; + rightBounds = defaultRightBounds; + + } + + renderRectL = { + x: Math.round( size.width * leftBounds[ 0 ] ), + y: Math.round( size.height * leftBounds[ 1 ] ), + width: Math.round( size.width * leftBounds[ 2 ] ), + height: Math.round( size.height * leftBounds[ 3 ] ) + }; + renderRectR = { + x: Math.round( size.width * rightBounds[ 0 ] ), + y: Math.round( size.height * rightBounds[ 1 ] ), + width: Math.round( size.width * rightBounds[ 2 ] ), + height: Math.round( size.height * rightBounds[ 3 ] ) + }; + + if ( renderTarget ) { + + renderer.setRenderTarget( renderTarget ); + renderTarget.scissorTest = true; + + } else { + + renderer.setRenderTarget( null ); + renderer.setScissorTest( true ); + + } + + if ( renderer.autoClear || forceClear ) renderer.clear(); + + if ( camera.parent === null ) camera.updateMatrixWorld(); + + camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale ); + camera.matrixWorld.decompose( cameraR.position, cameraR.quaternion, cameraR.scale ); + + var scale = this.scale; + cameraL.translateOnAxis( eyeTranslationL, scale ); + cameraR.translateOnAxis( eyeTranslationR, scale ); + + if ( vrDisplay.getFrameData ) { + + vrDisplay.depthNear = camera.near; + vrDisplay.depthFar = camera.far; + + vrDisplay.getFrameData( frameData ); + + cameraL.projectionMatrix.elements = frameData.leftProjectionMatrix; + cameraR.projectionMatrix.elements = frameData.rightProjectionMatrix; + + } else { + + cameraL.projectionMatrix = fovToProjection( eyeParamsL.fieldOfView, true, camera.near, camera.far ); + cameraR.projectionMatrix = fovToProjection( eyeParamsR.fieldOfView, true, camera.near, camera.far ); + + } + + // render left eye + if ( renderTarget ) { + + renderTarget.viewport.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); + renderTarget.scissor.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); + + } else { + + renderer.setViewport( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); + renderer.setScissor( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); + + } + renderer.render( scene, cameraL, renderTarget, forceClear ); + + // render right eye + if ( renderTarget ) { + + renderTarget.viewport.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); + renderTarget.scissor.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); + + } else { + + renderer.setViewport( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); + renderer.setScissor( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); + + } + renderer.render( scene, cameraR, renderTarget, forceClear ); + + if ( renderTarget ) { + + renderTarget.viewport.set( 0, 0, size.width, size.height ); + renderTarget.scissor.set( 0, 0, size.width, size.height ); + renderTarget.scissorTest = false; + renderer.setRenderTarget( null ); + + } else { + + renderer.setViewport( 0, 0, size.width, size.height ); + renderer.setScissorTest( false ); + + } + + if ( autoUpdate ) { + + scene.autoUpdate = true; + + } + + if ( scope.autoSubmitFrame ) { + + scope.submitFrame(); + + } + + return; + + } + + // Regular render mode if not HMD + + renderer.render( scene, camera, renderTarget, forceClear ); + + }; + + this.dispose = function() { + + window.removeEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); + + }; + + // + + function fovToNDCScaleOffset( fov ) { + + var pxscale = 2.0 / ( fov.leftTan + fov.rightTan ); + var pxoffset = ( fov.leftTan - fov.rightTan ) * pxscale * 0.5; + var pyscale = 2.0 / ( fov.upTan + fov.downTan ); + var pyoffset = ( fov.upTan - fov.downTan ) * pyscale * 0.5; + return { scale: [ pxscale, pyscale ], offset: [ pxoffset, pyoffset ] }; + + } + + function fovPortToProjection( fov, rightHanded, zNear, zFar ) { + + rightHanded = rightHanded === undefined ? true : rightHanded; + zNear = zNear === undefined ? 0.01 : zNear; + zFar = zFar === undefined ? 10000.0 : zFar; + + var handednessScale = rightHanded ? - 1.0 : 1.0; + + // start with an identity matrix + var mobj = new THREE.Matrix4(); + var m = mobj.elements; + + // and with scale/offset info for normalized device coords + var scaleAndOffset = fovToNDCScaleOffset( fov ); + + // X result, map clip edges to [-w,+w] + m[ 0 * 4 + 0 ] = scaleAndOffset.scale[ 0 ]; + m[ 0 * 4 + 1 ] = 0.0; + m[ 0 * 4 + 2 ] = scaleAndOffset.offset[ 0 ] * handednessScale; + m[ 0 * 4 + 3 ] = 0.0; + + // Y result, map clip edges to [-w,+w] + // Y offset is negated because this proj matrix transforms from world coords with Y=up, + // but the NDC scaling has Y=down (thanks D3D?) + m[ 1 * 4 + 0 ] = 0.0; + m[ 1 * 4 + 1 ] = scaleAndOffset.scale[ 1 ]; + m[ 1 * 4 + 2 ] = - scaleAndOffset.offset[ 1 ] * handednessScale; + m[ 1 * 4 + 3 ] = 0.0; + + // Z result (up to the app) + m[ 2 * 4 + 0 ] = 0.0; + m[ 2 * 4 + 1 ] = 0.0; + m[ 2 * 4 + 2 ] = zFar / ( zNear - zFar ) * - handednessScale; + m[ 2 * 4 + 3 ] = ( zFar * zNear ) / ( zNear - zFar ); + + // W result (= Z in) + m[ 3 * 4 + 0 ] = 0.0; + m[ 3 * 4 + 1 ] = 0.0; + m[ 3 * 4 + 2 ] = handednessScale; + m[ 3 * 4 + 3 ] = 0.0; + + mobj.transpose(); + + return mobj; + + } + + function fovToProjection( fov, rightHanded, zNear, zFar ) { + + var DEG2RAD = Math.PI / 180.0; + + var fovPort = { + upTan: Math.tan( fov.upDegrees * DEG2RAD ), + downTan: Math.tan( fov.downDegrees * DEG2RAD ), + leftTan: Math.tan( fov.leftDegrees * DEG2RAD ), + rightTan: Math.tan( fov.rightDegrees * DEG2RAD ) + }; + + return fovPortToProjection( fovPort, rightHanded, zNear, zFar ); + + } + +}; \ No newline at end of file diff --git a/js/external-libraries/vr/XRControllerModelFactory.js b/js/external-libraries/vr/XRControllerModelFactory.js new file mode 100644 index 0000000..ee1b80f --- /dev/null +++ b/js/external-libraries/vr/XRControllerModelFactory.js @@ -0,0 +1,299 @@ +import { + Mesh, + MeshBasicMaterial, + Object3D, + SphereGeometry, +} from 'three'; + +import { GLTFLoader } from '../loaders/GLTFLoader.js'; + +import { + Constants as MotionControllerConstants, + fetchProfile, + MotionController +} from '../libs/motion-controllers.module.js'; + +const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles'; +const DEFAULT_PROFILE = 'generic-trigger'; + +class XRControllerModel extends Object3D { + + constructor() { + + super(); + + this.motionController = null; + this.envMap = null; + + } + + setEnvironmentMap( envMap ) { + + if ( this.envMap == envMap ) { + + return this; + + } + + this.envMap = envMap; + this.traverse( ( child ) => { + + if ( child.isMesh ) { + + child.material.envMap = this.envMap; + child.material.needsUpdate = true; + + } + + } ); + + return this; + + } + + /** + * Polls data from the XRInputSource and updates the model's components to match + * the real world data + */ + updateMatrixWorld( force ) { + + super.updateMatrixWorld( force ); + + if ( ! this.motionController ) return; + + // Cause the MotionController to poll the Gamepad for data + this.motionController.updateFromGamepad(); + + // Update the 3D model to reflect the button, thumbstick, and touchpad state + Object.values( this.motionController.components ).forEach( ( component ) => { + + // Update node data based on the visual responses' current states + Object.values( component.visualResponses ).forEach( ( visualResponse ) => { + + const { valueNode, minNode, maxNode, value, valueNodeProperty } = visualResponse; + + // Skip if the visual response node is not found. No error is needed, + // because it will have been reported at load time. + if ( ! valueNode ) return; + + // Calculate the new properties based on the weight supplied + if ( valueNodeProperty === MotionControllerConstants.VisualResponseProperty.VISIBILITY ) { + + valueNode.visible = value; + + } else if ( valueNodeProperty === MotionControllerConstants.VisualResponseProperty.TRANSFORM ) { + + valueNode.quaternion.slerpQuaternions( + minNode.quaternion, + maxNode.quaternion, + value + ); + + valueNode.position.lerpVectors( + minNode.position, + maxNode.position, + value + ); + + } + + } ); + + } ); + + } + +} + +/** + * Walks the model's tree to find the nodes needed to animate the components and + * saves them to the motionContoller components for use in the frame loop. When + * touchpads are found, attaches a touch dot to them. + */ +function findNodes( motionController, scene ) { + + // Loop through the components and find the nodes needed for each components' visual responses + Object.values( motionController.components ).forEach( ( component ) => { + + const { type, touchPointNodeName, visualResponses } = component; + + if ( type === MotionControllerConstants.ComponentType.TOUCHPAD ) { + + component.touchPointNode = scene.getObjectByName( touchPointNodeName ); + if ( component.touchPointNode ) { + + // Attach a touch dot to the touchpad. + const sphereGeometry = new SphereGeometry( 0.001 ); + const material = new MeshBasicMaterial( { color: 0x0000FF } ); + const sphere = new Mesh( sphereGeometry, material ); + component.touchPointNode.add( sphere ); + + } else { + + console.warn( `Could not find touch dot, ${component.touchPointNodeName}, in touchpad component ${component.id}` ); + + } + + } + + // Loop through all the visual responses to be applied to this component + Object.values( visualResponses ).forEach( ( visualResponse ) => { + + const { valueNodeName, minNodeName, maxNodeName, valueNodeProperty } = visualResponse; + + // If animating a transform, find the two nodes to be interpolated between. + if ( valueNodeProperty === MotionControllerConstants.VisualResponseProperty.TRANSFORM ) { + + visualResponse.minNode = scene.getObjectByName( minNodeName ); + visualResponse.maxNode = scene.getObjectByName( maxNodeName ); + + // If the extents cannot be found, skip this animation + if ( ! visualResponse.minNode ) { + + console.warn( `Could not find ${minNodeName} in the model` ); + return; + + } + + if ( ! visualResponse.maxNode ) { + + console.warn( `Could not find ${maxNodeName} in the model` ); + return; + + } + + } + + // If the target node cannot be found, skip this animation + visualResponse.valueNode = scene.getObjectByName( valueNodeName ); + if ( ! visualResponse.valueNode ) { + + console.warn( `Could not find ${valueNodeName} in the model` ); + + } + + } ); + + } ); + +} + +function addAssetSceneToControllerModel( controllerModel, scene ) { + + // Find the nodes needed for animation and cache them on the motionController. + findNodes( controllerModel.motionController, scene ); + + // Apply any environment map that the mesh already has set. + if ( controllerModel.envMap ) { + + scene.traverse( ( child ) => { + + if ( child.isMesh ) { + + child.material.envMap = controllerModel.envMap; + child.material.needsUpdate = true; + + } + + } ); + + } + + // Add the glTF scene to the controllerModel. + controllerModel.add( scene ); + +} + +class XRControllerModelFactory { + + constructor( gltfLoader = null ) { + + this.gltfLoader = gltfLoader; + this.path = DEFAULT_PROFILES_PATH; + this._assetCache = {}; + + // If a GLTFLoader wasn't supplied to the constructor create a new one. + if ( ! this.gltfLoader ) { + + this.gltfLoader = new GLTFLoader(); + + } + + } + + createControllerModel( controller ) { + + const controllerModel = new XRControllerModel(); + let scene = null; + + controller.addEventListener( 'connected', ( event ) => { + + const xrInputSource = event.data; + + if ( xrInputSource.targetRayMode !== 'tracked-pointer' || ! xrInputSource.gamepad ) return; + + fetchProfile( xrInputSource, this.path, DEFAULT_PROFILE ).then( ( { profile, assetPath } ) => { + + controllerModel.motionController = new MotionController( + xrInputSource, + profile, + assetPath + ); + + const cachedAsset = this._assetCache[ controllerModel.motionController.assetUrl ]; + if ( cachedAsset ) { + + scene = cachedAsset.scene.clone(); + + addAssetSceneToControllerModel( controllerModel, scene ); + + } else { + + if ( ! this.gltfLoader ) { + + throw new Error( 'GLTFLoader not set.' ); + + } + + this.gltfLoader.setPath( '' ); + this.gltfLoader.load( controllerModel.motionController.assetUrl, ( asset ) => { + + this._assetCache[ controllerModel.motionController.assetUrl ] = asset; + + scene = asset.scene.clone(); + + addAssetSceneToControllerModel( controllerModel, scene ); + + }, + null, + () => { + + throw new Error( `Asset ${controllerModel.motionController.assetUrl} missing or malformed.` ); + + } ); + + } + + } ).catch( ( err ) => { + + console.warn( err ); + + } ); + + } ); + + controller.addEventListener( 'disconnected', () => { + + controllerModel.motionController = null; + controllerModel.remove( scene ); + scene = null; + + } ); + + return controllerModel; + + } + +} + +export { XRControllerModelFactory }; \ No newline at end of file diff --git a/js/external-libraries/vr/webxr-button.js b/js/external-libraries/vr/webxr-button.js new file mode 100644 index 0000000..4a654e6 --- /dev/null +++ b/js/external-libraries/vr/webxr-button.js @@ -0,0 +1,513 @@ +// Copyright 2016 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This is a stripped down and specialized version of WebVR-UI +// (https://github.com/googlevr/webvr-ui) that takes out most of the state +// management in favor of providing a simple way of requesting entry into WebXR +// for the needs of the sample pages. Functionality like beginning sessions +// is intentionally left out so that the sample pages can demonstrate them more +// clearly. + +// +// State consts +// + +// Not yet presenting, but ready to present +const READY_TO_PRESENT = 'ready'; + +// In presentation mode +const PRESENTING = 'presenting'; +const PRESENTING_FULLSCREEN = 'presenting-fullscreen'; + +// Checking device availability +const PREPARING = 'preparing'; + +// Errors +const ERROR_NO_PRESENTABLE_DISPLAYS = 'error-no-presentable-displays'; +const ERROR_BROWSER_NOT_SUPPORTED = 'error-browser-not-supported'; +const ERROR_REQUEST_TO_PRESENT_REJECTED = 'error-request-to-present-rejected'; +const ERROR_EXIT_PRESENT_REJECTED = 'error-exit-present-rejected'; +const ERROR_REQUEST_STATE_CHANGE_REJECTED = 'error-request-state-change-rejected'; +const ERROR_UNKOWN = 'error-unkown'; + +// +// DOM element +// + +const _LOGO_SCALE = 0.8; +let _WEBXR_UI_CSS_INJECTED = {}; + +/** + * Generate the innerHTML for the button + * + * @return {string} html of the button as string + * @param {string} cssPrefix + * @param {Number} height + * @private + */ +const generateInnerHTML = (cssPrefix, height)=> { + const logoHeight = height*_LOGO_SCALE; + const svgString = generateXRIconString(cssPrefix, logoHeight) + generateNoXRIconString(cssPrefix, logoHeight); + + return ``; +}; + +/** + * Inject the CSS string to the head of the document + * + * @param {string} cssText the css to inject + */ +const injectCSS = (cssText)=> { + // Create the css + const style = document.createElement('style'); + style.innerHTML = cssText; + + let head = document.getElementsByTagName('head')[0]; + head.insertBefore(style, head.firstChild); +}; + +/** + * Generate DOM element view for button + * + * @return {HTMLElement} + * @param {Object} options + */ +const createDefaultView = (options)=> { + const fontSize = options.height / 3; + if (options.injectCSS) { + // Check that css isnt already injected + if (!_WEBXR_UI_CSS_INJECTED[options.cssprefix]) { + injectCSS(generateCSS(options, fontSize)); + _WEBXR_UI_CSS_INJECTED[options.cssprefix] = true; + } + } + + const el = document.createElement('div'); + el.innerHTML = generateInnerHTML(options.cssprefix, fontSize); + return el.firstChild; +}; + + +const createXRIcon = (cssPrefix, height)=>{ + const el = document.createElement('div'); + el.innerHTML = generateXRIconString(cssPrefix, height); + return el.firstChild; +}; + +const createNoXRIcon = (cssPrefix, height)=>{ + const el = document.createElement('div'); + el.innerHTML = generateNoXRIconString(cssPrefix, height); + return el.firstChild; +}; + +const generateXRIconString = (cssPrefix, height)=> { + let aspect = 28 / 18; + return ` + + `; +}; + +const generateNoXRIconString = (cssPrefix, height)=>{ + let aspect = 28 / 18; + return ` + + + + `; +}; + +/** + * Generate the CSS string to inject + * + * @param {Object} options + * @param {Number} [fontSize=18] + * @return {string} + */ +const generateCSS = (options, fontSize=18)=> { + const height = options.height; + const borderWidth = 2; + const borderColor = options.background ? options.background : options.color; + const cssPrefix = options.cssprefix; + + let borderRadius; + if (options.corners == 'round') { + borderRadius = options.height / 2; + } else if (options.corners == 'square') { + borderRadius = 2; + } else { + borderRadius = options.corners; + } + + return (` + @font-face { + font-family: 'Karla'; + font-style: normal; + font-weight: 400; + src: local('Karla'), local('Karla-Regular'), + url(https://fonts.gstatic.com/s/karla/v5/31P4mP32i98D9CEnGyeX9Q.woff2) format('woff2'); + unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; + } + @font-face { + font-family: 'Karla'; + font-style: normal; + font-weight: 400; + src: local('Karla'), local('Karla-Regular'), + url(https://fonts.gstatic.com/s/karla/v5/Zi_e6rBgGqv33BWF8WTq8g.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, + U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; + } + button.${cssPrefix}-button { + font-family: 'Karla', sans-serif; + border: ${borderColor} ${borderWidth}px solid; + border-radius: ${borderRadius}px; + box-sizing: border-box; + background: ${options.background ? options.background : 'none'}; + height: ${height}px; + min-width: ${fontSize * 9.6}px; + display: inline-block; + position: relative; + cursor: pointer; + transition: border 0.5s; + } + button.${cssPrefix}-button:focus { + outline: none; + } + /* + * Logo + */ + .${cssPrefix}-logo { + width: ${height}px; + height: ${height}px; + position: absolute; + top:0px; + left:0px; + width: ${height - 4}px; + height: ${height - 4}px; + } + .${cssPrefix}-svg { + fill: ${options.color}; + margin-top: ${(height - fontSize * _LOGO_SCALE) / 2 - 2}px; + margin-left: ${height / 3 }px; + } + .${cssPrefix}-svg-error { + fill: ${options.color}; + display:none; + margin-top: ${(height - 28 / 18 * fontSize * _LOGO_SCALE) / 2 - 2}px; + margin-left: ${height / 3 }px; + } + /* + * Title + */ + .${cssPrefix}-title { + color: ${options.color}; + position: relative; + font-size: ${fontSize}px; + padding-left: ${height * 1.05}px; + padding-right: ${(borderRadius - 10 < 5) ? height / 3 : borderRadius - 10}px; + transition: color 0.5s; + } + /* + * disabled + */ + button.${cssPrefix}-button[disabled=true] { + opacity: ${options.disabledOpacity}; + } + button.${cssPrefix}-button[disabled=true] > .${cssPrefix}-logo > .${cssPrefix}-svg { + display:none; + } + button.${cssPrefix}-button[disabled=true] > .${cssPrefix}-logo > .${cssPrefix}-svg-error { + display:initial; + } + /* + * error + */ + button.${cssPrefix}-button[error=true] { + animation: errorShake 0.4s; + } + @keyframes errorShake { + 0% { transform: translate(1px, 0) } + 10% { transform: translate(-2px, 0) } + 20% { transform: translate(2px, 0) } + 30% { transform: translate(-2px, 0) } + 40% { transform: translate(2px, 0) } + 50% { transform: translate(-2px, 0) } + 60% { transform: translate(2px, 0) } + 70% { transform: translate(-2px, 0) } + 80% { transform: translate(2px, 0) } + 90% { transform: translate(-1px, 0) } + 100% { transform: translate(0px, 0) } + } + `); +}; + +// +// Button class +// + +export class WebXRButton { + /** + * Construct a new Enter XR Button + * @constructor + * @param {HTMLCanvasElement} sourceCanvas the canvas that you want to present with WebXR + * @param {Object} [options] optional parameters + * @param {HTMLElement} [options.domElement] provide your own domElement to bind to + * @param {Boolean} [options.injectCSS=true] set to false if you want to write your own styles + * @param {Function} [options.beforeEnter] should return a promise, opportunity to intercept request to enter + * @param {Function} [options.beforeExit] should return a promise, opportunity to intercept request to exit + * @param {Function} [options.onRequestStateChange] set to a function returning false to prevent default state changes + * @param {string} [options.textEnterXRTitle] set the text for Enter XR + * @param {string} [options.textXRNotFoundTitle] set the text for when a XR display is not found + * @param {string} [options.textExitXRTitle] set the text for exiting XR + * @param {string} [options.color] text and icon color + * @param {string} [options.background] set to false for no brackground or a color + * @param {string} [options.corners] set to 'round', 'square' or pixel value representing the corner radius + * @param {string} [options.disabledOpacity] set opacity of button dom when disabled + * @param {string} [options.cssprefix] set to change the css prefix from default 'webvr-ui' + */ + constructor(options) { + options = options || {}; + + options.color = options.color || 'rgb(80,168,252)'; + options.background = options.background || false; + options.disabledOpacity = options.disabledOpacity || 0.5; + options.height = options.height || 55; + options.corners = options.corners || 'square'; + options.cssprefix = options.cssprefix || 'webvr-ui'; + + // This reads VR as none of the samples are designed for other formats as of yet. + options.textEnterXRTitle = options.textEnterXRTitle || 'ENTER VR'; + options.textXRNotFoundTitle = options.textXRNotFoundTitle || 'VR NOT FOUND'; + options.textExitXRTitle = options.textExitXRTitle || 'EXIT VR'; + + options.onRequestSession = options.onRequestSession || (function() {}); + options.onEndSession = options.onEndSession || (function() {}); + + options.injectCSS = options.injectCSS !== false; + + this.options = options; + + this._enabled = false; + this.session = null; + + // Pass in your own domElement if you really dont want to use ours + this.domElement = options.domElement || createDefaultView(options); + this.__defaultDisplayStyle = this.domElement.style.display || 'initial'; + + // Bind button click events to __onClick + this.domElement.addEventListener('click', ()=> this.__onXRButtonClick()); + + this.__forceDisabled = false; + this.__setDisabledAttribute(true); + this.setTitle(this.options.textXRNotFoundTitle); + } + + /** + * Sets the enabled state of this button. + * @param {boolean} enabled + */ + set enabled(enabled) { + this._enabled = enabled; + this.__updateButtonState(); + return this; + } + + /** + * Gets the enabled state of this button. + * @return {boolean} + */ + get enabled() { + return this._enabled; + } + + /** + * Indicate that there's an active XRSession. Switches the button to "Exit XR" + * state if not null, or "Enter XR" state if null. + * @param {XRSession} session + * @return {EnterXRButton} + */ + setSession(session) { + this.session = session; + this.__updateButtonState(); + return this; + } + + /** + * Set the title of the button + * @param {string} text + * @return {EnterXRButton} + */ + setTitle(text) { + this.domElement.title = text; + ifChild(this.domElement, this.options.cssprefix, 'title', (title)=> { + if (!text) { + title.style.display = 'none'; + } else { + title.innerText = text; + title.style.display = 'initial'; + } + }); + + return this; + } + + /** + * Set the tooltip of the button + * @param {string} tooltip + * @return {EnterXRButton} + */ + setTooltip(tooltip) { + this.domElement.title = tooltip; + return this; + } + + /** + * Show the button + * @return {EnterXRButton} + */ + show() { + this.domElement.style.display = this.__defaultDisplayStyle; + return this; + } + + /** + * Hide the button + * @return {EnterXRButton} + */ + hide() { + this.domElement.style.display = 'none'; + return this; + } + + /** + * Enable the button + * @return {EnterXRButton} + */ + enable() { + this.__setDisabledAttribute(false); + this.__forceDisabled = false; + return this; + } + + /** + * Disable the button from being clicked + * @return {EnterXRButton} + */ + disable() { + this.__setDisabledAttribute(true); + this.__forceDisabled = true; + return this; + } + + /** + * clean up object for garbage collection + */ + remove() { + if (this.domElement.parentElement) { + this.domElement.parentElement.removeChild(this.domElement); + } + } + + /** + * Set the disabled attribute + * @param {boolean} disabled + * @private + */ + __setDisabledAttribute(disabled) { + if (disabled || this.__forceDisabled) { + this.domElement.setAttribute('disabled', 'true'); + } else { + this.domElement.removeAttribute('disabled'); + } + } + + /** + * Handling click event from button + * @private + */ + __onXRButtonClick() { + if (this.session) { + this.options.onEndSession(this.session); + } else if (this._enabled) { + let requestPromise = this.options.onRequestSession(); + if (requestPromise) { + requestPromise.catch((err) => { + // Reaching this point indicates that the session request has failed + // and we should communicate that to the user somehow. + let errorMsg = `XRSession creation failed: ${err.message}`; + this.setTooltip(errorMsg); + console.error(errorMsg); + + // Disable the button momentarily to indicate there was an issue. + this.__setDisabledAttribute(true); + this.domElement.setAttribute('error', 'true'); + setTimeout(() => { + this.__setDisabledAttribute(false); + this.domElement.setAttribute('error', 'false'); + }, 1000); + }); + } + } + } + + /** + * Updates the display of the button based on it's current state + * @private + */ + __updateButtonState() { + if (this.session) { + this.setTitle(this.options.textExitXRTitle); + this.setTooltip('Exit XR presentation'); + this.__setDisabledAttribute(false); + } else if (this._enabled) { + this.setTitle(this.options.textEnterXRTitle); + this.setTooltip('Enter XR'); + this.__setDisabledAttribute(false); + } else { + this.setTitle(this.options.textXRNotFoundTitle); + this.setTooltip('No XR headset found.'); + this.__setDisabledAttribute(true); + } + } +} + +/** + * Function checking if a specific css class exists as child of element. + * + * @param {HTMLElement} el element to find child in + * @param {string} cssPrefix css prefix of button + * @param {string} suffix class name + * @param {function} fn function to call if child is found + * @private + */ +const ifChild = (el, cssPrefix, suffix, fn)=> { + const c = el.querySelector('.' + cssPrefix + '-' + suffix); + c && fn(c); +}; \ No newline at end of file diff --git a/js/globals.js b/js/globals.js new file mode 100644 index 0000000..d5b393d --- /dev/null +++ b/js/globals.js @@ -0,0 +1,66 @@ +// var getQueryVariable = function (variable) { +// var query = window.location.search.substring(1); +// var vars = query.split("&"); +// for (var i = 0; i < vars.length; i++) { +// var pair = vars[i].split("="); +// if (pair[0] == variable) { +// return pair[1]; +// } +// } +// console.log('Query Variable ' + variable + ' not found'); +// return undefined; +// } +import {Atlas} from "./atlas"; + +var stringToBoolean = function (s) { + switch (s) { + case '1': + return true; + case '0': + return false; + } +}; + +var url_string = window.location.href; +var url = new URL(url_string); + +//I was still getting an undefined dataset and no index.txt alert so I'm gonna hardcode the values normally read from the URL for now. +var atlas = null; + +function setAtlas(value) { + atlas = value; +} + +function getAtlas() { + return atlas +} + +var folder = url.searchParams.get("dataset"); //"Demo6"; // +var dataFiles = {} + + +var labelLUT = url.searchParams.get("lut"); //"baltimore"; // +var isLoaded = parseInt(url.searchParams.get("load")); // 0 +var metric = stringToBoolean(url.searchParams.get("metric")); +if( metric == undefined){ + metric = false; +} +var mobile = stringToBoolean(url.searchParams.get("mobile")); + +if( mobile == undefined){ + mobile = false; +} + +function setDataFile(files) { + dataFiles=files +} + +function getDataFile() { + return dataFiles; +} + +console.log('This is ' + ((mobile)?'Mobile':'Desktop') + ' version'); + + + +export {labelLUT,atlas,folder,dataFiles,metric,mobile,isLoaded,setDataFile,getAtlas,setAtlas,getDataFile} \ No newline at end of file diff --git a/js/graphicsUtils.js b/js/graphicsUtils.js index c7c6516..efc4dfe 100644 --- a/js/graphicsUtils.js +++ b/js/graphicsUtils.js @@ -2,15 +2,36 @@ * Created by giorgioconte on 26/02/15. */ +import * as THREE from 'three' +// import * as math from 'mathjs' +import * as math from './external-libraries/math.min.js' +import {scaleColorGroup} from './utils/scale' + var shpereRadius = 3.0; // normal sphere radius var sphereResolution = 12; var dimensionFactor = 1; +var dimensionFactorLeftSphere = 1; +var dimensionFactorRightSphere = 1; +var dimensionFactorLeftBox = 1; +var dimensionFactorRightBox = 1; + +function getSphereResolution(){ + return sphereResolution; +} + +function setSphereResolution(value) { + sphereResolution = value +} var sphereNormal = new THREE.SphereGeometry( shpereRadius, sphereResolution, sphereResolution); var boxNormal = new THREE.BoxGeometry( 1.5*shpereRadius, 1.5*shpereRadius, 1.5*shpereRadius); +var leftSphereNormal = new THREE.SphereGeometry( shpereRadius, sphereResolution, sphereResolution); +var leftBoxNormal = new THREE.BoxGeometry( 1.5*shpereRadius, 1.5*shpereRadius, 1.5*shpereRadius); +var rightSphereNormal = new THREE.SphereGeometry( shpereRadius, sphereResolution, sphereResolution); +var rightBoxNormal = new THREE.BoxGeometry( 1.5*shpereRadius, 1.5*shpereRadius, 1.5*shpereRadius); // create normal edge geometry: sphere or cube -getNormalGeometry = function(hemisphere) { +var getNormalGeometry = function(hemisphere) { if(hemisphere == "left"){ return sphereNormal; } else if(hemisphere == "right"){ @@ -18,8 +39,25 @@ getNormalGeometry = function(hemisphere) { } }; +// create normal edge geometry: sphere or cube +var getNormalGeometry = function(hemisphere,side) { + if(hemisphere == "left"){ + if(side == "Left"){ + return leftSphereNormal; + } else { + return rightSphereNormal; + } + } else if(hemisphere == "right"){ + if(side == "Left"){ + return leftBoxNormal; + } else { + return rightBoxNormal; + } + } +}; + // scaling the glyphs -setDimensionFactor = function(value){ +var setDimensionFactor = function(value){ var val = 1/dimensionFactor*value; sphereNormal.scale(val, val, val); @@ -28,8 +66,49 @@ setDimensionFactor = function(value){ dimensionFactor = value; }; +// scaling the glyphs +var setDimensionFactorLeftSphere = function(value){ + + var val = 1/dimensionFactorLeftSphere*value; + leftSphereNormal.scale(val, val, val); + //boxNormal.scale(val, val, val); + + dimensionFactorLeftSphere = value; +}; + +// scaling the glyphs +var setDimensionFactorRightSphere = function(value){ + + var val = 1/dimensionFactorRightSphere*value; + rightSphereNormal.scale(val, val, val); + //boxNormal.scale(val, val, val); + + dimensionFactorRightSphere = value; +}; + + +// scaling the glyphs +var setDimensionFactorLeftBox = function(value){ + + var val = 1/dimensionFactorLeftBox*value; + //sphereNormal.scale(val, val, val); + leftBoxNormal.scale(val, val, val); + + dimensionFactorLeftBox = value; +}; + +// scaling the glyphs +var setDimensionFactorRightBox = function(value){ + + var val = 1/dimensionFactorRightBox*value; + //sphereNormal.scale(val, val, val); + rightBoxNormal.scale(val, val, val); + + dimensionFactorRightBox = value; +}; + // return the material for a node (vertex) according to its state: active or transparent -getNormalMaterial = function(model, group) { +var getNormalMaterial = function(model, group) { var material, opacity = 1.0; switch (model.getRegionState(group)){ case 'active': @@ -63,13 +142,13 @@ getNormalMaterial = function(model, group) { * @param v2 unit vector in the plane containing the circle * @returns {*} array of the coordinates of the points */ -sunflower = function(n, R, c, v1, v2) { +var sunflower = function(n, R, c, v1, v2) { var alpha = 2; - var b = Math.round(alpha*Math.sqrt(n)); // number of boundary points - var phi = (Math.sqrt(5)+1)/2; // golden ratio + var b = math.round(alpha*math.sqrt(n)); // number of boundary points + var phi = (math.sqrt(5)+1)/2; // golden ratio var k = math.range(1,n+1); - var theta = math.multiply(k, (2*Math.PI)/(phi*phi)); - var r = math.divide(math.sqrt(math.add(k,-0.5)), Math.sqrt(n-(b+1)/2)); + var theta = math.multiply(k, (2*math.pi)/(phi*phi)); + var r = math.divide(math.sqrt(math.add(k,-0.5)), math.sqrt(n-(b+1)/2)); var idx = math.larger(k, n-b); // r( k > n-b ) = 1; % put on the boundary r = math.add(math.dotMultiply(r, math.subtract(1,idx)),idx); @@ -79,4 +158,6 @@ sunflower = function(n, R, c, v1, v2) { math.add(math.add(math.multiply(tmp1,v1[1]*R), math.multiply(tmp2,v2[1]*R)), c[1]), math.add(math.add(math.multiply(tmp1,v1[2]*R), math.multiply(tmp2,v2[2]*R)), c[2])]; return math.transpose(points); -}; \ No newline at end of file +}; + +export {sphereResolution,getSphereResolution,setSphereResolution,sunflower, setDimensionFactorLeftSphere, setDimensionFactorRightSphere, setDimensionFactorLeftBox,setDimensionFactorRightBox, setDimensionFactor,getNormalGeometry,getNormalMaterial} diff --git a/js/main.js b/js/main.js index 0cdfe2e..6ee149c 100644 --- a/js/main.js +++ b/js/main.js @@ -2,30 +2,14 @@ * Created by giorgioconte on 31/01/15. */ -var stringToBoolean = function (s) { - switch (s){ - case '1': return true; - case '0': return false; - } -}; +import {scanFolder, loadLookUpTable, loadSubjectNetwork, loadSubjectTopology} from "./utils/parsingData"; +import {isLoaded, dataFiles} from "./globals"; +import {queue} from "./external-libraries/queue"; +import {modelLeft,modelRight} from './model'; +import {initSubjectMenu} from './GUI'; +import {initControls,initCanvas} from './drawing'; -var atlas = null; -var folder = getQueryVariable("dataset"); -var dataFiles = {}; - -var labelLUT = getQueryVariable("lut"); -var isLoaded = parseInt(getQueryVariable("load")); -var metric = stringToBoolean(getQueryVariable("metric")); -if( metric == undefined){ - metric = false; -} -var mobile = stringToBoolean(getQueryVariable("mobile")); -if( mobile == undefined){ - mobile = false; -} -console.log('This is ' + ((mobile)?'Mobile':'Desktop') + ' version'); - -init = function () { +var init = function () { console.log("Init ... "); @@ -40,10 +24,10 @@ init = function () { .defer(loadSubjectNetwork, dataFiles[idRight], modelRight) .awaitAll(function () { queue() - // PLACE depends on connection matrix + // PLACE depends on connection matrix .defer(loadSubjectTopology, dataFiles[idLeft], modelLeft) .defer(loadSubjectTopology, dataFiles[idRight], modelRight) - .awaitAll( function () { + .awaitAll(function () { console.log("Loading data done."); modelLeft.createGroups(); modelRight.createGroups(); @@ -55,43 +39,33 @@ init = function () { }; -function getQueryVariable(variable) { - var query = window.location.search.substring(1); - var vars = query.split("&"); - for (var i=0;i -1 && clusters[activeGroup].length > 1) { - groups[activeGroup] = clusters[activeGroup][level-1]; + groups[activeGroup] = clusters[activeGroup][level - 1]; clusteringGroupLevel = level; } }; @@ -216,15 +238,25 @@ function Model() { return threshold; }; + // store contralateral edge threshold and update GUI + this.setConThreshold = function (t) { + conthreshold = t; + }; + + // get edge threshold + this.getConThreshold = function () { + return conthreshold; + }; + // set connection matrix - this.setConnectionMatrix = function(d) { + this.setConnectionMatrix = function (d) { connectionMatrix = d.data; this.computeDistanceMatrix(); this.computeNodalStrength(); }; // prepare the dataset data - this.prepareDataset = function() { + this.prepareDataset = function () { dataset = []; for (var i = 0; i < labelKeys.length; i++) { var label = labelKeys[i]; @@ -240,7 +272,7 @@ function Model() { }; // get the dataset according to activeTopology - this.getDataset = function() { + this.getDataset = function () { for (var i = 0; i < dataset.length; i++) { dataset[i].position = centroids[activeTopology][i]; dataset[i].group = groups[activeGroup][i]; @@ -249,7 +281,7 @@ function Model() { }; // get connection matrix according to activeMatrix - this.getConnectionMatrix = function() { + this.getConnectionMatrix = function () { return connectionMatrix; }; @@ -264,26 +296,35 @@ function Model() { }; // return if a specific region is activated - this.isRegionActive = function(regionName) { + this.isRegionActive = function (regionName) { return regions[regionName].active; }; // toggle a specific region in order: active, transparent, inactive // set activation to false if inactive - this.toggleRegion = function(regionName) { - switch (regions[regionName].state) { - case 'active': - regions[regionName].state = 'transparent'; - regions[regionName].active = true; - break; - case 'transparent': - regions[regionName].state = 'inactive'; + this.toggleRegion = function (regionName, state) { + if (state) { + regions[regionName].state = state; + if (state === 'inactive') { regions[regionName].active = false; - break; - case 'inactive': - regions[regionName].state = 'active'; + } else { regions[regionName].active = true; - break; + } + } else { + switch (regions[regionName].state) { + case 'active': + regions[regionName].state = 'transparent'; + regions[regionName].active = true; + break; + case 'transparent': + regions[regionName].state = 'inactive'; + regions[regionName].active = false; + break; + case 'inactive': + regions[regionName].state = 'active'; + regions[regionName].active = true; + break; + } } }; @@ -292,11 +333,11 @@ function Model() { }; // get region state using its name - this.getRegionState = function(regionName) { + this.getRegionState = function (regionName) { return regions[regionName].state; }; - this.getRegionActivation = function(regionName) { + this.getRegionActivation = function (regionName) { return regions[regionName].active; }; @@ -309,15 +350,15 @@ function Model() { }; // set all regions active - this.setAllRegionsActivated = function() { + this.setAllRegionsActivated = function () { regions = {}; for (var i = 0; i < groups[activeGroup].length; i++) { var element = groups[activeGroup][i]; if (regions[element] === undefined) - regions[element] = { - active: true, - state: 'active' - } + regions[element] = { + active: true, + state: 'active' + } } }; @@ -327,7 +368,57 @@ function Model() { }; // get top n edges connected to a specific node - this.getTopConnectionsByNode = function(indexNode, n) { + this.getTopIpsiLateralConnectionsByNode = function (indexNode, n) { + var row = this.getConnectionMatrixRow(indexNode); + var tmprow = row.slice(); + var hemisphere = dataset[indexNode].hemisphere; + if (hemisphere) { + console.log("Hemi:", hemisphere); + for (var i = 0; i < row.length; i++){ + if(dataset[i].hemisphere !== hemisphere) { + tmprow[i] = 0; + } + } + } + console.log(row,tmprow); + //var sortedRow = this.getConnectionMatrixRow(indexNode).sort(function (a, b) { + var sortedRow = tmprow.sort(function (a, b) { + return b - a + }); //sort in a descending flavor + var indexes = new Array(n); + for (var i = 0; i < n; i++) { + indexes[i] = row.indexOf(sortedRow[i]); + } + return indexes; + }; + + // get top n edges connected to a specific node + this.getTopContraLateralConnectionsByNode = function (indexNode, n) { + var row = this.getConnectionMatrixRow(indexNode); + var tmprow = row.slice(); + var hemisphere = dataset[indexNode].hemisphere; + if (hemisphere) { + console.log("Hemi:", hemisphere); + for (var i = 0; i < row.length; i++){ + if(dataset[i].hemisphere === hemisphere) { + tmprow[i] = 0; + } + } + } + console.log(row,tmprow); + //var sortedRow = this.getConnectionMatrixRow(indexNode).sort(function (a, b) { + var sortedRow = tmprow.sort(function (a, b) { + return b - a + }); //sort in a descending flavor + var indexes = new Array(n); + for (var i = 0; i < n; i++) { + indexes[i] = row.indexOf(sortedRow[i]); + } + return indexes; + }; + + // get top n edges connected to a specific node + this.getTopConnectionsByNode = function (indexNode, n) { var row = this.getConnectionMatrixRow(indexNode); var sortedRow = this.getConnectionMatrixRow(indexNode).sort(function (a, b) { return b - a @@ -339,23 +430,28 @@ function Model() { return indexes; }; + this.getMaximumWeight = function () { return d3.max(connectionMatrix, function (d) { - return d3.max(d, function (d) { return d; }) + return d3.max(d, function (d) { + return d; + }) }); }; this.getMinimumWeight = function () { return d3.min(connectionMatrix, function (d) { - return d3.min(d, function (d) { return d; }) + return d3.min(d, function (d) { + return d; + }) }); }; - this.getNumberOfEdges = function() { + this.getNumberOfEdges = function () { return numberOfEdges; }; - this.setNumberOfEdges = function(n) { + this.setNumberOfEdges = function (n) { numberOfEdges = n; }; @@ -364,7 +460,7 @@ function Model() { return dataset[index]; }; - this.setMetricValues = function(data) { + this.setMetricValues = function (data) { metricValues = data.data; metricQuantileScale = d3.scale.quantile() @@ -383,7 +479,7 @@ function Model() { /* BCT Stuff*/ // compute nodal strength of a specific node given its row - this.getNodalStrength = function(idx) { + this.getNodalStrength = function (idx) { return nodesStrength[idx]; }; @@ -395,32 +491,32 @@ function Model() { }; // compute distance matrix = 1/(adjacency matrix) - this.computeDistanceMatrix = function() { + this.computeDistanceMatrix = function () { var nNodes = connectionMatrix.length; distanceMatrix = new Array(nNodes); graph = new Graph(); var idx = 0; // for every node, add the distance to all other nodes - for(var i = 0; i < nNodes; i++){ + for (var i = 0; i < nNodes; i++) { var vertexes = []; var row = new Array(nNodes); edgeIdx.push(new Array(nNodes)); edgeIdx[i].fill(-1); // indicates no connection - for(var j = 0; j < nNodes; j++){ - vertexes[j] = 1/connectionMatrix[i][j]; - row[j] = 1/connectionMatrix[i][j]; + for (var j = 0; j < nNodes; j++) { + vertexes[j] = 1 / connectionMatrix[i][j]; + row[j] = 1 / connectionMatrix[i][j]; if (j > i && Math.abs(connectionMatrix[i][j]) > 0) { edgeIdx[i][j] = idx; idx++; } } distanceMatrix[i] = row; - graph.addVertex(i,vertexes); + graph.addVertex(i, vertexes); } // mirror it - for(var i = 0; i < nNodes; i++) { - for(var j = i+1; j < nNodes; j++) { + for (var i = 0; i < nNodes; i++) { + for (var j = i + 1; j < nNodes; j++) { edgeIdx[j][i] = edgeIdx[i][j]; } } @@ -428,7 +524,7 @@ function Model() { }; // compute shortest path from a specific node to the rest of the nodes - this.computeShortestPathDistances = function(rootNode) { + this.computeShortestPathDistances = function (rootNode) { console.log("computing spt"); distanceArray = graph.shortestPath(String(rootNode)); maxDistance = d3.max(distanceArray); @@ -442,11 +538,11 @@ function Model() { return (graph) ? graph.getPreviousMap() : null; }; - this.getMaxNumberOfHops = function(){ + this.getMaxNumberOfHops = function () { return graph.getMaxNumberOfHops(); }; - this.setNumberOfHops = function(hops) { + this.setNumberOfHops = function (hops) { numberOfHops = hops; }; @@ -459,10 +555,10 @@ function Model() { // in case of other clustering techniques: Q-modularity, no hierarchy information is applied // clusters[topology][0] contains the clustering information. // clusters starts by 1 not 0. - this.computeNodesLocationForClusters = function(topology) { + this.computeNodesLocationForClusters = function (topology) { var platonic = new Platonics(); var isHierarchical = topology === "PLACE" || topology === "PACE"; - var level = isHierarchical ? clusteringLevel-1 : 0; + var level = isHierarchical ? clusteringLevel - 1 : 0; var cluster = clusters[topology][level]; var totalNNodes = cluster.length; var maxNumberOfClusters = d3.max(cluster) - d3.min(cluster) + 1; @@ -476,9 +572,9 @@ function Model() { platonic.createTetrahedron(); else if (maxNumberOfClusters < 7) platonic.createCube(); - else if (maxNumberOfClusters < 10) + else if (maxNumberOfClusters < 12) platonic.createDodecahedron(); - else if (maxNumberOfClusters < 20) + else if (maxNumberOfClusters <= 20) platonic.createIcosahedron(); else { console.error("Can not visualize clustering data."); @@ -486,24 +582,24 @@ function Model() { } // use one of the faces to compute primary variables var face = platonic.getFace(0); - var coneAxis = math.mean(face,0); + var coneAxis = math.mean(face, 0); coneAxis = math.divide(coneAxis, math.norm(coneAxis)); - var theta = Math.abs( Math.acos(math.dot(coneAxis, face[0]) )); - var coneAngle = theta*0.6; - var coneR = clusteringRadius*Math.sin(coneAngle/2); - var coneH = clusteringRadius*Math.cos(coneAngle/2); + var theta = Math.abs(Math.acos(math.dot(coneAxis, face[0]))); + var coneAngle = theta * 0.6; + var coneR = clusteringRadius * Math.sin(coneAngle / 2); + var coneH = clusteringRadius * Math.cos(coneAngle / 2); var v1 = [], v2 = [], center = []; - var centroids = new Array(totalNNodes+1); + var centroids = new Array(totalNNodes + 1); // assume clustering data starts at 1 for (var i = 0; i < nClusters; i++) { var clusterIdx = []; for (var s = 0; s < totalNNodes; s++) { - if (cluster[s] == (i+1)) clusterIdx.push(s); + if (cluster[s] == (i + 1)) clusterIdx.push(s); } var nNodes = clusterIdx.length; face = platonic.getFace(i); - coneAxis = math.mean(face,0); + coneAxis = math.mean(face, 0); coneAxis = math.divide(coneAxis, math.norm(coneAxis)); v1 = math.subtract(face[0], face[1]); v1 = math.divide(v1, math.norm(v1)); @@ -512,14 +608,14 @@ function Model() { var points = sunflower(nNodes, coneR, center, v1, v2); // normalize and store for (var k = 0; k < nNodes; k++) { - centroids[clusterIdx[k]+1] = math.multiply(clusteringRadius, math.divide(points[k], math.norm(points[k]))); + centroids[clusterIdx[k] + 1] = math.multiply(clusteringRadius, math.divide(points[k], math.norm(points[k]))); } } this.setCentroids(centroids, topology, 0); }; // clusters can be hierarchical such as PLACE and PACE or not - this.setClusters = function(data, loc, name) { + this.setClusters = function (data, loc, name) { var clusteringData = []; // data[0] is assumed to contain a string header for (var j = 1; j < data.length; j++) { @@ -533,11 +629,11 @@ function Model() { clusteringLevel = maxClusterHierarchicalLevel; console.log("Max clustering level to be used = " + maxClusterHierarchicalLevel); if (maxClusterHierarchicalLevel > 4) { - console.error("Hierarchical data requires " + maxClusterHierarchicalLevel + " levels."+ - "\n That is more than what can be visualized!!"); + console.error("Hierarchical data requires " + maxClusterHierarchicalLevel + " levels." + + "\n That is more than what can be visualized!!"); } temp = new Array(maxClusterHierarchicalLevel); // final levels - temp[maxClusterHierarchicalLevel-1] = clusteringData; + temp[maxClusterHierarchicalLevel - 1] = clusteringData; for (var i = maxClusterHierarchicalLevel - 2; i >= 0; i--) { temp[i] = math.ceil(math.divide(temp[i + 1], 2.0)); } @@ -547,7 +643,7 @@ function Model() { clusters[name] = temp; }; - this.setClusteringLevel = function(level) { + this.setClusteringLevel = function (level) { if (level == clusteringLevel) { return; } @@ -562,7 +658,7 @@ function Model() { this.computeNodesLocationForClusters(activeTopology); }; - this.setClusteringSphereRadius = function(r) { + this.setClusteringSphereRadius = function (r) { if (r == clusteringRadius) { return; } @@ -570,11 +666,11 @@ function Model() { this.computeNodesLocationForClusters(activeTopology); }; - this.getClusteringLevel = function() { + this.getClusteringLevel = function () { return clusteringLevel; }; - this.getMaxClusterHierarchicalLevel = function() { + this.getMaxClusterHierarchicalLevel = function () { return maxClusterHierarchicalLevel; }; @@ -589,27 +685,28 @@ function Model() { this.setTopology = function (data) { // the first line is assumed to contain the data indicator type var dataType; + if (data == null) { + console.log("data is null") + } for (var i = 0; i < data[0].length; i++) { dataType = data[0][i]; - if (dataType === "label") { - this.setLabelKeys(data, i); - } - else if (dataType ==="PLACE" || // structural - dataType ==="PACE" || // functional - dataType ==="Q" || - dataType ==="Q-Modularity" || - dataType.includes("Clustering") ) { - dataType = dataType.replace("Clustering", ""); - this.setClusters(data, i, dataType); - this.computeNodesLocationForClusters(dataType); - topologies.push(dataType); - clusteringTopologies.push(dataType); - } - else if (dataType === "") {} - else { // all other topologies - this.setCentroids(data, dataType, i); - topologies.push(dataType); - } + if (dataType === "" || dataType == null) { + } else if (dataType === "label") { + this.setLabelKeys(data, i); + } else if (dataType === "PLACE" || // structural + dataType === "PACE" || // functional + dataType === "Q" || + dataType === "Q-Modularity" || + dataType.includes("Clustering")) { + dataType = dataType.replace("Clustering", ""); + this.setClusters(data, i, dataType); + this.computeNodesLocationForClusters(dataType); + topologies.push(dataType); + clusteringTopologies.push(dataType); + } else { // all other topologies + this.setCentroids(data, dataType, i); + topologies.push(dataType); + } } activeTopology = topologies[0]; this.computeEdgesForTopology(activeTopology); @@ -626,7 +723,7 @@ function Model() { * 2) all edges of selected node neighbor * @param nodeIdx selected node index */ - this.performEBOnNode = function(nodeIdx) { + this.performEBOnNode = function (nodeIdx) { var edges_ = []; var edgeIndices = []; var nNodes = connectionMatrix.length; @@ -643,8 +740,12 @@ function Model() { } // selected node neighbors var neighbors = nodesDistances[activeTopology][nodeIdx] - .map(function(o, i) {return {idx: i, val: o}; }) // create map with value and index - .sort(function(a, b) {return a.val - b.val;}); // sort based on value + .map(function (o, i) { + return {idx: i, val: o}; + }) // create map with value and index + .sort(function (a, b) { + return a.val - b.val; + }); // sort based on value for (var i = 1; i < nNodes; i++) { // first one assumed to be self if (edges_.length >= 500) break; @@ -661,40 +762,46 @@ function Model() { } } } - fbundling.edges(edges_); - var results = fbundling(); - - for (i = 0; i 0) { var edge = []; edge.push(centroids[topology][i]); @@ -736,5 +849,6 @@ function Model() { } } -var modelLeft = new Model(); -var modelRight = new Model(); \ No newline at end of file +var modelLeft = new Model("Left"); +var modelRight = new Model("Right"); +export {modelRight, modelLeft} diff --git a/js/polyhedron.js b/js/polyhedron.js index a932211..6c3a689 100644 --- a/js/polyhedron.js +++ b/js/polyhedron.js @@ -8,6 +8,9 @@ * octahedron, dodecahedron or an icosahedron having 4, 6, 8, 12 and 20 faces respectively. * @constructor */ + +import * as math from 'mathjs' + function Platonics() { var vertices = []; @@ -166,11 +169,22 @@ function Platonics() { centerAndNormalize(); }; - centerAndNormalize = function() { - var vCentroid = math.mean(vertices,0); + var centerAndNormalize = function() { + try { + var vCentroid = math.mean(vertices,0); + } + catch (e) { + console.log("vertices error"); + console.log(vertices); + console.log("error: ",e); + } + + for (var i=0; i { // onSessionStarted); + xrButton.setSession(session); + // Set a flag on the session so we can differentiate it from the + // inline session. + session.isImmersive = true; + onSessionStarted(session); + }); + } + + // Called either when the user has explicitly ended the session (like in + // onEndSession()) or when the UA has ended the session for any reason. + // At this point the session object is no longer usable and should be + // discarded. + function onSessionEnded(event) { + xrButton.setSession(null); + + // In this simple case discard the WebGL context too, since we're not + // rendering anything else to the screen with it. + // renderer = null; + } + + + // Called when the user clicks the 'Exit XR' button. In response we end + // the session. + function onEndSession(session) { + session.end(); + } + + // Creates a WebGL context and initializes it with some common default state. + function createWebGLContext(glAttribs) { + glAttribs = glAttribs || {alpha: false}; + + let webglCanvas = document.createElement('canvas'); // + //document.getElementById('mycanvas' + name); // document.createElement('canvas'); + console.log("Canvas: " + webglCanvas); + let contextTypes = glAttribs.webgl2 ? ['webgl2'] : ['webgl', 'experimental-webgl']; + let context = null; + + for (let contextType of contextTypes) { + context = webglCanvas.getContext(contextType, glAttribs); + if (context) { + break; } - else - console.log("Disable VR for PV: " + name); - effect.exitPresent(); } - }; + + if (!context) { + let webglType = (glAttribs.webgl2 ? 'WebGL 2' : 'WebGL'); + console.error('This browser does not support ' + webglType + '.'); + return null; + } + + return context; + } // init Oculus Rift - this.initVR = function () { - vrControl = new THREE.VRControls(camera, function (message) { - console.log("VRControls: ", message); - }); - effect = new THREE.VREffect(renderer, function (message) { - console.log("VREffect ", message); + this.initXR = function () { + //init VR //todo: this is stub now + + console.log("Init XR for PV: " + name); + enableVR = true; + activateVR = false; + + xrButton = new WebXRButton({ + onRequestSession: onRequestSession, + onEndSession: onEndSession }); - effect.setSize(window.innerWidth/2., window.innerHeight); - - if (navigator.getVRDisplays) { - navigator.getVRDisplays() - .then(function (displays) { - if (displays.length > 0) { - console.log("VR Display found"); - effect.setVRDisplay(displays[0]); - vrControl.setVRDisplay(displays[0]); - } - }); - enableVR = true; - } else { - console.log("No VR Hardware found!"); - enableVR = false; + // document.querySelector('header').appendChild(xrButton.domElement); + document.getElementById('vrButton' + name).appendChild(xrButton.domElement); + + + // init VR + vrButton = document.getElementById('vrButton' + name); + console.log("vrButton: " + vrButton); + + //vrButton.addEventListener('click', function () { + //vrButton.style.display = 'none'; + //vrButton.innerHTML = 'Enter VR'; + // console.log("Click On VR Button: " + name); + //effect.requestPresent(); + //}, false); + + // Is WebXR available on this UA? + if (navigator.xr) { + // If the device allows creation of exclusive sessions set it as the + // target of the 'Enter XR' button. + navigator.xr.isSessionSupported('immersive-vr').then((supported) => { + xrButton.enabled = supported; + }); + + // Start up an inline session, which should always be supported on + // browsers that support WebXR regardless of the available hardware. + navigator.xr.requestSession('inline').then((session) => { + inlineSession = session; + onSessionStarted(session); + //updateFov(); //todo: make an FoV slider + }); } - if (mobile) { - initWebVRForMobile(); - initGearVRController(); - } - else - initOculusTouch(); - }; + } //this.initRXR - // init Oculus Touch controllers - // not supported in Firefox, only Google chromium - // check https://webvr.info/get-chrome/ - var initOculusTouch = function () { - if (!enableVR) - return; - controllerLeft = new THREE.ViveController( 0 ); - controllerRight = new THREE.ViveController( 1 ); - var loader = new THREE.OBJLoader(); - loader.setPath( 'js/external-libraries/vr/models/obj/vive-controller/' ); - loader.load( 'vr_controller_vive_1_5.obj', function ( object ) { + // Called when we've successfully acquired a XRSession. In response we + // will set up the necessary session state and kick off the frame loop. + function onSessionStarted(session) { + // THis line is left over from the immersive VR example: + // This informs the 'Enter XR' button that the session has started and + // that it should display 'Exit XR' instead. + //xrButton.setSession(session) // So, this is needed in "inline" mode.... not sure why. + // It actually breaks the "Enter VR" buttons - makes them start immersive mode on initXR - var loader = new THREE.TextureLoader(); - loader.setPath( 'js/external-libraries/vr/models/obj/vive-controller/' ); - var controller = object.children[ 0 ]; - controller.material.map = loader.load( 'onepointfive_texture.png' ); - controller.material.specularMap = loader.load( 'onepointfive_spec.png' ); + // Listen for the sessions 'end' event so we can respond if the user + // or UA ends the session for any reason. + session.addEventListener('end', onSessionEnded); - controllerLeft.add( object.clone() ); - controllerRight.add( object.clone() ); + // Create a WebGL context to render with, initialized to be compatible + // with the XRDisplay we're presenting to. + if (!gl) { + gl = createWebGLContext({ + xrCompatible: true + }); - controllerLeft.standingMatrix = vrControl.getStandingMatrix(); - controllerRight.standingMatrix = vrControl.getStandingMatrix(); + // In order for an inline session to be used we must attach the WebGL + // canvas to the document, which will serve as the output surface for + // the results of the inline session's rendering. + document.getElementById('canvas' + name).appendChild(gl.canvas); - scene.add(controllerLeft); - scene.add(controllerRight); - } ); + // The canvas is synced with the window size via CSS, but we still + // need to update the width and height attributes in order to keep + // the default framebuffer resolution in-sync. + function onResize() { + gl.canvas.width = gl.canvas.clientWidth * window.devicePixelRatio; + gl.canvas.height = gl.canvas.clientHeight * window.devicePixelRatio; + } - // controllerLeft.addEventListener('gripsup', function(e) { updateVRStatus('left'); }, true); - // controllerRight.addEventListener('gripsup', function(e) { updateVRStatus('right'); }, true); + window.addEventListener('resize', onResize); + onResize(); - oculusTouchExist = true; + // Installs the listeners necessary to allow users to look around with + // inline sessions using the mouse or touch. + addInlineViewListeners(gl.canvas); - console.log("Init Oculus Touch done"); - }; - var initGearVRController = function () { - if (!enableVR || !mobile) - return; + } //if (!gl) - // assume right handed user - controllerRight = new THREE.GearVRController(0); - //controllerRight.position.set( 25, - 50, 0 ); - - - var loader = new THREE.OBJLoader(); - loader.setPath( 'js/external-libraries/vr/models/obj/vive-controller/' ); - loader.load( 'vr_controller_vive_1_5.obj', function ( object ) { - var loader = new THREE.TextureLoader(); - loader.setPath( 'js/external-libraries/vr/models/obj/vive-controller/' ); - var controller = object.children[ 0 ]; - controller.material.map = loader.load( 'onepointfive_texture.png' ); - controller.material.specularMap = loader.load( 'onepointfive_spec.png' ); - controllerRight.add( object.clone() ); - controllerRight.standingMatrix = vrControl.getStandingMatrix(); - scene.add(controllerRight); - } ); - - gearVRControllerExist = true; - - console.log("Init Gear VR Controller done"); - }; - - var initWebVRForMobile = function () { - // Initialize the WebVR UI. - var uiOptions = { - color: 'black', - background: 'white', - corners: 'round', - height: 40, - disabledOpacity: 0.9 - }; - vrButton = new webvrui.EnterVRButton(renderer.domElement, uiOptions); - vrButton.on('exit', function () { updateVRStatus('disable'); }); - vrButton.on('hide', function() { - document.getElementById('vr'+name).style.display = 'none'; - }); - vrButton.on('show', function() { - document.getElementById('vr'+name).style.display = 'inherit'; - }); - document.getElementById('vrButton'+name).appendChild(vrButton.domElement); - document.getElementById('magicWindow'+name).addEventListener('click', function() { - vr = true; - activateVR = true; - activeVR = name.toLowerCase(); - console.log("Active VR = " + activeVR); - vrButton.requestEnterFullscreen(); - }); - }; - - // scan Gear VR controller - var scanGearVRController = function () { - var thumbPad = controllerRight.getButtonState('thumbpad'); - var trigger = controllerRight.getButtonState('trigger'); - var angleX = null, angleY = null; - var gamePadRight = controllerRight.getGamepad(); - if(gamePadRight && !trigger) { - angleX = gamePadRight.axes[0]; - angleY = gamePadRight.axes[1]; - if (thumbPad) { - brain.rotateX(0.2 * angleX); - brain.rotateZ(0.2 * angleY); - } else { - brain.position.z += 5 * angleX; - brain.position.x += 5 * angleY; - } - brain.matrixWorldNeedsUpdate = true; - } - var v3Origin = new THREE.Vector3(0,0,0); - var v3UnitUp = new THREE.Vector3(0,0,-100); - - // Find all nodes within 0.1 distance from left Touch Controller - var closestNodeIndexRight = 0, closestNodeDistanceRight = 99999.9; - for (var i = 0; i < brain.children.length; i++) { - var distToNodeIRight = controllerRight.position.distanceTo(brain.children[i].getWorldPosition()); - if ( (distToNodeIRight < closestNodeDistanceRight ) ) { - closestNodeDistanceRight = distToNodeIRight; - closestNodeIndexRight = i; - } - } - var isLeft = (activateVR == 'left'); - if(trigger) { - pointedNodeIdx = (closestNodeDistanceRight < 2.0) ? closestNodeIndexRight : -1; + // WebGL layers for inline sessions won't allocate their own framebuffer, + // which causes gl commands to naturally execute against the default + // framebuffer while still using the canvas dimensions to compute + // viewports and projection matrices. + let glLayer = new XRWebGLLayer(session, gl); - if (pointerRight) { - // Touch Controller pointer already on! scan for selection - if (thumbPad) { - updateNodeSelection(model, getPointedObject(controllerRight), isLeft); - } - } else { - pointerRight = drawPointer(v3Origin, v3UnitUp); - controllerRight.add(pointerRight); - } - updateNodeMoveOver(model, getPointedObject(controllerRight)); - } else { - if (pointerRight) { - controllerRight.remove(pointerRight); - } - pointerRight = null; - } - }; + session.updateRenderState({ + baseLayer: glLayer + }); - // scan the Oculus Touch for controls - var scanOculusTouch = function () { - var boostRotationSpeed = controllerLeft.getButtonState('grips') ? 0.1 : 0.02; - var boostMoveSpeed = controllerRight.getButtonState('grips') ? 5.0 : 1.0; - var angleX = null, angleY = null; - var gamePadLeft = controllerLeft.getGamepad(); - var gamePadRight = controllerRight.getGamepad(); - if(gamePadLeft) { - angleX = gamePadLeft.axes[0]; - angleY = gamePadLeft.axes[1]; - brain.rotateX(boostRotationSpeed * angleX); - brain.rotateZ(boostRotationSpeed * angleY); - brain.matrixWorldNeedsUpdate = true; - } - - if(gamePadRight) { - angleX = gamePadRight.axes[0]; - angleY = gamePadRight.axes[1]; - if(controllerRight.getButtonState('thumbpad')) { - brain.position.y += boostMoveSpeed * angleY; - } else { - brain.position.z += boostMoveSpeed * angleX; - brain.position.x += boostMoveSpeed * angleY; - } - brain.matrixWorldNeedsUpdate = true; - } + // Get a frame of reference, which is required for querying poses. In + // this case an 'local' frame of reference means that all poses will + // be relative to the location where the XRDevice was first detected. + let refSpaceType = session.isImmersive ? 'local' : 'viewer'; + session.requestReferenceSpace(refSpaceType).then((refSpace) => { + // Since we're dealing with multiple sessions now we need to track + // which XRReferenceSpace is associated with which XRSession. + if (session.isImmersive) { + xrImmersiveRefSpace = refSpace; + } else { + xrInlineRefSpace = refSpace; + } + session.requestAnimationFrame(onXRFrame); + }); - var v3Origin = new THREE.Vector3(0,0,0); - var v3UnitUp = new THREE.Vector3(0,0,-100.0); - // var v3UnitFwd = new THREE.Vector3(0,0,1); + } //onSessionStarted - // Find all nodes within 0.1 distance from left Touch Controller - var closestNodeIndexLeft = 0, closestNodeDistanceLeft = 99999.9; - var closestNodeIndexRight = 0, closestNodeDistanceRight = 99999.9; - for (var i = 0; i < brain.children.length; i++) { - var distToNodeILeft = controllerLeft.position.distanceTo(brain.children[i].getWorldPosition()); - if ( (distToNodeILeft < closestNodeDistanceLeft ) ) { - closestNodeDistanceLeft = distToNodeILeft; - closestNodeIndexLeft = i; + // Make the canvas listen for mouse and touch events so that we can + // adjust the viewer pose accordingly in inline sessions. + function addInlineViewListeners(canvas) { + canvas.addEventListener('mousemove', (event) => { + // Only rotate when the right button is pressed + if (event.buttons && 2) { + rotateView(event.movementX, event.movementY); } + }); - var distToNodeIRight = controllerRight.position.distanceTo(brain.children[i].getWorldPosition()); - if ( (distToNodeIRight < closestNodeDistanceRight ) ) { - closestNodeDistanceRight = distToNodeIRight; - closestNodeIndexRight = i; + // Keep track of touch-related state so that users can touch and drag on + // the canvas to adjust the viewer pose in an inline session. + let primaryTouch = undefined; + let prevTouchX = undefined; + let prevTouchY = undefined; + + // Keep track of all active touches, but only use the first touch to + // adjust the viewer pose. + canvas.addEventListener("touchstart", (event) => { + if (primaryTouch == undefined) { + let touch = event.changedTouches[0]; + primaryTouch = touch.identifier; + prevTouchX = touch.pageX; + prevTouchY = touch.pageY; } - } + }); - var isLeft = (activateVR == 'left'); - if(controllerLeft.getButtonState('trigger')) { - pointedNodeIdx = (closestNodeDistanceLeft < 2.0) ? closestNodeIndexLeft : -1; + // Update the set of active touches now that one or more touches + // finished. If the primary touch just finished, update the viewer pose + // based on the final touch movement. + canvas.addEventListener("touchend", (event) => { + for (let touch of event.changedTouches) { + if (primaryTouch == touch.identifier) { + primaryTouch = undefined; + rotateView(touch.pageX - prevTouchX, touch.pageY - prevTouchY); + } + } + }); - if (pointerLeft) { - // Touch Controller pointer already on! scan for selection - if (controllerLeft.getButtonState('grips')) { - updateNodeSelection(model, getPointedObject(controllerLeft), isLeft); + // Update the set of active touches now that one or more touches was + // cancelled. Don't update the viewer pose when the primary touch was + // cancelled. + canvas.addEventListener("touchcancel", (event) => { + for (let touch of event.changedTouches) { + if (primaryTouch == touch.identifier) { + primaryTouch = undefined; } - } else { - pointerLeft = drawPointer(v3Origin, v3UnitUp); - controllerLeft.add(pointerLeft); } - updateNodeMoveOver(model, getPointedObject(controllerLeft)); - } else { - if (pointerLeft) { - controllerLeft.remove(pointerLeft); + }); + + // Only use the delta between the most recent and previous events for + // the primary touch. Ignore the other touches. + canvas.addEventListener("touchmove", (event) => { + for (let touch of event.changedTouches) { + if (primaryTouch == touch.identifier) { + rotateView(touch.pageX - prevTouchX, touch.pageY - prevTouchY); + prevTouchX = touch.pageX; + prevTouchY = touch.pageY; + } } - pointerLeft = null; + }); + } //addInlineViewListeners + + // Called every time the XRSession requests that a new frame be drawn. + function onXRFrame(t, frame) { + let session = frame.session; + // Ensure that we're using the right frame of reference for the session. + let refSpace = session.isImmersive ? + xrImmersiveRefSpace : + xrInlineRefSpace; + + // Account for the click-and-drag mouse movement or touch movement when + // calculating the viewer pose for inline sessions. + if (!session.isImmersive) { + refSpace = getAdjustedRefSpace(refSpace); } - if(controllerRight.getButtonState('trigger')) { - pointedNodeIdx = (closestNodeDistanceRight < 2.0) ? closestNodeIndexRight : -1; - if (pointerRight) { - // Touch Controller pointer already on! scan for selection - if (controllerRight.getButtonState('grips')) { - updateNodeSelection(model, getPointedObject(controllerRight), isLeft); - } - } else { - pointerRight = drawPointer(v3Origin, v3UnitUp); - controllerRight.add(pointerRight); - } - updateNodeMoveOver(model, getPointedObject(controllerRight)); - } else { - if (pointerRight) { - controllerRight.remove(pointerRight); + + // Get the XRDevice pose relative to the Frame of Reference we created + // earlier. + let pose = frame.getViewerPose(refSpace); + + // Inform the session that we're ready for the next frame. + session.requestAnimationFrame(onXRFrame); + + // Getting the pose may fail if, for example, tracking is lost. So we + // have to check to make sure that we got a valid pose before attempting + // to render with it. If not in this case we'll just leave the + // framebuffer cleared, so tracking loss means the scene will simply + // disappear. + if (pose) { + let glLayer = session.renderState.baseLayer; + + // If we do have a valid pose, bind the WebGL layer's framebuffer, + // which is where any content to be displayed on the XRDevice must be + // rendered. + gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer); + + // Clear the framebuffer + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + // Loop through each of the views reported by the frame and draw them + // into the corresponding viewport. + for (let view of pose.views) { + let viewport = glLayer.getViewport(view); + gl.viewport(viewport.x, viewport.y, + viewport.width, viewport.height); + + // Draw this view of the scene. What happens in this function really + // isn't all that important. What is important is that it renders + // into the XRWebGLLayer's framebuffer, using the viewport into that + // framebuffer reported by the current view, and using the + // projection matrix and view transform from the current view. + // We bound the framebuffer and viewport up above, and are passing + // in the appropriate matrices here to be used when rendering. + //scene.draw(view.projectionMatrix, view.transform); + console.log("Draw Scene: " + view.transform.matrix + view.projectionMatrix); } - pointerRight = null; - } + + } //if pose + } //onXRFrame + + // Inline view adjustment code + // Allow the user to click and drag the mouse (or touch and drag the + // screen on handheld devices) to adjust the viewer pose for inline + // sessions. Samples after this one will hide this logic with a utility + // class (InlineViewerHelper). + let lookYaw = 0; + let lookPitch = 0; + const LOOK_SPEED = 0.0025; + + // XRReferenceSpace offset is immutable, so return a new reference space + // that has an updated orientation. + function getAdjustedRefSpace(refSpace) { + // Represent the rotational component of the reference space as a + // quaternion. + let invOrientation = quat.create(); + quat.rotateX(invOrientation, invOrientation, -lookPitch); + quat.rotateY(invOrientation, invOrientation, -lookYaw); + let xform = new XRRigidTransform( + {x: 0, y: 0, z: 0}, + {x: invOrientation[0], y: invOrientation[1], z: invOrientation[2], w: invOrientation[3]}); + return refSpace.getOffsetReferenceSpace(xform); + } + + // vrButton.addEventListener('mouseover', function () { + // //vrButton.style.display = 'none'; + // //vrButton.innerHTML = 'Enter VR NOW'; + // console.log("Mouse Over VR Button: " + name); + // //effect.requestPresent(); + // }, false); + //effect.requestPresent(); + // I found some VR button HTML in the visualization.html file and tried to light them up + // with OnClicks but they didn't seem to want to do anything so I tried that example class + // and it worked a bit better. + + + + // OLD InitVR Code Here: + // if (mobile) { + // console.log("Init VR for PV: " + name); + // enableVR = true; + // activateVR = true; + // // init VR + // vrButton = document.getElementById('vrButton' + name); + // vrButton.addEventListener('click', function () { + // vrButton.style.display = 'none'; + // //effect.requestPresent(); + // }, false); + // //effect.requestPresent(); + // } else { + // console.log("Init VR for PV: " + name); + // enableVR = true; + // activateVR = false; + // // init VR + // vrButton = document.getElementById('vrButton' + name); + // vrButton.addEventListener('click', function () { + // vrButton.style.display = 'none'; + // //effect.requestPresent(); + // }, false); + // //effect.requestPresent(); + // } +// }; + + //on resize + this.onWindowResize = function () { + if (enableVR) //todo: Is this still required in WebXR model? + return; + camera.aspect = canvas.clientWidth / canvas.clientHeight; + camera.updateProjectionMatrix(); + renderer.setSize(canvas.clientWidth, canvas.clientHeight); + }; + //listen for resize event + window.addEventListener('resize', this.onWindowResize, false); + + + // init Oculus Touch controllers + // not supported in Firefox, only Google chromium + // check https://webvr.info/get-chrome/ + // var initOculusTouch = function () { + // if (!enableVR) + // return; + // + // controllerLeft = new THREE.ViveController(0); + // controllerRight = new THREE.ViveController(1); + // + // var loader = new THREE.OBJLoader(); + // loader.setPath('js/external-libraries/vr/models/obj/vive-controller/'); + // loader.load('vr_controller_vive_1_5.obj', function (object) { + // + // var loader = new THREE.TextureLoader(); + // loader.setPath('js/external-libraries/vr/models/obj/vive-controller/'); + // + // var controller = object.children[0]; + // controller.material.map = loader.load('onepointfive_texture.png'); + // controller.material.specularMap = loader.load('onepointfive_spec.png'); + // + // controllerLeft.add(object.clone()); + // controllerRight.add(object.clone()); + // + // controllerLeft.standingMatrix = vrControl.getStandingMatrix(); + // controllerRight.standingMatrix = vrControl.getStandingMatrix(); + // + // scene.add(controllerLeft); + // scene.add(controllerRight); + // }); + // + // // controllerLeft.addEventListener('gripsup', function(e) { updateVRStatus('left'); }, true); + // // controllerRight.addEventListener('gripsup', function(e) { updateVRStatus('right'); }, true); + // + // oculusTouchExist = true; + // + // console.log("Init Oculus Touch done"); + // }; + + // var initGearVRController = function () { + // if (!enableVR || !mobile) + // return; + // + // // assume right handed user + // controllerRight = new THREE.GearVRController(0); + // //controllerRight.position.set( 25, - 50, 0 ); + // + // + // var loader = new THREE.OBJLoader(); + // loader.setPath('js/external-libraries/vr/models/obj/vive-controller/'); + // loader.load('vr_controller_vive_1_5.obj', function (object) { + // var loader = new THREE.TextureLoader(); + // loader.setPath('js/external-libraries/vr/models/obj/vive-controller/'); + // var controller = object.children[0]; + // controller.material.map = loader.load('onepointfive_texture.png'); + // controller.material.specularMap = loader.load('onepointfive_spec.png'); + // controllerRight.add(object.clone()); + // controllerRight.standingMatrix = vrControl.getStandingMatrix(); + // scene.add(controllerRight); + // }); + // + // gearVRControllerExist = true; + // + // console.log("Init Gear VR Controller done"); + // }; + + // var initWebVRForMobile = function () { + // // Initialize the WebVR UI. + // var uiOptions = { + // color: 'black', + // background: 'white', + // corners: 'round', + // height: 40, + // disabledOpacity: 0.9 + // }; + // vrButton = new webvrui.EnterVRButton(renderer.domElement, uiOptions); + // vrButton.on('exit', function () { + // updateVRStatus('disable'); + // }); + // vrButton.on('hide', function () { + // document.getElementById('vr' + name).style.display = 'none'; + // }); + // vrButton.on('show', function () { + // document.getElementById('vr' + name).style.display = 'inherit'; + // }); + // document.getElementById('vrButton' + name).appendChild(vrButton.domElement); + // document.getElementById('magicWindow' + name).addEventListener('click', function () { + // vr = true; + // activateVR = true; + // activeVR = name.toLowerCase(); + // console.log("Active VR = " + activeVR); + // vrButton.requestEnterFullscreen(); + // }); + // }; + + // scan Gear VR controller + // var scanGearVRController = function () { + // var thumbPad = controllerRight.getButtonState('thumbpad'); + // var trigger = controllerRight.getButtonState('trigger'); + // var angleX = null, angleY = null; + // var gamePadRight = controllerRight.getGamepad(); + // if (gamePadRight && !trigger) { + // angleX = gamePadRight.axes[0]; + // angleY = gamePadRight.axes[1]; + // if (thumbPad) { + // brain.rotateX(0.2 * angleX); + // brain.rotateZ(0.2 * angleY); + // } else { + // brain.position.z += 5 * angleX; + // brain.position.x += 5 * angleY; + // } + // brain.matrixWorldNeedsUpdate = true; + // } + // var v3Origin = new THREE.Vector3(0, 0, 0); + // var v3UnitUp = new THREE.Vector3(0, 0, -100); + // + // // Find all nodes within 0.1 distance from left Touch Controller + // var closestNodeIndexRight = 0, closestNodeDistanceRight = 99999.9; + // for (var i = 0; i < brain.children.length; i++) { + // var distToNodeIRight = controllerRight.position.distanceTo(brain.children[i].getWorldPosition()); + // if ((distToNodeIRight < closestNodeDistanceRight)) { + // closestNodeDistanceRight = distToNodeIRight; + // closestNodeIndexRight = i; + // } + // } + // + // var isLeft = (activateVR == 'left'); + // if (trigger) { + // pointedNodeIdx = (closestNodeDistanceRight < 2.0) ? closestNodeIndexRight : -1; + // + // if (pointerRight) { + // // Touch Controller pointer already on! scan for selection + // if (thumbPad) { + // updateNodeSelection(model, getPointedObject(controllerRight), isLeft); + // } + // } else { + // pointerRight = drawPointer(v3Origin, v3UnitUp); + // controllerRight.add(pointerRight); + // } + // updateNodeMoveOver(model, getPointedObject(controllerRight)); + // } else { + // if (pointerRight) { + // controllerRight.remove(pointerRight); + // } + // pointerRight = null; + // } + // }; + + // scan the Oculus Touch for controls + // var scanOculusTouch = function () { + // var boostRotationSpeed = controllerLeft.getButtonState('grips') ? 0.1 : 0.02; + // var boostMoveSpeed = controllerRight.getButtonState('grips') ? 5.0 : 1.0; + // var angleX = null, angleY = null; + // var gamePadLeft = controllerLeft.getGamepad(); + // var gamePadRight = controllerRight.getGamepad(); + // if (gamePadLeft) { + // angleX = gamePadLeft.axes[0]; + // angleY = gamePadLeft.axes[1]; + // brain.rotateX(boostRotationSpeed * angleX); + // brain.rotateZ(boostRotationSpeed * angleY); + // brain.matrixWorldNeedsUpdate = true; + // } + // + // if (gamePadRight) { + // angleX = gamePadRight.axes[0]; + // angleY = gamePadRight.axes[1]; + // if (controllerRight.getButtonState('thumbpad')) { + // brain.position.y += boostMoveSpeed * angleY; + // } else { + // brain.position.z += boostMoveSpeed * angleX; + // brain.position.x += boostMoveSpeed * angleY; + // } + // brain.matrixWorldNeedsUpdate = true; + // } + // + // var v3Origin = new THREE.Vector3(0, 0, 0); + // var v3UnitUp = new THREE.Vector3(0, 0, -100.0); + // // var v3UnitFwd = new THREE.Vector3(0,0,1); + // + // // Find all nodes within 0.1 distance from left Touch Controller + // var closestNodeIndexLeft = 0, closestNodeDistanceLeft = 99999.9; + // var closestNodeIndexRight = 0, closestNodeDistanceRight = 99999.9; + // for (var i = 0; i < brain.children.length; i++) { + // var distToNodeILeft = controllerLeft.position.distanceTo(brain.children[i].getWorldPosition()); + // if ((distToNodeILeft < closestNodeDistanceLeft)) { + // closestNodeDistanceLeft = distToNodeILeft; + // closestNodeIndexLeft = i; + // } + // + // var distToNodeIRight = controllerRight.position.distanceTo(brain.children[i].getWorldPosition()); + // if ((distToNodeIRight < closestNodeDistanceRight)) { + // closestNodeDistanceRight = distToNodeIRight; + // closestNodeIndexRight = i; + // } + // } + // + // var isLeft = (activateVR == 'left'); + // if (controllerLeft.getButtonState('trigger')) { + // pointedNodeIdx = (closestNodeDistanceLeft < 2.0) ? closestNodeIndexLeft : -1; + // + // if (pointerLeft) { + // // Touch Controller pointer already on! scan for selection + // if (controllerLeft.getButtonState('grips')) { + // updateNodeSelection(model, getPointedObject(controllerLeft), isLeft); + // } + // } else { + // pointerLeft = drawPointer(v3Origin, v3UnitUp); + // controllerLeft.add(pointerLeft); + // } + // updateNodeMoveOver(model, getPointedObject(controllerLeft)); + // } else { + // if (pointerLeft) { + // controllerLeft.remove(pointerLeft); + // } + // pointerLeft = null; + // } + // + // if (controllerRight.getButtonState('trigger')) { + // pointedNodeIdx = (closestNodeDistanceRight < 2.0) ? closestNodeIndexRight : -1; + // + // if (pointerRight) { + // // Touch Controller pointer already on! scan for selection + // if (controllerRight.getButtonState('grips')) { + // updateNodeSelection(model, getPointedObject(controllerRight), isLeft); + // } + // } else { + // pointerRight = drawPointer(v3Origin, v3UnitUp); + // controllerRight.add(pointerRight); + // } + // updateNodeMoveOver(model, getPointedObject(controllerRight)); + // } else { + // if (pointerRight) { + // controllerRight.remove(pointerRight); + // } + // pointerRight = null; + // } + // }; + // draw a pointing line var drawPointer = function (start, end) { - var material = new THREE.LineBasicMaterial(); - var geometry = new THREE.Geometry(); - geometry.vertices.push( - start, - end - ); + var material = new THREE.LineBasicMaterial({color: 0xFFFFFF}); + var points = []; + points.push(start); + points.push(end); + var geometry = new THREE.BufferGeometry().setFromPoints(points); return new THREE.Line(geometry, material); }; + // get the object pointed by the controller + var getPointedObject = function (controller) { + var raycaster = new THREE.Raycaster(); + raycaster.setFromCamera({x: 0, y: 0}, camera); + var intersects = raycaster.intersectObjects(brain.children, true); + if (intersects.length > 0) { + return intersects[0].object; + } + return null; + } + // initialize scene: init 3js scene, canvas, renderer and camera; add axis and light to the scene var initScene = function () { renderer.setSize(canvas.clientWidth, window.innerHeight); @@ -343,54 +740,70 @@ function PreviewArea(canvas_, model_, name_) { scene.add(brain); //Adding light - scene.add( new THREE.HemisphereLight(0x606060, 0x080820, 1.5)); - scene.add( new THREE.AmbientLight(0x606060, 1.5)); - var light = new THREE.PointLight( 0xffffff, 1.0, 10000 ); - light.position.set( 1000, 1000, 100 ); + scene.add(new THREE.HemisphereLight(0x606060, 0x080820, 1.5)); + scene.add(new THREE.AmbientLight(0x606060, 1.5)); + var light = new THREE.PointLight(0xffffff, 1.0, 10000); + light.position.set(1000, 1000, 100); scene.add(light); - var axisHelper = new THREE.AxisHelper( 5 ); - scene.add( axisHelper ); - addNodeLabel(); + var axeshelper = new THREE.AxesHelper(5); + scene.add(axeshelper); + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.dampingFactor = 0.25; + controls.enableZoom = true; + controls.autoRotate = false; + controls.autoRotateSpeed = 0.5; + controls.enablePan = true; + controls.enableKeys = true; + controls.minDistance = 10; + controls.maxDistance = 1000; + + //addNodeLabel(); }; this.resetCamera = function () { - camera.position.set(50,50,50); + camera.position.set(50, 50, 50); }; this.resetBrainPosition = function () { brain.updateMatrix(); - brain.position.set(0,0,0); - brain.rotation.set(0,0,0); - brain.scale.set(1,1,1); + brain.position.set(0, 0, 0); + brain.rotation.set(0, 0, 0); + brain.scale.set(1, 1, 1); brain.updateMatrix(); brain.matrixWorldNeedsUpdate = true; }; // create 3js elements: scene, canvas, camera and controls; and init them and add skybox to the scene - this.createCanvas = function() { + this.createCanvas = function () { scene = new THREE.Scene(); - renderer = new THREE.WebGLRenderer({antialias: true}); + renderer = new THREE.WebGLRenderer({ + antialias: true, + context: gl + }); camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / window.innerHeight, 0.1, 3000); initScene(); - if (!mobile) { - controls = new THREE.TrackballControls(camera, renderer.domElement); - controls.rotateSpeed = 0.5; - } + console.log("createCanvas"); addSkybox(); }; // initialize scene: init 3js scene, canvas, renderer and camera; add axis and light to the scene + //todo is this sort of infinite recursion intentional? this.setEventListeners = function (onMouseDown, onMouseUp, onDocumentMouseMove) { canvas.addEventListener('mousedown', onMouseDown, true); - canvas.addEventListener('mouseup', function (e) { onMouseUp(model, e);}); - canvas.addEventListener('mousemove', function (e) { onDocumentMouseMove(model, e); }, true); + canvas.addEventListener('mouseup', function (e) { + onMouseUp(model, e); + }); + canvas.addEventListener('mousemove', function (e) { + onDocumentMouseMove(model, e); + }, true); }; // update node scale according to selection status this.updateNodeGeometry = function (nodeIndex, status) { var scale = 1.0; - switch (status){ + switch (status) { case 'normal': scale = 1.0; break; @@ -398,10 +811,10 @@ function PreviewArea(canvas_, model_, name_) { scale = 1.2; break; case 'selected': - scale = (8/3); + scale = (8 / 3); break; case 'root': - scale = (10/3); + scale = (10 / 3); break; } glyphs[nodeIndex].scale.set(scale, scale, scale); @@ -409,13 +822,13 @@ function PreviewArea(canvas_, model_, name_) { this.updateNodesColor = function () { var dataset = model.getDataset(); - for (var i=0; i < glyphs.length; ++i){ + for (var i = 0; i < glyphs.length; ++i) { glyphs[i].material.color = new THREE.Color(scaleColorGroup(model, dataset[i].group)); } }; var removeNodesFromScene = function () { - for (var i=0; i < glyphs.length; ++i){ + for (var i = 0; i < glyphs.length; ++i) { brain.remove(glyphs[i]); delete glyphNodeDictionary[glyphs[i].uuid]; } @@ -423,7 +836,7 @@ function PreviewArea(canvas_, model_, name_) { }; this.removeEdgesFromScene = function () { - for(var i=0; i < displayedEdges.length; ++i){ + for (var i = 0; i < displayedEdges.length; ++i) { brain.remove(displayedEdges[i]); } displayedEdges = []; @@ -432,52 +845,71 @@ function PreviewArea(canvas_, model_, name_) { }; this.removeShortestPathEdgesFromScene = function () { - for(var i=0; i < shortestPathEdges.length; i++){ + for (var i = 0; i < shortestPathEdges.length; i++) { brain.remove(shortestPathEdges[i]); } shortestPathEdges = []; }; var animatePV = function () { - if (enableVR && activateVR) { - if (oculusTouchExist) { - controllerLeft.update(); - controllerRight.update(); - scanOculusTouch(); - } - vrControl.update(); - } - else { - if (mobile) { - if (gearVRControllerExist) { - controllerRight.update(); - scanGearVRController(); - } - vrControl.update(); - } - else - controls.update(); - } + // if (enableVR && activateVR) { + // // if (oculusTouchExist) { //todo: Change old WebVR code to WebXR + // // controllerLeft.update(); + // // controllerRight.update(); + // // scanOculusTouch(); + // // console.log("scanOculusTouch"); + // // } + // //vrControl.update(); //todo: Change old WebVR code to WebXR + // console.log("vrControl.update()"); + // } else if (mobile && 0) { // todo: get code to work and re-enable by deleting && 0 + // if (gearVRControllerExist) { + // controllerRight.update(); + // scanGearVRController(); + // console.log("gearVRControllerExist"); + // } + // //vrControl.update(); // todo: get code working then enable + // console.log("vrControl.update()"); + // } else { + + controls.update(); // todo: this only executes when not VR or Mobile in Old WebVR Model. Consider in WebXR + //console.log("controls.update() called"); + //} + if (enableRender) - effect.render(scene, camera); + //changed from effect.render to renderer.render + renderer.render(scene, camera); + + + //effect.requestAnimationFrame(animatePV); //effect no longer has this function. Maybe it is no longer required - effect.requestAnimationFrame(animatePV); + window.requestAnimationFrame(animatePV); }; this.requestAnimate = function () { - effect.requestAnimationFrame(animatePV); + //effect.requestAnimationFrame(animatePV); //effect no longer has this function. Maybe it is no longer required + //window.requestAnimationFrame(animatePV); + animatePV(); + controls.update() + renderer.render(scene, camera); + console.log("requestAnimate called"); }; - this.enableRender = function (state) { enableRender = state; }; + this.enableRender = function (state) { + enableRender = state; + }; - this.isVRAvailable = function () { return enableVR; }; + this.isVRAvailable = function () { + return enableVR; + }; - this.isPresenting = function () { vrButton.isPresenting(); }; + // this.isPresenting = function () { + // vrButton.isPresenting(); + // }; this.redrawEdges = function () { this.removeEdgesFromScene(); - if (spt) + if (getSpt()) this.updateShortestPathEdges(); this.drawConnections(); }; @@ -488,7 +920,7 @@ function PreviewArea(canvas_, model_, name_) { }; // updating scenes: redrawing glyphs and displayed edges - this.updateScene = function (){ + this.updateScene = function () { updateNodesPositions(); this.updateNodesVisibility(); this.redrawEdges(); @@ -500,8 +932,8 @@ function PreviewArea(canvas_, model_, name_) { var dataset = model.getDataset(); var material, geometry; - for(var i=0; i < dataset.length; i++){ - geometry = getNormalGeometry(dataset[i].hemisphere); + for (var i = 0; i < dataset.length; i++) { + geometry = getNormalGeometry(dataset[i].hemisphere,name); material = getNormalMaterial(model, dataset[i].group); glyphs[i] = new THREE.Mesh(geometry, material); brain.add(glyphs[i]); @@ -514,21 +946,21 @@ function PreviewArea(canvas_, model_, name_) { // update the nodes positions according to the latest in the model var updateNodesPositions = function () { var dataset = model.getDataset(); - for(var i=0; i < dataset.length; i++){ + for (var i = 0; i < dataset.length; i++) { glyphs[i].position.set(dataset[i].position.x, dataset[i].position.y, dataset[i].position.z); } }; this.updateNodesVisibility = function () { var dataset = model.getDataset(); - for(var i=0; i < dataset.length; i++){ + for (var i = 0; i < dataset.length; i++) { var opacity = 1.0; - if(root && root == i){ // root node + if (getRoot && getRoot == i) { // root node opacity = 1.0; } if (shouldDrawRegion(dataset[i])) { - switch (model.getRegionState(dataset[i].group)){ + switch (model.getRegionState(dataset[i].group)) { case 'active': opacity = 1.0; break; @@ -551,12 +983,12 @@ function PreviewArea(canvas_, model_, name_) { // don't draw edges belonging to inactive nodes this.drawConnections = function () { var nodeIdx; - for(var i= 0; i < nodesSelected.length; i++){ - nodeIdx = nodesSelected[i]; + for (var i = 0; i < getNodesSelected().length; i++) { + nodeIdx = getNodesSelected()[i]; // draw only edges belonging to active nodes - if(model.isRegionActive(model.getGroupNameByNodeIndex(nodeIdx))) { + if ((nodeIdx >= 0) && model.isRegionActive(model.getGroupNameByNodeIndex(nodeIdx))) { // two ways to draw edges - if(thresholdModality) { + if (getThresholdModality()) { // 1) filter edges according to threshold this.drawEdgesGivenNode(nodeIdx); } else { @@ -567,7 +999,7 @@ function PreviewArea(canvas_, model_, name_) { } // draw all edges belonging to the shortest path array - for(i=0; i < shortestPathEdges.length; i++){ + for (i = 0; i < shortestPathEdges.length; i++) { displayedEdges[displayedEdges.length] = shortestPathEdges[i]; brain.add(shortestPathEdges[i]); } @@ -577,15 +1009,16 @@ function PreviewArea(canvas_, model_, name_) { // skew the color distribution according to the nodes strength var computeColorGradient = function (c1, c2, n, p) { - var gradient = new Float32Array( n * 3 ); - var p1 = p; var p2 = 1-p1; + var gradient = new Float32Array(n * 3); + var p1 = p; + var p2 = 1 - p1; for (var i = 0; i < n; ++i) { // skew the color distribution according to the nodes strength - var r = i/(n-1); - var rr = (r*r*(p2-0.5) + r*(0.5-p2*p2))/(p1*p2); - gradient[ i * 3 ] = c2.r + (c1.r - c2.r)*rr; - gradient[ i * 3 + 1 ] = c2.g + (c1.g - c2.g)*rr; - gradient[ i * 3 + 2 ] = c2.b + (c1.b - c2.b)*rr + var r = i / (n - 1); + var rr = (r * r * (p2 - 0.5) + r * (0.5 - p2 * p2)) / (p1 * p2); + gradient[i * 3] = c2.r + (c1.r - c2.r) * rr; + gradient[i * 3 + 1] = c2.g + (c1.g - c2.g) * rr; + gradient[i * 3 + 2] = c2.b + (c1.b - c2.b) * rr } return gradient; }; @@ -593,24 +1026,24 @@ function PreviewArea(canvas_, model_, name_) { // set the color of displayed edges this.updateEdgeColors = function () { var edge, c1, c2; - for(var i = 0; i < displayedEdges.length; i++){ + for (var i = 0; i < displayedEdges.length; i++) { edge = displayedEdges[i]; c1 = glyphs[edge.nodes[0]].material.color; c2 = glyphs[edge.nodes[1]].material.color; - edge.geometry.addAttribute( 'color', new THREE.BufferAttribute( computeColorGradient(c1,c2,edge.nPoints, edge.p1), 3 ) ); + edge.geometry.setAttribute('color', new THREE.BufferAttribute(computeColorGradient(c1, c2, edge.nPoints, edge.p1), 3)); } - for(i = 0; i < shortestPathEdges.length; i++){ + for (i = 0; i < shortestPathEdges.length; i++) { edge = displayedEdges[i]; c1 = glyphs[edge.nodes[0]].material.color; c2 = glyphs[edge.nodes[1]].material.color; - edge.geometry.addAttribute( 'color', new THREE.BufferAttribute( computeColorGradient(c1,c2,edge.nPoints, edge.p1), 3 ) ); + edge.geometry.setAttribute('color', new THREE.BufferAttribute(computeColorGradient(c1, c2, edge.nPoints, edge.p1), 3)); } }; this.updateEdgeOpacity = function (opacity) { edgeOpacity = opacity; - for(var i = 0; i < displayedEdges.length; i++){ + for (var i = 0; i < displayedEdges.length; i++) { displayedEdges[i].material.opacity = opacity; } }; @@ -623,37 +1056,40 @@ function PreviewArea(canvas_, model_, name_) { // line.setGeometry( geometry ); // material = new THREE.MeshLineMaterial(); // var mesh = new THREE.Mesh(line.geometry, material); - var createLine = function(edge, ownerNode, nodes){ + var createLine = function (edge, ownerNode, nodes) { var material = new THREE.LineBasicMaterial({ transparent: true, opacity: edgeOpacity, - vertexColors: THREE.VertexColors + vertexColors: true //THREE.VertexColors // Due to limitations in the ANGLE layer on Windows platforms linewidth will always be 1. }); var geometry = new THREE.BufferGeometry(); var n = edge.length; - var positions = new Float32Array( n * 3 ); + var positions = new Float32Array(n * 3); for (var i = 0; i < n; i++) { - positions[ i * 3 ] = edge[i].x; - positions[ i * 3 + 1 ] = edge[i].y; - positions[ i * 3 + 2 ] = edge[i].z; + positions[i * 3] = edge[i].x; + positions[i * 3 + 1] = edge[i].y; + positions[i * 3 + 2] = edge[i].z; } - geometry.addAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) ); + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); var s1 = model.getNodalStrength(nodes[0]), s2 = model.getNodalStrength(nodes[1]); - var p1 = s1/(s1+s2); + var p1 = s1 / (s1 + s2); var c1 = new THREE.Color(scaleColorGroup(model, model.getGroupNameByNodeIndex(nodes[0]))),// glyphs[nodes[0]].material.color, c2 = new THREE.Color(scaleColorGroup(model, model.getGroupNameByNodeIndex(nodes[1])));// glyphs[nodes[1]].material.color; - geometry.addAttribute( 'color', new THREE.BufferAttribute( computeColorGradient(c1,c2,n,p1), 3 ) ); + geometry.setAttribute('color', new THREE.BufferAttribute(computeColorGradient(c1, c2, n, p1), 3)); // geometry.colors = colorGradient; - var line = new THREE.Line(geometry, material); + var line = new THREE.Line(geometry, material); line.name = ownerNode; line.nPoints = n; line.nodes = nodes; line.p1 = p1; + line.material.linewidth = 1; + line.material.vertexColors = true; //THREE.VertexColors; + return line; }; @@ -666,14 +1102,27 @@ function PreviewArea(canvas_, model_, name_) { // draw the top n edges connected to a specific node this.drawTopNEdgesByNode = function (nodeIndex, n) { - var row = model.getTopConnectionsByNode(nodeIndex, n); + var row = []; + if(!getEnableContra() && !getEnableIpsi()) { + row = model.getTopConnectionsByNode(nodeIndex, n ); + } else { + if(getEnableContra()) { + row = row.concat(model.getTopContraLateralConnectionsByNode(nodeIndex, n )); + } + if (getEnableIpsi()) { + row = row.concat(model.getTopIpsiLateralConnectionsByNode(nodeIndex, n )); + } + } + console.log("contra"+getEnableContra()); + console.log("ipsi"+getEnableIpsi()); + var edges = model.getActiveEdges(); var edgeIdx = model.getEdgesIndeces(); - if (enableEB) { + if (getEnableEB()) { model.performEBOnNode(nodeIndex); } - for (var i =0; i < row.length; ++i) { - if ((nodeIndex != row[i]) && model.isRegionActive(model.getGroupNameByNodeIndex(i)) && visibleNodes[i]) { + for (var i = 0; i < row.length; ++i) { + if ((nodeIndex != row[i]) && model.isRegionActive(model.getGroupNameByNodeIndex(i)) && getVisibleNodes(i)) { displayedEdges[displayedEdges.length] = drawEdgeWithName(edges[edgeIdx[nodeIndex][row[i]]], nodeIndex, [nodeIndex, row[i]]); } } @@ -682,32 +1131,55 @@ function PreviewArea(canvas_, model_, name_) { }; // draw edges given a node following edge threshold - this.drawEdgesGivenNode = function(indexNode) { + this.drawEdgesGivenNode = function (indexNode) { + var dataset = model.getDataset(); var row = model.getConnectionMatrixRow(indexNode); var edges = model.getActiveEdges(); var edgeIdx = model.getEdgesIndeces(); - if (enableEB) { + if (getEnableEB( )) { model.performEBOnNode(indexNode); - } + } + + // It can get too cluttered if both ipsi- + if (getEnableIpsi() && getEnableContra()) { + for (var i = 0; i < row.length; i++) { + + var myThreshold = model.getThreshold(); - for(var i=0; i < row.length ; i++){ - if((i != indexNode) && Math.abs(row[i]) > model.getThreshold() && model.isRegionActive(model.getGroupNameByNodeIndex(i)) && visibleNodes[i]) { - displayedEdges[displayedEdges.length] = drawEdgeWithName(edges[edgeIdx[indexNode][i]], indexNode, [indexNode, i]); + if (dataset[indexNode].hemisphere !== dataset[i].hemisphere) { + myThreshold = model.getConThreshold(); + } + + if ((i != indexNode) && + (Math.abs(row[i]) > myThreshold) && + model.isRegionActive(model.getGroupNameByNodeIndex(i)) && + getVisibleNodes(i) ) { + displayedEdges[displayedEdges.length] = drawEdgeWithName(edges[edgeIdx[indexNode][i]], indexNode, [indexNode, i]); + } + } + } else { + for (var i = 0; i < row.length; i++) { + if ((i != indexNode) && Math.abs(row[i]) > model.getThreshold() && model.isRegionActive(model.getGroupNameByNodeIndex(i)) && getVisibleNodes(i) && + ((getEnableIpsi() && (dataset[indexNode].hemisphere === dataset[i].hemisphere)) || + (getEnableContra() && (dataset[indexNode].hemisphere !== dataset[i].hemisphere)) || + (!getEnableIpsi() && !getEnableContra()) ) ) { + displayedEdges[displayedEdges.length] = drawEdgeWithName(edges[edgeIdx[indexNode][i]], indexNode, [indexNode, i]); + } } } }; // give a specific node index, remove all edges from a specific node in a specific scene - this.removeEdgesGivenNode = function(indexNode) { + this.removeEdgesGivenNode = function (indexNode) { var l = displayedEdges.length; // keep a list of removed edges indexes var removedEdges = []; - for(var i=0; i < l; i++){ + for (var i = 0; i < l; i++) { var edge = displayedEdges[i]; //removing only the edges that starts from that node - if(edge.name == indexNode && shortestPathEdges.indexOf(edge) == -1){ + if (edge.name == indexNode && shortestPathEdges.indexOf(edge) == -1) { removedEdges[removedEdges.length] = i; brain.remove(edge); } @@ -715,67 +1187,61 @@ function PreviewArea(canvas_, model_, name_) { // update the displayedEdges array var updatedDisplayEdges = []; - for(i=0; i < displayedEdges.length; i++){ + for (i = 0; i < displayedEdges.length; i++) { //if the edge should not be removed - if( removedEdges.indexOf(i) == -1){ + if (removedEdges.indexOf(i) == -1) { updatedDisplayEdges[updatedDisplayEdges.length] = displayedEdges[i]; } } - for(i=0; i < shortestPathEdges.length; i++){ + for (i = 0; i < shortestPathEdges.length; i++) { updatedDisplayEdges[updatedDisplayEdges.length] = shortestPathEdges[i]; } displayedEdges = updatedDisplayEdges; }; // draw skybox from images - var addSkybox = function(){ + var addSkybox = function () { + console.log("Adding skybox"); var folder = 'darkgrid'; var images = [ - 'images/'+folder+'/negx.png', - 'images/'+folder+'/negy.png', - 'images/'+folder+'/negz.png', - 'images/'+folder+'/posx.png', - 'images/'+folder+'/posy.png', - 'images/'+folder+'/posz.png' + './images/' + folder + '/negx.png', + './images/' + folder + '/negy.png', + './images/' + folder + '/negz.png', + './images/' + folder + '/posx.png', + './images/' + folder + '/posy.png', + './images/' + folder + '/posz.png' ]; - - var cubemap = THREE.ImageUtils.loadTextureCube(images); // load textures - cubemap.format = THREE.RGBFormat; - - var shader = THREE.ShaderLib['cube']; // init cube shader from built-in lib - shader.uniforms['tCube'].value = cubemap; // apply textures to shader - - // create shader material - var skyBoxMaterial = new THREE.ShaderMaterial( { - fragmentShader: shader.fragmentShader, - vertexShader: shader.vertexShader, - uniforms: shader.uniforms, - depthWrite: false, - side: THREE.BackSide + //create skybox using images + var skybox = new THREE.CubeTextureLoader().load(images); + //set the scene background property with the resulting texture + scene.background = skybox; + //activate background + scene.background.needsUpdate = true; + // + var geometry = new THREE.SphereGeometry(5000, 60, 40); + geometry.scale(-1, 1, 1); + var material = new THREE.MeshBasicMaterial({ + map: new THREE.TextureLoader().load('./images/' + folder + '/posy.png') }); + var mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + }; // end addSkybox - // create skybox mesh - var skybox = new THREE.Mesh( - new THREE.CubeGeometry(1500, 1500, 1500), - skyBoxMaterial - ); - - skybox.name = "skybox"; - scene.add(skybox); - }; // toggle skybox visibility - this.setSkyboxVisibility = function(visible){ - var results = scene.children.filter(function(d) {return d.name == "skybox"}); + this.setSkyboxVisibility = function (visible) { + var results = scene.children.filter(function (d) { + return d.name == "skybox" + }); var skybox = results[0]; skybox.visible = visible; }; // draw a selected node: increase it's size this.drawSelectedNode = function (nodeIndex) { - if(nodesSelected.indexOf(nodeIndex) == -1) { - nodesSelected[nodesSelected.length] = nodeIndex; + if (getNodesSelected().indexOf(nodeIndex) == -1) { + setNodesSelected(getNodesSelected().length, nodeIndex); } this.updateNodeGeometry(nodeIndex, 'selected'); }; @@ -783,80 +1249,81 @@ function PreviewArea(canvas_, model_, name_) { // get intersected object beneath the mouse pointer // detects which scene: left or right // return undefined if no object was found - this.getIntersectedObject = function(vector) { + this.getIntersectedObject = function (vector) { raycaster.setFromCamera(vector, camera); - var objectsIntersected = raycaster.intersectObjects( glyphs ); - return (objectsIntersected[0])? objectsIntersected[0] : undefined; + var objectsIntersected = raycaster.intersectObjects(glyphs); + return (objectsIntersected[0]) ? objectsIntersected[0] : undefined; }; // callback when window is resized - this.resizeScene = function(){ - if (vrButton && vrButton.isPresenting()) { - camera.aspect = window.innerWidth / window.innerHeight; - renderer.setSize(window.innerWidth, window.innerHeight); - console.log("Resize for Mobile VR"); - } else { + this.resizeScene = function () { + //todo disabled for now straight to else vrButton.isPresenting() ... actually removing all WebVR for now + // if (vrButton && 0) { + // camera.aspect = window.innerWidth / window.innerHeight; + // renderer.setSize(window.innerWidth, window.innerHeight); + // console.log("Resize for Mobile VR"); + // } else { camera.aspect = window.innerWidth / 2.0 / window.innerHeight; renderer.setSize(window.innerWidth / 2.0, window.innerHeight); console.log("Resize"); - } + //} camera.updateProjectionMatrix(); }; // compute shortest path info for a node - this.computeShortestPathForNode = function(nodeIndex) { + this.computeShortestPathForNode = function (nodeIndex) { console.log("Compute Shortest Path for node " + nodeIndex); - root = nodeIndex; + setRoot(nodeIndex); model.computeShortestPathDistances(nodeIndex); }; // draw shortest path from root node up to a number of hops this.updateShortestPathBasedOnHops = function () { var hops = model.getNumberOfHops(); - var hierarchy = model.getHierarchy(root); + var hierarchy = model.getHierarchy(getRoot); var edges = model.getActiveEdges(); var edgeIdx = model.getEdgesIndeces(); var previousMap = model.getPreviousMap(); this.removeShortestPathEdgesFromScene(); - for(var i = 0; i < hierarchy.length; ++i) { - if( i < hops + 1 ) { + for (var i = 0; i < hierarchy.length; ++i) { + if (i < hops + 1) { //Visible node branch - for(var j=0; j < hierarchy[i].length; j++){ - visibleNodes[hierarchy[i][j]] = true; + for (var j = 0; j < hierarchy[i].length; j++) { + setVisibleNodes(hierarchy[i][j], true); var prev = previousMap[hierarchy[i][j]]; - if(prev){ - shortestPathEdges[shortestPathEdges.length] = createLine(edges[edgeIdx[prev][hierarchy[i][j]]] , prev, [prev, i]); + if (prev) { + shortestPathEdges[shortestPathEdges.length] = createLine(edges[edgeIdx[prev][hierarchy[i][j]]], prev, [prev, i]); } } } else { - for(var j=0; j < hierarchy[i].length; ++j){ - visibleNodes[hierarchy[i][j]] = false; + for (var j = 0; j < hierarchy[i].length; ++j) { + setVisibleNodes(hierarchy[i][j], false); } } } }; this.updateShortestPathBasedOnDistance = function () { - nodesSelected = []; + clrNodesSelected(); this.removeShortestPathEdgesFromScene(); // show only nodes with shortest paths distance less than a threshold - var threshold = model.getDistanceThreshold()/100.*model.getMaximumDistance(); + var threshold = model.getDistanceThreshold() / 100. * model.getMaximumDistance(); var distanceArray = model.getDistanceArray(); - for(var i=0; i < visibleNodes.length; i++){ - visibleNodes[i] = (distanceArray[i] <= threshold); + for (var i = 0; i < getVisibleNodesLength(); i++) { + setVisibleNodes(i, (distanceArray[i] <= threshold)); } var edges = model.getActiveEdges(); var edgeIdx = model.getEdgesIndeces(); var previousMap = model.getPreviousMap(); - for(i=0; i < visibleNodes.length; ++i) { - if(visibleNodes[i]){ + for (i = 0; i < getVisibleNodesLength(); ++i) { + if (getVisibleNodes(i)) { var prev = previousMap[i]; - if(prev) { + if (prev) { shortestPathEdges[shortestPathEdges.length] = createLine(edges[edgeIdx[prev][i]], prev, [prev, i]); } } @@ -864,18 +1331,18 @@ function PreviewArea(canvas_, model_, name_) { }; this.updateShortestPathEdges = function () { - switch (shortestPathVisMethod) { + switch (getShortestPathVisMethod()) { case (SHORTEST_DISTANCE): - this.updateShortestPathBasedOnDistance(); + this.updateShortestPathBasedOnDistance(); break; case (NUMBER_HOPS): - this.updateShortestPathBasedOnHops(); + this.updateShortestPathBasedOnHops(); break; } }; // prepares the shortest path from a = root to node b - this.getShortestPathFromRootToNode = function(target) { + this.getShortestPathFromRootToNode = function (target) { this.removeShortestPathEdgesFromScene(); var i = target; @@ -884,11 +1351,11 @@ function PreviewArea(canvas_, model_, name_) { var edgeIdx = model.getEdgesIndeces(); var previousMap = model.getPreviousMap(); - visibleNodes.fill(true); - while(previousMap[i]!= null){ + setVisibleNodes(getVisibleNodes().fill(true)); + while (previousMap[i] != null) { prev = previousMap[i]; - visibleNodes[prev] = true; - shortestPathEdges[shortestPathEdges.length] = createLine(edges[edgeIdx[prev][i]], prev, [prev, i] ); + setVisibleNodes(prev, true); + shortestPathEdges[shortestPathEdges.length] = createLine(edges[edgeIdx[prev][i]], prev, [prev, i]); i = prev; } @@ -897,12 +1364,12 @@ function PreviewArea(canvas_, model_, name_) { // get intersected object pointed to by Vive/Touch Controller pointer // return undefined if no object was found - var getPointedObject = function(controller) { + var getPointedObject = function (controller) { var gamePad = controller.getGamepad(); if (gamePad) { var orientation = new THREE.Quaternion().fromArray(gamePad.pose.orientation); - var v3orientation = new THREE.Vector3(0,0,-1.0); + var v3orientation = new THREE.Vector3(0, 0, -1.0); v3orientation.applyQuaternion(orientation); var ray = new THREE.Raycaster(controller.position, v3orientation); var objectsIntersected = ray.intersectObjects(glyphs); @@ -917,10 +1384,10 @@ function PreviewArea(canvas_, model_, name_) { // Update the text and position according to selected node // The alignment, size and offset parameters are set by experimentation // TODO needs more experimentation - this.updateNodeLabel = function(text, nodeIndex) { + this.updateNodeLabel = function (text, nodeIndex) { var context = nspCanvas.getContext('2d'); context.textAlign = 'left'; - context.clearRect(0, 0, 256*4, 256); + context.clearRect(0, 0, 256 * 4, 256); context.fillText(text, 5, 120); nodeNameMap.needsUpdate = true; @@ -930,11 +1397,11 @@ function PreviewArea(canvas_, model_, name_) { }; // Adding Node label Sprite - var addNodeLabel = function() { + var addNodeLabel = function () { nspCanvas = document.createElement('canvas'); var size = 256; - nspCanvas.width = size*4; + nspCanvas.width = size * 4; nspCanvas.height = size; var context = nspCanvas.getContext('2d'); context.fillStyle = '#ffffff'; @@ -953,24 +1420,37 @@ function PreviewArea(canvas_, model_, name_) { }); nodeLabelSprite = new THREE.Sprite(mat); - nodeLabelSprite.scale.set( 100, 50, 1 ); - nodeLabelSprite.position.set( 0, 0, 0 ); + nodeLabelSprite.scale.set(100, 50, 1); + nodeLabelSprite.position.set(0, 0, 0); brain.add(nodeLabelSprite); }; - this.getCamera = function() { return camera; }; + this.getCamera = function () { + return camera; + }; - this.syncCameraWith = function(cam) { + this.syncCameraWith = function (cam) { camera.copy(cam); camera.position.copy(cam.position); - camera.rotation.copy(cam.rotation); camera.zoom = cam.zoom; - // camera.quaternion.copy(cam.quaternion); - // camera.updateMatrix(); }; + this.getGlyph = function (nodeIndex) { + if (nodeIndex) { + return glyphs[nodeIndex]; + } else { + return null; + } + } + + this.getGlyphCount = function () { + return glyphs.length; + } + // PreviewArea construction this.createCanvas(); - this.initVR(); + this.initXR(); this.drawRegions(); -} \ No newline at end of file +} + +export { PreviewArea } diff --git a/js/previewAreaOLD.js b/js/previewAreaOLD.js new file mode 100644 index 0000000..a7fab4a --- /dev/null +++ b/js/previewAreaOLD.js @@ -0,0 +1,984 @@ +/** + * Created by Johnson on 2/15/2017. + */ + +/** + * This class controls the preview 3D area. It controls the creation of glyphs (nodes), edges, shortest path edges. It + * also executes the update requests to those objects. It init the VR environment when requested. + * @param canvas_ a WebGl canvas + * @param model_ a Model object + * @constructor + */ + +import * as THREE from 'three' +//import {isLoaded, dataFiles,mobile} from "./globals"; +import {mobile} from "./globals"; + + + +function PreviewAreaOLD(canvas_, model_, name_) { + var name = name_; + var model = model_; + var canvas = canvas_; + var camera = null, renderer = null, controls = null, scene = null, raycaster = null; + var nodeLabelSprite = null, nodeNameMap = null, nspCanvas = null; + + // VR stuff + var vrControl = null, effect = null; + var controllerLeft = null, controllerRight = null, oculusTouchExist = false, gearVRControllerExist = false, enableRender = true; + var pointerLeft = null, pointerRight = null; // left and right controller pointers for pointing at things + + var enableVR = false; + var activateVR = false; + // nodes and edges + var brain = null; // three js group housing all glyphs and edges + var glyphs = []; + var displayedEdges = []; + // shortest path + var shortestPathEdges = []; + + var edgeOpacity = 1.0; + + var vrButton = null; + + // request VR activation - desktop case + this.activateVR = function (activate) { + if (activate == activateVR) + return; + activateVR = activate; + if (!mobile) { + if (activateVR) { + console.log("Activate VR for PV: " + name); + effect.requestPresent(); + } + else + console.log("Disable VR for PV: " + name); + effect.exitPresent(); + } + }; + + // init Oculus Rift + this.initVR = function () { + vrControl = new THREE.VRControls(camera, function (message) { + console.log("VRControls: ", message); + }); + effect = new THREE.VREffect(renderer, function (message) { + console.log("VREffect ", message); + }); + effect.setSize(window.innerWidth/2., window.innerHeight); + + if (navigator.getVRDisplays) { + navigator.getVRDisplays() + .then(function (displays) { + if (displays.length > 0) { + console.log("VR Display found"); + effect.setVRDisplay(displays[0]); + vrControl.setVRDisplay(displays[0]); + } + }); + enableVR = true; + } else { + console.log("No VR Hardware found!"); + enableVR = false; + } + + if (mobile) { + initWebVRForMobile(); + initGearVRController(); + } + else + initOculusTouch(); + }; + + // init Oculus Touch controllers + // not supported in Firefox, only Google chromium + // check https://webvr.info/get-chrome/ + var initOculusTouch = function () { + if (!enableVR) + return; + + controllerLeft = new THREE.ViveController( 0 ); + controllerRight = new THREE.ViveController( 1 ); + + var loader = new THREE.OBJLoader(); + loader.setPath( 'js/external-libraries/vr/models/obj/vive-controller/' ); + loader.load( 'vr_controller_vive_1_5.obj', function ( object ) { + + var loader = new THREE.TextureLoader(); + loader.setPath( 'js/external-libraries/vr/models/obj/vive-controller/' ); + + var controller = object.children[ 0 ]; + controller.material.map = loader.load( 'onepointfive_texture.png' ); + controller.material.specularMap = loader.load( 'onepointfive_spec.png' ); + + controllerLeft.add( object.clone() ); + controllerRight.add( object.clone() ); + + controllerLeft.standingMatrix = vrControl.getStandingMatrix(); + controllerRight.standingMatrix = vrControl.getStandingMatrix(); + + scene.add(controllerLeft); + scene.add(controllerRight); + } ); + + // controllerLeft.addEventListener('gripsup', function(e) { updateVRStatus('left'); }, true); + // controllerRight.addEventListener('gripsup', function(e) { updateVRStatus('right'); }, true); + + oculusTouchExist = true; + + console.log("Init Oculus Touch done"); + }; + + var initGearVRController = function () { + if (!enableVR || !mobile) + return; + + // assume right handed user + controllerRight = new THREE.GearVRController(0); + //controllerRight.position.set( 25, - 50, 0 ); + + + var loader = new THREE.OBJLoader(); + loader.setPath( 'js/external-libraries/vr/models/obj/vive-controller/' ); + loader.load( 'vr_controller_vive_1_5.obj', function ( object ) { + var loader = new THREE.TextureLoader(); + loader.setPath( 'js/external-libraries/vr/models/obj/vive-controller/' ); + var controller = object.children[ 0 ]; + controller.material.map = loader.load( 'onepointfive_texture.png' ); + controller.material.specularMap = loader.load( 'onepointfive_spec.png' ); + controllerRight.add( object.clone() ); + controllerRight.standingMatrix = vrControl.getStandingMatrix(); + scene.add(controllerRight); + } ); + + gearVRControllerExist = true; + + console.log("Init Gear VR Controller done"); + }; + + var initWebVRForMobile = function () { + // Initialize the WebVR UI. + var uiOptions = { + color: 'black', + background: 'white', + corners: 'round', + height: 40, + disabledOpacity: 0.9 + }; + vrButton = new webvrui.EnterVRButton(renderer.domElement, uiOptions); + vrButton.on('exit', function () { updateVRStatus('disable'); }); + vrButton.on('hide', function() { + document.getElementById('vr'+name).style.display = 'none'; + }); + vrButton.on('show', function() { + document.getElementById('vr'+name).style.display = 'inherit'; + }); + document.getElementById('vrButton'+name).appendChild(vrButton.domElement); + document.getElementById('magicWindow'+name).addEventListener('click', function() { + vr = true; + activateVR = true; + activeVR = name.toLowerCase(); + console.log("Active VR = " + activeVR); + vrButton.requestEnterFullscreen(); + }); + }; + + // scan Gear VR controller + var scanGearVRController = function () { + var thumbPad = controllerRight.getButtonState('thumbpad'); + var trigger = controllerRight.getButtonState('trigger'); + var angleX = null, angleY = null; + var gamePadRight = controllerRight.getGamepad(); + if(gamePadRight && !trigger) { + angleX = gamePadRight.axes[0]; + angleY = gamePadRight.axes[1]; + if (thumbPad) { + brain.rotateX(0.2 * angleX); + brain.rotateZ(0.2 * angleY); + } else { + brain.position.z += 5 * angleX; + brain.position.x += 5 * angleY; + } + brain.matrixWorldNeedsUpdate = true; + } + var v3Origin = new THREE.Vector3(0,0,0); + var v3UnitUp = new THREE.Vector3(0,0,-100); + + // Find all nodes within 0.1 distance from left Touch Controller + var closestNodeIndexRight = 0, closestNodeDistanceRight = 99999.9; + for (var i = 0; i < brain.children.length; i++) { + var distToNodeIRight = controllerRight.position.distanceTo(brain.children[i].getWorldPosition()); + if ( (distToNodeIRight < closestNodeDistanceRight ) ) { + closestNodeDistanceRight = distToNodeIRight; + closestNodeIndexRight = i; + } + } + + var isLeft = (activateVR == 'left'); + if(trigger) { + pointedNodeIdx = (closestNodeDistanceRight < 2.0) ? closestNodeIndexRight : -1; + + if (pointerRight) { + // Touch Controller pointer already on! scan for selection + if (thumbPad) { + updateNodeSelection(model, getPointedObject(controllerRight), isLeft); + } + } else { + pointerRight = drawPointer(v3Origin, v3UnitUp); + controllerRight.add(pointerRight); + } + updateNodeMoveOver(model, getPointedObject(controllerRight)); + } else { + if (pointerRight) { + controllerRight.remove(pointerRight); + } + pointerRight = null; + } + }; + + // scan the Oculus Touch for controls + var scanOculusTouch = function () { + var boostRotationSpeed = controllerLeft.getButtonState('grips') ? 0.1 : 0.02; + var boostMoveSpeed = controllerRight.getButtonState('grips') ? 5.0 : 1.0; + var angleX = null, angleY = null; + var gamePadLeft = controllerLeft.getGamepad(); + var gamePadRight = controllerRight.getGamepad(); + if(gamePadLeft) { + angleX = gamePadLeft.axes[0]; + angleY = gamePadLeft.axes[1]; + brain.rotateX(boostRotationSpeed * angleX); + brain.rotateZ(boostRotationSpeed * angleY); + brain.matrixWorldNeedsUpdate = true; + } + + if(gamePadRight) { + angleX = gamePadRight.axes[0]; + angleY = gamePadRight.axes[1]; + if(controllerRight.getButtonState('thumbpad')) { + brain.position.y += boostMoveSpeed * angleY; + } else { + brain.position.z += boostMoveSpeed * angleX; + brain.position.x += boostMoveSpeed * angleY; + } + brain.matrixWorldNeedsUpdate = true; + } + + var v3Origin = new THREE.Vector3(0,0,0); + var v3UnitUp = new THREE.Vector3(0,0,-100.0); + // var v3UnitFwd = new THREE.Vector3(0,0,1); + + // Find all nodes within 0.1 distance from left Touch Controller + var closestNodeIndexLeft = 0, closestNodeDistanceLeft = 99999.9; + var closestNodeIndexRight = 0, closestNodeDistanceRight = 99999.9; + for (var i = 0; i < brain.children.length; i++) { + var distToNodeILeft = controllerLeft.position.distanceTo(brain.children[i].getWorldPosition()); + if ( (distToNodeILeft < closestNodeDistanceLeft ) ) { + closestNodeDistanceLeft = distToNodeILeft; + closestNodeIndexLeft = i; + } + + var distToNodeIRight = controllerRight.position.distanceTo(brain.children[i].getWorldPosition()); + if ( (distToNodeIRight < closestNodeDistanceRight ) ) { + closestNodeDistanceRight = distToNodeIRight; + closestNodeIndexRight = i; + } + } + + var isLeft = (activateVR == 'left'); + if(controllerLeft.getButtonState('trigger')) { + pointedNodeIdx = (closestNodeDistanceLeft < 2.0) ? closestNodeIndexLeft : -1; + + if (pointerLeft) { + // Touch Controller pointer already on! scan for selection + if (controllerLeft.getButtonState('grips')) { + updateNodeSelection(model, getPointedObject(controllerLeft), isLeft); + } + } else { + pointerLeft = drawPointer(v3Origin, v3UnitUp); + controllerLeft.add(pointerLeft); + } + updateNodeMoveOver(model, getPointedObject(controllerLeft)); + } else { + if (pointerLeft) { + controllerLeft.remove(pointerLeft); + } + pointerLeft = null; + } + + if(controllerRight.getButtonState('trigger')) { + pointedNodeIdx = (closestNodeDistanceRight < 2.0) ? closestNodeIndexRight : -1; + + if (pointerRight) { + // Touch Controller pointer already on! scan for selection + if (controllerRight.getButtonState('grips')) { + updateNodeSelection(model, getPointedObject(controllerRight), isLeft); + } + } else { + pointerRight = drawPointer(v3Origin, v3UnitUp); + controllerRight.add(pointerRight); + } + updateNodeMoveOver(model, getPointedObject(controllerRight)); + } else { + if (pointerRight) { + controllerRight.remove(pointerRight); + } + pointerRight = null; + } + }; + + // draw a pointing line + var drawPointer = function (start, end) { + var material = new THREE.LineBasicMaterial(); + var geometry = new THREE.Geometry(); + geometry.vertices.push( + start, + end + ); + return new THREE.Line(geometry, material); + }; + + // initialize scene: init 3js scene, canvas, renderer and camera; add axis and light to the scene + var initScene = function () { + renderer.setSize(canvas.clientWidth, window.innerHeight); + renderer.setPixelRatio(window.devicePixelRatio); + canvas.appendChild(renderer.domElement); + raycaster = new THREE.Raycaster(); + camera.position.z = 50; + + brain = new THREE.Group(); + scene.add(brain); + + //Adding light + scene.add( new THREE.HemisphereLight(0x606060, 0x080820, 1.5)); + scene.add( new THREE.AmbientLight(0x606060, 1.5)); + var light = new THREE.PointLight( 0xffffff, 1.0, 10000 ); + light.position.set( 1000, 1000, 100 ); + scene.add(light); + + var axisHelper = new THREE.AxisHelper( 5 ); + scene.add( axisHelper ); + addNodeLabel(); + }; + + this.resetCamera = function () { + camera.position.set(50,50,50); + }; + + this.resetBrainPosition = function () { + brain.updateMatrix(); + brain.position.set(0,0,0); + brain.rotation.set(0,0,0); + brain.scale.set(1,1,1); + brain.updateMatrix(); + brain.matrixWorldNeedsUpdate = true; + }; + + // create 3js elements: scene, canvas, camera and controls; and init them and add skybox to the scene + this.createCanvas = function() { + scene = new THREE.Scene(); + renderer = new THREE.WebGLRenderer({antialias: true}); + camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / window.innerHeight, 0.1, 3000); + initScene(); + if (!mobile) { + controls = new THREE.TrackballControls(camera, renderer.domElement); + controls.rotateSpeed = 0.5; + } + addSkybox(); + }; + + // initialize scene: init 3js scene, canvas, renderer and camera; add axis and light to the scene + this.setEventListeners = function (onMouseDown, onMouseUp, onDocumentMouseMove) { + canvas.addEventListener('mousedown', onMouseDown, true); + canvas.addEventListener('mouseup', function (e) { onMouseUp(model, e);}); + canvas.addEventListener('mousemove', function (e) { onDocumentMouseMove(model, e); }, true); + }; + + // update node scale according to selection status + this.updateNodeGeometry = function (nodeIndex, status) { + var scale = 1.0; + switch (status){ + case 'normal': + scale = 1.0; + break; + case 'mouseover': + scale = 1.2; + break; + case 'selected': + scale = (8/3); + break; + case 'root': + scale = (10/3); + break; + } + glyphs[nodeIndex].scale.set(scale, scale, scale); + }; + + this.updateNodesColor = function () { + var dataset = model.getDataset(); + for (var i=0; i < glyphs.length; ++i){ + glyphs[i].material.color = new THREE.Color(scaleColorGroup(model, dataset[i].group)); + } + }; + + var removeNodesFromScene = function () { + for (var i=0; i < glyphs.length; ++i){ + brain.remove(glyphs[i]); + delete glyphNodeDictionary[glyphs[i].uuid]; + } + glyphs = []; + }; + + this.removeEdgesFromScene = function () { + for(var i=0; i < displayedEdges.length; ++i){ + brain.remove(displayedEdges[i]); + } + displayedEdges = []; + + this.removeShortestPathEdgesFromScene(); + }; + + this.removeShortestPathEdgesFromScene = function () { + for(var i=0; i < shortestPathEdges.length; i++){ + brain.remove(shortestPathEdges[i]); + } + shortestPathEdges = []; + }; + + var animatePV = function () { + if (enableVR && activateVR) { + if (oculusTouchExist) { + controllerLeft.update(); + controllerRight.update(); + scanOculusTouch(); + } + vrControl.update(); + } + else { + if (mobile) { + if (gearVRControllerExist) { + controllerRight.update(); + scanGearVRController(); + } + vrControl.update(); + } + else + controls.update(); + } + + if (enableRender) + effect.render(scene, camera); + + effect.requestAnimationFrame(animatePV); + }; + + this.requestAnimate = function () { + effect.requestAnimationFrame(animatePV); + }; + + this.enableRender = function (state) { enableRender = state; }; + + this.isVRAvailable = function () { return enableVR; }; + + this.isPresenting = function () { vrButton.isPresenting(); }; + + this.redrawEdges = function () { + this.removeEdgesFromScene(); + if (spt) + this.updateShortestPathEdges(); + this.drawConnections(); + }; + + // determine if a region should be drawn + var shouldDrawRegion = function (region) { + return (model.isRegionActive(region.group) && atlas.getLabelVisibility(region.label)); + }; + + // updating scenes: redrawing glyphs and displayed edges + this.updateScene = function (){ + updateNodesPositions(); + this.updateNodesVisibility(); + this.redrawEdges(); + }; + + // draw the brain regions as glyphs (the nodes) + // assumes all nodes are visible, nothing is selected + this.drawRegions = function () { + var dataset = model.getDataset(); + var material, geometry; + + for(var i=0; i < dataset.length; i++){ + geometry = getNormalGeometry(dataset[i].hemisphere); + material = getNormalMaterial(model, dataset[i].group); + glyphs[i] = new THREE.Mesh(geometry, material); + brain.add(glyphs[i]); + glyphNodeDictionary[glyphs[i].uuid] = i; + glyphs[i].position.set(dataset[i].position.x, dataset[i].position.y, dataset[i].position.z); + glyphs[i].userData.hemisphere = dataset[i].hemisphere; + } + }; + + // update the nodes positions according to the latest in the model + var updateNodesPositions = function () { + var dataset = model.getDataset(); + for(var i=0; i < dataset.length; i++){ + glyphs[i].position.set(dataset[i].position.x, dataset[i].position.y, dataset[i].position.z); + } + }; + + this.updateNodesVisibility = function () { + var dataset = model.getDataset(); + for(var i=0; i < dataset.length; i++){ + var opacity = 1.0; + if(root && root == i){ // root node + opacity = 1.0; + } + + if (shouldDrawRegion(dataset[i])) { + switch (model.getRegionState(dataset[i].group)){ + case 'active': + opacity = 1.0; + break; + case 'transparent': + opacity = 0.3; + break; + case 'inactive': + opacity = 0.0; + break; + } + } else { + opacity = 0.0; + } + glyphs[i].material.opacity = opacity; + } + }; + + + // draw all connections between the selected nodes, needs the connection matrix. + // don't draw edges belonging to inactive nodes + this.drawConnections = function () { + var nodeIdx; + for(var i= 0; i < nodesSelected.length; i++){ + nodeIdx = nodesSelected[i]; + // draw only edges belonging to active nodes + if(model.isRegionActive(model.getGroupNameByNodeIndex(nodeIdx))) { + // two ways to draw edges + if(thresholdModality) { + // 1) filter edges according to threshold + this.drawEdgesGivenNode(nodeIdx); + } else { + // 2) draw top n edges connected to the selected node + this.drawTopNEdgesByNode(nodeIdx, model.getNumberOfEdges()); + } + } + } + + // draw all edges belonging to the shortest path array + for(i=0; i < shortestPathEdges.length; i++){ + displayedEdges[displayedEdges.length] = shortestPathEdges[i]; + brain.add(shortestPathEdges[i]); + } + + // setEdgesColor(); + }; + + // skew the color distribution according to the nodes strength + var computeColorGradient = function (c1, c2, n, p) { + var gradient = new Float32Array( n * 3 ); + var p1 = p; var p2 = 1-p1; + for (var i = 0; i < n; ++i) { + // skew the color distribution according to the nodes strength + var r = i/(n-1); + var rr = (r*r*(p2-0.5) + r*(0.5-p2*p2))/(p1*p2); + gradient[ i * 3 ] = c2.r + (c1.r - c2.r)*rr; + gradient[ i * 3 + 1 ] = c2.g + (c1.g - c2.g)*rr; + gradient[ i * 3 + 2 ] = c2.b + (c1.b - c2.b)*rr + } + return gradient; + }; + + // set the color of displayed edges + this.updateEdgeColors = function () { + var edge, c1, c2; + for(var i = 0; i < displayedEdges.length; i++){ + edge = displayedEdges[i]; + c1 = glyphs[edge.nodes[0]].material.color; + c2 = glyphs[edge.nodes[1]].material.color; + edge.geometry.addAttribute( 'color', new THREE.BufferAttribute( computeColorGradient(c1,c2,edge.nPoints, edge.p1), 3 ) ); + } + + for(i = 0; i < shortestPathEdges.length; i++){ + edge = displayedEdges[i]; + c1 = glyphs[edge.nodes[0]].material.color; + c2 = glyphs[edge.nodes[1]].material.color; + edge.geometry.addAttribute( 'color', new THREE.BufferAttribute( computeColorGradient(c1,c2,edge.nPoints, edge.p1), 3 ) ); + } + }; + + this.updateEdgeOpacity = function (opacity) { + edgeOpacity = opacity; + for(var i = 0; i < displayedEdges.length; i++){ + displayedEdges[i].material.opacity = opacity; + } + }; + + // create a line using start and end points and give it a name + // TODO use this to allow different line sizes + // https://github.com/spite/THREE.MeshLine#create-a-threemeshline-and-assign-the-geometry + // geometry.vertices.push(end); + // var line = new THREE.MeshLine(); + // line.setGeometry( geometry ); + // material = new THREE.MeshLineMaterial(); + // var mesh = new THREE.Mesh(line.geometry, material); + var createLine = function(edge, ownerNode, nodes){ + var material = new THREE.LineBasicMaterial({ + transparent: true, + opacity: edgeOpacity, + vertexColors: THREE.VertexColors + // Due to limitations in the ANGLE layer on Windows platforms linewidth will always be 1. + }); + + var geometry = new THREE.BufferGeometry(); + var n = edge.length; + + var positions = new Float32Array( n * 3 ); + for (var i = 0; i < n; i++) { + positions[ i * 3 ] = edge[i].x; + positions[ i * 3 + 1 ] = edge[i].y; + positions[ i * 3 + 2 ] = edge[i].z; + } + geometry.addAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) ); + + var s1 = model.getNodalStrength(nodes[0]), s2 = model.getNodalStrength(nodes[1]); + var p1 = s1/(s1+s2); + var c1 = new THREE.Color(scaleColorGroup(model, model.getGroupNameByNodeIndex(nodes[0]))),// glyphs[nodes[0]].material.color, + c2 = new THREE.Color(scaleColorGroup(model, model.getGroupNameByNodeIndex(nodes[1])));// glyphs[nodes[1]].material.color; + geometry.addAttribute( 'color', new THREE.BufferAttribute( computeColorGradient(c1,c2,n,p1), 3 ) ); + + // geometry.colors = colorGradient; + var line = new THREE.Line(geometry, material); + line.name = ownerNode; + line.nPoints = n; + line.nodes = nodes; + line.p1 = p1; + return line; + }; + + var drawEdgeWithName = function (edge, ownerNode, nodes) { + var line = createLine(edge, ownerNode, nodes); + brain.add(line); + return line; + }; + + // draw the top n edges connected to a specific node + this.drawTopNEdgesByNode = function (nodeIndex, n) { + + var row = model.getTopConnectionsByNode(nodeIndex, n); + var edges = model.getActiveEdges(); + var edgeIdx = model.getEdgesIndeces(); + if (enableEB) { + model.performEBOnNode(nodeIndex); + } + for (var i =0; i < row.length; ++i) { + if ((nodeIndex != row[i]) && model.isRegionActive(model.getGroupNameByNodeIndex(i)) && visibleNodes[i]) { + displayedEdges[displayedEdges.length] = drawEdgeWithName(edges[edgeIdx[nodeIndex][row[i]]], nodeIndex, [nodeIndex, row[i]]); + } + } + + // setEdgesColor(); + }; + + // draw edges given a node following edge threshold + this.drawEdgesGivenNode = function(indexNode) { + + var row = model.getConnectionMatrixRow(indexNode); + var edges = model.getActiveEdges(); + var edgeIdx = model.getEdgesIndeces(); + if (enableEB) { + model.performEBOnNode(indexNode); + } + + for(var i=0; i < row.length ; i++){ + if((i != indexNode) && Math.abs(row[i]) > model.getThreshold() && model.isRegionActive(model.getGroupNameByNodeIndex(i)) && visibleNodes[i]) { + displayedEdges[displayedEdges.length] = drawEdgeWithName(edges[edgeIdx[indexNode][i]], indexNode, [indexNode, i]); + } + } + }; + + // give a specific node index, remove all edges from a specific node in a specific scene + this.removeEdgesGivenNode = function(indexNode) { + var l = displayedEdges.length; + + // keep a list of removed edges indexes + var removedEdges = []; + for(var i=0; i < l; i++){ + var edge = displayedEdges[i]; + //removing only the edges that starts from that node + if(edge.name == indexNode && shortestPathEdges.indexOf(edge) == -1){ + removedEdges[removedEdges.length] = i; + brain.remove(edge); + } + } + + // update the displayedEdges array + var updatedDisplayEdges = []; + for(i=0; i < displayedEdges.length; i++){ + //if the edge should not be removed + if( removedEdges.indexOf(i) == -1){ + updatedDisplayEdges[updatedDisplayEdges.length] = displayedEdges[i]; + } + } + + for(i=0; i < shortestPathEdges.length; i++){ + updatedDisplayEdges[updatedDisplayEdges.length] = shortestPathEdges[i]; + } + displayedEdges = updatedDisplayEdges; + }; + + // draw skybox from images + var addSkybox = function(){ + var folder = 'darkgrid'; + var images = [ + 'images/'+folder+'/negx.png', + 'images/'+folder+'/negy.png', + 'images/'+folder+'/negz.png', + 'images/'+folder+'/posx.png', + 'images/'+folder+'/posy.png', + 'images/'+folder+'/posz.png' + ]; + + var cubemap = THREE.ImageUtils.loadTextureCube(images); // load textures + cubemap.format = THREE.RGBFormat; + + var shader = THREE.ShaderLib['cube']; // init cube shader from built-in lib + shader.uniforms['tCube'].value = cubemap; // apply textures to shader + + // create shader material + var skyBoxMaterial = new THREE.ShaderMaterial( { + fragmentShader: shader.fragmentShader, + vertexShader: shader.vertexShader, + uniforms: shader.uniforms, + depthWrite: false, + side: THREE.BackSide + }); + + // create skybox mesh + var skybox = new THREE.Mesh( + new THREE.CubeGeometry(1500, 1500, 1500), + skyBoxMaterial + ); + + skybox.name = "skybox"; + scene.add(skybox); + }; + + // toggle skybox visibility + this.setSkyboxVisibility = function(visible){ + var results = scene.children.filter(function(d) {return d.name == "skybox"}); + var skybox = results[0]; + skybox.visible = visible; + }; + + // draw a selected node: increase it's size + this.drawSelectedNode = function (nodeIndex) { + if(nodesSelected.indexOf(nodeIndex) == -1) { + nodesSelected[nodesSelected.length] = nodeIndex; + } + this.updateNodeGeometry(nodeIndex, 'selected'); + }; + + // get intersected object beneath the mouse pointer + // detects which scene: left or right + // return undefined if no object was found + this.getIntersectedObject = function(vector) { + raycaster.setFromCamera(vector, camera); + var objectsIntersected = raycaster.intersectObjects( glyphs ); + return (objectsIntersected[0])? objectsIntersected[0] : undefined; + }; + + // callback when window is resized + this.resizeScene = function(){ + if (vrButton && vrButton.isPresenting()) { + camera.aspect = window.innerWidth / window.innerHeight; + renderer.setSize(window.innerWidth, window.innerHeight); + console.log("Resize for Mobile VR"); + } else { + camera.aspect = window.innerWidth / 2.0 / window.innerHeight; + renderer.setSize(window.innerWidth / 2.0, window.innerHeight); + console.log("Resize"); + } + camera.updateProjectionMatrix(); + }; + + // compute shortest path info for a node + this.computeShortestPathForNode = function(nodeIndex) { + console.log("Compute Shortest Path for node " + nodeIndex); + root = nodeIndex; + model.computeShortestPathDistances(nodeIndex); + }; + + // draw shortest path from root node up to a number of hops + this.updateShortestPathBasedOnHops = function () { + var hops = model.getNumberOfHops(); + var hierarchy = model.getHierarchy(root); + var edges = model.getActiveEdges(); + var edgeIdx = model.getEdgesIndeces(); + var previousMap = model.getPreviousMap(); + + this.removeShortestPathEdgesFromScene(); + + for(var i = 0; i < hierarchy.length; ++i) { + if( i < hops + 1 ) { + //Visible node branch + for(var j=0; j < hierarchy[i].length; j++){ + visibleNodes[hierarchy[i][j]] = true; + var prev = previousMap[hierarchy[i][j]]; + if(prev){ + shortestPathEdges[shortestPathEdges.length] = createLine(edges[edgeIdx[prev][hierarchy[i][j]]] , prev, [prev, i]); + } + } + } else { + for(var j=0; j < hierarchy[i].length; ++j){ + visibleNodes[hierarchy[i][j]] = false; + } + } + } + }; + + this.updateShortestPathBasedOnDistance = function () { + nodesSelected = []; + this.removeShortestPathEdgesFromScene(); + + // show only nodes with shortest paths distance less than a threshold + var threshold = model.getDistanceThreshold()/100.*model.getMaximumDistance(); + var distanceArray = model.getDistanceArray(); + for(var i=0; i < visibleNodes.length; i++){ + visibleNodes[i] = (distanceArray[i] <= threshold); + } + + var edges = model.getActiveEdges(); + var edgeIdx = model.getEdgesIndeces(); + var previousMap = model.getPreviousMap(); + + for(i=0; i < visibleNodes.length; ++i) { + if(visibleNodes[i]){ + var prev = previousMap[i]; + if(prev) { + shortestPathEdges[shortestPathEdges.length] = createLine(edges[edgeIdx[prev][i]], prev, [prev, i]); + } + } + } + }; + + this.updateShortestPathEdges = function () { + switch (shortestPathVisMethod) { + case (SHORTEST_DISTANCE): + this.updateShortestPathBasedOnDistance(); + break; + case (NUMBER_HOPS): + this.updateShortestPathBasedOnHops(); + break; + } + }; + + // prepares the shortest path from a = root to node b + this.getShortestPathFromRootToNode = function(target) { + this.removeShortestPathEdgesFromScene(); + + var i = target; + var prev; + var edges = model.getActiveEdges(); + var edgeIdx = model.getEdgesIndeces(); + var previousMap = model.getPreviousMap(); + + visibleNodes.fill(true); + while(previousMap[i]!= null){ + prev = previousMap[i]; + visibleNodes[prev] = true; + shortestPathEdges[shortestPathEdges.length] = createLine(edges[edgeIdx[prev][i]], prev, [prev, i] ); + i = prev; + } + + this.drawConnections(); + }; + + // get intersected object pointed to by Vive/Touch Controller pointer + // return undefined if no object was found + var getPointedObject = function(controller) { + + var gamePad = controller.getGamepad(); + if (gamePad) { + var orientation = new THREE.Quaternion().fromArray(gamePad.pose.orientation); + var v3orientation = new THREE.Vector3(0,0,-1.0); + v3orientation.applyQuaternion(orientation); + var ray = new THREE.Raycaster(controller.position, v3orientation); + var objectsIntersected = ray.intersectObjects(glyphs); + if (objectsIntersected[0]) { + //console.log(objectsIntersected[0]); + return objectsIntersected[0]; + } + } + return undefined; + }; + + // Update the text and position according to selected node + // The alignment, size and offset parameters are set by experimentation + // TODO needs more experimentation + this.updateNodeLabel = function(text, nodeIndex) { + var context = nspCanvas.getContext('2d'); + context.textAlign = 'left'; + context.clearRect(0, 0, 256*4, 256); + context.fillText(text, 5, 120); + + nodeNameMap.needsUpdate = true; + var pos = glyphs[nodeIndex].position; + nodeLabelSprite.position.set(pos.x, pos.y, pos.z); + nodeLabelSprite.needsUpdate = true; + }; + + // Adding Node label Sprite + var addNodeLabel = function() { + + nspCanvas = document.createElement('canvas'); + var size = 256; + nspCanvas.width = size*4; + nspCanvas.height = size; + var context = nspCanvas.getContext('2d'); + context.fillStyle = '#ffffff'; + context.textAlign = 'left'; + context.font = '24px Arial'; + context.fillText("", 0, 0); + + nodeNameMap = new THREE.Texture(nspCanvas); + nodeNameMap.needsUpdate = true; + + var mat = new THREE.SpriteMaterial({ + map: nodeNameMap, + transparent: false, + useScreenCoordinates: false, + color: 0xffffff + }); + + nodeLabelSprite = new THREE.Sprite(mat); + nodeLabelSprite.scale.set( 100, 50, 1 ); + nodeLabelSprite.position.set( 0, 0, 0 ); + brain.add(nodeLabelSprite); + }; + + this.getCamera = function() { return camera; }; + + this.syncCameraWith = function(cam) { + camera.copy(cam); + camera.position.copy(cam.position); + camera.rotation.copy(cam.rotation); + camera.zoom = cam.zoom; + // camera.quaternion.copy(cam.quaternion); + // camera.updateMatrix(); + }; + + // PreviewArea construction + this.createCanvas(); + this.initVR(); + this.drawRegions(); +} + +//export {PreviewAreaOLD} \ No newline at end of file diff --git a/js/utils/Dijkstra.js b/js/utils/Dijkstra.js index c3b212b..5a59681 100644 --- a/js/utils/Dijkstra.js +++ b/js/utils/Dijkstra.js @@ -3,6 +3,10 @@ var updateNeeded = true; +function setUpdateNeeded(value) { + updateNeeded = value; +} + function PriorityQueue () { this._nodes = []; @@ -117,3 +121,5 @@ function Graph() { return (hierarchy) ? hierarchy.length : 0; } } + +export {Graph,setUpdateNeeded} \ No newline at end of file diff --git a/js/utils/gpu-forcebundling.js b/js/utils/gpu-forcebundling.js index 40c3a48..06587fb 100644 --- a/js/utils/gpu-forcebundling.js +++ b/js/utils/gpu-forcebundling.js @@ -4,8 +4,10 @@ * Jieting Wu et.al. 'Texture-Based Edge Bundling: A Web-Based Approach for Interactively Visualizing Large Graphs' */ -(function() { - d3.GPUForceEdgeBundling = function () { +import * as vizit from './GPGPUtility'; + +//(function() { +var GPUForceEdgeBundling = function () { var nodes = [], // {'nodeid':{'x':,'y':},..} edges = [], // [{'source':'nodeid1', 'target':'nodeid2'},..] nEdges, // number of edges @@ -450,4 +452,6 @@ return forcebundle; } -})(); +//})(); + +export { GPUForceEdgeBundling } diff --git a/js/utils/parsingData.js b/js/utils/parsingData.js index 89f1d67..48356c5 100644 --- a/js/utils/parsingData.js +++ b/js/utils/parsingData.js @@ -2,12 +2,16 @@ * Created by giorgioconte on 31/01/15. */ -var folder; +import {labelLUT, dataFiles, atlas, folder, setDataFile, setAtlas} from "../globals"; +import {Atlas} from "../atlas" +import * as Papa from "papaparse"; + +//var folder='Demo6'; var setFolder = function (folderName, callback) { folder = folderName; console.log("Source folder set to: ", folder); - callback(null,null); + callback(null, null); }; // it is assumed all data folder should have an index.txt file describing its contents @@ -25,28 +29,29 @@ var scanFolder = function (callback) { }, complete: function (results) { console.log("Loading subjects ..."); - dataFiles = results.data; + setDataFile(results.data); console.log("Subjects loaded"); - callback(null,null); - } - }); -}; - -var loadIcColors = function (callback) { - Papa.parse("./data//WB2s1IC.csv", { - download: true, - delimiter: ",", - dynamicTyping: true, - error:"continue", - skipEmptyLines: true, - complete: function (results) { - modelLeft.setICColor(results); - modelRight.setICColor(results); callback(null, null); } }); }; +//temporary disabled todo: fix this? +// var loadIcColors = function (callback) { +// Papa.parse("./data//WB2s1IC.csv", { +// download: true, +// delimiter: ",", +// dynamicTyping: true, +// error: "continue", +// skipEmptyLines: true, +// complete: function (results) { +// modelLeft.setICColor(results); +// modelRight.setICColor(results); +// callback(null, null); +// } +// }); +// }; + // the look up table is common for all subjects of a dataset, provides information about a specific Atlas // for each label we have: // label#: label number in the Atlas (mandatory) @@ -57,7 +62,7 @@ var loadIcColors = function (callback) { // rich_club: rich club affiliation: region name vs non-RichClub (optional) var loadLookUpTable = function (callback) { var labelsLUTFilename = "LookupTable_" + labelLUT + ".csv"; - Papa.parse("data/"+labelsLUTFilename, { + Papa.parse("data/" + labelsLUTFilename, { download: true, delimiter: ";", dynamicTyping: true, @@ -65,7 +70,7 @@ var loadLookUpTable = function (callback) { skipEmptyLines: true, complete: function (results) { console.log("Setting up Look-up Table"); - atlas = new Atlas(results); + setAtlas(new Atlas(results)); console.log("Look up table loaded ... "); callback(null, null); } @@ -73,31 +78,31 @@ var loadLookUpTable = function (callback) { }; var loadSubjectNetwork = function (fileNames, model, callback) { - Papa.parse("data/"+folder + "/" + fileNames.network,{ + Papa.parse("data/" + folder + "/" + fileNames.network, { download: true, dynamicTyping: true, delimiter: ',', header: false, skipEmptyLines: true, - complete: function(results){ + complete: function (results) { model.setConnectionMatrix(results); console.log("NW loaded ... "); - callback(null,null); + callback(null, null); } }); }; var loadSubjectTopology = function (fileNames, model, callback) { - Papa.parse("data/"+folder + "/" + fileNames.topology,{ + Papa.parse("data/" + folder + "/" + fileNames.topology, { download: true, dynamicTyping: true, delimiter: ',', header: false, skipEmptyLines: true, - complete: function(results){ + complete: function (results) { model.setTopology(results.data); console.log("Topology loaded ... "); - callback(null,null); + callback(null, null); } }); }; @@ -118,18 +123,20 @@ var loadColorMap = function (callback) { };*/ -var loadMetricValues = function(callback){ +var loadMetricValues = function (callback) { console.log("Loading metric file"); - Papa.parse("data/Anatomic/metric.csv",{ + Papa.parse("data/Anatomic/metric.csv", { download: true, delimiter: ',', dynamicTyping: true, header: false, skipEmptyLines: true, - complete: function(results){ + complete: function (results) { modelLeft.setMetricValues(results); modelRight.setMetricValues(results); - callback(null,null); + callback(null, null); } }) -}; \ No newline at end of file +}; + +export {scanFolder, loadMetricValues, loadLookUpTable, loadSubjectTopology, loadSubjectNetwork} \ No newline at end of file diff --git a/js/utils/scale.js b/js/utils/scale.js index e0198d6..67b89e0 100644 --- a/js/utils/scale.js +++ b/js/utils/scale.js @@ -2,6 +2,9 @@ * Created by giorgioconte on 02/02/15. */ +import * as d3 from '../external-libraries/d3' +import {modelLeft,modelRight} from '../model'; + // var connectionMatrixScale; var groupColor = d3.scale.category10(); var metric = false; @@ -25,7 +28,7 @@ var colorMap = { 'Caudate':'#ad494a' }; -scaleColorGroup = function(model, group) { +var scaleColorGroup = function(model, group) { var color; var filteredGroup; @@ -59,8 +62,15 @@ scaleColorGroup = function(model, group) { }; // set group color according to the activeGroup number of elements -setColorGroupScale = function() { - groupColor = (modelLeft.getActiveGroup().length <= 10) ? d3.scale.category10() : d3.scale.category20(); +var setColorGroupScale = function (side) { //model) { + var model; + if (side !== "Left") { + model = modelRight + } else { + model = modelLeft; + } + //groupColor = (modelLeft.getActiveGroup().length <= 10) ? d3.scale.category10() : d3.scale.category20(); + groupColor = (model.getActiveGroup().length <= 10) ? d3.scale.category10() : d3.scale.category20(); }; // return a power scale function for the adjacency matrix @@ -90,4 +100,6 @@ setColorGroupScale = function() { var round = function(number, digits){ digits = Math.pow(10,digits); return Math.round(number*digits)/digits; -}; \ No newline at end of file +}; + +export {scaleColorGroup,setColorGroupScale} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..6cc6a05 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "NeuroCave", + "version": "1.0.0", + "description": "![alt text](readme_images/f2500withInset.png \"NeuroCave\")", + "main": "index.js", + "dependencies": { + "d3": "^7.6.1", + "mathjs": "^11.3.1", + "papaparse": "^5.3.2", + "queue": "^6.0.2", + "three": "^0.145.0" + }, + "devDependencies": { + "@babel/core": "^7.19.6", + "babel-loader": "^8.2.5", + "path": "^0.12.7", + "webpack": "^5.75.0", + "webpack-cli": "^4.10.0", + "http-server": "^14.1.1" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "webpack --watch --mode=development", + "prod": "webpack --mode=production", + "local": "webpack --mode=production && http-server ./" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/iMammal/NeuroCave.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/iMammal/NeuroCave/issues" + }, + "homepage": "https://github.com/iMammal/NeuroCave#readme" +} diff --git a/style/style.css b/style/style.css index a422346..5f25455 100644 --- a/style/style.css +++ b/style/style.css @@ -114,6 +114,15 @@ button > input[type="checkbox"] { z-index: 2 } +#legendLeft { + position: absolute; + width: 200px; + height: 200px; + bottom: 10px; + left: 35%; + z-index: 2 +} + #shortestPath { @@ -133,6 +142,29 @@ button > input[type="checkbox"] { z-index: 3; } +#colorCodingLeft { + position: absolute; + width: 175px; + height: 25px; + top: 400px; + left: 35%; + z-index: 3; +} + +#syncColorLeft { + position: relative; + height: 25px; + top: 5px; + left: 20px; +} + +#syncColorRight { + position: relative; + height: 25px; + top: 5px; + left: 20px; +} + #viewLeft { position: absolute; height: 100px; @@ -226,7 +258,7 @@ button > input[type="checkbox"] { #nodeInfoPanel { position: absolute; width: 300px; - height: 130px; + height: 120px; bottom: 10px; left: 10px; z-index: 2; @@ -238,10 +270,40 @@ button > input[type="checkbox"] { vertical-align: middle; } +#nodeInfoPanelLeft { + position: absolute; + width: 300px; + height: 160px; + bottom: 10px; + left: 10px; + z-index: 3; + color: antiquewhite; +} + +#nodeInfoPanelLeft > p { + text-align:center; + vertical-align: middle; +} + +#nodeInfoPanelRight { + position: absolute; + width: 360px; + height: 160px; + bottom: 10px; + right: 30%; + z-index: 3; + color: antiquewhite; +} + +#nodeInfoPanelRight > p { + text-align:center; + vertical-align: middle; +} + #edgeInfoPanel { position: absolute; width: 200px; - height: 200px; + height: 360px; top: 10px; right: 10px; z-index: 2 diff --git a/visualization.html b/visualization.html index f4932af..15c2273 100644 --- a/visualization.html +++ b/visualization.html @@ -12,45 +12,48 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + +- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -58,18 +61,27 @@
-
-
+
+
+ -