# HG changeset patch # User Sebastien Jodogne # Date 1712693581 -7200 # Node ID 967f947014ac79fe57d25ce2194381840c7240f8 # Parent 111d952e7fa0f987b882f0adb1765e074745f232 adding experimental support for nexus models diff -r 111d952e7fa0 -r 967f947014ac .reuse/dep5 --- a/.reuse/dep5 Sat Apr 06 17:19:16 2024 +0200 +++ b/.reuse/dep5 Tue Apr 09 22:13:01 2024 +0200 @@ -3,7 +3,7 @@ Upstream-Contact: Sebastien Jodogne Source: https://orthanc.uclouvain.be/ -Files: NEWS README WebApplications/o3dv.html WebApplications/three.html Resources/CreateThreeDist.txt +Files: NEWS README WebApplications/o3dv.html WebApplications/three.html Resources/CreateThreeDist.txt Resources/Nexus.txt Copyright: 2023-2024 Sebastien Jodogne, UCLouvain, Belgium License: GPL-3.0-or-later @@ -18,3 +18,7 @@ Files: Resources/Orthanc/Sdk-1.0.0/orthanc/OrthancCPlugin.h Copyright: 2012-2015 Sebastien Jodogne , University Hospital of Liege (Belgium) License: GPL-3.0-or-later + +Files: Resources/Nexus/* +Copyright: 2012-2018, Visual Computing Lab, ISTI - CNR +License: MIT diff -r 111d952e7fa0 -r 967f947014ac CMakeLists.txt --- a/CMakeLists.txt Sat Apr 06 17:19:16 2024 +0200 +++ b/CMakeLists.txt Tue Apr 09 22:13:01 2024 +0200 @@ -174,6 +174,12 @@ O3DV_HTML ${CMAKE_SOURCE_DIR}/WebApplications/o3dv.html O3DV_JS ${CMAKE_SOURCE_DIR}/WebApplications/o3dv.js ORTHANC_EXPLORER ${CMAKE_SOURCE_DIR}/Sources/OrthancExplorer.js + + NEXUS_HTML ${CMAKE_SOURCE_DIR}/Resources/Nexus/threejs.html + NEXUS_JS ${CMAKE_SOURCE_DIR}/Resources/Nexus/js/nexus.js + NEXUS_MECO_JS ${CMAKE_SOURCE_DIR}/Resources/Nexus/js/meco.js + NEXUS_THREE_JS ${CMAKE_SOURCE_DIR}/Resources/Nexus/js/nexus_three.js + NEXUS_TRACKBALL_JS ${CMAKE_SOURCE_DIR}/Resources/Nexus/js/TrackballControls.js ) add_custom_command( diff -r 111d952e7fa0 -r 967f947014ac LICENSES/MIT.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSES/MIT.txt Tue Apr 09 22:13:01 2024 +0200 @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff -r 111d952e7fa0 -r 967f947014ac Resources/Nexus.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Nexus.txt Tue Apr 09 22:13:01 2024 +0200 @@ -0,0 +1,7 @@ +Homepage: https://vcg.isti.cnr.it/nexus/ + +GitHub: https://github.com/cnr-isti-vclab/nexus + +The files in this folder come from folder "html" in release 4.2 (Nexus 2018). + +WARNING: Releases 4.2.1, 4.2.2, and 4.3 do not seem to work anymore. diff -r 111d952e7fa0 -r 967f947014ac Resources/Nexus/js/TrackballControls.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Nexus/js/TrackballControls.js Tue Apr 09 22:13:01 2024 +0200 @@ -0,0 +1,604 @@ +/** + * @author Eberhard Graether / http://egraether.com/ + * @author Mark Lundin / http://mark-lundin.com + * @author Simone Manini / http://daron1337.github.io + * @author Luca Antiga / http://lantiga.github.io + */ + +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.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(), + + _movePrev = new THREE.Vector2(), + _moveCurr = new THREE.Vector2(), + + _lastAxis = new THREE.Vector3(), + _lastAngle = 0, + + _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 getMouseOnScreen( pageX, pageY ) { + + vector.set( + ( pageX - _this.screen.left ) / _this.screen.width, + ( pageY - _this.screen.top ) / _this.screen.height + ); + + return vector; + + }; + + }() ); + + var getMouseOnCircle = ( function () { + + var vector = new THREE.Vector2(); + + return function getMouseOnCircle( pageX, pageY ) { + + vector.set( + ( ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / ( _this.screen.width * 0.5 ) ), + ( ( _this.screen.height + 2 * ( _this.screen.top - pageY ) ) / _this.screen.width ) // screen.width intentional + ); + + return vector; + + }; + + }() ); + + this.rotateCamera = ( function() { + + var axis = new THREE.Vector3(), + quaternion = new THREE.Quaternion(), + eyeDirection = new THREE.Vector3(), + objectUpDirection = new THREE.Vector3(), + objectSidewaysDirection = new THREE.Vector3(), + moveDirection = new THREE.Vector3(), + angle; + + return function rotateCamera() { + + moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 ); + angle = moveDirection.length(); + + if ( angle ) { + + _eye.copy( _this.object.position ).sub( _this.target ); + + eyeDirection.copy( _eye ).normalize(); + objectUpDirection.copy( _this.object.up ).normalize(); + objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize(); + + objectUpDirection.setLength( _moveCurr.y - _movePrev.y ); + objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x ); + + moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) ); + + axis.crossVectors( moveDirection, _eye ).normalize(); + + angle *= _this.rotateSpeed; + quaternion.setFromAxisAngle( axis, angle ); + + _eye.applyQuaternion( quaternion ); + _this.object.up.applyQuaternion( quaternion ); + + _lastAxis.copy( axis ); + _lastAngle = angle; + + } else if ( ! _this.staticMoving && _lastAngle ) { + + _lastAngle *= Math.sqrt( 1.0 - _this.dynamicDampingFactor ); + _eye.copy( _this.object.position ).sub( _this.target ); + quaternion.setFromAxisAngle( _lastAxis, _lastAngle ); + _eye.applyQuaternion( quaternion ); + _this.object.up.applyQuaternion( quaternion ); + + } + + _movePrev.copy( _moveCurr ); + + }; + + }() ); + + + this.zoomCamera = function () { + + var factor; + + if ( _state === STATE.TOUCH_ZOOM_PAN ) { + + factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; + _touchZoomDistanceStart = _touchZoomDistanceEnd; + _eye.multiplyScalar( factor ); + + } else { + + 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 panCamera() { + + 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 ) ); + _zoomStart.copy( _zoomEnd ); + + } + + if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) { + + _this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) ); + _zoomStart.copy( _zoomEnd ); + + } + + } + + }; + + 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; + + _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; + + } + + function mousedown( event ) { + + if ( _this.enabled === false ) return; + + if ( _state === STATE.NONE ) { + + _state = event.button; + + } + + if ( _state === STATE.ROTATE && ! _this.noRotate ) { + + _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); + _movePrev.copy( _moveCurr ); + + } 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; + + if ( _state === STATE.ROTATE && ! _this.noRotate ) { + + _movePrev.copy( _moveCurr ); + _moveCurr.copy( getMouseOnCircle( 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; + + _state = STATE.NONE; + + document.removeEventListener( 'mousemove', mousemove ); + document.removeEventListener( 'mouseup', mouseup ); + _this.dispatchEvent( endEvent ); + + } + + function mousewheel( event ) { + + if ( _this.enabled === false ) return; + + 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; + _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); + _movePrev.copy( _moveCurr ); + break; + + default: // 2 or more + _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; + + } + + _this.dispatchEvent( startEvent ); + + } + + function touchmove( event ) { + + if ( _this.enabled === false ) return; + + switch ( event.touches.length ) { + + case 1: + _movePrev.copy( _moveCurr ); + _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); + break; + + default: // 2 or more + 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; + + } + + } + + function touchend( event ) { + + if ( _this.enabled === false ) return; + + switch ( event.touches.length ) { + + case 0: + _state = STATE.NONE; + break; + + case 1: + _state = STATE.TOUCH_ROTATE; + _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); + _movePrev.copy( _moveCurr ); + break; + + } + + _this.dispatchEvent( endEvent ); + + } + + function contextmenu( event ) { + + event.preventDefault(); + + } + + this.dispose = function() { + + this.domElement.removeEventListener( 'contextmenu', contextmenu, false ); + this.domElement.removeEventListener( 'mousedown', mousedown, false ); + this.domElement.removeEventListener( 'mousewheel', mousewheel, false ); + this.domElement.removeEventListener( 'MozMousePixelScroll', mousewheel, false ); // firefox + + this.domElement.removeEventListener( 'touchstart', touchstart, false ); + this.domElement.removeEventListener( 'touchend', touchend, false ); + this.domElement.removeEventListener( 'touchmove', touchmove, false ); + + document.removeEventListener( 'mousemove', mousemove, false ); + document.removeEventListener( 'mouseup', mouseup, false ); + + window.removeEventListener( 'keydown', keydown, false ); + window.removeEventListener( 'keyup', keyup, false ); + + }; + + this.domElement.addEventListener( 'contextmenu', contextmenu, false ); + this.domElement.addEventListener( 'mousedown', mousedown, false ); + this.domElement.addEventListener( 'mousewheel', mousewheel, false ); + this.domElement.addEventListener( 'MozMousePixelScroll', 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; diff -r 111d952e7fa0 -r 967f947014ac Resources/Nexus/js/meco.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Nexus/js/meco.js Tue Apr 09 22:13:01 2024 +0200 @@ -0,0 +1,954 @@ +/* +Nexus +Copyright (c) 2012-2018, Visual Computing Lab, ISTI - CNR +All rights reserved. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +onmessage = function(job) { + if(typeof(job.data) == "string") return; + var node = job.data.node; + var signature = job.data.signature; + var patches = job.data.patches; +// var now =new Date().getTime(); + + var size; + if(!node.buffer) return; + else size = node.buffer.byteLength; + var buffer; + for(var i =0 ; i < 1; i++) { + var coder = new MeshCoder(signature, node, patches); + buffer = coder.decode(node.buffer); + } + node.buffer = buffer; + node.owner = job.owner; +// var elapsed = new Date().getTime() - now; +// var t = node.nface; +// console.log("Z Time: " + elapsed + " Size: " + size + " KT/s " + (t/(elapsed)) + " Mbps " + (8*1000*node.buffer.byteLength/elapsed)/(1<<20)); + postMessage(node); +} + +// actually bitstreams expects a little endian uin64 type. convert it to 2 uint32 + +BitStream = function(array) { + this.a = array; + for(var i = 0; i < array.length; i += 2) { + var s = array[i]; + array[i] = array[i+1]; + array[i+1] = s; + } + this.position = 0; + this.bitsPending = 0; +}; + +BitStream.prototype = { + read: function(bits) { + var bitBuffer = 0; + while(bits > 0) { + var partial; + var bitsConsumed; + if (this.bitsPending > 0) { + var byte = (this.a[this.position - 1] & (0xffffffff >>> (32 - this.bitsPending)))>>>0; + bitsConsumed = Math.min(this.bitsPending, bits); + this.bitsPending -= bitsConsumed; + partial = byte >>> this.bitsPending; + } else { + bitsConsumed = Math.min(32, bits); + this.bitsPending = 32 - bitsConsumed; + partial = this.a[this.position++] >>> this.bitsPending; + } + bits -= bitsConsumed; + bitBuffer = ((bitBuffer << bitsConsumed) | partial)>>>0; + } + return bitBuffer; + }, + replace: function(bits, value) { + //zero last part + value = (value & (0xffffffff >>> 32 - bits)) >>> 0; + value = (value | read(bits)) >>> 0; + return value; + } +}; + +Stream = function(buffer) { + this.data = buffer; + this.buffer = new Uint8Array(buffer); + this.pos = 0; +} + +Stream.prototype = { + readChar: function() { + var c = this.buffer[this.pos++]; + if(c > 127) c -= 256; + return c; + }, + readUChar: function() { + return this.buffer[this.pos++]; + }, + readInt: function() { + var c = this.buffer[this.pos + 3] + c <<= 8; + c |= this.buffer[this.pos + 2]; + c <<= 8; + c |= this.buffer[this.pos + 1]; + c <<= 8; + c |= this.buffer[this.pos + 0]; + this.pos += 4; + return c; + }, + readArray: function(n) { + var a = this.buffer.subarray(this.pos, this.pos+n); + this.pos += n; + return a; + }, + readBitStream:function() { + var n = this.readInt(); + var pad = this.pos & 0x3; + if(pad != 0) + this.pos += 4 - pad; + var b = new BitStream(new Uint32Array(this.data, this.pos, n*2)); + this.pos += n*8; + return b; + } +}; + +function Tunstall(wordsize, lookup_size) { + this.wordsize = wordsize? wordsize : 8; + this.lookup_size = lookup_size? lookup_size : 8; +} + +Tunstall.prototype = { + decompress: function(stream) { + var nsymbols = stream.readUChar(); + this.probabilities = stream.readArray(nsymbols*2); + this.createDecodingTables(); + var size = stream.readInt(); + var data = new Uint8Array(size); + var compressed_size = stream.readInt(); + var compressed_data = stream.readArray(compressed_size); + if(size) + this._decompress(compressed_data, compressed_size, data, size); + return data; + }, + + createDecodingTables: function() { + //read symbol,prob,symbol,prob as uchar. + //Here probabilities will range from 0 to 0xffff for better precision + + var n_symbols = this.probabilities.length/2; + if(n_symbols <= 1) return; + + var queues = []; //array of arrays + var buffer = []; + + //initialize adding all symbols to queues + for(var i = 0; i < n_symbols; i++) { + var symbol = this.probabilities[i*2]; + var s = [(this.probabilities[i*2+1])<<8, buffer.length, 1]; //probability, position in the buffer, length + queues[i] = [s]; + buffer.push(this.probabilities[i*2]); //symbol + } + var dictionary_size = 1< max_prob) { + best = i; + max_prob = p; + } + } + var symbol = queues[best][0]; + var pos = buffer.length; + + for(var i = 0; i < n_symbols; i++) { + var sym = this.probabilities[i*2]; + var prob = this.probabilities[i*2+1]<<8; + var s = [((prob*symbol[0])>>>16), pos, symbol[2]+1]; //combine probabilities, keep track of buffer, keep length of queue + + for(var k = 0; k < symbol[2]; k++) + buffer[pos+k] = buffer[symbol[1] + k]; //copy sequence of symbols + + pos += symbol[2]; + buffer[pos++] = sym; //append symbol + queues[i].push(s); + } + table_length += (n_symbols-1)*(symbol[2] + 1) +1; + n_words += n_symbols -1; + queues[best].shift(); //remove first thing + } + + this.index = new Uint32Array(n_words); + this.lengths = new Uint32Array(n_words); + this.table = new Uint8Array(table_length); + var word = 0; + var pos = 0; + for(i = 0; i < queues.length; i++) { + var queue = queues[i]; + for(var k = 0; k < queue.length; k++) { + var s = queue[k]; + this.index[word] = pos; + this.lengths[word] = s[2]; //length + word++; + + for(var j = 0; j < s[2]; j++) + this.table[pos + j] = buffer[s[1] + j]; //buffer of offset + pos += s[2]; //length + } + } + }, + _decompress: function(input, input_size, output, output_size) { + var input_pos = 0; + var output_pos = 0; + if(this.probabilities.length == 2) { + var symbol = this.probabilities[0]; + for(var i = 0; i < output_size; i++) + output[i] = symbol; + return; + } + + while(input_pos < input_size-1) { + var symbol = input[input_pos++]; + var start = this.index[symbol]; + var end = start + this.lengths[symbol]; + for(var i = start; i < end; i++) + output[output_pos++] = this.table[i]; + } + + //last symbol might override so we check. + var symbol = input[input_pos]; + var start = this.index[symbol]; + var end = start + output_size - output_pos; + var length = output_size - output_pos; + for(var i = start; i < end; i++) + output[output_pos++] = this.table[i]; + + return output; + } +} + +ZPoint = function(h, l) { + this.lo = l; + this.hi = h; +} + +ZPoint.prototype = { + copy: function(z) { + this.lo = z.lo; + this.hi = z.hi; + }, + setBit: function(d) { + if(d < 32) + this.lo = (this.lo | (1<>>0; + else + this.hi = (this.hi | (1<<(d-32)))>>>0; + }, + toPoint: function(min, step, buffer, pos) { + var x = this.morton3(this.lo, this.hi>>>1); + var y = this.morton3(this.lo>>>1, this.hi>>>2); + var z = this.morton3((this.lo>>>2 | (this.hi & 0x1)<<30 )>>>0, this.hi>>>3); //first hi bit needs to go into low. + + buffer[pos+0] = (x + min[0])*step; + buffer[pos+1] = (y + min[1])*step; + buffer[pos+2] = (z + min[2])*step; + }, + morton3: function(lo, hi) { + lo = ( lo & 0x49249249)>>>0; + lo = ((lo | (lo >>> 2 )) & 0xc30c30c3)>>>0; + lo = ((lo | (lo >>> 4 )) & 0x0f00f00f)>>>0; + lo = ((lo | (lo >>> 8 )) & 0xff0000ff)>>>0; + lo = ((lo | (lo >>> 16)) & 0x0000ffff)>>>0; + + hi = ( hi & 0x49249249)>>>0; + hi = ((hi | (hi >> 2 )) & 0xc30c30c3)>>>0; + hi = ((hi | (hi >> 4 )) & 0x0f00f00f)>>>0; + hi = ((hi | (hi >> 8 )) & 0xff0000ff)>>>0; + hi = ((hi | (hi >> 16)) & 0x0000ffff)>>>0; + + return ((hi<<11) | lo)>>>0; + } +}; + +//node is an object with nvert, nface +//patches is an array of offsets in the index, triangle are grouped by those offsets +//signature tells wether mesh has indices, normals, colors, etc. {'colors': true, 'normals':true, 'indices': true } + +function MeshCoder(signature, node, patches) { + this.sig = signature; + this.node = node; + this.patches = patches; + + this.last = new Int32Array(this.node.nvert); + this.last_count = 0; +} + +MeshCoder.prototype = { + //assumes input is an ArrayBuffer +decode: function(input) { + var t = this; + + t.buffer = new ArrayBuffer(t.node.nvert*(12 + t.sig.texcoords*8 + t.sig.normals*6 + t.sig.colors*4) + t.node.nface*t.sig.indices*6); + + var size = t.node.nvert*12; //float + t.coords = new Float32Array(t.buffer, 0, t.node.nvert*3); + + if(t.sig.texcoords) { + t.texcoords = new Float32Array(t.buffer, size, t.node.nvert*2); + size += t.node.nvert*8; //float + } + if(t.sig.normals) { + t.normals = new Int16Array(t.buffer, size, t.node.nvert*3); + size += t.node.nvert*6; //short + } + if(t.sig.colors) { + t.colors = new Uint8ClampedArray(t.buffer, size, t.node.nvert*4); + size += t.node.nvert*4; //chars + } + if(t.sig.indices) { + t.faces = new Uint16Array(t.buffer, size, t.node.nface*3); + size += t.node.nface*6; //short + } + + t.stream = new Stream(input); + + t.stack = new Float32Array(12); //min0, min1, min2, step, tmin0, tmin1, tstep + + t.stack[3] = t.stream.readInt(); + t.stack[4] = t.stream.readInt(); + t.stack[5] = t.stream.readInt(); + + t.coord_q = t.stream.readChar(); + t.coord_bits = t.stream.readChar()*3; + + t.stack[6] = Math.pow(2.0, t.coord_q); + + if(t.sig.texcoords) { + t.stack[9] = t.stream.readInt(); + t.stack[10] = t.stream.readInt(); + + t.texcoord_q = t.stream.readChar(); + t.texcoord_bits = t.stream.readChar()*2; + t.stack[11] = Math.pow(2.0, t.texcoord_q); + } + + if(t.sig.indices) { + t.decodeFaces(); + +// var faces = window.performance.now() - start; +// start += faces; + } else { + t.decodeCoordinates(); + +// var coords = window.performance.now() - start; +// start += coords; + } + + if(t.sig.normals) + t.decodeNormals(); +// var normals = window.performance.now() - start; +// start += normals; + if(t.sig.colors) + t.decodeColors(); +// var colors = window.performance.now() - start; +// start += colors; +// console.log("Decode " + (faces + coords + normals + colors) + "ms. C: " + coords + " F: " + faces + " N: " + normals + " C: " + colors); + + return t.buffer; +}, + +decodeCoordinates: function() { + var t = this; + t.min = [t.stack[3], t.stack[4], t.stack[5]]; + + var step = Math.pow(2.0, t.coord_q); + + var hi_bits = Math.max(t.coord_bits - 32, 0); + var lo_bits = Math.min(t.coord_bits, 32); + + var bitstream = t.stream.readBitStream(); + + var tunstall = new Tunstall; + var diffs = tunstall.decompress(t.stream); + + var hi = bitstream.read(hi_bits); + var lo = bitstream.read(lo_bits); + var p = new ZPoint(hi, lo); + var count = 0; + p.toPoint(t.min, step, t.coords, count); + count += 3; + for(var i = 1; i < t.node.nvert; i++) { + var d = diffs[i-1]; + p.setBit(d, 1); + if(d > 32) { + p.hi = (p.hi & ~((1<<(d-32))-1))>>>0; + var e = bitstream.read(d - 32); + p.hi = (p.hi | e)>>>0; + p.lo = bitstream.read(32); + } else { + + if(d == 32) { + p.lo = bitstream.read(d); + } else { + var e = bitstream.read(d); + p.lo = (p.lo & ~((1<>>0; + p.lo = (p.lo | e)>>>0; + } + } + p.toPoint(t.min, step, t.coords, count); + count += 3; + } +}, + +decodeFaces: function() { + if(!this.node.nface) return; + + this.vertex_count = 0; + var start = 0; + for(var p = 0; p < this.patches.length; p++) { + var end = this.patches[p]; + this.decodeConnectivity(end - start, start*3); + start = end; + } + //dequantize positions + var tot = this.node.nvert*3; + var coords = this.coords; + var stack = this.stack; + for(var i = 0; i < tot; ) { + coords[i] = (coords[i] + stack[3])*stack[6]; i++; + coords[i] = (coords[i] + stack[4])*stack[6]; i++; + coords[i] = (coords[i] + stack[5])*stack[6]; i++; + } + if(this.sig.texcoords) { + var t_tot = this.node.nvert*2; + var t_coords = this.texcoords; + for(var i = 0; i < tot; ) { + t_coords[i] = (t_coords[i] + stack[9])*stack[11]; i++; + t_coords[i] = (t_coords[i] + stack[10])*stack[11]; i++; + } + } +}, + +decodeNormals: function() { + var norm_q = this.stream.readChar(); + + var dtunstall = new Tunstall; + var diffs = dtunstall.decompress(this.stream); + + var stunstall = new Tunstall; + var signs = stunstall.decompress(this.stream); + var bitstream = this.stream.readBitStream(); + + var side = (1<<(16 - norm_q))>>>0; + var diffcount = 0; + var signcount = 0; + + if(!this.sig.indices) { + for(var k = 0; k < 2; k++) { + var on = 0; + for(var i = 0; i < this.node.nvert; i++) { + var d = this.decodeDiff(diffs[diffcount++], bitstream); + on = on + d; + this.normals[3*i + k] = on*side; + } + } + for(var i = 0; i < this.node.nvert; i++) { + var offset = i*3; + var x = this.normals[offset + 0]; + var y = this.normals[offset + 1]; + var z = 32767.0*32767.0 - x*x - y*y; + + if(z < 0) z = 0; + z = Math.sqrt(z); + if(z > 32767) z = 32767; + if(signs[i] == 0) + z = -z; + this.normals[offset + 2] = z; + } + return; + } + + var boundary = this.markBoundary(); + this.computeNormals(); + + if(this.sig.texcoords) //hack, fixing normals makes it worse actually + return; + + var stat = 0; + //get difference between original and predicted + for(var i = 0; i < this.node.nvert; i++) { + if(!boundary[i]) continue; + var offset = i*3; + var x = (this.normals[offset + 0]/side); + var y = (this.normals[offset + 1]/side); + var dx = this.decodeDiff(diffs[diffcount++], bitstream); + var dy = this.decodeDiff(diffs[diffcount++], bitstream); + x = (x + dx)*side; + y = (y + dy)*side; + + var z = 32767.0*32767.0 - x*x - y*y; + + if(z < 0) z = 0; + z = Math.sqrt(z); + //sign + if(z > 32767.0) z = 32767.0; + var signbit = signs[signcount++]; +// if(this.normals[offset+2] < 0 != signbit) + if((this.normals[offset+2] < 0 && signbit == 0) || (this.normals[offset+2] > 0 && signbit == 1)) + z = -z; + this.normals[offset + 0] = x; + this.normals[offset + 1] = y; + this.normals[offset + 2] = z; + } +}, + +decodeColors: function() { + var color_q = []; + for(var k = 0; k < 4; k++) + color_q[k] = this.stream.readChar(); + + var diffs = []; + for(var k = 0; k < 4; k++) { + var tunstall = new Tunstall;; + diffs[k] = tunstall.decompress(this.stream); + } + var bitstream = this.stream.readBitStream(); + + var count = 0; + if(this.sig.indices) { + for(var i = 0; i < this.node.nvert; i++) { + var last = this.last[i]*4; + var offset = i*4; + + for(var k = 0; k < 4; k++) { + var c = this.decodeDiff(diffs[k][count], bitstream); + + if(last >= 0) + c += this.colors[last + k]; + this.colors[offset] = c; + offset++; + } + count++; + } + } else { + for(var k = 0; k < 4; k++) + this.colors[k] = this.decodeDiff(diffs[k][count], bitstream); + count++; + + var offset = 4; + for(var i = 1; i < this.node.nvert; i++) { + for(var k = 0; k < 4; k++) { + var d = this.decodeDiff(diffs[k][count], bitstream); + this.colors[offset] = this.colors[offset-4] + d; + offset ++; + } + count++; + } + } + + var steps = []; + for(var k = 0; k < 4; k++) + steps[k] = (1<<(8 - color_q[k])); + + //convert to rgb + for(var i = 0; i < this.node.nvert; i++) { + var offset = i*4; + + var e0 = this.colors[offset + 0] * steps[0]; + var e1 = this.colors[offset + 1] * steps[1]; + var e2 = this.colors[offset + 2] * steps[2]; + + this.colors[offset + 0] = (e2 + e0)&0xff; + this.colors[offset + 1] = e0; + this.colors[offset + 2] = (e1 + e0)&0xff; + } +}, + +//how to determine if a vertex is a boundary without topology: +//for each edge a vertex is in, add or subtract the id of the other vertex depending on order +//for internal vertices sum is zero. +//unless we have strange configurations and a lot of sfiga, zero wont happen. //TODO think about this +markBoundary: function() { +// var boundary = new Uint8Array(this.node.nvert); + var count = new Uint32Array(this.node.nvert); + + var offset = 0; + for(var i = 0; i < this.node.nface; i++) { + count[this.faces[offset + 0]] += this.faces[offset + 1] - this.faces[offset + 2]; + count[this.faces[offset + 1]] += this.faces[offset + 2] - this.faces[offset + 0]; + count[this.faces[offset + 2]] += this.faces[offset + 0] - this.faces[offset + 1]; + offset += 3; + } + return count; +// for(var i = 0; i < this.node.nvert; i++) +// if(count[i] != 0) +// boundary[i] = true; +// return boundary; +}, + +norm: function(buffer, a, b, c) { //a b c offsets in the buffer + var ba0 = buffer[b+0] - buffer[a+0]; + var ba1 = buffer[b+1] - buffer[a+1]; + var ba2 = buffer[b+2] - buffer[a+2]; + + var ca0 = buffer[c+0] - buffer[a+0]; + var ca1 = buffer[c+1] - buffer[a+1]; + var ca2 = buffer[c+2] - buffer[a+2]; + + var p = []; + p[0] = ba1*ca2 - ba2*ca1; + p[1] = ba2*ca0 - ba0*ca2; + p[2] = ba0*ca1 - ba1*ca0; + return p; +}, + +normalize: function(buffer, offset) { + var x = buffer[offset + 0]; + var y = buffer[offset + 1]; + var z = buffer[offset + 2]; + var n = Math.sqrt(x*x + y*y + z*z); + if(n > 0) { + buffer[offset + 0] = x/n; + buffer[offset + 1] = y/n; + buffer[offset + 2] = z/n; + } +}, + +computeNormals:function() { + var tmp_normals = new Float32Array(this.node.nvert*3); + + var offset = 0; + for(var i = 0; i < this.node.nface; i++) { + var a = 3*this.faces[offset + 0]; + var b = 3*this.faces[offset + 1]; + var c = 3*this.faces[offset + 2]; + + var buffer = this.coords; + var ba0 = buffer[b+0] - buffer[a+0]; + var ba1 = buffer[b+1] - buffer[a+1]; + var ba2 = buffer[b+2] - buffer[a+2]; + + var ca0 = buffer[c+0] - buffer[a+0]; + var ca1 = buffer[c+1] - buffer[a+1]; + var ca2 = buffer[c+2] - buffer[a+2]; + + var n0 = ba1*ca2 - ba2*ca1; + var n1 = ba2*ca0 - ba0*ca2; + var n2 = ba0*ca1 - ba1*ca0; + + tmp_normals[a + 0] += n0; + tmp_normals[a + 1] += n1; + tmp_normals[a + 2] += n2; + tmp_normals[b + 0] += n0; + tmp_normals[b + 1] += n1; + tmp_normals[b + 2] += n2; + tmp_normals[c + 0] += n0; + tmp_normals[c + 1] += n1; + tmp_normals[c + 2] += n2; + offset += 3; + } + + //normalize + var offset = 0; + for(var i = 0; i < this.node.nvert; i++) { + var x = tmp_normals[offset + 0]; + var y = tmp_normals[offset + 1]; + var z = tmp_normals[offset + 2]; + var n = Math.sqrt(x*x + y*y + z*z); + if(n > 0) { + tmp_normals[offset + 0] = x/n; + tmp_normals[offset + 1] = y/n; + tmp_normals[offset + 2] = z/n; + } + this.normals[offset + 0] = tmp_normals[offset + 0]*32767; + this.normals[offset + 1] = tmp_normals[offset + 1]*32767; + this.normals[offset + 2] = tmp_normals[offset + 2]*32767; + offset += 3; + } +}, + +decodeDiff: function(diff, bitstream) { + var val; + if(diff == 0) { + val = 1; + } else { + val = 1<<(diff); + val |= bitstream.read(diff); + }; + val--; //vall is always >= 1 + if(val & 0x1) + val = -((val+1)>>1); + else + val = val>>1; + return val; +}, + +/* an edge is: uint16_t face, uint16_t side, uint32_t prev, next, bool deleted +I do not want to create millions of small objects, I will use aUint32Array. +Problem is how long, sqrt(nface) we will over blow using nface. +*/ + +decodeConnectivity: function(length, start) { + + var t = this; + var ctunstall = new Tunstall; + var clers = ctunstall.decompress(this.stream); + var cler_count = 0; + + var dtunstall = new Tunstall; + var diffs = dtunstall.decompress(this.stream); + var diff_count = 0; + + var tdiffs; + var tdiff_count = 0; + if(t.sig.texcoords) { + var ttunstall = new Tunstall; + tdiffs = ttunstall.decompress(this.stream); + } + + var bitstream = this.stream.readBitStream(bitstream); + + var current_face = 0; //keep track of connected component start + //t.vertex_count = 0; + var front = new Uint32Array(this.node.nface*18); + var front_count = 0; //count each integer so it's front_back*5 + function addFront(_v0, _v1, _v2, _prev, _next) { + front[front_count++] = _v0; + front[front_count++] = _v1; + front[front_count++] = _v2; + front[front_count++] = _prev; + front[front_count++] = _next; + front[front_count++] = 0; //deleted + } + function _next(t) { + t++; + if(t == 3) t = 0; + return t; + } + function _prev(t) { + t--; + if(t == -1) t = 2; + return t; + } + + var delayed = []; + var faceorder = []; + + var faces_count = start; //count indices in this.faces array + var totfaces = length; +// var estimated = [0, 0, 0]; //no! use stack. + var stack = this.stack; + var coords = this.coords; + var texcoords = this.texcoords; + var hasTexCoords = t.sig.texcoords; + + while(totfaces > 0) { + if(!faceorder.length && !delayed.length) { + if(current_face == this.node.nface) break; //no more faces to encode exiting + + stack[0] = stack[1] = stack[2] = 0; + stack[7] = stack[8] = 0; //texcoords + var last_index = -1; + var index = []; + for(var k = 0; k < 3; k++) { + this.last[this.last_count++] = last_index; + var diff = diffs[diff_count++]; + var tdiff = diff && hasTexCoords? tdiffs[tdiff_count++] : 0; + var v = this.decodeVertex(bitstream, diff, tdiff); + index[k] = v; + this.faces[faces_count++] = v; + stack[0] = coords[v*3]; + stack[1] = coords[v*3+1]; + stack[2] = coords[v*3+2]; + if(t.sig.texcoords) { + stack[7] = texcoords[v*2]; + stack[8] = texcoords[v*2+1]; + } + last_index = v; + } + var current_edge = front_count; + for(var k = 0; k < 3; k++) { + faceorder.push(front_count); + front[front_count++] = index[_next(k)]; + front[front_count++] = index[_prev(k)]; + front[front_count++] = index[k]; + front[front_count++] = current_edge + _prev(k)*6; + front[front_count++] = current_edge + _next(k)*6; + front_count++; +// addFront(index[_next(k)], index[_prev(k)], index[k], current_edge + _prev(k)*6, current_edge + _next(k)*6); + } + current_face++; + totfaces--; + continue; + } + var f; + if(faceorder.length) + f = faceorder.shift(); + else + f = delayed.pop(); + + var edge_start = f; + + if(front[edge_start + 5]) continue; //deleted + front[edge_start + 5] = 1; //set edge as deleted anyway + + var c = clers[cler_count++]; + if(c == 4) continue; //BOUNDARY + + var v0 = front[edge_start + 0]; + var v1 = front[edge_start + 1]; + var v2 = front[edge_start + 2]; + var prev = front[edge_start + 3]; + var next = front[edge_start + 4]; + + var first_edge = front_count; //points to new edge to be inserted + var opposite = -1; + if(c == 0) { //VERTEX + //predict position based on v0, v1 and v2 + for(var k = 0; k < 3; k++) + stack[k] = coords[v0*3 + k] + coords[v1*3 + k] - coords[v2*3 + k]; + + if(hasTexCoords) + for(var k = 0; k < 2; k++) + stack[7+k] = texcoords[v0*2 + k] + texcoords[v1*2 + k] - texcoords[v2*2 + k]; + + var diff = diffs[diff_count++]; + var tdiff = diff && hasTexCoords? tdiffs[tdiff_count++] : 0; + opposite = this.decodeVertex(bitstream, diff, tdiff); + if(diff != 0) + this.last[this.last_count++] = v1; + + front[prev + 4] = first_edge; + front[next + 3] = first_edge + 6; + faceorder.unshift(front_count); + + front[front_count++] = v0; + front[front_count++] = opposite; + front[front_count++] = v1; + front[front_count++] = prev; + front[front_count++] = first_edge+6; + front_count++; +// addFront(v0, opposite, v1, prev, first_edge + 6); + + faceorder.push(front_count); + + front[front_count++] = opposite; + front[front_count++] = v1; + front[front_count++] = v0; + front[front_count++] = first_edge; + front[front_count++] = next; + front_count++; +// addFront(opposite, v1, v0, first_edge, next); + + } else if(c == 3) { //END + front[prev + 5] = 1; + front[next + 5] = 1; + front[front[prev + 3] + 4] = front[next + 4]; + front[front[next + 4] + 3] = front[prev + 3]; + opposite = front[prev + 0]; + + } else if(c == 1) { //LEFT + front[prev + 5] = 1; //deleted + front[front[prev + 3] + 4] = first_edge; + front[next + 3] = first_edge; + opposite = front[prev + 0]; + + faceorder.unshift(front_count); + + front[front_count++] = opposite; + front[front_count++] = v1; + front[front_count++] = v0; + front[front_count++] = front[prev +3]; + front[front_count++] = next; + front_count++; +// addFront(opposite, v1, v0, front[prev + 3], next); + + } else if(c == 2) { //RIGHT + front[next + 5] = 1; + front[front[next + 4] + 3] = first_edge; + front[prev + 4] = first_edge; + opposite = front[next + 1]; + + + faceorder.unshift(front_count); + + front[front_count++] = v0; + front[front_count++] = opposite; + front[front_count++] = v1; + front[front_count++] = prev; + front[front_count++] = front[next+4]; + front_count++; +// addFront(v0, opposite, v1, prev, front[next + 4]); + + } else if(c == 5) { //DELAY + front[edge_start + 5] = 0; + delayed.push(edge_start); + continue; + } + this.faces[faces_count++] = v1; + this.faces[faces_count++] = v0; + this.faces[faces_count++] = opposite; + totfaces--; + } +}, + +decodeVertex: function(bitstream, diff, tdiff) { + if(diff == 0) + return bitstream.read(16); + + var v = this.vertex_count++; + + var max = 1<<(diff-1); + + for(var k = 0; k < 3; k++) { + var d = bitstream.read(diff) - max; + this.coords[v*3+k] = this.stack[k] + d; //stack 0-3 is used as extimated + } + if(this.sig.texcoords) { + var tmax = 1<<(tdiff-1); + for(var k = 0; k < 2; k++) { + var d = bitstream.read(tdiff) - tmax; + this.texcoords[v*2+k] = this.stack[7+k] + d; //stack 7-9 is used as extimated + } + } + return v; +}, + +decodeDiff: function(diff, bitstream) { + var val; + if(diff == 0) { + return 0; + } + val = 1<>>= 1; + else + val = -(val>>>1); + + return val; +} + +}; + +var tot = 0; diff -r 111d952e7fa0 -r 967f947014ac Resources/Nexus/js/nexus.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Nexus/js/nexus.js Tue Apr 09 22:13:01 2024 +0200 @@ -0,0 +1,1257 @@ +/* +Nexus +Copyright (c) 2012-2018, Visual Computing Lab, ISTI - CNR +All rights reserved. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +Nexus = function() { + +/* WORKER INITIALIZED ONCE */ + +var meco; +var corto; + +var scripts = document.getElementsByTagName('script'); +var i, j, k; +var path; +for(i = 0; i < scripts.length; i++) { + var attrs = scripts[i].attributes; + for(j = 0; j < attrs.length; j++) { + var a = attrs[j]; + if(a.name != 'src') continue; + if(!a.value) continue; + if(a.value.search('nexus.js') >= 0) { + path = a.value; + break; + } + } +} +var meco = null; +function loadMeco() { + meco = new Worker(path.replace('nexus.js', 'meco.js')); + + meco.onerror = function(e) { console.log(e); } + meco.requests = {}; + meco.count = 0; + meco.postRequest = function(sig, node, patches) { + var signature = { + texcoords: sig.texcoords ? 1 : 0, + colors : sig.colors ? 1 : 0, + normals : sig.normals ? 1 : 0, + indices : sig.indices ? 1 : 0 + }; + meco.postMessage({ + signature:signature, + node:{ nface: node.nface, nvert: node.nvert, buffer:node.buffer, request:this.count}, + patches:patches + }); + this.requests[this.count++] = node; + }; + meco.onmessage = function(e) { + var node = this.requests[e.data.request]; + node.buffer = e.data.buffer; + readyNode(node); + }; +} + +var corto = null; + +function loadCorto() { + corto = new Worker(path.replace('nexus.js', 'corto.js')); + corto.requests = {}; + corto.count = 0; + corto.postRequest = function(node) { + corto.postMessage({ buffer: node.buffer, request:this.count, rgba_colors: true, short_normals: true }); + this.requests[this.count++] = node; + } + corto.onmessage = function(e) { + var node = this.requests[e.data.request]; + node.buffer = e.data.buffer; + node.model = e.data.model; + readyNode(node); + }; +} + +/* UTILITIES */ + +function getUint64(view) { + var s = 0; + var lo = view.getUint32(view.offset, true); + var hi = view.getUint32(view.offset + 4, true); + view.offset += 8; + return ((hi * (1 << 32)) + lo); +} + +function getUint32(view) { + var s = view.getUint32(view.offset, true); + view.offset += 4; + return s; +} + +function getUint16(view) { + var s = view.getUint16(view.offset, true); + view.offset += 2; + return s; +} + +function getFloat32(view) { + var s = view.getFloat32(view.offset, true); + view.offset += 4; + return s; +} + +/* MATRIX STUFF */ + +function vecMul(m, v, r) { + var w = m[3]*v[0] + m[7]*v[1] + m[11]*v[2] + m[15]; + + r[0] = (m[0]*v[0] + m[4]*v[1] + m[8 ]*v[2] + m[12 ])/w; + r[1] = (m[1]*v[0] + m[5]*v[1] + m[9 ]*v[2] + m[13 ])/w; + r[2] = (m[2]*v[0] + m[6]*v[1] + m[10]*v[2] + m[14])/w; +} + + +function matMul(a, b, r) { + r[ 0] = a[0]*b[0] + a[4]*b[1] + a[8]*b[2] + a[12]*b[3]; + r[ 1] = a[1]*b[0] + a[5]*b[1] + a[9]*b[2] + a[13]*b[3]; + r[ 2] = a[2]*b[0] + a[6]*b[1] + a[10]*b[2] + a[14]*b[3]; + r[ 3] = a[3]*b[0] + a[7]*b[1] + a[11]*b[2] + a[15]*b[3]; + + r[ 4] = a[0]*b[4] + a[4]*b[5] + a[8]*b[6] + a[12]*b[7]; + r[ 5] = a[1]*b[4] + a[5]*b[5] + a[9]*b[6] + a[13]*b[7]; + r[ 6] = a[2]*b[4] + a[6]*b[5] + a[10]*b[6] + a[14]*b[7]; + r[ 7] = a[3]*b[4] + a[7]*b[5] + a[11]*b[6] + a[15]*b[7]; + + r[ 8] = a[0]*b[8] + a[4]*b[9] + a[8]*b[10] + a[12]*b[11]; + r[ 9] = a[1]*b[8] + a[5]*b[9] + a[9]*b[10] + a[13]*b[11]; + r[10] = a[2]*b[8] + a[6]*b[9] + a[10]*b[10] + a[14]*b[11]; + r[11] = a[3]*b[8] + a[7]*b[9] + a[11]*b[10] + a[15]*b[11]; + + r[12] = a[0]*b[12] + a[4]*b[13] + a[8]*b[14] + a[12]*b[15]; + r[13] = a[1]*b[12] + a[5]*b[13] + a[9]*b[14] + a[13]*b[15]; + r[14] = a[2]*b[12] + a[6]*b[13] + a[10]*b[14] + a[14]*b[15]; + r[15] = a[3]*b[12] + a[7]*b[13] + a[11]*b[14] + a[15]*b[15]; +} + +function matInv(m, t) { + var s = 1.0/( + m[12]* m[9]*m[6]*m[3]-m[8]*m[13]*m[6]*m[3]-m[12]*m[5]*m[10]*m[3]+m[4]*m[13]*m[10]*m[3]+ + m[8]*m[5]*m[14]*m[3]-m[4]*m[9]*m[14]*m[3]-m[12]*m[9]*m[2]*m[7]+m[8]*m[13]*m[2]*m[7]+ + m[12]*m[1]*m[10]*m[7]-m[0]*m[13]*m[10]*m[7]-m[8]*m[1]*m[14]*m[7]+m[0]*m[9]*m[14]*m[7]+ + m[12]*m[5]*m[2]*m[11]-m[4]*m[13]*m[2]*m[11]-m[12]*m[1]*m[6]*m[11]+m[0]*m[13]*m[6]*m[11]+ + m[4]*m[1]*m[14]*m[11]-m[0]*m[5]*m[14]*m[11]-m[8]*m[5]*m[2]*m[15]+m[4]*m[9]*m[2]*m[15]+ + m[8]*m[1]*m[6]*m[15]-m[0]*m[9]*m[6]*m[15]-m[4]*m[1]*m[10]*m[15]+m[0]*m[5]*m[10]*m[15] + ); + + t[ 0] = (m[9]*m[14]*m[7]-m[13]*m[10]*m[7]+m[13]*m[6]*m[11]-m[5]*m[14]*m[11]-m[9]*m[6]*m[15]+m[5]*m[10]*m[15])*s; + t[ 1] = (m[13]*m[10]*m[3]-m[9]*m[14]*m[3]-m[13]*m[2]*m[11]+m[1]*m[14]*m[11]+m[9]*m[2]*m[15]-m[1]*m[10]*m[15])*s; + t[ 2] = (m[5]*m[14]*m[3]-m[13]*m[6]*m[3]+m[13]*m[2]*m[7]-m[1]*m[14]*m[7]-m[5]*m[2]*m[15]+m[1]*m[6]*m[15])*s; + t[ 3] = (m[9]*m[6]*m[3]-m[5]*m[10]*m[3]-m[9]*m[2]*m[7]+m[1]*m[10]*m[7]+m[5]*m[2]*m[11]-m[1]*m[6]*m[11])*s; + + t[ 4] = (m[12]*m[10]*m[7]-m[8]*m[14]*m[7]-m[12]*m[6]*m[11]+m[4]*m[14]*m[11]+m[8]*m[6]*m[15]-m[4]*m[10]*m[15])*s; + t[ 5] = (m[8]*m[14]*m[3]-m[12]*m[10]*m[3]+m[12]*m[2]*m[11]-m[0]*m[14]*m[11]-m[8]*m[2]*m[15]+m[0]*m[10]*m[15])*s; + t[ 6] = (m[12]*m[6]*m[3]-m[4]*m[14]*m[3]-m[12]*m[2]*m[7]+m[0]*m[14]*m[7]+m[4]*m[2]*m[15]-m[0]*m[6]*m[15])*s; + t[ 7] = (m[4]*m[10]*m[3]-m[8]*m[6]*m[3]+m[8]*m[2]*m[7]-m[0]*m[10]*m[7]-m[4]*m[2]*m[11]+m[0]*m[6]*m[11])*s; + + t[ 8] = (m[8]*m[13]*m[7]-m[12]*m[9]*m[7]+m[12]*m[5]*m[11]-m[4]*m[13]*m[11]-m[8]*m[5]*m[15]+m[4]*m[9]*m[15])*s; + t[ 9] = (m[12]*m[9]*m[3]-m[8]*m[13]*m[3]-m[12]*m[1]*m[11]+m[0]*m[13]*m[11]+m[8]*m[1]*m[15]-m[0]*m[9]*m[15])*s; + t[10] = (m[4]*m[13]*m[3]-m[12]*m[5]*m[3]+m[12]*m[1]*m[7]-m[0]*m[13]*m[7]-m[4]*m[1]*m[15]+m[0]*m[5]*m[15])*s; + t[11] = (m[8]*m[5]*m[3]-m[4]*m[9]*m[3]-m[8]*m[1]*m[7]+m[0]*m[9]*m[7]+m[4]*m[1]*m[11]-m[0]*m[5]*m[11])*s; + + t[12] = (m[12]*m[9]*m[6]-m[8]*m[13]*m[6]-m[12]*m[5]*m[10]+m[4]*m[13]*m[10]+m[8]*m[5]*m[14]-m[4]*m[9]*m[14])*s; + t[13] = (m[8]*m[13]*m[2]-m[12]*m[9]*m[2]+m[12]*m[1]*m[10]-m[0]*m[13]*m[10]-m[8]*m[1]*m[14]+m[0]*m[9]*m[14])*s; + t[14] = (m[12]*m[5]*m[2]-m[4]*m[13]*m[2]-m[12]*m[1]*m[6]+m[0]*m[13]*m[6]+m[4]*m[1]*m[14]-m[0]*m[5]*m[14])*s; + t[15] = (m[4]*m[9]*m[2]-m[8]*m[5]*m[2]+m[8]*m[1]*m[6]-m[0]*m[9]*m[6]-m[4]*m[1]*m[10]+m[0]*m[5]*m[10])*s; +} + +/* PRIORITY QUEUE */ + +PriorityQueue = function(max_length) { + this.error = new Float32Array(max_length); + this.data = new Int32Array(max_length); + this.size = 0; +} + +PriorityQueue.prototype = { + push: function(data, error) { + this.data[this.size] = data; + this.error[this.size] = error; + this.bubbleUp(this.size); + this.size++; + }, + + pop: function() { + var result = this.data[0]; + this.size--; + if(this.size > 0) { + this.data[0] = this.data[this.size]; + this.error[0] = this.error[this.size]; + this.sinkDown(0); + } + return result; + }, + + bubbleUp: function(n) { + var data = this.data[n]; + var error = this.error[n]; + while (n > 0) { + var pN = ((n+1)>>1) -1; + var pError = this.error[pN]; + if(pError > error) + break; + //swap + this.data[n] = this.data[pN]; + this.error[n] = pError; + this.data[pN] = data; + this.error[pN] = error; + n = pN; + } + }, + + sinkDown: function(n) { + var data = this.data[n]; + var error = this.error[n]; + + while(true) { + var child2N = (n + 1) * 2; + var child1N = child2N - 1; + var swap = -1; + if (child1N < this.size) { + var child1Error = this.error[child1N]; + if(child1Error > error) + swap = child1N; + } + if (child2N < this.size) { + var child2Error = this.error[child2N]; + if (child2Error > (swap == -1 ? error : child1Error)) + swap = child2N; + } + + if (swap == -1) break; + + this.data[n] = this.data[swap]; + this.error[n] = this.error[swap]; + this.data[swap] = data; + this.error[swap] = error; + n = swap; + } + } +}; + + +/* HEADER AND PARSING */ + +var padding = 256; +var Debug = { + nodes : false, //color each node + culling : false, //visibility culling disabled + draw : false, //final rendering call disabled + extract : false, //no extraction + request : false, //no network requests + worker : false //no web workers +}; + + +var glP = WebGLRenderingContext.prototype; +var attrGlMap = [glP.NONE, glP.BYTE, glP.UNSIGNED_BYTE, glP.SHORT, glP.UNSIGNED_SHORT, glP.INT, glP.UNSIGNED_INT, glP.FLOAT, glP.DOUBLE]; +var attrSizeMap = [0, 1, 1, 2, 2, 4, 4, 4, 8]; + + +var targetError = 2.0; +var targetFps = 15; +var maxPending = 3; +var maxBlocked = 3; +var maxReqAttempt = 2; +var maxCacheSize = 512*(1<<20); //TODO DEBUG +var drawBudget = 5*(1<<20); + + +/* MESH DEFINITION */ + +Mesh = function() { + var t = this; + t.onLoad = null; + t.reqAttempt = 0; +} + +Mesh.prototype = { + open: function(url) { + var mesh = this; + mesh.url = url; + mesh.httpRequest( + 0, + 88, + function() { +// console.log("Loading header for " + mesh.url); + var view = new DataView(this.response); + view.offset = 0; + mesh.reqAttempt++; + var header = mesh.importHeader(view); + if(!header) { + console.log("Empty header!"); + if(mesh.reqAttempt < maxReqAttempt) mesh.open(mesh.url + '?' + Math.random()); // BLINK ENGINE CACHE BUG PATCH + return null; + } + mesh.reqAttempt = 0; + for(i in header) + mesh[i] = header[i]; + mesh.vertex = mesh.signature.vertex; + mesh.face = mesh.signature.face; + mesh.renderMode = mesh.face.index?["FILL", "POINT"]:["POINT"]; + mesh.compressed = (mesh.signature.flags & (2 | 4)); //meco or corto + mesh.meco = (mesh.signature.flags & 2); + mesh.corto = (mesh.signature.flags & 4); + mesh.requestIndex(); + }, + function() { console.log("Open request error!");}, + function() { console.log("Open request abort!");} + ); + }, + + httpRequest: function(start, end, load, error, abort, type) { + if(!type) type = 'arraybuffer'; + var r = new XMLHttpRequest(); + r.open('GET', this.url, true); + r.responseType = type; + r.setRequestHeader("Range", "bytes=" + start + "-" + (end -1)); + r.onload = function(){ + switch (this.status){ + case 0: +// console.log("0 response: server unreachable.");//returned in chrome for local files + case 206: +// console.log("206 response: partial content loaded."); + load.bind(this)(); + break; + case 200: +// console.log("200 response: server does not support byte range requests."); + } + }; + r.onerror = error; + r.onabort = abort; + r.send(); + return r; + }, + + requestIndex: function() { + var mesh = this; + var end = 88 + mesh.nodesCount*44 + mesh.patchesCount*12 + mesh.texturesCount*68; + mesh.httpRequest( + 88, + end, + function() { mesh.handleIndex(this.response); }, + function() { console.log("Index request error!");}, + function() { console.log("Index request abort!");} + ); + }, + + handleIndex: function(buffer) { + var t = this; + var view = new DataView(buffer); + view.offset = 0; + + var n = t.nodesCount; + + t.noffsets = new Uint32Array(n); + t.nvertices = new Uint32Array(n); + t.nfaces = new Uint32Array(n); + t.nerrors = new Float32Array(n); + t.nspheres = new Float32Array(n*5); + t.nsize = new Float32Array(n); + t.nfirstpatch = new Uint32Array(n); + + for(i = 0; i < n; i++) { + t.noffsets[i] = padding*getUint32(view); //offset + t.nvertices[i] = getUint16(view); //verticesCount + t.nfaces[i] = getUint16(view); //facesCount + t.nerrors[i] = getFloat32(view); + view.offset += 8; //skip cone + for(k = 0; k < 5; k++) + t.nspheres[i*5+k] = getFloat32(view); //sphere + tight + t.nfirstpatch[i] = getUint32(view); //first patch + } + t.sink = n -1; + + t.patches = new Uint32Array(view.buffer, view.offset, t.patchesCount*3); //noded, lastTriangle, texture + t.nroots = t.nodesCount; + for(j = 0; j < t.nroots; j++) { + for(i = t.nfirstpatch[j]; i < t.nfirstpatch[j+1]; i++) { + if(t.patches[i*3] < t.nroots) + t.nroots = t.patches[i*3]; + } + } + + view.offset += t.patchesCount*12; + + t.textures = new Uint32Array(t.texturesCount); + t.texref = new Uint32Array(t.texturesCount); + for(i = 0; i < t.texturesCount; i++) { + t.textures[i] = padding*getUint32(view); + view.offset += 16*4; //skip proj matrix + } + + t.vsize = 12 + (t.vertex.normal?6:0) + (t.vertex.color?4:0) + (t.vertex.texCoord?8:0); + t.fsize = 6; + //problem: I have no idea how much space a texture is needed in GPU. 10x factor assumed. + + var tmptexsize = new Uint32Array(n-1); + var tmptexcount = new Uint32Array(n-1); + for(var i = 0; i < n-1; i++) { + for(var p = t.nfirstpatch[i]; p != t.nfirstpatch[i+1]; p++) { + var tex = t.patches[p*3+2]; + tmptexsize[i] += t.textures[tex+1] - t.textures[tex]; + tmptexcount[i]++; + } + t.nsize[i] = t.vsize*t.nvertices[i] + t.fsize*t.nfaces[i]; + } + for(var i = 0; i < n-1; i++) { + t.nsize[i] += 10*tmptexsize[i]/tmptexcount[i]; + } + + t.status = new Uint8Array(n); //0 for none, 1 for ready, 2+ for waiting data + t.frames = new Uint32Array(n); + t.errors = new Float32Array(n); //biggest error of instances + t.ibo = new Array(n); + t.vbo = new Array(n); + t.texids = new Array(n); + + t.isReady = true; + if(t.onLoad) t.onLoad(); + }, + + importAttribute: function(view) { + var a = {}; + a.type = view.getUint8(view.offset++, true); + a.size = view.getUint8(view.offset++, true); + a.glType = attrGlMap[a.type]; + a.normalized = a.type < 7; + a.stride = attrSizeMap[a.type]*a.size; + if(a.size == 0) return null; + return a; + }, + + importElement: function(view) { + var e = []; + for(i = 0; i < 8; i++) + e[i] = this.importAttribute(view); + return e; + }, + + importVertex: function(view) { //enum POSITION, NORMAL, COLOR, TEXCOORD, DATA0 + var e = this.importElement(view); + var color = e[2]; + if(color) { + color.type = 2; //unsigned byte + color.glType = attrGlMap[2]; + } + return { position: e[0], normal: e[1], color: e[2], texCoord: e[3], data: e[4] }; + }, + + //enum INDEX, NORMAL, COLOR, TEXCOORD, DATA0 + importFace: function(view) { + var e = this.importElement(view); + var color = e[2]; + if(color) { + color.type = 2; //unsigned byte + color.glType = attrGlMap[2]; + } + return { index: e[0], normal: e[1], color: e[2], texCoord: e[3], data: e[4] }; + }, + + importSignature: function(view) { + var s = {}; + s.vertex = this.importVertex(view); + s.face = this.importFace(view); + s.flags = getUint32(view); + return s; + }, + + importHeader: function(view) { + var magic = getUint32(view); + if(magic != 0x4E787320) return null; + var h = {}; + h.version = getUint32(view); + h.verticesCount = getUint64(view); + h.facesCount = getUint64(view); + h.signature = this.importSignature(view); + h.nodesCount = getUint32(view); + h.patchesCount = getUint32(view); + h.texturesCount = getUint32(view); + h.sphere = { + center: [getFloat32(view), getFloat32(view), getFloat32(view)], + radius: getFloat32(view) + }; + return h; + } +}; + +Instance = function(gl) { + this.gl = gl; + this.onLoad = function() {}; + this.onUpdate = function() {}; + this.drawBudget = drawBudget; + this.attributes = { 'position':0, 'normal':1, 'color':2, 'uv':3 }; +} + +Instance.prototype = { + open: function(url) { + var t = this; + t.context = getContext(t.gl); + + t.modelMatrix = new Float32Array(16); + t.viewMatrix = new Float32Array(16); + t.projectionMatrix = new Float32Array(16); + t.modelView = new Float32Array(16); + t.modelViewInv = new Float32Array(16); + t.modelViewProj = new Float32Array(16); + t.modelViewProjInv = new Float32Array(16); + t.planes = new Float32Array(24); + t.viewport = new Float32Array(4); + t.viewpoint = new Float32Array(4); + + t.context.meshes.forEach(function(m) { + if(m.url == url){ + t.mesh = m; + t.renderMode = t.mesh.renderMode; + t.mode = t.renderMode[0]; + t.onLoad(); + } + }); + + if(!t.mesh) { + t.mesh = new Mesh(); + t.mesh.onLoad = function() { t.renderMode = t.mesh.renderMode; t.mode = t.renderMode[0]; t.onLoad(); } + t.mesh.open(url); + t.context.meshes.push(t.mesh); + } + }, + + close: function() { + //remove instance from mesh. + }, + + get isReady() { return this.mesh.isReady; }, + setPrimitiveMode : function (mode) { this.mode = mode; }, + get datasetRadius() { if(!this.isReady) return 1.0; return this.mesh.sphere.radius; }, + get datasetCenter() { if(!this.isReady) return [0, 0, 0]; return this.mesh.sphere.center; }, + + updateView: function(viewport, projection, modelView) { + var t = this; + + for(var i = 0; i < 16; i++) { + t.projectionMatrix[i] = projection[i]; + t.modelView[i] = modelView[i]; + } + for(var i = 0; i < 4; i++) + t.viewport[i] = viewport[i]; + + matMul(t.projectionMatrix, t.modelView, t.modelViewProj); + matInv(t.modelViewProj, t.modelViewProjInv); + + matInv(t.modelView, t.modelViewInv); + t.viewpoint[0] = t.modelViewInv[12]; + t.viewpoint[1] = t.modelViewInv[13]; + t.viewpoint[2] = t.modelViewInv[14]; + t.viewpoint[3] = 1.0; + + + var m = t.modelViewProj; + var mi = t.modelViewProjInv; + var p = t.planes; + + //left, right, bottom, top as Ax + By + Cz + D = 0; + p[0] = m[0] + m[3]; p[1] = m[4] + m[7]; p[2] = m[8] + m[11]; p[3] = m[12] + m[15]; + p[4] = -m[0] + m[3]; p[5] = -m[4] + m[7]; p[6] = -m[8] + m[11]; p[7] = -m[12] + m[15]; + p[8] = m[1] + m[3]; p[9] = m[5] + m[7]; p[10] = m[9] + m[11]; p[11] = m[13] + m[15]; + p[12] = -m[1] + m[3]; p[13] = -m[5] + m[7]; p[14] = -m[9] + m[11]; p[15] = -m[13] + m[15]; + p[16] = -m[2] + m[3]; p[17] = -m[6] + m[7]; p[18] = -m[10] + m[11]; p[19] = -m[14] + m[15]; + p[20] = -m[2] + m[3]; p[21] = -m[6] + m[7]; p[22] = -m[10] + m[11]; p[23] = -m[14] + m[15]; + + for(var i = 0; i < 16; i+= 4) { + var l = Math.sqrt(p[i]*p[i] + p[i+1]*p[i+1] + p[i+2]*p[i+2]); + p[i] /= l; p[i+1] /= l; p[i+2] /= l; p[i+3] /= l; + } + //side is M'(1,0,0,1) - M'(-1,0,0,1) and they lie on the planes + var r3 = mi[3] + mi[15]; + var r0 = (mi[0] + mi[12 ])/r3; + var r1 = (mi[1] + mi[13 ])/r3; + var r2 = (mi[2] + mi[14 ])/r3; + + var l3 = -mi[3] + mi[15]; + var l0 = (-mi[0] + mi[12 ])/l3 - r0; + var l1 = (-mi[1] + mi[13 ])/l3 - r1; + var l2 = (-mi[2] + mi[14 ])/l3 - r2; + + var side = Math.sqrt(l0*l0 + l1*l1 + l2*l2); + + //center of the scene is M'*(0, 0, 0, 1) + var c0 = mi[12]/mi[15] - t.viewpoint[0]; + var c1 = mi[13]/mi[15] - t.viewpoint[1]; + var c2 = mi[14]/mi[15] - t.viewpoint[2]; + var dist = Math.sqrt(c0*c0 + c1*c1 + c2*c2); + + t.resolution = (2*side/dist)/ t.viewport[2]; + }, + + traversal : function () { + var t = this; + if(Debug.extract == true) + return; + + if(!t.isReady) return; + + var n = t.mesh.nodesCount; + t.visited = new Uint8Array(n); + t.blocked = new Uint8Array(n); + t.selected = new Uint8Array(n); + + t.visitQueue = new PriorityQueue(n); + for(var i = 0; i < t.mesh.nroots; i++) + t.insertNode(i); + + var candidatesCount = 0; + t.targetError = t.context.currentError; + t.currentError = 1e20; + t.drawSize = 0; + + var nblocked = 0; + var requested = 0; + while(t.visitQueue.size && nblocked < maxBlocked) { + var error = t.visitQueue.error[0]; + var node = t.visitQueue.pop(); + if ((requested < maxPending) && (t.mesh.status[node] == 0)) { + t.context.candidates.push({id: node, instance:t, mesh:t.mesh, frame:t.context.frame, error:error}); + requested++; + } + + var blocked = t.blocked[node] || !t.expandNode(node, error); + if (blocked) + nblocked++; + else { + t.selected[node] = 1; + t.currentError = error; + } + t.insertChildren(node, blocked); + } + }, + + insertNode: function (node) { + var t = this; + t.visited[node] = 1; + + var error = t.nodeError(node); + if(node > 0 && error < t.targetError) return; //2% speed TODO check if needed + + var errors = t.mesh.errors; + var frames = t.mesh.frames; + if(frames[node] != t.context.frame || errors[node] < error) { + errors[node] = error; + frames[node] = t.context.frame; + } + t.visitQueue.push(node, error); + }, + + insertChildren : function (node, block) { + var t = this; + for(var i = t.mesh.nfirstpatch[node]; i < t.mesh.nfirstpatch[node+1]; ++i) { + var child = t.mesh.patches[i*3]; + if (child == t.mesh.sink) return; + if (block) t.blocked[child] = 1; + if (!t.visited[child]) + t.insertNode(child); + } + }, + + expandNode : function (node, error) { + var t = this; + if(node > 0 && error < t.targetError) { +// console.log("Reached error", error, t.targetError); + return false; + } + + if(t.drawSize > t.drawBudget) { +// console.log("Reached drawsize", t.drawSize, t.drawBudget); + return false; + } + + if(t.mesh.status[node] != 1) { //not ready +// console.log("Node " + node + " still not loaded (cache?)"); + return false; + } + + var sp = t.mesh.nspheres; + var off = node*5; + if(t.isVisible(sp[off], sp[off+1], sp[off+2], sp[off+3])) //expanded radius + t.drawSize += t.mesh.nvertices[node]*0.8; + //we are adding half of the new faces. (but we are using the vertices so *2) + + return true; + }, + + nodeError : function (n, tight) { + var t = this; + var spheres = t.mesh.nspheres; + var b = t.viewpoint; + var off = n*5; + var cx = spheres[off+0]; + var cy = spheres[off+1]; + var cz = spheres[off+2]; + var r = spheres[off+3]; + if(tight) + r = spheres[off+4]; + var d0 = b[0] - cx; + var d1 = b[1] - cy; + var d2 = b[2] - cz; + var dist = Math.sqrt(d0*d0 + d1*d1 + d2*d2) - r; + if (dist < 0.1) + dist = 0.1; + + //resolution is how long is a pixel at distance 1. + var error = t.mesh.nerrors[n]/(t.resolution*dist); //in pixels + + if (!t.isVisible(cx, cy. cz, spheres[off+4])) + error /= 1000.0; + return error; + }, + + isVisible : function (x, y, z, r) { + var p = this.planes; + for (i = 0; i < 24; i +=4) { + if(p[i]*x + p[i+1]*y + p[i+2]*z +p[i+3] + r < 0) //ax+by+cz+w = 0; + return false; + } + return true; + }, + + renderNodes: function() { + var t = this; + var m = t.mesh; + var gl = t.gl; + var attr = t.attributes; + + var vertexEnabled = gl.getVertexAttrib(attr.position, gl.VERTEX_ATTRIB_ARRAY_ENABLED); + var normalEnabled = gl.getVertexAttrib(attr.normal, gl.VERTEX_ATTRIB_ARRAY_ENABLED); + var colorEnabled = attr.color >= 0? gl.getVertexAttrib(attr.color, gl.VERTEX_ATTRIB_ARRAY_ENABLED): false; + var uvEnabled = attr.uv >= 0? gl.getVertexAttrib(attr.uv, gl.VERTEX_ATTRIB_ARRAY_ENABLED): false; + + gl.enableVertexAttribArray(attr.position); + if(m.vertex.texCoord && attr.uv >= 0) gl.enableVertexAttribArray(attr.uv); + if(m.vertex.normal && attr.normal >= 0) gl.enableVertexAttribArray(attr.normal); + if(m.vertex.color && attr.color >= 0) gl.enableVertexAttribArray(attr.color); + gl.vertexAttrib4fv(2, [0.8, 0.8, 0.8, 1.0]); + + if(Debug.nodes) { + gl.disableVertexAttribArray(2); + gl.disableVertexAttribArray(3); + } + + var rendered = 0; + var last_texture = -1; + for(var n = 0; n < m.nodesCount; n++) { + if(!t.selected[n]) continue; + + if(t.mode != "POINT") { + var skip = true; + for(var p = m.nfirstpatch[n]; p < m.nfirstpatch[n+1]; p++) { + var child = m.patches[p*3]; + if(!t.selected[child]) { + skip = false; + break; + } + } + if(skip) continue; + } + + + var sp = t.mesh.nspheres; + var off = n*5; + if(!t.isVisible(sp[off], sp[off+1], sp[off+2], sp[off+4])) //tight radius + continue; + + gl.bindBuffer(gl.ARRAY_BUFFER, m.vbo[n]); + if(t.mode != "POINT") + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, m.ibo[n]); + + var nv = m.nvertices[n]; + + gl.vertexAttribPointer(attr.position, 3, gl.FLOAT, false, 12, 0); + var offset = nv*12; + if(m.vertex.texCoord && attr.uv >= 0) + gl.vertexAttribPointer(attr.uv, 2, gl.FLOAT, false, 8, offset), offset += nv*8; + if(m.vertex.normal && attr.normal >= 0) + gl.vertexAttribPointer(attr.normal, 3, gl.SHORT, true, 6, offset), offset += nv*6; + if(m.vertex.color && attr.color >= 0) + gl.vertexAttribPointer(attr.color, 4, gl.UNSIGNED_BYTE, true, 4, offset); + + if (Debug.nodes) { + var error = t.nodeError(n, true); + var palette = { + 1: [1, 1, 1, 1], + 2: [0.5, 1, 1, 1], + 4: [0, 1, 1, 1], + 8: [0, 1, 0.5, 1], + 12: [0, 1, 0, 1], + 16: [0, 1, 0, 1], + 20: [1, 1, 0, 1], + 30: [1, 0.5, 0, 1], + 1e20: [1, 0, 0, 1] }; + + for(i in palette) + if(i > error) { + gl.vertexAttrib4fv(attr.color, palette[i]); + break; + } +// gl.vertexAttrib4fv(2, [(n*200 %255)/255.0, (n*140 %255)/255.0,(n*90 %255)/255.0, 1]); + } + + if (Debug.draw) continue; + + if(t.mode == "POINT") { + var pointsize = Math.ceil(0.30*t.currentError); + if(pointsize > 2) pointsize = 2; + gl.vertexAttrib1fv(4, [pointsize]); + + var error = t.nodeError(n); + var fraction = (error/t.currentError - 1); + if(fraction > 1) fraction = 1; + + var count = fraction * nv; + if(count != 0) { + if(m.vertex.texCoord) { + var texid = m.patches[m.nfirstpatch[n]*3+2]; + if(texid != -1 && texid != last_texture) { //bind texture + var tex = m.texids[texid]; + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, tex); + } + } + gl.drawArrays(gl.POINTS, 0, count); + rendered += count; + } + continue; + } + + //concatenate renderings to remove useless calls. except we have textures. + var offset = 0; + var end = 0; + var last = m.nfirstpatch[n+1]-1; + for (var p = m.nfirstpatch[n]; p < m.nfirstpatch[n+1]; ++p) { + var child = m.patches[p*3]; + + if(!t.selected[child]) { + end = m.patches[p*3+1]; + if(p < last) //if textures we do not join. TODO: should actually check for same texture of last one. + continue; + } + if(end > offset) { + if(m.vertex.texCoord) { + var texid = m.patches[p*3+2]; + if(texid != -1 && texid != last_texture) { //bind texture + var tex = m.texids[texid]; + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, tex); + last_texture = texid; + } + } + gl.drawElements(gl.TRIANGLES, (end - offset) * 3, gl.UNSIGNED_SHORT, offset * 6); + rendered += end - offset; + } + offset = m.patches[p*3+1]; + } + } + + t.context.rendered += rendered; + if(!vertexEnabled) gl.disableVertexAttribArray(attr.position); + if(!normalEnabled && attr.normal >= 0) gl.disableVertexAttribArray(attr.normal); + if(!colorEnabled && attr.color >= 0) gl.disableVertexAttribArray(attr.color); + if(!uvEnabled && attr.uv >= 0) gl.disableVertexAttribArray(attr.uv); + + gl.bindBuffer(gl.ARRAY_BUFFER, null); + if(t.mode != "POINT") + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); + }, + + render: function() { + this.traversal(); + this.renderNodes(); + } +}; + + +//keep track of meshes and which GL they belong to. (no sharing between contexts) +var contexts = []; + +function getContext(gl) { + var c = null; + if(!gl.isTexture) throw "Something wrong"; + contexts.forEach(function(g) { + if(g.gl == gl) c = g; + }); + if(c) return c; + c = { gl:gl, meshes:[], frame:0, cacheSize:0, candidates:[], pending:0, + targetFps: targetFps, targetError: targetError, currentError: targetError }; + contexts.push(c); + return c; +} + + + +function beginFrame(gl, fps) { //each context has a separate frame count. + var c = getContext(gl); + + c.frame++; + c.candidates = []; + if(fps && c.targetFps) { + var r = c.targetFps/fps; + if(r > 1.1) + c.currentError *= 1.05; + if(r < 0.9) + c.currentError *= 0.95; + + if(c.currentError < c.targetError) + c.currentError = c.targetError; + if(c.currentError > 10) c.currentError = 10; + } +// console.log("current", c.currentError, "fps", fps, "targetFps", c.targetFps, "rendered", c.rendered); + c.rendered = 0; +} + +function endFrame(gl) { + updateCache(gl); +} + +function removeNode(context, node) { + var n = node.id; + var m = node.mesh; + if(m.status[n] == 0) return; + +// console.log("Removing " + m.url + " node: " + n); + m.status[n] = 0; + + if (m.georeq.readyState != 4) m.georeq.abort(); + if (m.texreq.readyState != 4) m.texreq.abort(); + + context.cacheSize -= m.nsize[n]; + context.gl.deleteBuffer(m.vbo[n]); + context.gl.deleteBuffer(m.ibo[n]); + m.vbo[n] = m.ibo[n] = null; + + if(!m.vertex.texCoord) return; + var tex = m.patches[m.nfirstpatch[n]*3+2]; //TODO assuming one texture per node + m.texref[tex]--; + + if(m.texref[tex] == 0 && m.texids[tex]) { + context.gl.deleteTexture(m.texids[tex]); + m.texids[tex] = null; + } +} + +function requestNode(context, node) { + var n = node.id; + var m = node.mesh; + + m.status[n] = 2; //pending + + context.pending++; + context.cacheSize += m.nsize[n]; + + node.reqAttempt = 0; + node.context = context; + node.nvert = m.nvertices[n]; + node.nface = m.nfaces[n]; + +// console.log("Requesting " + m.url + " node: " + n); + requestNodeGeometry(context, node); + requestNodeTexture(context, node); +} + +function requestNodeGeometry(context, node) { + var n = node.id; + var m = node.mesh; + + m.status[n]++; //pending + m.georeq = m.httpRequest( + m.noffsets[n], + m.noffsets[n+1], + function() { loadNodeGeometry(this, context, node); }, + function() { +// console.log("Geometry request error!"); + recoverNode(context, node, 0); + }, + function() { +// console.log("Geometry request abort!"); + removeNode(context, node); + }, + 'arraybuffer' + ); +} + +function requestNodeTexture(context, node) { + var n = node.id; + var m = node.mesh; + + if(!m.vertex.texCoord) return; + + var tex = m.patches[m.nfirstpatch[n]*3+2]; + m.texref[tex]++; + if(m.texids[tex]) + return; + + m.status[n]++; //pending + m.texreq = m.httpRequest( + m.textures[tex], + m.textures[tex+1], + function() { loadNodeTexture(this, context, node, tex); }, + function() { +// console.log("Texture request error!"); + recoverNode(context, node, 1); + }, + function() { +// console.log("Texture request abort!"); + removeNode(context, node); + }, + 'blob' + ); +} + +function recoverNode(context, node, id) { + var n = node.id; + var m = node.mesh; + if(m.status[n] == 0) return; + + m.status[n]--; + + if(node.reqAttempt > maxReqAttempt) { +// console.log("Max request limit for " + m.url + " node: " + n); + removeNode(context, node); + return; + } + + node.reqAttempt++; + + switch (id){ + case 0: + requestNodeGeometry(context, node); + console.log("Recovering geometry for " + m.url + " node: " + n); + break; + case 1: + requestNodeTexture(context, node); + console.log("Recovering texture for " + m.url + " node: " + n); + break; + } +} + +function loadNodeGeometry(request, context, node) { + var n = node.id; + var m = node.mesh; + if(m.status[n] == 0) return; + + node.buffer = request.response; + + if(!m.compressed) + readyNode(node); + else if(m.meco) { + var sig = { texcoords: m.vertex.texCoord, normals:m.vertex.normal, colors:m.vertex.color, indices: m.face.index } + var patches = []; + for(var k = m.nfirstpatch[n]; k < m.nfirstpatch[n+1]; k++) + patches.push(m.patches[k*3+1]); + if(!meco) loadMeco(); + meco.postRequest(sig, node, patches); + } else { + if(!corto) loadCorto(); + corto.postRequest(node); + } +} + +function loadNodeTexture(request, context, node, texid) { + var n = node.id; + var m = node.mesh; + if(m.status[n] == 0) return; + + var blob = request.response; + + var urlCreator = window.URL || window.webkitURL; + var img = document.createElement('img'); + img.onerror = function(e) { console.log("Texture loading error!"); }; + img.src = urlCreator.createObjectURL(blob); + + var gl = context.gl; + img.onload = function() { + urlCreator.revokeObjectURL(img.src); + + var flip = gl.getParameter(gl.UNPACK_FLIP_Y_WEBGL); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); + var tex = m.texids[texid] = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + var s = gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flip); + + m.status[n]--; + + if(m.status[n] == 2) { + m.status[n]--; //ready + node.reqAttempt = 0; + node.context.pending--; + node.instance.onUpdate(); + updateCache(gl); + } + } +} + +function scramble(n, coords, normals, colors) { + while (n > 0) { + var i = Math.floor(Math.random() * n); + n--; + for(var k =0; k < 3; k++) { + var v = coords[n*3+k]; + coords[n*3+k] = coords[i*3+k]; + coords[i*3+k] = v; + + if(normals) { + var v = normals[n*3+k]; + normals[n*3+k] = normals[i*3+k]; + normals[i*3+k] = v; + } + if(colors) { + var v = colors[n*4+k]; + colors[n*4+k] = colors[i*4+k]; + colors[i*4+k] = v; + } + } + } +} + +function readyNode(node) { + var m = node.mesh; + var n = node.id; + var nv = m.nvertices[n]; + var nf = m.nfaces[n]; + var model = node.model; + + var vertices; + var indices; + + if(!m.corto) { + vertices = new Uint8Array(node.buffer, 0, nv*m.vsize); + //dangerous alignment 16 bits if we had 1b attribute + indices = new Uint8Array(node.buffer, nv*m.vsize, nf*m.fsize); + if(nf == 0) { + var off = nv*12; + var v = new Float32Array(node.buffer, 0, nv*3); + if(m.vertex.normal) { + var no = new Int16Array(node.buffer, off, nv*3); off += nv*6; + } + if(m.vertex.color) { + var co = new Uint8Array(node.buffer, off, nv*4); + } + scramble(nv, v, no, co); + } + } else { + indices = node.model.index; + vertices = new ArrayBuffer(nv*m.vsize); + var v = new Float32Array(vertices, 0, nv*3); + v.set(model.position, 0, nv*3); + var off = nv*12; + if(model.uv) { + var uv = new Float32Array(vertices, off, nv*2); + uv.set(model.uv); off += nv*8; + } + if(model.normal) { + var no = new Int16Array(vertices, off, nv*3); + no.set(model.normal); off += nv*6; + } + if(model.color) { + var co = new Uint8Array(vertices, off, nv*4); + co.set(model.color); off += nv*4; + } + if(nf == 0) + scramble(nv, v, no, co); + } + + var gl = node.context.gl; + var vbo = m.vbo[n] = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vbo); + gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); + var ibo = m.ibo[n] = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); + + m.status[n]--; + + if(m.status[n] == 2) { + m.status[n]--; //ready + node.reqAttempt = 0; + node.context.pending--; + node.instance.onUpdate(); + updateCache(gl); + } +} + +function updateCache(gl) { + var context = getContext(gl); + + var best = null; + context.candidates.forEach(function(e) { + if(e.mesh.status[e.id] == 0 && (!best || e.error > best.error)) best = e; + }); + context.candidates = []; + if(!best) return; + + while(context.cacheSize > maxCacheSize) { + var worst = null; + //find worst in cache + context.meshes.forEach(function(m) { + var n = m.nodesCount; + for(i = 0; i < n; i++) + if(!worst || (m.status[i] == 1 && m.errors[i] < worst.error)) + worst = {error: m.errors[i], frame: m.frames[i], mesh:m, id:i}; + }); + + if(!worst || (worst.error >= best.error && worst.frame == best.frame)) + return; + removeNode(context, worst); + } + + requestNode(context, best); + + if(context.pending < maxPending) + updateCache(gl); +} + +//nodes are loaded asincronously, just update mesh content (VBO) cache size is kept globally. +//but this could be messy. + +function setTargetError(gl, error) { + var context = getContext(gl); + context.targetError = error; +} +function setTargetFps(gl, fps) { + var context = getContext(gl); + context.targetFps = fps; +} +function setMaxCacheSize(gl, size) { + var context = getContext(gl); + context.maxCacheSize = size; +} + +return { Mesh: Mesh, Renderer: Instance, Renderable: Instance, Instance:Instance, + Debug: Debug, contexts: contexts, beginFrame:beginFrame, endFrame:endFrame, + setTargetError:setTargetError, setTargetFps:setTargetFps, setMaxCacheSize:setMaxCacheSize }; + +}(); diff -r 111d952e7fa0 -r 967f947014ac Resources/Nexus/js/nexus_three.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Nexus/js/nexus_three.js Tue Apr 09 22:13:01 2024 +0200 @@ -0,0 +1,108 @@ +function NexusObject(url, renderer, render, material) { + var gl = renderer.context; + var geometry = new THREE.BufferGeometry(); + var positions = new Float32Array(3); + + + geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3)); + + + if(!material) + this.autoMaterial = true; + + THREE.Mesh.call( this, geometry, material); + this.frustumCulled = false; + + var mesh = this; + var instance = this.instance = new Nexus.Instance(gl); + instance.open(url); + instance.onLoad = function() { + var s = 1/instance.mesh.sphere.radius; + var pos = instance.mesh.sphere.center; + mesh.position.set(-pos[0]*s, -pos[1]*s, -pos[2]*s); + mesh.scale.set(s, s, s); + if(mesh.autoMaterial) + mesh.material = new THREE.MeshLambertMaterial( { color: 0xffffff } ); + + if(this.mesh.vertex.normal) { + var normals = new Float32Array(3); + geometry.addAttribute( 'normal', new THREE.BufferAttribute(normals, 3)); + } + if(this.mesh.vertex.color) { + var colors = new Float32Array(4); + geometry.addAttribute( 'color', new THREE.BufferAttribute(colors, 4)); + if(mesh.autoMaterial) + mesh.material = new THREE.MeshLambertMaterial({ vertexColors: THREE.VertexColors }); + } + + if(this.mesh.vertex.texCoord) { + var uv = new Float32Array(2); + geometry.addAttribute( 'uv', new THREE.BufferAttribute(uv, 2)); + if(mesh.autoMaterial) { + var texture = new THREE.DataTexture( new Uint8Array([1, 1, 1]), 1, 1, THREE.RGBFormat ); + texture.needsUpdate = true; + mesh.material = new THREE.MeshLambertMaterial( { color: 0xffffff, map: texture } ); + } + } + + if(this.mesh.face.index) { + var indices = new Uint32Array(3); + geometry.setIndex(new THREE.BufferAttribute( indices, 3) ); + } + render(); + }; + instance.onUpdate = function() { render(); } + + this.onAfterRender = function(renderer, scene, camera, geometry, material, group) { + if(!instance.isReady) return; + var s = renderer.getSize(); + instance.updateView([0, 0, s.width, s.height], + camera.projectionMatrix.elements, + mesh.modelViewMatrix.elements); + var program = renderer.context.getParameter(gl.CURRENT_PROGRAM); + instance.attributes['position'] = renderer.context.getAttribLocation(program, "position"); + instance.attributes['normal'] = renderer.context.getAttribLocation(program, "normal"); + instance.attributes['color'] = renderer.context.getAttribLocation(program, "color"); + instance.attributes['uv'] = renderer.context.getAttribLocation(program, "uv"); + + instance.render(); + } +} + +NexusObject.prototype = Object.create(THREE.Mesh.prototype); + +NexusObject.prototype.raycast = function(raycaster, intersects) { + var instance = this.instance; + var nexus = instance.mesh; + if(!nexus.sphere) return; + var sp = nexus.sphere; + var c = sp.center; + var center = new THREE.Vector3(c[0], c[1], c[2]); + var sphere = new THREE.Sphere(center, sp.radius); + sphere.applyMatrix4( this.matrixWorld ); + + if ( raycaster.ray.intersectsSphere( sphere ) === false ) return; + //just check the last level spheres. + if(!nexus.sink) return; + + var distance = -1.0; + for(var i = 0; i < nexus.sink; i++) { + var patch = nexus.nfirstpatch[i]; + if(nexus.patches[patch*3] != nexus.sink) + continue; + var x = nexus.nspheres[i*5]; + var y = nexus.nspheres[i*5+1]; + var z = nexus.nspheres[i*5+2]; + var r = nexus.nspheres[i*5+4]; //tight radius + var sphere = new THREE.Sphere(new THREE.Vector3(x, y, z), r); + sphere.applyMatrix4( this.matrixWorld ); + if ( raycaster.ray.intersectsSphere( sphere ) != false ) { + var d = sphere.center.lengthSq(); + if(distance == -1.0 || d < distance) + distance = d; + } + } + if(distance == -1.0) return; + + intersects.push({ distance: distance, object: this} ); +} diff -r 111d952e7fa0 -r 967f947014ac Resources/Nexus/threejs.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Nexus/threejs.html Tue Apr 09 22:13:01 2024 +0200 @@ -0,0 +1,103 @@ + + + +Nexus threejs + + + + + + + + + + +
+ + + + + + + diff -r 111d952e7fa0 -r 967f947014ac Sources/OrthancExplorer.js --- a/Sources/OrthancExplorer.js Sat Apr 06 17:19:16 2024 +0200 +++ b/Sources/OrthancExplorer.js Tue Apr 09 22:13:01 2024 +0200 @@ -24,6 +24,9 @@ const STL_PLUGIN_SOP_CLASS_UID_STL = '1.2.840.10008.5.1.4.1.1.104.3'; const STL_PLUGIN_SOP_CLASS_UID_RT_STRUCT = '1.2.840.10008.5.1.4.1.1.481.3'; +const STL_PLUGIN_SOP_CLASS_UID_RAW = '1.2.840.10008.5.1.4.1.1.66'; + +const STL_PLUGIN_NEXUS_CREATOR_VERSION_UID = '1.2.826.0.1.3680043.8.498.90514926286349109728701975613711986292'; function AddStlViewer(target, name, callback) { @@ -391,11 +394,36 @@ } +function AddOpenStlNexusButton(instanceId, id, parent) { + $.ajax({ + url: '/instances/' + instanceId + '/content/0008,9123', + success: function(creatorVersionUid) { + if (creatorVersionUid == STL_PLUGIN_NEXUS_CREATOR_VERSION_UID) { + var b = $('') + .attr('id', id) + .attr('data-role', 'button') + .attr('href', '#') + .attr('data-icon', 'search') + .attr('data-theme', 'e') + .text('Nexus 3D viewer') + .button(); + + b.insertAfter($('#' + parent)); + + b.click(function() { + window.open('../stl/nexus/threejs.html?model=../../instances/' + instanceId + '/nexus'); + }); + } + } + }); +} + $('#series').live('pagebeforeshow', function() { var seriesId = $.mobile.pageData.uuid; $('#stl-viewer-series').remove(); + $('#stl-nexus-series').remove(); $('#stl-generate-rtstruct-series').remove(); GetResource('/series/' + seriesId, function(series) { @@ -413,6 +441,9 @@ else if (sopClassUid == STL_PLUGIN_SOP_CLASS_UID_RT_STRUCT) { AddGenerateFromRtStructButton(instanceId, 'stl-generate-rtstruct-series', 'series-info'); } + else if (sopClassUid == STL_PLUGIN_SOP_CLASS_UID_RAW) { + AddOpenStlNexusButton(instanceId, 'stl-nexus-series', 'series-info'); + } } }); @@ -425,6 +456,7 @@ var instanceId = $.mobile.pageData.uuid; $('#stl-viewer-instance').remove(); + $('#stl-nexus-instance').remove(); $('#stl-generate-rtstruct-instance').remove(); $.ajax({ @@ -438,6 +470,9 @@ else if (sopClassUid == STL_PLUGIN_SOP_CLASS_UID_RT_STRUCT) { AddGenerateFromRtStructButton(instanceId, 'stl-generate-rtstruct-instance', 'instance-info'); } + else if (sopClassUid == STL_PLUGIN_SOP_CLASS_UID_RAW) { + AddOpenStlNexusButton(instanceId, 'stl-nexus-instance', 'instance-info'); + } } }); diff -r 111d952e7fa0 -r 967f947014ac Sources/Plugin.cpp --- a/Sources/Plugin.cpp Sat Apr 06 17:19:16 2024 +0200 +++ b/Sources/Plugin.cpp Tue Apr 09 22:13:01 2024 +0200 @@ -30,6 +30,7 @@ #include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" +#include #include #include #include @@ -123,7 +124,7 @@ return; } - std::string file = request->groups[0]; + const std::string file = request->groups[0]; if (boost::starts_with(file, "libs/")) { @@ -701,6 +702,173 @@ } +void ServeNexusAssets(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "GET"); + return; + } + + const std::string file = request->groups[0]; + + Orthanc::EmbeddedResources::FileResourceId resourceId; + Orthanc::MimeType mimeType; + + if (file == "threejs.html") + { + resourceId = Orthanc::EmbeddedResources::NEXUS_HTML; + mimeType = Orthanc::MimeType_Html; + } + else if (file == "js/meco.js") + { + resourceId = Orthanc::EmbeddedResources::NEXUS_MECO_JS; + mimeType = Orthanc::MimeType_JavaScript; + } + else if (file == "js/nexus.js") + { + resourceId = Orthanc::EmbeddedResources::NEXUS_JS; + mimeType = Orthanc::MimeType_JavaScript; + } + else if (file == "js/nexus_three.js") + { + resourceId = Orthanc::EmbeddedResources::NEXUS_THREE_JS; + mimeType = Orthanc::MimeType_JavaScript; + } + else if (file == "js/TrackballControls.js") + { + resourceId = Orthanc::EmbeddedResources::NEXUS_TRACKBALL_JS; + mimeType = Orthanc::MimeType_JavaScript; + } + else + { + OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 404); + return; + } + + std::string s; + Orthanc::EmbeddedResources::GetFileResource(s, resourceId); + OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), Orthanc::EnumerationToString(mimeType)); +} + + +static Orthanc::MemoryStringCache nexusCache_; + +void ExtractNexusModel(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + return; + } + + const std::string instanceId = request->groups[0]; + + std::string range; + + for (uint32_t i = 0; i < request->headersCount; i++) + { + if (std::string(request->headersKeys[i]) == "range") + { + range = request->headersValues[i]; + } + } + + static const std::string BYTES = "bytes="; + + if (!boost::starts_with(range, BYTES)) + { + OrthancPluginSendHttpStatusCode(context, output, 416); // Range not satisfiable + return; + } + + std::vector tokens; + Orthanc::Toolbox::TokenizeString(tokens, range.substr(BYTES.length()), '-'); + + uint64_t start, end; + + if (tokens.size() != 2 || + !Orthanc::SerializationToolbox::ParseUnsignedInteger64(start, tokens[0]) || + !Orthanc::SerializationToolbox::ParseUnsignedInteger64(end, tokens[1]) || + start < 0 || + start > end) + { + OrthancPluginSendHttpStatusCode(context, output, 416); // Range not satisfiable + return; + } + + uint64_t modelSize; + std::string part; + +#if 0 + { + // Use no cache + std::string model; + if (!OrthancPlugins::RestApiGetString(model, "/instances/" + instanceId + "/content/4205-1001", false)) + { + OrthancPluginSendHttpStatusCode(context, output, 404); + return; + } + + modelSize = model.size(); + + if (end >= modelSize) + { + OrthancPluginSendHttpStatusCode(context, output, 416); // Range not satisfiable + return; + } + + part = model.substr(start, end - start + 1); + } +#else + { + Orthanc::MemoryStringCache::Accessor accessor(nexusCache_); + + std::string model; + if (!accessor.Fetch(model, instanceId)) + { + if (OrthancPlugins::RestApiGetString(model, "/instances/" + instanceId + "/content/4205-1001", false)) + { + accessor.Add(instanceId, model); + } + else + { + OrthancPluginSendHttpStatusCode(context, output, 404); + return; + } + } + + modelSize = model.size(); + + if (end >= modelSize) + { + OrthancPluginSendHttpStatusCode(context, output, 416); // Range not satisfiable + return; + } + + part = model.substr(start, end - start + 1); + } +#endif + + std::string s = ("bytes " + boost::lexical_cast(start) + "-" + + boost::lexical_cast(end) + "/" + + boost::lexical_cast(modelSize)); + OrthancPluginSetHttpHeader(context, output, "Content-Range", s.c_str()); + + s = boost::lexical_cast(part.size()); + OrthancPluginSetHttpHeader(context, output, "Content-Length", s.c_str()); + OrthancPluginSetHttpHeader(context, output, "Content-Type", "application/octet-stream"); + + OrthancPluginSendHttpStatus(context, output, 206 /* partial content */, part.c_str(), part.size()); +} + + extern "C" { ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) @@ -748,6 +916,10 @@ OrthancPlugins::RegisterRestCallback("/stl/encode-nifti", true); } + nexusCache_.SetMaximumSize(512 * 1024 * 1024); // Cache of 512MB for Nexus + OrthancPlugins::RegisterRestCallback("/instances/([0-9a-f-]+)/nexus", true); + OrthancPlugins::RegisterRestCallback("/stl/nexus/(.*)", true); + OrthancPlugins::OrthancConfiguration globalConfiguration; OrthancPlugins::OrthancConfiguration configuration; globalConfiguration.GetSection(configuration, "STL");