diff --git a/src/firefly/bin/firefly b/src/firefly/bin/firefly index b93aa409..4294b88f 100644 --- a/src/firefly/bin/firefly +++ b/src/firefly/bin/firefly @@ -74,7 +74,7 @@ def define_parser(): parser.add_argument('--copy_source', action='store_true', help = 'Flag to tell the ``firefly`` command to copy the source files for Firefly into the directory specified by ``directory``. (If this flag is not supplied, the default behavior is to set copy_source=False).') parser.add_argument('--multiple_rooms', action='store_true', - help = 'flag to enable multiple rooms. If set, the user will be prompted in the browser to enter a string to define the room for the given session, which would allow multiple users to interact with separate Firefly instances on a server. (if this flag is not supplied, the default behavior is to set multiple_rooms=False) ') + help = 'flag to enable multiple rooms. If set, the user will be prompted in the browser to enter a string to define the room for the given session, which would allow multiple users to interact with separate Firefly instances on a server. (If this flag is not supplied, the default behavior is to set multiple_rooms=False) ') return parser diff --git a/src/firefly/index.html b/src/firefly/index.html index 9ccf6c14..16998fbe 100644 --- a/src/firefly/index.html +++ b/src/firefly/index.html @@ -117,6 +117,7 @@ + diff --git a/src/firefly/ntbks/passing_settings.ipynb b/src/firefly/ntbks/passing_settings.ipynb new file mode 100644 index 00000000..c0f6e2b6 --- /dev/null +++ b/src/firefly/ntbks/passing_settings.ipynb @@ -0,0 +1,120 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "bb752e39", + "metadata": {}, + "source": [ + "# Passing settings in Firefly between JS and Python\n", + "\n", + "Currently we only support passing settings back and forth, but in the future this will include getting data in Python from selections made within Firefly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50bdd5cb", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import IFrame\n", + "from firefly.server import spawnFireflyServer\n", + "import requests\n", + "import json" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f17443e", + "metadata": {}, + "outputs": [], + "source": [ + "# define the port and start the firefly server\n", + "port = 5500\n", + "process = spawnFireflyServer(port, method = 'flask')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8c02224", + "metadata": {}, + "outputs": [], + "source": [ + "IFrame(f'http://localhost:{port:d}/combined', width = 800, height = 500)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f3592d6", + "metadata": {}, + "outputs": [], + "source": [ + "# send a get request to receive the current settings from Firefly\n", + "r = requests.get(url = f'http://localhost:{port:d}/get_settings')#, params={'room':'myroom'})\n", + "settings = r.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14d69657", + "metadata": {}, + "outputs": [], + "source": [ + "print(settings['useStereo'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac656d62", + "metadata": {}, + "outputs": [], + "source": [ + "# change a setting and pass it back to firefly \n", + "settings['useStereo'] = not settings['useStereo']\n", + "requests.post(f'http://localhost:{port:d}/post_settings', json=json.dumps({'settings':settings}))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbe82312", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9826d92c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/firefly/ntbks/selecting_data.ipynb b/src/firefly/ntbks/selecting_data.ipynb new file mode 100644 index 00000000..e1fdb14e --- /dev/null +++ b/src/firefly/ntbks/selecting_data.ipynb @@ -0,0 +1,197 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "bb752e39", + "metadata": {}, + "source": [ + "# Selecting data points in Firefly and receiving data in Python\n", + "\n", + "This is a test notebook working on accessing data in Python from selections made within Firefly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50bdd5cb", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import IFrame\n", + "from firefly.server import spawnFireflyServer, quitAllFireflyServers\n", + "import requests\n", + "import json\n", + "import os\n", + "\n", + "import matplotlib.pyplot as plt\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f17443e", + "metadata": {}, + "outputs": [], + "source": [ + "# define the port and start the firefly server\n", + "port = 5500\n", + "directory = os.path.join(os.getcwd(),'..')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4985582d", + "metadata": {}, + "outputs": [], + "source": [ + "# start the server\n", + "process = spawnFireflyServer(port, method = 'flask', directory = directory)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8c02224", + "metadata": {}, + "outputs": [], + "source": [ + "# launch the iframe \n", + "IFrame(f'http://localhost:{port:d}/combined', width = 800, height = 500)" + ] + }, + { + "cell_type": "markdown", + "id": "c172460f", + "metadata": {}, + "source": [ + "## Get the selected data in Python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f3592d6", + "metadata": {}, + "outputs": [], + "source": [ + "# send a get request to receive the current settings from Firefly\n", + "# for larger amounts of data, you will need to increase the waitTime (in seconds) via params (see below; the default is 10s)\n", + "r = requests.get(url = f'http://localhost:{port:d}/get_selected_data', params = {'waitTime':60})\n", + "if r.status_code == 200:\n", + " # success\n", + " selection = r.json()\n", + " print(selection['Gas']['Coordinates_flat'][:100])\n", + "else:\n", + " print('Error: {}'.format(r.status_code), r.content)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c9d996d", + "metadata": {}, + "outputs": [], + "source": [ + "# plot x, y for the selected points\n", + "partsKeys = list(selection.keys())\n", + "part0 = selection[partsKeys[0]]\n", + "x = part0['Coordinates_flat'][0::3]\n", + "y = part0['Coordinates_flat'][1::3]\n", + "f, ax = plt.subplots()\n", + "ax.scatter(x[:1000],y[:1000])" + ] + }, + { + "cell_type": "markdown", + "id": "b2637fdf", + "metadata": {}, + "source": [ + "## You can also get and set the settings with similar commands" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bef07ea9", + "metadata": {}, + "outputs": [], + "source": [ + "# send a get request to receive the current settings from Firefly\n", + "r = requests.get(url = f'http://localhost:{port:d}/get_settings')#, params = {'room':'myroom'})\n", + "settings = r.json()\n", + "print(settings)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6cbf1cd9", + "metadata": {}, + "outputs": [], + "source": [ + "print(settings['useStereo'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac656d62", + "metadata": {}, + "outputs": [], + "source": [ + "# change a setting and pass it back to firefly \n", + "settings['useStereo'] = not settings['useStereo']\n", + "requests.post(f'http://localhost:{port:d}/post_settings', json=json.dumps({'settings':settings}))" + ] + }, + { + "cell_type": "markdown", + "id": "ddf98dc4", + "metadata": {}, + "source": [ + "## Quite the Firefly server" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9826d92c", + "metadata": {}, + "outputs": [], + "source": [ + "return_code = quitAllFireflyServers()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53db44ea", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/firefly/server.py b/src/firefly/server.py index 5b0bb53d..bc480284 100644 --- a/src/firefly/server.py +++ b/src/firefly/server.py @@ -1,3 +1,10 @@ +# to run locally for development +# Note: if you have installed the pypi version of firefly previously, you must uninstall it first +# (and/or create a new conda env) +# $ pip install -e . +# $ firefly --method="flask" --directory="/foo/bar/Firefly/src/firefly" + + import os import sys import json @@ -11,9 +18,12 @@ import numpy as np -from flask import Flask, render_template, request, session, current_app +from flask import Flask, Response, abort, render_template, request, session, current_app from flask_socketio import SocketIO, emit, join_room, leave_room +from eventlet import event +from eventlet.timeout import Timeout + from firefly.data_reader import SimpleReader #in principle, we could read in the data here... @@ -30,16 +40,12 @@ app.config['SECRET_KEY'] = 'secret!' socketio = SocketIO(app, async_mode=async_mode) - namespace = '/Firefly' default_room = 'default_Firefly_AMG_ABG' rooms = {} #will be updated below -#number of seconds between updates -seconds = 0.01 - #for the stream fps = 30 @@ -49,6 +55,10 @@ #check if the GUI is separated to see if we need to send a reload signal (currently not used) GUIseparated = False +events = {} + + + ####### setting the room (to keep each session distinct) @socketio.on('join', namespace=namespace) def on_join(message): @@ -77,7 +87,7 @@ def disconnect(): # will fire when user connects @socketio.on('connect', namespace=namespace) def connect(): - # if there is a room defined, emit that. If there is no room defined, then the client will be prompted to enter one before joining + print("======= socket connected") emit('room_check',{'room': default_room}, namespace=namespace) @@ -90,14 +100,14 @@ def connection_test(message): ######for viewer -#will receive data from viewer +#will receive data for viewer @socketio.on('viewer_input', namespace=namespace) def viewer_input(message): if (request.sid in rooms): socketio.emit('update_viewerParams', message, namespace=namespace, to=rooms[request.sid]) #######for GUI -#will receive data from gui +#will receive data for gui @socketio.on('gui_input', namespace=namespace) def gui_input(message): if (request.sid in rooms): @@ -241,6 +251,154 @@ def stream_input(): return 'Done' +@app.route('/get_settings', methods = ['GET']) +def get_settings(): + global events + events = {} + print('======= received request for settings from user') + + # I have not tested to make sure this works with passing a room + room = request.args.get('room') + if (not room): + room = default_room + + waitTime = request.args.get('timeout') + if (not waitTime): + waitTime = 10 #seconds + + try: + print('======= gettings settings data') + + # send a request to JS to return the settings + socketio.emit('output_settings', {'data':None}, namespace=namespace, to=room) + + # wait for the settings to come back + timeout = Timeout(waitTime) + try: + e = events[room] = event.Event() + resp = e.wait() + except Timeout: + print('!!!!!!!!!!!!!!! TIMEOUT') + return Response('Timeout. Please increase the waitTime using the params keyword', status = 504) + # abort(504) + finally: + events.pop(room, None) + timeout.cancel() + + return json.dumps(resp) + + except: + print('!!!!!!!!!!!!!!! ERROR') + return Response('Unknown error. Please try again', status = 500) + +# receive settings from JS and send it back via events to the GET location below +@socketio.on('send_settings', namespace=namespace) +def send_settings(message): + try: + e = events[message['room']] + e.send(message['settings']) + except: + pass + + +@app.route('/post_settings', methods = ['POST']) +def post_settings(): + print('======= received settings from server ...') + jsondata = request.get_json() + data = json.loads(jsondata) + settings = data['settings'] + + if ('room' in data): + room = data['room'] + else: + room = default_room + + if (room): + socketio.emit('input_settings', settings, namespace=namespace, to=room) + print('======= done') + return 'Done' + else: + print('User must specify a name for the websocket "room" connected to an active firefly instance.') + return 'Error' + +@app.route('/get_selected_data', methods = ['GET']) +def get_selected_data(): + global events + events = {} + print('======= received request for selected data from user') + + # I have not tested to make sure this works with passing a room + room = request.args.get('room') + if (not room): + room = default_room + + waitTime = request.args.get('waitTime') + if (not waitTime): + waitTime = 10 #seconds + + try: + print(f'======= gettings selected data, waiting {waitTime}s') + + # send a request to JS to return the settings + socketio.emit('output_selected_data', {'data':None}, namespace=namespace, to=room) + + # wait for all the data to come back + timeout = Timeout(int(waitTime)) + try: + e = events[room] = event.Event() + resp = e.wait() + except Timeout: + print('!!!!!!!!!!!!!!! TIMEOUT') + return Response('Timeout. Please increase the waitTime using the params keyword', status = 504) + # abort(504) + finally: + events.pop(room, None) + timeout.cancel() + + return json.dumps(resp) + + except: + print('!!!!!!!!!!!!!!! ERROR') + return Response('Unknown error. Please try again', status = 500) + +def compileData(current, new, keyList): + + def getPath(dataDict, path): + # https://stackoverflow.com/questions/59323310/python-get-pointer-to-an-item-in-a-nested-dictionary-list-combination-based-on-a + insertPosition = dataDict + for k in path: + insertPosition = insertPosition[k] + return insertPosition + + getPath(current, keyList).extend(new) + + return current + + +# receive selecte data from JS and send it back via events to the GET location below +selectedData = {} +@socketio.on('send_selected_data', namespace=namespace) +def send_selected_data(message): + global selectedData + # print('have', message['pass'], message['keyList'], message['done']) + try: + e = events[message['room']] + + # the first pass should be for the data structure + if (message['pass'] == 'structure'): + selectedData = message['data'] + # print('data structure = ', data) + + if (message['pass'] == 'data'): + try: + selectedData = compileData(selectedData, message['data'], message['keyList']) + except: + print('error compiling data', message['keyList'], message['done']) + if (message['done']): + e.send(selectedData) + except: + pass + def reload(): #currently not used if (GUIseparated): @@ -274,9 +432,10 @@ def startFlaskServer( """ global default_room - if (multiple_rooms): default_room = None + if (multiple_rooms): default_room = None if (directory is None or directory == "None"): directory = os.path.dirname(__file__) + old_dir = os.getcwd() try: print(f"Launching Firefly at: http://localhost:{port}") @@ -292,7 +451,7 @@ def startFlaskServer( fps = frames_per_second dec = decimation_factor - socketio.run(app, host='0.0.0.0', port=port)#, use_reloader=True) + socketio.run(app, host='0.0.0.0', port=port, use_reloader=True) except: raise finally: os.chdir(old_dir) @@ -349,6 +508,7 @@ def spawnFireflyServer( a string to define the room for the given session (which would allow multiple users to interact with separate Firefly instances on a server), defaults to False. :type multiple_rooms: bool, optional + :return: subprocess.Popen :rtype: subprocess handler :raises RuntimeError: if max_time elapses without a successful Firefly server being initialized. diff --git a/src/firefly/static/css/mainStyles.css b/src/firefly/static/css/mainStyles.css index f88c3485..b07f439b 100644 --- a/src/firefly/static/css/mainStyles.css +++ b/src/firefly/static/css/mainStyles.css @@ -110,13 +110,14 @@ canvas{ padding-left: 50%; margin-left:-20%; width:40%; + margin-bottom:5vmin; } #splashdiv5 { font-size:10px; font-size:2vmin; margin:0; padding:0; - padding-top:15vmin; + padding-top:10vmin; } #splashdivLoader { margin:0; @@ -267,4 +268,13 @@ a:visited { } #flyExplainerHider:hover{ color: black; +} +.loaderText{ + text-align:center; + color:white; + /* font-size:20px; */ + margin:0; + padding:0; + padding-top:10px; + width:100% } \ No newline at end of file diff --git a/src/firefly/static/js/gui/GUIParams.js b/src/firefly/static/js/gui/GUIParams.js index 8b257176..dfae29ad 100644 --- a/src/firefly/static/js/gui/GUIParams.js +++ b/src/firefly/static/js/gui/GUIParams.js @@ -187,6 +187,12 @@ function defineGUIParams(){ this.VideoCapture_format = 0; // index of format this.VideoCapture_formats = ['.gif','.png','.jpg']//,'.webm'] // webm doesn't seem to be working :\ + // I could change this to match how this is defined in viewerParams... + this.selector = new function() { + this.radius = 10.; + this.distance = 100.; + this.active = false; + } this.GUIState_variables = [ 'built','current','id','name','builder','parent','children','url','button','segments','d3Element' @@ -222,6 +228,10 @@ function defineGUIParams(){ 'loadNewData':{ 'id':'loadNewData', 'builder':createLoadNewDataSegment + }, + 'dataSelector':{ + 'id':'dataSelector', + 'builder':createDataSelectorSegment } }, 'camera':{ diff --git a/src/firefly/static/js/gui/GUIconstructors.js b/src/firefly/static/js/gui/GUIconstructors.js index 3e2bd6db..f7c328d3 100644 --- a/src/firefly/static/js/gui/GUIconstructors.js +++ b/src/firefly/static/js/gui/GUIconstructors.js @@ -55,7 +55,6 @@ function createDecimationSegment(container,parent,name){ var segment = container.append('div') .attr('id', name+'Div') .style('width',(GUIParams.containerWidth - 10) + 'px') - .style('margin-left','5px') .style('margin-top','10px') .style('display','inline-block') segment.append('div') @@ -135,11 +134,9 @@ function createPresetSegment(container,parent,name){ .attr('class','button') .style('width',(GUIParams.containerWidth - 10) + 'px') .style('margin-left','0px') // TODO: padding is being double counted in main/general/data pane. RIP - .on('click',function(){ - sendToViewer([{'savePreset':null}]); - }) + .on('click',savePreset) .append('span') - .text('Save Settings'); + .text('Save Settings'); return segment_height; } function createResetSegment(container,parent,name){ @@ -182,6 +179,7 @@ function createLoadNewDataSegment(container,parent,name){ .attr('id','loadNewDataButton') .attr('class','button') .style('width',(GUIParams.containerWidth - 10) + 'px') + .style('margin-left','0px') .on('click',function(){ sendToViewer([{'loadNewData':null}]); }) @@ -192,6 +190,101 @@ function createLoadNewDataSegment(container,parent,name){ return segment_height; } +function createDataSelectorSegment(container, parent, name){ + var segment_height = 25; + + // on/off checkbox + var new_container = container.append('div') + .attr('id','dataSelectorCheckBoxContainer'); + + var checkbox = new_container.append('input') + .attr('id',name+'Elm') + .attr('value',GUIParams.selector.active) + .attr('type','checkbox') + .attr('autocomplete','off') + .on('change',function(){ + sendToViewer([{'toggleDataSelector':this.checked}]); + GUIParams.selector.active = this.checked; + }) + .style('margin','8px 0px 0px 0px') + + if (GUIParams.selector.active) checkbox.attr('checked',true); + + new_container.append('label') + .attr('for','dataSelectorCheckBoxContainer') + .text('Enable data selector sphere') + .style('margin-left','10px') + + // radius slider + segment_height += 35; + + var segment = container.append('div') + .attr('id', name+'RsliderDiv') + .style('width',(GUIParams.containerWidth - 10) + 'px') + .style('margin-top','10px') + .style('display','inline-block') + segment.append('div') + .attr('class','pLabelDiv') + .style('width','62px') + .style('display','inline-block') + .text('Radius'); + segment.append('div') + .attr('class','NSliderClass') + .attr('id','DSRSlider') + .style('margin-left','18px') + .style('width',(GUIParams.containerWidth - 122) + 'px'); + segment.append('input') + .attr('class','NMaxTClass') + .attr('id','DSRMaxT') + .attr('type','text') + .style('left',(GUIParams.containerWidth - 45) + 'px') + .style('width','40px'); + createDataSelectorRadiusSlider(); + + // z distance slider + segment_height += 35; + + var segment = container.append('div') + .attr('id', name+'DsliderDiv') + .style('width',(GUIParams.containerWidth - 10) + 'px') + .style('margin-top','10px') + .style('display','inline-block') + segment.append('div') + .attr('class','pLabelDiv') + .style('width','62px') + .style('display','inline-block') + .text('Distance'); + segment.append('div') + .attr('class','NSliderClass') + .attr('id','DSZSlider') + .style('margin-left','18px') + .style('width',(GUIParams.containerWidth - 122) + 'px'); + segment.append('input') + .attr('class','NMaxTClass') + .attr('id','DSZMaxT') + .attr('type','text') + .style('left',(GUIParams.containerWidth - 45) + 'px') + .style('width','40px'); + createDataSelectorDistanceSlider(); + + // download button + segment_height += 35; + + //save preset button + container.append('div').attr('id','downloadSelectedDataDiv') + .append('button') + .attr('id','downloadSelectedDatatButton') + .attr('class','button') + .style('width',(GUIParams.containerWidth - 10) + 'px') + .style('margin-left','0px') + .on('click',function(){ + if (GUIParams.selector.active) downloadSelection(); // should there be a warning if the selector is not enabled? + }) + .append('span') + .text('Download selected data'); + + return segment_height; +} function createCenterTextBoxesSegment(container,parent,name){ // TODO disabling the lock checkbox is tied disabling the center text box diff --git a/src/firefly/static/js/gui/GUIsocket.js b/src/firefly/static/js/gui/GUIsocket.js index 76dc7eab..cbf9ea91 100644 --- a/src/firefly/static/js/gui/GUIsocket.js +++ b/src/firefly/static/js/gui/GUIsocket.js @@ -11,6 +11,7 @@ function connectGUISocket(){ // this happens when the server connects. // all other functions below here are executed when the server emits to that name. socketParams.socket.on('connect', function() { + console.log('sending connection from gui') socketParams.socket.emit('connection_test', {data: 'GUI connected!'}); }); // socketParams.socket.on('connection_response', function(msg) { @@ -30,7 +31,7 @@ function connectGUISocket(){ socketParams.socket.on('update_GUIParams', function(msg) { - //console.log('===have commands from viewer', msg) + // console.log('===have commands from viewer', msg) setParams(msg); }); @@ -362,3 +363,12 @@ function updateOctreeLoadingBarUI(input){ //d3.select('#' + input.p + 'octreeLoadingText').text(input.p + ' (' + Math.round(frac*100) + '%)'); } } + +function savePreset(){ + // NOTE: AMG moved this to the viewer side because all the other functions are on that side. + // But in a split screen mode, it is probably better for the download to happen on the GUI side... + sendToGUI([{'savePresetViewer':null}]); +} + + + diff --git a/src/firefly/static/js/gui/sliders.js b/src/firefly/static/js/gui/sliders.js index 3e8d78ac..ac3ab0af 100644 --- a/src/firefly/static/js/gui/sliders.js +++ b/src/firefly/static/js/gui/sliders.js @@ -509,4 +509,62 @@ function createFilterSliders(p){ } }); +} + +function createDataSelectorRadiusSlider(){ + var initialValue = parseFloat(GUIParams.selector.radius); + + var sliderArgs = { + start: [initialValue], + connect: [true, false], + tooltips: false, + steps: [0.01], + range: { + 'min': [0], + 'max': [initialValue] + }, + format: wNumb({ + decimals: 2 + }) + } + + var slider = document.getElementById('DSRSlider'); + var text = [document.getElementById('DSRMaxT')]; + var varToSet = [initialValue, "selector", "radius"] + var varArgs = {'f':'setViewerParamByKey','v':varToSet}; + + createSlider(slider, text, sliderArgs, varArgs, [null, 1]); + + //reformat + w = parseInt(d3.select("#DSRSlider").style("width").slice(0,-2)); + d3.select("#DSRSlider").select('.noUi-base').style('width',w-10+"px"); +} + +function createDataSelectorDistanceSlider(){ + var initialValue = parseFloat(GUIParams.selector.distance); + + var sliderArgs = { + start: [initialValue], + connect: [true, false], + tooltips: false, + steps: [0.01], + range: { + 'min': [0], + 'max': [initialValue] + }, + format: wNumb({ + decimals: 2 + }) + } + + var slider = document.getElementById('DSZSlider'); + var text = [document.getElementById('DSZMaxT')]; + var varToSet = [initialValue, "selector", "distance"] + var varArgs = {'f':'setViewerParamByKey','v':varToSet}; + + createSlider(slider, text, sliderArgs, varArgs, [null, 1]); + + //reformat + w = parseInt(d3.select("#DSZSlider").style("width").slice(0,-2)); + d3.select("#DSZSlider").select('.noUi-base').style('width',w-10+"px"); } \ No newline at end of file diff --git a/src/firefly/static/js/misc/selector.js b/src/firefly/static/js/misc/selector.js new file mode 100644 index 00000000..13563ac0 --- /dev/null +++ b/src/firefly/static/js/misc/selector.js @@ -0,0 +1,175 @@ +function createSelector(){ + // wireframe sphere on front half + const geometry1 = new THREE.SphereGeometry(1, 16, 16, 0, Math.PI, 0, Math.PI); + const wireframe = new THREE.WireframeGeometry(geometry1); + const line = new THREE.LineSegments(wireframe); + line.material.depthTest = true; + line.material.opacity = 0.9; + line.material.transparent = true; + + // back half of sphere filled in + const geometry2 = new THREE.SphereGeometry(1, 16, 16, Math.PI, Math.PI, 0, Math.PI); + const material = new THREE.MeshBasicMaterial({ color: "black" }); + material.depthTest = true; + material.opacity = 0.7; + material.transparent = true; + material.side = THREE.DoubleSide; + const sphere = new THREE.Mesh(geometry2, material); + + // create a group to hold the two elements of the selector + group = new THREE.Object3D(); + group.add(sphere); + group.add(line); + + // for now I will place the selector to be right in front of the camera + viewerParams.camera.add(group); + group.position.set(0, 0, -viewerParams.selector.distance); + + viewerParams.selector.object3D = group; + viewerParams.selector.object3D.scale.set(viewerParams.selector.radius, viewerParams.selector.radius, viewerParams.selector.radius); + + // run this later (in WebGLStart) so that the particles are created first + // toggleDataSelector(viewerParams.selector.active); + +} + +function updateSelector(){ + // update the center, radius and send to the shader + viewerParams.selector.object3D.getWorldPosition(viewerParams.selector.center); + viewerParams.selector.object3D.scale.set(viewerParams.selector.radius, viewerParams.selector.radius, viewerParams.selector.radius); + viewerParams.selector.object3D.position.set(0, 0, -viewerParams.selector.distance); + + viewerParams.partsKeys.forEach(function(p,i){ + viewerParams.partsMesh[p].forEach(function(m, j){ + m.material.uniforms.selectorCenter.value = [viewerParams.selector.center.x, viewerParams.selector.center.y, viewerParams.selector.center.z]; + m.material.uniforms.selectorRadius.value = viewerParams.selector.radius; + }) + }) +} + +function gatherSelectedData(){ + // add some notification to the screen, maybe with a progress bar? + + // find the data that is inside the selected region + // is there a way to do this without looping through every particle? + // this actually runs much more quickly than I anticipated (at least on our default sample data) + var selected = {}; + var structure = {}; + viewerParams.partsKeys.forEach(function(p,i){ + var j = 0; + + // create the arrays to hold the output + selected[p] = {}; + structure[p] = {}; + viewerParams.inputDataAttributes[p].forEach(function(key){ + selected[p][key] = []; + structure[p][key] = []; + }) + + while (j < viewerParams.parts[p].Coordinates_flat.length){ + var p0 = viewerParams.parts[p].Coordinates_flat.slice(j, j + 3); + var pos = new THREE.Vector3(p0[0], p0[1], p0[2]); + if (pos.distanceTo(viewerParams.selector.center) < viewerParams.selector.radius) { + // compile the output + var index = Math.floor(j/3); + Object.keys(selected[p]).forEach(function(key){ + if (key.includes('flat')){ + selected[p][key].push(viewerParams.parts[p][key].slice(j, j + 3)); + } else { + selected[p][key].push(viewerParams.parts[p][key].slice(index, index + 1)[0]); + } + }) + } + j += 3; + } + + // I need to flatten any of the arrays that have the word "flat" in them + Object.keys(selected[p]).forEach(function(key){ + if (key.includes('flat')) selected[p][key] = selected[p][key].flat(); + }) + }) + console.log({'selected':selected, 'structure':structure}); + + return {'selected':selected, 'structure':structure} +} + +function downloadSelection(selection = null){ + // download the data that is physically inside the selector sphere + console.log('downloading selected data...'); + if (!selection) selection = gatherSelectedData() + + downloadObjectAsJson(selection.selected, 'Firefly_data_selection'); +} + + +function sendSelectedData(selection = null, sizeLimit = 5e4){ + // the sizeLimit is in bytes. I am not sure what that limit should be. + // It is not clear how this is propagated through sockets to flask, and there may also a timeout component that I am unclear about. + // but 5e4 bytes seems to work + viewerParams.selector.sendingData = true; + + console.log('sending selected data to flask...'); + if (!selection) selection = gatherSelectedData(); + var size = roughSizeOfObject(selection.selected); + console.log('size of object (bytes) = ', size); + + // send to Flask + // chunk the data into pieces to avoid cutting off the connection + var done = false; + + // first send only the data structure, excluding any lists + socketParams.socket.emit('send_selected_data', {'data':selection.structure, 'room':socketParams.room, 'keyList':null, 'pass':'structure', 'done': done}); + + + // set the list of times (doesn't appear to be a good way to do this inside the sendData loop below) + var times = []; + var totalCount = 0; + Object.keys(selection.selected).forEach(function(k1, i){ + Object.keys(selection.selected[k1]).forEach(function(k2, j){ + var data = selection.selected[k1][k2]; + var size = roughSizeOfObject(data); + var nchunks = Math.ceil(size/sizeLimit); + for (let k = 0; k < nchunks; k += 1){ + totalCount += 1; + times.push(50*totalCount); + } + }) + }) + + // draw the loading bar + viewerParams.loadfrac = 0; + drawLoadingBar('ContentContainer', "z-index:3; position:absolute; bottom:15vh", 'Sending data to Python ...'); + + // send the data to flask + var count = 0; + var keys1 = Object.keys(selection.selected); + keys1.forEach(function(k1, i){ + var keys2 = Object.keys(selection.selected[k1]); + keys2.forEach(function(k2, j){ + var data = selection.selected[k1][k2]; + var size = roughSizeOfObject(data); + var nchunks = Math.ceil(size/sizeLimit); + for (let k = 0; k < nchunks; k+= 1){ + setTimeout(function(){ + count += 1; + // console.log('count, size (bytes), keys = ', totalCount-count, size, [k1, k2]); + if (count >= totalCount) done = true; + socketParams.socket.emit('send_selected_data', {'data':data.slice(nchunks*k, nchunks*k + sizeLimit), 'room':socketParams.room, 'keyList':[k1, k2], 'pass':'data', 'done': done}); + // update the loading bar + viewerParams.loadfrac = count/totalCount; + updateLoadingBar(); + if (done){ + viewerParams.selector.sendingData = false; + d3.select('#ContentContainer').selectAll('#loaderContainer').remove(); + } + },times.shift()) + } + }) + + }) + + + + +} + diff --git a/src/firefly/static/js/misc/socketParams.js b/src/firefly/static/js/misc/socketParams.js index d8255c0b..d22a483e 100644 --- a/src/firefly/static/js/misc/socketParams.js +++ b/src/firefly/static/js/misc/socketParams.js @@ -18,10 +18,17 @@ function defineSocketParams(){ // Connect to the Socket.IO server. // The connection URL has the following format: // http[s]://:[/] - this.socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + this.namespace);//, { - // 'reconnectionDelay': 10000, - // 'reconnectionDelayMax': 20000, - // }); + this.socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + this.namespace, + { + rememberTransport: false, + transports: ["websocket"], + forceNew: true, + reconnection: true, + maxHttpBufferSize: 1e9, //1Gb, but I'm not sure this actually sets the limit + pingTimeout: 1e7, + }); + + // this.socket.io._timeout = 1e9; } } diff --git a/src/firefly/static/js/misc/utils.js b/src/firefly/static/js/misc/utils.js index 936ccfb2..d3ce46b0 100644 --- a/src/firefly/static/js/misc/utils.js +++ b/src/firefly/static/js/misc/utils.js @@ -174,4 +174,50 @@ function parseTranslateStyle(elem){ return out; +} + +function downloadObjectAsJson(exportObj, exportName){ + // to download an object as a json file + // https://stackoverflow.com/questions/19721439/download-json-object-as-a-file-from-browser + var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportObj)); + var downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.setAttribute("href",dataStr); + downloadAnchorNode.setAttribute("download", exportName + ".json"); + document.body.appendChild(downloadAnchorNode); // required for firefox + downloadAnchorNode.click(); + downloadAnchorNode.remove(); + } + + function roughSizeOfObject( object ) { + // https://stackoverflow.com/questions/1248302/how-to-get-the-size-of-a-javascript-object + var objectList = []; + var stack = [ object ]; + var bytes = 0; + + while ( stack.length ) { + var value = stack.pop(); + + if ( typeof value === 'boolean' ) { + bytes += 4; + } + else if ( typeof value === 'string' ) { + bytes += value.length * 2; + } + else if ( typeof value === 'number' ) { + bytes += 8; + } + else if + ( + typeof value === 'object' + && objectList.indexOf( value ) === -1 + ) + { + objectList.push( value ); + + for( var i in value ) { + stack.push( value[ i ] ); + } + } + } + return bytes; } \ No newline at end of file diff --git a/src/firefly/static/js/viewer/applyUISelections.js b/src/firefly/static/js/viewer/applyUISelections.js index a1b87c2c..8571728b 100644 --- a/src/firefly/static/js/viewer/applyUISelections.js +++ b/src/firefly/static/js/viewer/applyUISelections.js @@ -565,19 +565,30 @@ function createPreset(){ preset.startTween = copyValue(viewerParams.updateTween); preset.loaded = true; + return preset; } -function savePreset(){ - var preset = createPreset(); +function savePresetViewer(){ + preset = creatPreset(); //https://stackoverflow.com/questions/33780271/export-a-json-object-to-a-text-file - var str = JSON.stringify(preset) + var str = JSON.stringify(GUIparams.preset) //Save the file contents as a DataURI var dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(str); saveFile(dataUri,'preset.json'); + // send to Flask + if (viewerParams.usingSocket) sendPreset(preset); +} + +function sendPreset(preset = null){ + + if (!preset) preset = createPreset(); + + // send to Flask + socketParams.socket.emit('send_settings', {'settings':preset, 'room':socketParams.room}); } function updateFriction(value){ @@ -666,6 +677,31 @@ function setCmapReversed(args){ var p = args[0]; var checked = args[1]; - viewerParams.colormapReversed[p] = checked; - if (viewerParams.showColormap[p]) populateColormapImage(p, checked) + if (ckey) { + viewerParams.colormapReversed[p][ckey] = checked; + } else { + viewerParams.colormapReversed[p] = checked; + } + if (viewerParams.showColormap[p]) populateColormapAxis(p, checked) + + //console.log('reversing particle colormap', args); +} + + +function toggleDataSelector(value){ + //turn the data selector sphere on/off + viewerParams.selector.active = value; + viewerParams.selector.object3D.visible = value; + + // turn off the selection in the shader by setting the radius to zero + if (!value){ + viewerParams.selector.object3D.scale.set(0,0,0); + viewerParams.partsKeys.forEach(function(p,i){ + viewerParams.partsMesh[p].forEach(function(m, j){ + m.material.uniforms.selectorRadius.value = 0.; + }) + }) + } + + // console.log('data selector ', viewerParams.selector.active); } \ No newline at end of file diff --git a/src/firefly/static/js/viewer/createPartsMesh.js b/src/firefly/static/js/viewer/createPartsMesh.js index 668332c7..98db4e60 100644 --- a/src/firefly/static/js/viewer/createPartsMesh.js +++ b/src/firefly/static/js/viewer/createPartsMesh.js @@ -103,6 +103,8 @@ function createParticleMaterial(p, color=null,minPointScale=null,maxPointScale=n velVectorWidth: {value: viewerParams.velVectorWidth[p]}, velGradient: {value: viewerParams.velGradient[p]}, useDepth: {value: +viewerParams.depthTest[p]}, + selectorCenter: {value: [viewerParams.selector.center.x, viewerParams.selector.center.y, viewerParams.selector.center.z]}, + selectorRadius: {value: viewerParams.selector.radius} }, diff --git a/src/firefly/static/js/viewer/initViewer.js b/src/firefly/static/js/viewer/initViewer.js index ff8c3742..48cd6e93 100644 --- a/src/firefly/static/js/viewer/initViewer.js +++ b/src/firefly/static/js/viewer/initViewer.js @@ -11,8 +11,12 @@ function connectViewerSocket(){ // this happens when the server connects. // all other functions below here are executed when the server emits to that name. socketParams.socket.on('connect', function() { + console.log("sending connection from viewer") socketParams.socket.emit('connection_test', {data: 'Viewer connected!'}); }); + socketParams.socket.on('disconnect', function(message) { + console.log("viewer is disconnected", message) + }); // socketParams.socket.on('connection_response', function(msg) { // console.log('connection response', msg); // }); @@ -47,7 +51,6 @@ function connectViewerSocket(){ }); socketParams.socket.on('input_data', function(msg) { - //only tested for local (GUI + viewer in one window) console.log("======== have new data : ", Object.keys(msg)); @@ -89,6 +92,36 @@ function connectViewerSocket(){ }); + socketParams.socket.on('output_settings', function(msg){ + //only tested for combined endpoint + console.log("======== sending settings to server"); + sendPreset(); + }); + + socketParams.socket.on('input_settings', function(msg) { + //only tested for combined endpoint + console.log("======== have new settings : ", Object.keys(msg)); + //for now, the user is required to pass the entire settings object (if we change that, this next line will probably break firefly) + viewerParams.parts.options = msg; + applyOptions(); + + // do something here to update the GUI. For now I will just remake it! + var forGUIAppend = []; + // forGUIAppend.push({'setGUIParamByKey':[false,"collapseGUIAtStart"]}); + forGUIAppend.push({'makeUI':viewerParams.local}); + sendInitGUI(prepend=[], append=forGUIAppend) + }); + + socketParams.socket.on('output_selected_data', function(msg){ + //only tested for combined endpoint + if (viewerParams.selector.active){ + console.log("======== sending selected data to server"); + sendSelectedData(); + } else { + console.log("======== data selector not active") + } + }); + socketParams.socket.on('update_streamer', function(msg) { viewerParams.streamReady = true; }); @@ -226,8 +259,22 @@ function callLoadData(args){ loadData(WebGLStart, prefix); } +function getInputDataAttributes(){ + // get the attributes that the user supplied with the data (before Firefly adds) + viewerParams.partsKeys.forEach(function(p){ + viewerParams.inputDataAttributes[p] = []; + Object.keys(viewerParams.parts[p]).forEach(function(key){ + if (!key.includes('Key')){ + viewerParams.inputDataAttributes[p].push(key); + } + }) + }) + console.log('input data keys ... ', viewerParams.inputDataAttributes); +} + // launch the app control flow, >> ends in animate << function WebGLStart(){ + getInputDataAttributes(); //reset the window title if (viewerParams.parts.hasOwnProperty('options')){ @@ -250,7 +297,9 @@ function WebGLStart(){ Promise.all([ createPartsMesh(), ]).then(function(){ - + + toggleDataSelector(viewerParams.selector.active); + //begin the animation // keep track of runtime for crashing the app rather than the computer var currentTime = new Date(); @@ -470,6 +519,9 @@ function initScene() { // var canvas = d3.select('canvas').node(); // var gl = canvas.getContext('webgl'); // console.log(gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE), gl.getParameter(gl.POINT_SMOOTH)); + + // selector (temporary) + createSelector(); } // apply any settings from options file @@ -553,14 +605,13 @@ function applyOptions(){ } //check if we are starting in Stereo - if (options.hasOwnProperty('useStereo') && options.useStereo){ - viewerParams.normalRenderer = viewerParams.renderer; - viewerParams.renderer = viewerParams.effect; - viewerParams.useStereo = true; + if (options.hasOwnProperty('useStereo')){ + checkStereoLock(options.useStereo) if (viewerParams.haveUI){ - var evalString = 'elm = document.getElementById("StereoCheckBox"); elm.checked = true; elm.value = true;' + var evalString = 'elm = document.getElementById("StereoCheckBox"); elm.checked = ' + options.useStereo + '; elm.value = ' + options.useStereo + ';' forGUI.push({'evalCommand':[evalString]}) - } } + } + } //modify the initial stereo separation if (options.hasOwnProperty('stereoSep') && options.stereoSep != null){ @@ -1007,8 +1058,12 @@ function sendInitGUI(prepend=[], append=[]){ forGUI.push(x); }) - forGUI.push({'setGUIParamByKey':[true,"GUIready"]}); + // for the data selector + forGUI.push({'setGUIParamByKey':[viewerParams.selector.active,"selector","active"]}); + forGUI.push({'setGUIParamByKey':[viewerParams.selector.radius,"selector","radius"]}); + forGUI.push({'setGUIParamByKey':[viewerParams.selector.distance,"selector","distance"]}); + forGUI.push({'setGUIParamByKey':[true,"GUIready"]}); //forGUI.forEach(function (value){console.log(value.setGUIParamByKey)}); sendToGUI(forGUI); @@ -1283,25 +1338,29 @@ function countParts(){ } // callLoadData -> , connectViewerSocket -> -function drawLoadingBar(){ +function drawLoadingBar(containerID = 'splashdivLoader', styles = '', textContent = null){ d3.select('#loadDataButton').style('display','none'); d3.select('#selectStartupButton').style('display','none'); var screenWidth = parseFloat(window.innerWidth); //Make an SVG Container - var splash = d3.select("#splashdivLoader") - - splash.selectAll('svg').remove(); - - var svg = splash.append("svg") + var parent = document.getElementById(containerID); + d3.select(parent).selectAll('#loaderContainer').remove(); + var elem = document.createElement('div'); + elem.style.cssText = 'width:100%;' + styles; + elem.id = 'loaderContainer'; + parent.appendChild(elem); + + var svg = d3.select(elem).append("svg") + .attr('id','loadingBar') .attr("width", screenWidth) .attr("height", viewerParams.loadingSizeY); - viewerParams.svgContainer = svg.append("g") + var svgContainer = svg.append("g") - viewerParams.svgContainer.append("rect") + svgContainer.append("rect") .attr('id','loadingRectOutline') .attr("x", (screenWidth - viewerParams.loadingSizeX)/2) .attr("y", 0) @@ -1311,7 +1370,7 @@ function drawLoadingBar(){ .attr('stroke','var(--logo-color1)') .attr('stroke-width', '3') - viewerParams.svgContainer.append("rect") + svgContainer.append("rect") .attr('id','loadingRect') .attr("x", (screenWidth - viewerParams.loadingSizeX)/2) .attr("y", 0)//(screenHeight - sizeY)/2) @@ -1319,6 +1378,8 @@ function drawLoadingBar(){ .attr('fill','var(--logo-color1)') .attr("width",viewerParams.loadingSizeX*viewerParams.loadfrac); + if (textContent) d3.select(elem).append('div').attr('class','loaderText').text(textContent); + window.addEventListener('resize', moveLoadingBar); @@ -1327,8 +1388,13 @@ function drawLoadingBar(){ // drawLoadingBar -> function moveLoadingBar(){ var screenWidth = parseFloat(window.innerWidth); - d3.selectAll('#loadingRectOutline').attr('x', (screenWidth - viewerParams.loadingSizeX)/2); - d3.selectAll('#loadingRect').attr('x', (screenWidth - viewerParams.loadingSizeX)/2); + viewerParams.loadingSizeX = 0.9*screenWidth; + d3.selectAll('#loadingRectOutline') + .attr('width', viewerParams.loadingSizeX) + .attr('x', (screenWidth - viewerParams.loadingSizeX)/2); + d3.selectAll('#loadingRect') + .attr("width",viewerParams.loadingSizeX*viewerParams.loadfrac) + .attr('x', (screenWidth - viewerParams.loadingSizeX)/2); } // compileJSONData -> diff --git a/src/firefly/static/js/viewer/renderLoop.js b/src/firefly/static/js/viewer/renderLoop.js index 8dabef2a..d923663e 100644 --- a/src/firefly/static/js/viewer/renderLoop.js +++ b/src/firefly/static/js/viewer/renderLoop.js @@ -20,7 +20,7 @@ function animate(time) { // get the memory usage update_memory_usage(); - + if (viewerParams.initialize_time){ //console.log(seconds-viewerParams.initialize_time + ' seconds to initialize'); //console.log(viewerParams.memoryUsage/1e9 + ' GB allocated'); @@ -116,7 +116,10 @@ function update(time){ initControls(false); } + if (viewerParams.selector.active) updateSelector() + } + } function update_keypress(time){ @@ -624,7 +627,7 @@ function update_memory_usage(){ function update_framerate(seconds,time){ // if we spent more than 1.5 seconds drawing the last frame, send the app to sleep - if ( viewerParams.sleepTimeout != null && (seconds-viewerParams.currentTime) > viewerParams.sleepTimeout){ + if ( viewerParams.sleepTimeout != null && (seconds-viewerParams.currentTime) > viewerParams.sleepTimeout && (!viewerParams.selector.sendingData)){ console.log("Putting the app to sleep, taking too long!",(seconds-viewerParams.currentTime)); viewerParams.pauseAnimation=true; showSleep(); @@ -640,7 +643,7 @@ function update_framerate(seconds,time){ // and put in a weirdly high value (like >100 fps) that biases the mean high viewerParams.FPS = viewerParams.fps_list.slice().sort(function(a, b){return a-b})[15] - if ((viewerParams.drawPass % Math.min(Math.round(viewerParams.FPS),60)) == 0){ + if ((viewerParams.drawPass % Math.min(Math.round(viewerParams.FPS),60)) == 0 && viewerParams.haveUI){ // only send this if the parameters have changed (to avoid clogging the socket) if (Math.abs(viewerParams.FPS - viewerParams.FPS0) > 0.1 || Math.abs(viewerParams.memoryUsage - viewerParams.memoryUsage0) > 1e7){ viewerParams.FPS0 = viewerParams.FPS; diff --git a/src/firefly/static/js/viewer/viewerParams.js b/src/firefly/static/js/viewer/viewerParams.js index 186dc142..bd81c161 100644 --- a/src/firefly/static/js/viewer/viewerParams.js +++ b/src/firefly/static/js/viewer/viewerParams.js @@ -151,11 +151,10 @@ function defineViewerParams(){ //for the loading bar var screenWidth = window.innerWidth; var screenHeight = window.innerHeight; - this.loadingSizeX = screenWidth*0.5; - this.loadingSizeY = screenHeight*0.1; + this.loadingSizeX = screenWidth*0.9; + this.loadingSizeY = screenHeight*0.05; this.loadfrac = 0.; this.drawfrac = 0.; - this.svgContainer = null; //the startup file this.startup = "data/startup.json"; @@ -319,6 +318,20 @@ function defineViewerParams(){ } + this.selector = new function() { + // settings for the selection region + // currently set as a sphere + + this.object3D = null; + this.center = new THREE.Vector3(0,0,0); + this.radius = 10.; + this.distance = 100.; + this.active = false; + this.sendingData = false; + } + this.inputDataAttributes = {}; + + setDefaultViewerParams(this); }; } diff --git a/src/firefly/static/shaders/fragment.glsl.js b/src/firefly/static/shaders/fragment.glsl.js index ce9b7936..32fcbf93 100644 --- a/src/firefly/static/shaders/fragment.glsl.js +++ b/src/firefly/static/shaders/fragment.glsl.js @@ -8,6 +8,9 @@ varying float vColormapMag; varying float vAlpha; varying float vPointSize; varying vec4 vColor; +varying float vInsideSelector; +varying float vDistFromSelectorCenter; +varying vec3 vSelectorCenter; uniform bool showColormap; uniform float colormap; @@ -144,6 +147,9 @@ void main(void) { gl_FragColor.a *= vAlpha; + // gl_FragColor = vec4(10./vDistFromSelectorCenter, 0., 0., 1.); + if (vInsideSelector > 0.) gl_FragColor = vec4(vec3(1.) - gl_FragColor.rgb, 1.); + // if (vInsideSelector < 1.) discard; } } -`; \ No newline at end of file +`; diff --git a/src/firefly/static/shaders/vertex.glsl.js b/src/firefly/static/shaders/vertex.glsl.js index 32356a75..03c8fddf 100644 --- a/src/firefly/static/shaders/vertex.glsl.js +++ b/src/firefly/static/shaders/vertex.glsl.js @@ -12,6 +12,9 @@ varying float vColormapMag; varying float vAlpha; varying float vPointSize; varying vec4 vColor; +varying float vInsideSelector; +varying float vDistFromSelectorCenter; +varying vec3 vSelectorCenter; varying vec2 vUv; //for the column density @@ -25,6 +28,9 @@ uniform float uVertexScale; //from the GUI uniform float velTime; +uniform vec3 selectorCenter; +uniform float selectorRadius; + const float PI = 3.1415926535897932384626433832795; // vectors are substantially smaller (b.c. they're built by discarding) so we need to scale them // to compensate, otherwise they are /tiny/ @@ -62,6 +68,16 @@ void main(void) { gl_Position = projectionMatrix * mvPosition; + // check if point is inside the sphere selector + float distFromSelectorCenter = length(position.xyz - selectorCenter.xyz); + // float distFromSelectorCenter = length(position.xyz - vec3(20)); + //float distFromSelectorCenter = length(position.xyz); + vDistFromSelectorCenter = distFromSelectorCenter; + vSelectorCenter = selectorCenter; + vInsideSelector = 0.; + if (distFromSelectorCenter <= selectorRadius) { + vInsideSelector = 1.; + } } `; diff --git a/src/firefly/templates/VR.html b/src/firefly/templates/VR.html index 2a5dcdac..76c39dee 100644 --- a/src/firefly/templates/VR.html +++ b/src/firefly/templates/VR.html @@ -116,6 +116,7 @@ + diff --git a/src/firefly/templates/combined.html b/src/firefly/templates/combined.html index 43caee9a..368d2c3c 100644 --- a/src/firefly/templates/combined.html +++ b/src/firefly/templates/combined.html @@ -119,6 +119,7 @@ + @@ -147,7 +148,8 @@ //called upon loading connectGUISocket(); connectViewerSocket(); - runLocal(true); + //function runLocal(useSockets=true, showGUI=true, allowVRControls=false, startStereo=false){ + runLocal(true, true, false, false); diff --git a/src/firefly/templates/default.html b/src/firefly/templates/default.html index 6951bf0a..e4614d96 100644 --- a/src/firefly/templates/default.html +++ b/src/firefly/templates/default.html @@ -118,6 +118,7 @@ + diff --git a/src/firefly/templates/viewer.html b/src/firefly/templates/viewer.html index ba438210..2d197be4 100644 --- a/src/firefly/templates/viewer.html +++ b/src/firefly/templates/viewer.html @@ -89,6 +89,7 @@ +