From 30c189e6249c81d90868b1e17e86722007d3ccd5 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Tue, 15 Nov 2016 09:10:05 -0800 Subject: [PATCH 01/50] channelCount math cleanup --- libraries/audio-client/src/AudioClient.cpp | 37 ++++++++-------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 062991c187..0f55537586 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -237,14 +237,6 @@ QAudioDeviceInfo getNamedAudioDeviceForMode(QAudio::Mode mode, const QString& de return result; } -int numDestinationSamplesRequired(const QAudioFormat& sourceFormat, const QAudioFormat& destinationFormat, - int numSourceSamples) { - float ratio = (float) destinationFormat.channelCount() / sourceFormat.channelCount(); - ratio *= (float) destinationFormat.sampleRate() / sourceFormat.sampleRate(); - - return (numSourceSamples * ratio) + 0.5f; -} - #ifdef Q_OS_WIN QString friendlyNameForAudioDevice(IMMDevice* pEndpoint) { QString deviceName; @@ -427,15 +419,15 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, } bool sampleChannelConversion(const int16_t* sourceSamples, int16_t* destinationSamples, unsigned int numSourceSamples, - const QAudioFormat& sourceAudioFormat, const QAudioFormat& destinationAudioFormat) { - if (sourceAudioFormat.channelCount() == 2 && destinationAudioFormat.channelCount() == 1) { + const int sourceChannelCount, const int destinationChannelCount) { + if (sourceChannelCount == 2 && destinationChannelCount == 1) { // loop through the stereo input audio samples and average every two samples for (uint i = 0; i < numSourceSamples; i += 2) { destinationSamples[i / 2] = (sourceSamples[i] / 2) + (sourceSamples[i + 1] / 2); } return true; - } else if (sourceAudioFormat.channelCount() == 1 && destinationAudioFormat.channelCount() == 2) { + } else if (sourceChannelCount == 1 && destinationChannelCount == 2) { // loop through the mono input audio and repeat each sample twice for (uint i = 0; i < numSourceSamples; ++i) { @@ -451,26 +443,24 @@ bool sampleChannelConversion(const int16_t* sourceSamples, int16_t* destinationS void possibleResampling(AudioSRC* resampler, const int16_t* sourceSamples, int16_t* destinationSamples, unsigned int numSourceSamples, unsigned int numDestinationSamples, - const QAudioFormat& sourceAudioFormat, const QAudioFormat& destinationAudioFormat) { + const int sourceChannelCount, const int destinationChannelCount) { if (numSourceSamples > 0) { if (!resampler) { if (!sampleChannelConversion(sourceSamples, destinationSamples, numSourceSamples, - sourceAudioFormat, destinationAudioFormat)) { + sourceChannelCount, destinationChannelCount)) { // no conversion, we can copy the samples directly across memcpy(destinationSamples, sourceSamples, numSourceSamples * AudioConstants::SAMPLE_SIZE); } } else { - if (sourceAudioFormat.channelCount() != destinationAudioFormat.channelCount()) { - float channelCountRatio = (float)destinationAudioFormat.channelCount() / sourceAudioFormat.channelCount(); + if (sourceChannelCount != destinationChannelCount) { - int numChannelCoversionSamples = (int)(numSourceSamples * channelCountRatio); + int numChannelCoversionSamples = (numSourceSamples * destinationChannelCount) / sourceChannelCount; int16_t* channelConversionSamples = new int16_t[numChannelCoversionSamples]; - sampleChannelConversion(sourceSamples, channelConversionSamples, - numSourceSamples, - sourceAudioFormat, destinationAudioFormat); + sampleChannelConversion(sourceSamples, channelConversionSamples, numSourceSamples, + sourceChannelCount, destinationChannelCount); resampler->render(channelConversionSamples, destinationSamples, numChannelCoversionSamples); @@ -480,7 +470,7 @@ void possibleResampling(AudioSRC* resampler, unsigned int numAdjustedSourceSamples = numSourceSamples; unsigned int numAdjustedDestinationSamples = numDestinationSamples; - if (sourceAudioFormat.channelCount() == 2 && destinationAudioFormat.channelCount() == 2) { + if (sourceChannelCount == 2 && destinationChannelCount == 2) { numAdjustedSourceSamples /= 2; numAdjustedDestinationSamples /= 2; } @@ -857,7 +847,8 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { static QByteArray loopBackByteArray; int numInputSamples = inputByteArray.size() / AudioConstants::SAMPLE_SIZE; - int numLoopbackSamples = numDestinationSamplesRequired(_inputFormat, _outputFormat, numInputSamples); + //int numLoopbackSamples = ((int64_t)numInputSamples * _outputFormat.channelCount() * _outputFormat.sampleRate()) / (_inputFormat.channelCount() * _inputFormat.sampleRate()); + int numLoopbackSamples = (numInputSamples * _outputFormat.channelCount()) / _inputFormat.channelCount(); loopBackByteArray.resize(numLoopbackSamples * AudioConstants::SAMPLE_SIZE); @@ -865,7 +856,7 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { int16_t* loopbackSamples = reinterpret_cast(loopBackByteArray.data()); // upmix mono to stereo - if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, _inputFormat, _outputFormat)) { + if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, _inputFormat.channelCount(), _outputFormat.channelCount())) { // no conversion, just copy the samples memcpy(loopbackSamples, inputSamples, numInputSamples * AudioConstants::SAMPLE_SIZE); } @@ -923,7 +914,7 @@ void AudioClient::handleAudioInput() { possibleResampling(_inputToNetworkResampler, inputAudioSamples.get(), networkAudioSamples, inputSamplesRequired, numNetworkSamples, - _inputFormat, _desiredInputFormat); + _inputFormat.channelCount(), _desiredInputFormat.channelCount()); // Remove DC offset if (!_isStereoInput) { From b19a44e0465ea987e9c791ab3d81909ada1fa9fa Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Tue, 15 Nov 2016 10:37:32 -0800 Subject: [PATCH 02/50] On Windows, set audio format to match the internal mix format. This works around multichannel bugs in the Qt audio framework. --- libraries/audio-client/src/AudioClient.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 0f55537586..769b7f7646 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -379,7 +379,23 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, adjustedAudioFormat = desiredAudioFormat; -#ifdef Q_OS_ANDROID +#if defined(Q_OS_WIN) + + // NOTE: On Windows, testing for supported formats is unreliable for channels > 2, + // due to WAVEFORMATEX based implementation. To work around, the sample rate and + // channel count are directly set to the WASAPI shared-mode mix format. + + adjustedAudioFormat = audioDevice.preferredFormat(); // returns mixFormat + + adjustedAudioFormat.setCodec("audio/pcm"); + adjustedAudioFormat.setSampleSize(16); + adjustedAudioFormat.setSampleType(QAudioFormat::SignedInt); + adjustedAudioFormat.setByteOrder(QAudioFormat::LittleEndian); + + // resampling should produce an integral number of samples + return (adjustedAudioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE == 0); + +#elif defined(Q_OS_ANDROID) // FIXME: query the native sample rate of the device? adjustedAudioFormat.setSampleRate(48000); #else From 171cb140878e6cafb4a68dda40f7d91ad6c2e780 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 16 Nov 2016 10:38:51 -0800 Subject: [PATCH 03/50] New WASAPI Qt plugins that implement multichannel formats using WAVEFORMATEXTENSIBLE. QAudioInput, QAudioOutput, and IsFormatSupported() now support channels > 2. dwChannelMapping is always set to 0xffffffff (1:1 passthru). --- cmake/externals/wasapi/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/externals/wasapi/CMakeLists.txt b/cmake/externals/wasapi/CMakeLists.txt index bacdb5b0b7..67f47d68fc 100644 --- a/cmake/externals/wasapi/CMakeLists.txt +++ b/cmake/externals/wasapi/CMakeLists.txt @@ -6,8 +6,8 @@ if (WIN32) include(ExternalProject) ExternalProject_Add( ${EXTERNAL_NAME} - URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi3.zip - URL_MD5 1a2433f80a788a54c70f505ff4f43ac1 + URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi4.zip + URL_MD5 2abde5340a64d387848f12b9536a7e85 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" From ec53c6a0308388a0f6833efa73a7a64c40ebed03 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 17 Nov 2016 06:56:49 -0800 Subject: [PATCH 04/50] Add support for mono or multichannel audio output. At the end of the audio pipeline, optional upmix/downmix to the device channel format. --- libraries/audio-client/src/AudioClient.cpp | 112 +++++++++++++++++---- libraries/audio/src/AudioRingBuffer.h | 36 +++++++ 2 files changed, 126 insertions(+), 22 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 769b7f7646..be5e980217 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -61,6 +61,10 @@ static const auto DEFAULT_ORIENTATION_GETTER = [] { return Quaternions::IDENTITY static const int DEFAULT_BUFFER_FRAMES = 1; +// OUTPUT_CHANNEL_COUNT is audio pipeline output format, which is always 2 channel. +// _outputFormat.channelCount() is device output format, which may be 1 or multichannel. +static const int OUTPUT_CHANNEL_COUNT = 2; + static const bool DEFAULT_STARVE_DETECTION_ENABLED = true; static const int STARVE_DETECTION_THRESHOLD = 3; static const int STARVE_DETECTION_PERIOD = 10 * 1000; // 10 Seconds @@ -140,7 +144,7 @@ AudioClient::AudioClient() : _reverbOptions(&_scriptReverbOptions), _inputToNetworkResampler(NULL), _networkToOutputResampler(NULL), - _audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO), + _audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT), _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_receivedAudioStream, this), _stats(&_receivedAudioStream), @@ -381,9 +385,8 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, #if defined(Q_OS_WIN) - // NOTE: On Windows, testing for supported formats is unreliable for channels > 2, - // due to WAVEFORMATEX based implementation. To work around, the sample rate and - // channel count are directly set to the WASAPI shared-mode mix format. + // On Windows, using WASAPI shared mode, the sample rate and channel count must + // exactly match the internal mix format. Any other format will fail to open. adjustedAudioFormat = audioDevice.preferredFormat(); // returns mixFormat @@ -392,7 +395,9 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, adjustedAudioFormat.setSampleType(QAudioFormat::SignedInt); adjustedAudioFormat.setByteOrder(QAudioFormat::LittleEndian); - // resampling should produce an integral number of samples + assert(audioDevice.isFormatSupported(adjustedAudioFormat)); + + // converting to/from this rate must produce an integral number of samples return (adjustedAudioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE == 0); #elif defined(Q_OS_ANDROID) @@ -402,7 +407,6 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, // // Attempt the device sample rate in decreasing order of preference. - // On Windows, using WASAPI shared mode, only a match with the hardware sample rate will succeed. // if (audioDevice.supportedSampleRates().contains(48000)) { adjustedAudioFormat.setSampleRate(48000); @@ -508,7 +512,7 @@ void AudioClient::start() { _desiredInputFormat.setChannelCount(1); _desiredOutputFormat = _desiredInputFormat; - _desiredOutputFormat.setChannelCount(2); + _desiredOutputFormat.setChannelCount(OUTPUT_CHANNEL_COUNT); QAudioDeviceInfo inputDeviceInfo = defaultAudioDeviceForMode(QAudio::AudioInput); qCDebug(audioclient) << "The default audio input device is" << inputDeviceInfo.deviceName(); @@ -830,6 +834,36 @@ void AudioClient::setReverbOptions(const AudioEffectOptions* options) { } } +static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { + + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 2 + N samples + *dest++ = left; + *dest++ = right; + for (int n = 0; n < numExtraChannels; n++) { + *dest++ = 0; + } + } +} + +static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { + + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 1 sample + *dest++ = (int16_t)((left + right) / 2); + } +} + void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { // If there is server echo, reverb will be applied to the recieved audio stream so no need to have it here. bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); @@ -863,8 +897,7 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { static QByteArray loopBackByteArray; int numInputSamples = inputByteArray.size() / AudioConstants::SAMPLE_SIZE; - //int numLoopbackSamples = ((int64_t)numInputSamples * _outputFormat.channelCount() * _outputFormat.sampleRate()) / (_inputFormat.channelCount() * _inputFormat.sampleRate()); - int numLoopbackSamples = (numInputSamples * _outputFormat.channelCount()) / _inputFormat.channelCount(); + int numLoopbackSamples = (numInputSamples * OUTPUT_CHANNEL_COUNT) / _inputFormat.channelCount(); loopBackByteArray.resize(numLoopbackSamples * AudioConstants::SAMPLE_SIZE); @@ -872,7 +905,7 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { int16_t* loopbackSamples = reinterpret_cast(loopBackByteArray.data()); // upmix mono to stereo - if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, _inputFormat.channelCount(), _outputFormat.channelCount())) { + if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, _inputFormat.channelCount(), OUTPUT_CHANNEL_COUNT)) { // no conversion, just copy the samples memcpy(loopbackSamples, inputSamples, numInputSamples * AudioConstants::SAMPLE_SIZE); } @@ -883,7 +916,29 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { _sourceReverb.render(loopbackSamples, loopbackSamples, numLoopbackSamples/2); } - _loopbackOutputDevice->write(loopBackByteArray); + // if required, upmix or downmix to deviceChannelCount + int deviceChannelCount = _outputFormat.channelCount(); + if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { + + _loopbackOutputDevice->write(loopBackByteArray); + + } else { + + static QByteArray deviceByteArray; + + int numDeviceSamples = (numLoopbackSamples * deviceChannelCount) / OUTPUT_CHANNEL_COUNT; + + deviceByteArray.resize(numDeviceSamples * AudioConstants::SAMPLE_SIZE); + + int16_t* deviceSamples = reinterpret_cast(deviceByteArray.data()); + + if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { + channelUpmix(loopbackSamples, deviceSamples, numLoopbackSamples, deviceChannelCount - OUTPUT_CHANNEL_COUNT); + } else { + channelDownmix(loopbackSamples, deviceSamples, numLoopbackSamples); + } + _loopbackOutputDevice->write(deviceByteArray); + } } void AudioClient::handleAudioInput() { @@ -1177,9 +1232,9 @@ bool AudioClient::outputLocalInjector(bool isStereo, AudioInjector* injector) { } void AudioClient::outputFormatChanged() { - _outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * _outputFormat.channelCount() * _outputFormat.sampleRate()) / + _outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * OUTPUT_CHANNEL_COUNT * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); - _receivedAudioStream.outputFormatChanged(_outputFormat.sampleRate(), _outputFormat.channelCount()); + _receivedAudioStream.outputFormatChanged(_outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); } bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceInfo) { @@ -1323,9 +1378,8 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice assert(_desiredOutputFormat.sampleSize() == 16); assert(_outputFormat.sampleSize() == 16); - int channelCount = (_desiredOutputFormat.channelCount() == 2 && _outputFormat.channelCount() == 2) ? 2 : 1; - _networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), channelCount); + _networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); } else { qCDebug(audioclient) << "No resampling required for network output to match actual output format."; @@ -1335,8 +1389,11 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice // setup our general output device for audio-mixer audio _audioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); + int osDefaultBufferSize = _audioOutput->bufferSize(); - int requestedSize = _sessionOutputBufferSizeFrames *_outputFrameSize * AudioConstants::SAMPLE_SIZE; + int deviceChannelCount = _outputFormat.channelCount(); + int deviceFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); + int requestedSize = _sessionOutputBufferSizeFrames * deviceFrameSize * AudioConstants::SAMPLE_SIZE; _audioOutput->setBufferSize(requestedSize); connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); @@ -1348,14 +1405,13 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _audioOutput->start(&_audioOutputIODevice); lock.unlock(); - qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)_outputFrameSize << + qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << "os default:" << osDefaultBufferSize << "period size:" << _audioOutput->periodSize(); // setup a loopback audio output device _loopbackAudioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); - _timeSinceLastReceived.start(); supportedFormat = true; @@ -1454,15 +1510,27 @@ float AudioClient::gainForSource(float distance, float volume) { } qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { - auto samplesRequested = maxSize / AudioConstants::SAMPLE_SIZE; + + // samples requested from OUTPUT_CHANNEL_COUNT + int deviceChannelCount = _audio->_outputFormat.channelCount(); + int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount; + int samplesPopped; int bytesWritten; - if ((samplesPopped = _receivedAudioStream.popSamples((int)samplesRequested, false)) > 0) { + if ((samplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { qCDebug(audiostream, "Read %d samples from buffer (%d available)", samplesPopped, _receivedAudioStream.getSamplesAvailable()); AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); - lastPopOutput.readSamples((int16_t*)data, samplesPopped); - bytesWritten = samplesPopped * AudioConstants::SAMPLE_SIZE; + + // if required, upmix or downmix to deviceChannelCount + if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { + lastPopOutput.readSamples((int16_t*)data, samplesPopped); + } else if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { + lastPopOutput.readSamplesWithUpmix((int16_t*)data, samplesPopped, deviceChannelCount - OUTPUT_CHANNEL_COUNT); + } else { + lastPopOutput.readSamplesWithDownmix((int16_t*)data, samplesPopped); + } + bytesWritten = (samplesPopped * AudioConstants::SAMPLE_SIZE) * deviceChannelCount / OUTPUT_CHANNEL_COUNT; } else { // nothing on network, don't grab anything from injectors, and just return 0s // this will flood the log: qCDebug(audioclient, "empty/partial network buffer"); diff --git a/libraries/audio/src/AudioRingBuffer.h b/libraries/audio/src/AudioRingBuffer.h index 7ccb32ce10..29e7a9e998 100644 --- a/libraries/audio/src/AudioRingBuffer.h +++ b/libraries/audio/src/AudioRingBuffer.h @@ -105,6 +105,8 @@ public: void readSamples(int16_t* dest, int numSamples); void readSamplesWithFade(int16_t* dest, int numSamples, float fade); + void readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels); + void readSamplesWithDownmix(int16_t* dest, int numSamples); private: int16_t* atShiftedBy(int i); @@ -225,6 +227,40 @@ inline void AudioRingBuffer::ConstIterator::readSamplesWithFade(int16_t* dest, i } } +inline void AudioRingBuffer::ConstIterator::readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels) { + int16_t* at = _at; + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *at; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + int16_t right = *at; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + + // write 2 + N samples + *dest++ = left; + *dest++ = right; + for (int n = 0; n < numExtraChannels; n++) { + *dest++ = 0; + } + } +} + +inline void AudioRingBuffer::ConstIterator::readSamplesWithDownmix(int16_t* dest, int numSamples) { + int16_t* at = _at; + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *at; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + int16_t right = *at; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + + // write 1 sample + *dest++ = (int16_t)((left + right) / 2); + } +} + inline AudioRingBuffer::ConstIterator AudioRingBuffer::nextOutput() const { return ConstIterator(_buffer, _bufferLength, _nextOutput); } From fd08936fb3c6c89e189cea39dfc543627c217510 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 17 Nov 2016 13:42:43 -0800 Subject: [PATCH 05/50] Improved failure logs --- libraries/audio-client/src/AudioClient.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index be5e980217..a05d550fd8 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -395,10 +395,16 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, adjustedAudioFormat.setSampleType(QAudioFormat::SignedInt); adjustedAudioFormat.setByteOrder(QAudioFormat::LittleEndian); - assert(audioDevice.isFormatSupported(adjustedAudioFormat)); - + if (!audioDevice.isFormatSupported(adjustedAudioFormat)) { + qCDebug(audioclient) << "WARNING: The mix format is" << adjustedAudioFormat << "but isFormatSupported() failed."; + return false; + } // converting to/from this rate must produce an integral number of samples - return (adjustedAudioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE == 0); + if (adjustedAudioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE != 0) { + qCDebug(audioclient) << "WARNING: The current sample rate [" << adjustedAudioFormat.sampleRate() << "] is not supported."; + return false; + } + return true; #elif defined(Q_OS_ANDROID) // FIXME: query the native sample rate of the device? From 1eb17b978672b2d05fab10e221829e56ada10b12 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 17 Nov 2016 14:11:09 -0800 Subject: [PATCH 06/50] fix bug that caused polyvox rendering to mishandle its gpu buffers --- .../src/RenderablePolyVoxEntityItem.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 7defe347ca..bef31a967f 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -606,11 +606,16 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { Transform transform(voxelToWorldMatrix()); batch.setModelTransform(transform); batch.setInputFormat(mesh->getVertexFormat()); - batch.setInputBuffer(gpu::Stream::POSITION, mesh->getVertexBuffer()); + + // batch.setInputStream(0, mesh->getVertexStream()); + batch.setInputBuffer(gpu::Stream::POSITION, mesh->getVertexBuffer()._buffer, + 0, + sizeof(PolyVox::PositionMaterialNormal)); batch.setInputBuffer(gpu::Stream::NORMAL, mesh->getVertexBuffer()._buffer, sizeof(float) * 3, - mesh->getVertexBuffer()._stride); + sizeof(PolyVox::PositionMaterialNormal)); + batch.setIndexBuffer(gpu::UINT32, mesh->getIndexBuffer()._buffer, 0); if (!_xTextureURL.isEmpty() && !_xTexture) { From 8530156227d675daeaf3e8a80f100ff8f4044a61 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 17 Nov 2016 16:06:35 -0800 Subject: [PATCH 07/50] experimenting --- .../src/RenderablePolyVoxEntityItem.cpp | 35 ++++++++++++------- .../src/RenderablePolyVoxEntityItem.h | 1 + 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index bef31a967f..aa7ec0776f 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -600,21 +600,29 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { _pipeline = gpu::Pipeline::create(program, state); } + + if (!_vertexFormat) { + auto vf = std::make_shared(); + vf->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); + vf->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 12); + _vertexFormat = vf; + } + gpu::Batch& batch = *args->_batch; batch.setPipeline(_pipeline); Transform transform(voxelToWorldMatrix()); batch.setModelTransform(transform); - batch.setInputFormat(mesh->getVertexFormat()); +// batch.setInputFormat(mesh->getVertexFormat()); + batch.setInputFormat(_vertexFormat); // batch.setInputStream(0, mesh->getVertexStream()); batch.setInputBuffer(gpu::Stream::POSITION, mesh->getVertexBuffer()._buffer, 0, sizeof(PolyVox::PositionMaterialNormal)); - batch.setInputBuffer(gpu::Stream::NORMAL, - mesh->getVertexBuffer()._buffer, + /* batch.setInputBuffer(gpu::Stream::NORMAL, mesh->getVertexBuffer()._buffer, sizeof(float) * 3, - sizeof(PolyVox::PositionMaterialNormal)); + sizeof(PolyVox::PositionMaterialNormal));*/ batch.setIndexBuffer(gpu::UINT32, mesh->getIndexBuffer()._buffer, 0); @@ -1102,7 +1110,6 @@ void RenderablePolyVoxEntityItem::getMesh() { auto entity = std::static_pointer_cast(getThisPointer()); - QtConcurrent::run([entity, voxelSurfaceStyle] { model::MeshPointer mesh(new model::Mesh()); @@ -1152,20 +1159,24 @@ void RenderablePolyVoxEntityItem::getMesh() { (gpu::Byte*)vecVertices.data()); auto vertexBufferPtr = gpu::BufferPointer(vertexBuffer); gpu::Resource::Size vertexBufferSize = 0; - if (vertexBufferPtr->getSize() > sizeof(float) * 3) { - vertexBufferSize = vertexBufferPtr->getSize() - sizeof(float) * 3; + gpu::Resource::Size normalBufferSize = sizeof(float) * 3; + if (vertexBufferPtr->getSize() > sizeof(PolyVox::PositionMaterialNormal)) { + vertexBufferSize = vertexBufferPtr->getSize() - sizeof(float) * 4; + normalBufferSize = vertexBufferPtr->getSize() - sizeof(float); } - gpu::BufferView vertexBufferView(vertexBufferPtr, 0, vertexBufferSize, + gpu::BufferView vertexBufferView(vertexBufferPtr, 0, + vertexBufferPtr->getSize(), + // vertexBufferSize, sizeof(PolyVox::PositionMaterialNormal), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW)); mesh->setVertexBuffer(vertexBufferView); mesh->addAttribute(gpu::Stream::NORMAL, - gpu::BufferView(vertexBufferPtr, - sizeof(float) * 3, - vertexBufferPtr->getSize() - sizeof(float) * 3, + gpu::BufferView(vertexBufferPtr, sizeof(float) * 3, + vertexBufferPtr->getSize() , + // normalBufferSize, sizeof(PolyVox::PositionMaterialNormal), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW))); - entity->setMesh(mesh); + entity->setMesh(mesh); }); } diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 1b6ea34bda..e8656607d7 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -149,6 +149,7 @@ private: // may not match _voxelVolumeSize. model::MeshPointer _mesh; + gpu::Stream::FormatPointer _vertexFormat; bool _meshDirty { true }; // does collision-shape need to be recomputed? bool _meshInitialized { false }; From 1714d4fe069d285337df7e749a7ce0b1e1be317c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Nov 2016 05:40:57 -0800 Subject: [PATCH 08/50] fix build --- .../src/RenderablePolyVoxEntityItem.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index aa7ec0776f..81bb5e8b2b 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -1158,12 +1158,10 @@ void RenderablePolyVoxEntityItem::getMesh() { auto vertexBuffer = std::make_shared(vecVertices.size() * sizeof(PolyVox::PositionMaterialNormal), (gpu::Byte*)vecVertices.data()); auto vertexBufferPtr = gpu::BufferPointer(vertexBuffer); - gpu::Resource::Size vertexBufferSize = 0; - gpu::Resource::Size normalBufferSize = sizeof(float) * 3; - if (vertexBufferPtr->getSize() > sizeof(PolyVox::PositionMaterialNormal)) { - vertexBufferSize = vertexBufferPtr->getSize() - sizeof(float) * 4; - normalBufferSize = vertexBufferPtr->getSize() - sizeof(float); - } + // if (vertexBufferPtr->getSize() > sizeof(PolyVox::PositionMaterialNormal)) { + // vertexBufferSize = vertexBufferPtr->getSize() - sizeof(float) * 4; + // normalBufferSize = vertexBufferPtr->getSize() - sizeof(float); + // } gpu::BufferView vertexBufferView(vertexBufferPtr, 0, vertexBufferPtr->getSize(), // vertexBufferSize, From cfd4294743e4a8bd3624a53bfc4d4a7b46584739 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 18 Nov 2016 11:31:12 -0800 Subject: [PATCH 09/50] Turn Edit.js on when importing SVO --- scripts/system/edit.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 1382c94f9c..0ba630b3ff 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -240,11 +240,8 @@ var toolBar = (function () { hoverState: 3, defaultState: 1 }); - activeButton.clicked.connect(function () { - that.setActive(!isActive); - activeButton.writeProperty("buttonState", isActive ? 0 : 1); - activeButton.writeProperty("defaultState", isActive ? 0 : 1); - activeButton.writeProperty("hoverState", isActive ? 2 : 3); + activeButton.clicked.connect(function() { + that.toggle(); }); toolBar = Toolbars.getToolbar(EDIT_TOOLBAR); @@ -440,6 +437,14 @@ var toolBar = (function () { entityListTool.clearEntityList(); }; + + that.toggle = function () { + that.setActive(!isActive); + activeButton.writeProperty("buttonState", isActive ? 0 : 1); + activeButton.writeProperty("defaultState", isActive ? 0 : 1); + activeButton.writeProperty("hoverState", isActive ? 2 : 3); + }; + that.setActive = function (active) { if (active === isActive) { return; @@ -1093,7 +1098,6 @@ function handeMenuEvent(menuItem) { } } } else if (menuItem === "Import Entities" || menuItem === "Import Entities from URL") { - var importURL = null; if (menuItem === "Import Entities") { var fullPath = Window.browse("Select Model to Import", "", "*.json"); @@ -1105,6 +1109,9 @@ function handeMenuEvent(menuItem) { } if (importURL) { + if (!isActive && (Entities.canRez() && Entities.canRezTmp())) { + toolBar.toggle(); + } importSVO(importURL); } } else if (menuItem === "Entity List...") { @@ -1185,8 +1192,6 @@ function importSVO(importURL) { if (isActive) { selectionManager.setSelections(pastedEntityIDs); } - - Window.raiseMainWindow(); } else { Window.notifyEditError("Can't import objects: objects would be out of bounds."); } From 47f82a80468ee437df357b4ede23bf4d8a939591 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 18 Nov 2016 13:50:11 -0800 Subject: [PATCH 10/50] send the mac address with domain-server check in --- domain-server/src/NodeConnectionData.cpp | 3 +++ domain-server/src/NodeConnectionData.h | 1 + libraries/networking/src/NodeList.cpp | 23 +++++++++++++++++++ .../networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 3 ++- 5 files changed, 30 insertions(+), 2 deletions(-) diff --git a/domain-server/src/NodeConnectionData.cpp b/domain-server/src/NodeConnectionData.cpp index 13bb9123d8..93d6802d84 100644 --- a/domain-server/src/NodeConnectionData.cpp +++ b/domain-server/src/NodeConnectionData.cpp @@ -29,6 +29,9 @@ NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, c // NOTE: QDataStream::readBytes() - The buffer is allocated using new []. Destroy it with the delete [] operator. delete[] rawBytes; + + // read the hardware address sent by the client + dataStream >> newHeader.hardwareAddress; } dataStream >> newHeader.nodeType diff --git a/domain-server/src/NodeConnectionData.h b/domain-server/src/NodeConnectionData.h index 9264db637e..bcbbdf0a40 100644 --- a/domain-server/src/NodeConnectionData.h +++ b/domain-server/src/NodeConnectionData.h @@ -28,6 +28,7 @@ public: HifiSockAddr senderSockAddr; QList interestList; QString placeName; + QString hardwareAddress; QByteArray protocolVersion; }; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 7a778edaad..361070b306 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -346,6 +347,28 @@ void NodeList::sendDomainServerCheckIn() { // include the protocol version signature in our connect request QByteArray protocolVersionSig = protocolVersionsSignature(); packetStream.writeBytes(protocolVersionSig.constData(), protocolVersionSig.size()); + + // if possible, include the MAC address for the current interface in our connect request + QString hardwareAddress; + + for (auto networkInterface : QNetworkInterface::allInterfaces()) { + for (auto interfaceAddress : networkInterface.addressEntries()) { + if (interfaceAddress.ip() == _localSockAddr.getAddress()) { + // this is the interface whose local IP matches what we've detected the current IP to be + hardwareAddress = networkInterface.hardwareAddress(); + + // stop checking interfaces and addresses + break; + } + } + + // stop looping if this was the current interface + if (!hardwareAddress.isEmpty()) { + break; + } + } + + packetStream << hardwareAddress; } // pack our data to send to the domain-server including diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 88285602e1..b2fca69b03 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -67,7 +67,7 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(DomainConnectionDeniedVersion::IncludesExtraInfo); case PacketType::DomainConnectRequest: - return static_cast(DomainConnectRequestVersion::HasProtocolVersions); + return static_cast(DomainConnectRequestVersion::HasMACAddress); case PacketType::DomainServerAddedNode: return static_cast(DomainServerAddedNodeVersion::PermissionsGrid); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 502ecc3951..8d63b972cc 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -207,7 +207,8 @@ enum class AvatarMixerPacketVersion : PacketVersion { enum class DomainConnectRequestVersion : PacketVersion { NoHostname = 17, HasHostname, - HasProtocolVersions + HasProtocolVersions, + HasMACAddress }; enum class DomainConnectionDeniedVersion : PacketVersion { From 704476c1973b206ccf6d768cf0908a3ee4fec49a Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 7 Nov 2016 17:09:12 -0800 Subject: [PATCH 11/50] Initial progressgit add -A! --- interface/src/Application.cpp | 38 +- interface/src/Application.h | 2 +- .../scripting/WindowScriptingInterface.cpp | 8 +- .../src/scripting/WindowScriptingInterface.h | 3 +- interface/src/ui/Gif.h | 825 ++++++++++++++++++ scripts/system/snapshot.js | 2 +- 6 files changed, 869 insertions(+), 9 deletions(-) create mode 100644 interface/src/ui/Gif.h diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 089983d8ca..a7140403ca 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -149,6 +149,7 @@ #include "ui/AddressBarDialog.h" #include "ui/AvatarInputs.h" #include "ui/DialogsManager.h" +#include "ui/Gif.h" #include "ui/LoginDialog.h" #include "ui/overlays/Cube3DOverlay.h" #include "ui/Snapshot.h" @@ -2511,7 +2512,7 @@ void Application::keyPressEvent(QKeyEvent* event) { } else if (isOption && !isShifted && !isMeta) { Menu::getInstance()->triggerOption(MenuOption::ScriptEditor); } else if (!isOption && !isShifted && isMeta) { - takeSnapshot(true); + takeSnapshot(true, "still"); } break; @@ -5428,14 +5429,43 @@ void Application::toggleLogDialog() { } } -void Application::takeSnapshot(bool notify, float aspectRatio) { - postLambdaEvent([notify, aspectRatio, this] { +void Application::takeSnapshot(bool notify, const QString& format, float aspectRatio) { + postLambdaEvent([notify, format, aspectRatio, this] { QMediaPlayer* player = new QMediaPlayer(); QFileInfo inf = QFileInfo(PathUtils::resourcesPath() + "sounds/snap.wav"); player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); - QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + //QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + + //if (!format.compare("animated")) + //{ + QImage frame; + GifWriter myGifWriter; + char* cstr; + + QString path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + path.append(QDir::separator()); + path.append("test.gif"); + + string fname = path.toStdString(); + cstr = new char[fname.size() + 1]; + strcpy(cstr, fname.c_str()); + + GifBegin(&myGifWriter, cstr, 1, 1, 0); + + uint8_t test[4] = { 0xFF, 0x00, 0x00, 0x00 }; + + for (uint8_t itr = 0; itr < 30; itr++) + { + test[0] = 0xFF / (itr + 1); + //frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(500); + + GifWriteFrame(&myGifWriter, test, 1, 1, 0); + } + + GifEnd(&myGifWriter); + //} emit DependencyManager::get()->snapshotTaken(path, notify); }); diff --git a/interface/src/Application.h b/interface/src/Application.h index 4c98be9c2d..d1150fb30f 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -266,7 +266,7 @@ public: float getAvatarSimrate() const { return _avatarSimCounter.rate(); } float getAverageSimsPerSecond() const { return _simCounter.rate(); } - void takeSnapshot(bool notify, float aspectRatio = 0.0f); + void takeSnapshot(bool notify, const QString& format = "still", float aspectRatio = 0.0f); void shareSnapshot(const QString& filename); model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; } diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 0f9dd698fd..0abdddf0d8 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -199,8 +199,12 @@ void WindowScriptingInterface::copyToClipboard(const QString& text) { QApplication::clipboard()->setText(text); } -void WindowScriptingInterface::takeSnapshot(bool notify, float aspectRatio) { - qApp->takeSnapshot(notify, aspectRatio); +void WindowScriptingInterface::takeSnapshotStill(bool notify, float aspectRatio) { + qApp->takeSnapshot(notify, "still", aspectRatio); +} + +void WindowScriptingInterface::takeSnapshotAnimated(bool notify, float aspectRatio) { + qApp->takeSnapshot(notify, "animated", aspectRatio); } void WindowScriptingInterface::shareSnapshot(const QString& path) { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index f4a89ae221..6c06b4d60e 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -52,7 +52,8 @@ public slots: QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void showAssetServer(const QString& upload = ""); void copyToClipboard(const QString& text); - void takeSnapshot(bool notify = true, float aspectRatio = 0.0f); + void takeSnapshotStill(bool notify = true, float aspectRatio = 0.0f); + void takeSnapshotAnimated(bool notify = true, float aspectRatio = 0.0f); void shareSnapshot(const QString& path); bool isPhysicsEnabled(); diff --git a/interface/src/ui/Gif.h b/interface/src/ui/Gif.h new file mode 100644 index 0000000000..83f094aee8 --- /dev/null +++ b/interface/src/ui/Gif.h @@ -0,0 +1,825 @@ +// +// gif.h +// by Charlie Tangora +// Public domain. +// Email me : ctangora -at- gmail -dot- com +// +// This file offers a simple, very limited way to create animated GIFs directly in code. +// +// Those looking for particular cleverness are likely to be disappointed; it's pretty +// much a straight-ahead implementation of the GIF format with optional Floyd-Steinberg +// dithering. (It does at least use delta encoding - only the changed portions of each +// frame are saved.) +// +// So resulting files are often quite large. The hope is that it will be handy nonetheless +// as a quick and easily-integrated way for programs to spit out animations. +// +// Only RGBA8 is currently supported as an input format. (The alpha is ignored.) +// +// USAGE: +// Create a GifWriter struct. Pass it to GifBegin() to initialize and write the header. +// Pass subsequent frames to GifWriteFrame(). +// Finally, call GifEnd() to close the file handle and free memory. +// + +#ifndef gif_h +#define gif_h + +#include // for FILE* +#include // for memcpy and bzero +#include // for integer typedefs + +// Define these macros to hook into a custom memory allocator. +// TEMP_MALLOC and TEMP_FREE will only be called in stack fashion - frees in the reverse order of mallocs +// and any temp memory allocated by a function will be freed before it exits. +// MALLOC and FREE are used only by GifBegin and GifEnd respectively (to allocate a buffer the size of the image, which +// is used to find changed pixels for delta-encoding.) + +#ifndef GIF_TEMP_MALLOC +#include +#define GIF_TEMP_MALLOC malloc +#endif + +#ifndef GIF_TEMP_FREE +#include +#define GIF_TEMP_FREE free +#endif + +#ifndef GIF_MALLOC +#include +#define GIF_MALLOC malloc +#endif + +#ifndef GIF_FREE +#include +#define GIF_FREE free +#endif + +const int kGifTransIndex = 0; + +struct GifPalette +{ + uint8_t bitDepth; + + uint8_t r[256]; + uint8_t g[256]; + uint8_t b[256]; + + // k-d tree over RGB space, organized in heap fashion + // i.e. left child of node i is node i*2, right child is node i*2+1 + // nodes 256-511 are implicitly the leaves, containing a color + uint8_t treeSplitElt[255]; + uint8_t treeSplit[255]; +}; + +// max, min, and abs functions +int GifIMax(int l, int r) { return l>r ? l : r; } +int GifIMin(int l, int r) { return l(1 << pPal->bitDepth) - 1) + { + int ind = treeRoot - (1 << pPal->bitDepth); + if (ind == kGifTransIndex) return; + + // check whether this color is better than the current winner + int r_err = r - ((int32_t)pPal->r[ind]); + int g_err = g - ((int32_t)pPal->g[ind]); + int b_err = b - ((int32_t)pPal->b[ind]); + int diff = GifIAbs(r_err) + GifIAbs(g_err) + GifIAbs(b_err); + + if (diff < bestDiff) + { + bestInd = ind; + bestDiff = diff; + } + + return; + } + + // take the appropriate color (r, g, or b) for this node of the k-d tree + int comps[3]; comps[0] = r; comps[1] = g; comps[2] = b; + int splitComp = comps[pPal->treeSplitElt[treeRoot]]; + + int splitPos = pPal->treeSplit[treeRoot]; + if (splitPos > splitComp) + { + // check the left subtree + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); + if (bestDiff > splitPos - splitComp) + { + // cannot prove there's not a better value in the right subtree, check that too + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); + } + } + else + { + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); + if (bestDiff > splitComp - splitPos) + { + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); + } + } +} + +void GifSwapPixels(uint8_t* image, int pixA, int pixB) +{ + uint8_t rA = image[pixA * 4]; + uint8_t gA = image[pixA * 4 + 1]; + uint8_t bA = image[pixA * 4 + 2]; + uint8_t aA = image[pixA * 4 + 3]; + + uint8_t rB = image[pixB * 4]; + uint8_t gB = image[pixB * 4 + 1]; + uint8_t bB = image[pixB * 4 + 2]; + uint8_t aB = image[pixA * 4 + 3]; + + image[pixA * 4] = rB; + image[pixA * 4 + 1] = gB; + image[pixA * 4 + 2] = bB; + image[pixA * 4 + 3] = aB; + + image[pixB * 4] = rA; + image[pixB * 4 + 1] = gA; + image[pixB * 4 + 2] = bA; + image[pixB * 4 + 3] = aA; +} + +// just the partition operation from quicksort +int GifPartition(uint8_t* image, const int left, const int right, const int elt, int pivotIndex) +{ + const int pivotValue = image[(pivotIndex)* 4 + elt]; + GifSwapPixels(image, pivotIndex, right - 1); + int storeIndex = left; + bool split = 0; + for (int ii = left; ii neededCenter) + GifPartitionByMedian(image, left, pivotIndex, com, neededCenter); + + if (pivotIndex < neededCenter) + GifPartitionByMedian(image, pivotIndex + 1, right, com, neededCenter); + } +} + +// Builds a palette by creating a balanced k-d tree of all pixels in the image +void GifSplitPalette(uint8_t* image, int numPixels, int firstElt, int lastElt, int splitElt, int splitDist, int treeNode, bool buildForDither, GifPalette* pal) +{ + if (lastElt <= firstElt || numPixels == 0) + return; + + // base case, bottom of the tree + if (lastElt == firstElt + 1) + { + if (buildForDither) + { + // Dithering needs at least one color as dark as anything + // in the image and at least one brightest color - + // otherwise it builds up error and produces strange artifacts + if (firstElt == 1) + { + // special case: the darkest color in the image + uint32_t r = 255, g = 255, b = 255; + for (int ii = 0; iir[firstElt] = r; + pal->g[firstElt] = g; + pal->b[firstElt] = b; + + return; + } + + if (firstElt == (1 << pal->bitDepth) - 1) + { + // special case: the lightest color in the image + uint32_t r = 0, g = 0, b = 0; + for (int ii = 0; iir[firstElt] = r; + pal->g[firstElt] = g; + pal->b[firstElt] = b; + + return; + } + } + + // otherwise, take the average of all colors in this subcube + uint64_t r = 0, g = 0, b = 0; + for (int ii = 0; iir[firstElt] = (uint8_t)r; + pal->g[firstElt] = (uint8_t)g; + pal->b[firstElt] = (uint8_t)b; + + return; + } + + // Find the axis with the largest range + int minR = 255, maxR = 0; + int minG = 255, maxG = 0; + int minB = 255, maxB = 0; + for (int ii = 0; ii maxR) maxR = r; + if (r < minR) minR = r; + + if (g > maxG) maxG = g; + if (g < minG) minG = g; + + if (b > maxB) maxB = b; + if (b < minB) minB = b; + } + + int rRange = maxR - minR; + int gRange = maxG - minG; + int bRange = maxB - minB; + + // and split along that axis. (incidentally, this means this isn't a "proper" k-d tree but I don't know what else to call it) + int splitCom = 1; + if (bRange > gRange) splitCom = 2; + if (rRange > bRange && rRange > gRange) splitCom = 0; + + int subPixelsA = numPixels * (splitElt - firstElt) / (lastElt - firstElt); + int subPixelsB = numPixels - subPixelsA; + + GifPartitionByMedian(image, 0, numPixels, splitCom, subPixelsA); + + pal->treeSplitElt[treeNode] = splitCom; + pal->treeSplit[treeNode] = image[subPixelsA * 4 + splitCom]; + + GifSplitPalette(image, subPixelsA, firstElt, splitElt, splitElt - splitDist, splitDist / 2, treeNode * 2, buildForDither, pal); + GifSplitPalette(image + subPixelsA * 4, subPixelsB, splitElt, lastElt, splitElt + splitDist, splitDist / 2, treeNode * 2 + 1, buildForDither, pal); +} + +// Finds all pixels that have changed from the previous image and +// moves them to the fromt of th buffer. +// This allows us to build a palette optimized for the colors of the +// changed pixels only. +int GifPickChangedPixels(const uint8_t* lastFrame, uint8_t* frame, int numPixels) +{ + int numChanged = 0; + uint8_t* writeIter = frame; + + for (int ii = 0; iibitDepth = bitDepth; + + // SplitPalette is destructive (it sorts the pixels by color) so + // we must create a copy of the image for it to destroy + int imageSize = width*height * 4 * sizeof(uint8_t); + uint8_t* destroyableImage = (uint8_t*)GIF_TEMP_MALLOC(imageSize); + memcpy(destroyableImage, nextFrame, imageSize); + + int numPixels = width*height; + if (lastFrame) + numPixels = GifPickChangedPixels(lastFrame, destroyableImage, numPixels); + + const int lastElt = 1 << bitDepth; + const int splitElt = lastElt / 2; + const int splitDist = splitElt / 2; + + GifSplitPalette(destroyableImage, numPixels, 1, lastElt, splitElt, splitDist, 1, buildForDither, pPal); + + GIF_TEMP_FREE(destroyableImage); + + // add the bottom node for the transparency index + pPal->treeSplit[1 << (bitDepth - 1)] = 0; + pPal->treeSplitElt[1 << (bitDepth - 1)] = 0; + + pPal->r[0] = pPal->g[0] = pPal->b[0] = 0; +} + +// Implements Floyd-Steinberg dithering, writes palette value to alpha +void GifDitherImage(const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal) +{ + int numPixels = width*height; + + // quantPixels initially holds color*256 for all pixels + // The extra 8 bits of precision allow for sub-single-color error values + // to be propagated + int32_t* quantPixels = (int32_t*)GIF_TEMP_MALLOC(sizeof(int32_t)*numPixels * 4); + + for (int ii = 0; iir[bestInd]) * 256; + int32_t g_err = nextPix[1] - int32_t(pPal->g[bestInd]) * 256; + int32_t b_err = nextPix[2] - int32_t(pPal->b[bestInd]) * 256; + + nextPix[0] = pPal->r[bestInd]; + nextPix[1] = pPal->g[bestInd]; + nextPix[2] = pPal->b[bestInd]; + nextPix[3] = bestInd; + + // Propagate the error to the four adjacent locations + // that we haven't touched yet + int quantloc_7 = (yy*width + xx + 1); + int quantloc_3 = (yy*width + width + xx - 1); + int quantloc_5 = (yy*width + width + xx); + int quantloc_1 = (yy*width + width + xx + 1); + + if (quantloc_7 < numPixels) + { + int32_t* pix7 = quantPixels + 4 * quantloc_7; + pix7[0] += GifIMax(-pix7[0], r_err * 7 / 16); + pix7[1] += GifIMax(-pix7[1], g_err * 7 / 16); + pix7[2] += GifIMax(-pix7[2], b_err * 7 / 16); + } + + if (quantloc_3 < numPixels) + { + int32_t* pix3 = quantPixels + 4 * quantloc_3; + pix3[0] += GifIMax(-pix3[0], r_err * 3 / 16); + pix3[1] += GifIMax(-pix3[1], g_err * 3 / 16); + pix3[2] += GifIMax(-pix3[2], b_err * 3 / 16); + } + + if (quantloc_5 < numPixels) + { + int32_t* pix5 = quantPixels + 4 * quantloc_5; + pix5[0] += GifIMax(-pix5[0], r_err * 5 / 16); + pix5[1] += GifIMax(-pix5[1], g_err * 5 / 16); + pix5[2] += GifIMax(-pix5[2], b_err * 5 / 16); + } + + if (quantloc_1 < numPixels) + { + int32_t* pix1 = quantPixels + 4 * quantloc_1; + pix1[0] += GifIMax(-pix1[0], r_err / 16); + pix1[1] += GifIMax(-pix1[1], g_err / 16); + pix1[2] += GifIMax(-pix1[2], b_err / 16); + } + } + } + + // Copy the palettized result to the output buffer + for (int ii = 0; iir[bestInd]; + outFrame[1] = pPal->g[bestInd]; + outFrame[2] = pPal->b[bestInd]; + outFrame[3] = bestInd; + } + + if (lastFrame) lastFrame += 4; + outFrame += 4; + nextFrame += 4; + } +} + +// Simple structure to write out the LZW-compressed portion of the image +// one bit at a time +struct GifBitStatus +{ + uint8_t bitIndex; // how many bits in the partial byte written so far + uint8_t byte; // current partial byte + + uint32_t chunkIndex; + uint8_t chunk[256]; // bytes are written in here until we have 256 of them, then written to the file +}; + +// insert a single bit +void GifWriteBit(GifBitStatus& stat, uint32_t bit) +{ + bit = bit & 1; + bit = bit << stat.bitIndex; + stat.byte |= bit; + + ++stat.bitIndex; + if (stat.bitIndex > 7) + { + // move the newly-finished byte to the chunk buffer + stat.chunk[stat.chunkIndex++] = stat.byte; + // and start a new byte + stat.bitIndex = 0; + stat.byte = 0; + } +} + +// write all bytes so far to the file +void GifWriteChunk(FILE* f, GifBitStatus& stat) +{ + fputc(stat.chunkIndex, f); + fwrite(stat.chunk, 1, stat.chunkIndex, f); + + stat.bitIndex = 0; + stat.byte = 0; + stat.chunkIndex = 0; +} + +void GifWriteCode(FILE* f, GifBitStatus& stat, uint32_t code, uint32_t length) +{ + for (uint32_t ii = 0; ii> 1; + + if (stat.chunkIndex == 255) + { + GifWriteChunk(f, stat); + } + } +} + +// The LZW dictionary is a 256-ary tree constructed as the file is encoded, +// this is one node +struct GifLzwNode +{ + uint16_t m_next[256]; +}; + +// write a 256-color (8-bit) image palette to the file +void GifWritePalette(const GifPalette* pPal, FILE* f) +{ + fputc(0, f); // first color: transparency + fputc(0, f); + fputc(0, f); + + for (int ii = 1; ii<(1 << pPal->bitDepth); ++ii) + { + uint32_t r = pPal->r[ii]; + uint32_t g = pPal->g[ii]; + uint32_t b = pPal->b[ii]; + + fputc(r, f); + fputc(g, f); + fputc(b, f); + } +} + +// write the image header, LZW-compress and write out the image +void GifWriteLzwImage(FILE* f, uint8_t* image, uint32_t left, uint32_t top, uint32_t width, uint32_t height, uint32_t delay, GifPalette* pPal) +{ + // graphics control extension + fputc(0x21, f); + fputc(0xf9, f); + fputc(0x04, f); + fputc(0x05, f); // leave prev frame in place, this frame has transparency + fputc(delay & 0xff, f); + fputc((delay >> 8) & 0xff, f); + fputc(kGifTransIndex, f); // transparent color index + fputc(0, f); + + fputc(0x2c, f); // image descriptor block + + fputc(left & 0xff, f); // corner of image in canvas space + fputc((left >> 8) & 0xff, f); + fputc(top & 0xff, f); + fputc((top >> 8) & 0xff, f); + + fputc(width & 0xff, f); // width and height of image + fputc((width >> 8) & 0xff, f); + fputc(height & 0xff, f); + fputc((height >> 8) & 0xff, f); + + //fputc(0, f); // no local color table, no transparency + //fputc(0x80, f); // no local color table, but transparency + + fputc(0x80 + pPal->bitDepth - 1, f); // local color table present, 2 ^ bitDepth entries + GifWritePalette(pPal, f); + + const int minCodeSize = pPal->bitDepth; + const uint32_t clearCode = 1 << pPal->bitDepth; + + fputc(minCodeSize, f); // min code size 8 bits + + GifLzwNode* codetree = (GifLzwNode*)GIF_TEMP_MALLOC(sizeof(GifLzwNode) * 4096); + + memset(codetree, 0, sizeof(GifLzwNode) * 4096); + int32_t curCode = -1; + uint32_t codeSize = minCodeSize + 1; + uint32_t maxCode = clearCode + 1; + + GifBitStatus stat; + stat.byte = 0; + stat.bitIndex = 0; + stat.chunkIndex = 0; + + GifWriteCode(f, stat, clearCode, codeSize); // start with a fresh LZW dictionary + + for (uint32_t yy = 0; yy= (1ul << codeSize)) + { + // dictionary entry count has broken a size barrier, + // we need more bits for codes + codeSize++; + } + if (maxCode == 4095) + { + // the dictionary is full, clear it out and begin anew + GifWriteCode(f, stat, clearCode, codeSize); // clear tree + + memset(codetree, 0, sizeof(GifLzwNode) * 4096); + curCode = -1; + codeSize = minCodeSize + 1; + maxCode = clearCode + 1; + } + + curCode = nextValue; + } + } + } + + // compression footer + GifWriteCode(f, stat, curCode, codeSize); + GifWriteCode(f, stat, clearCode, codeSize); + GifWriteCode(f, stat, clearCode + 1, minCodeSize + 1); + + // write out the last partial chunk + while (stat.bitIndex) GifWriteBit(stat, 0); + if (stat.chunkIndex) GifWriteChunk(f, stat); + + fputc(0, f); // image block terminator + + GIF_TEMP_FREE(codetree); +} + +struct GifWriter +{ + FILE* f; + uint8_t* oldImage; + bool firstFrame; +}; + +// Creates a gif file. +// The input GIFWriter is assumed to be uninitialized. +// The delay value is the time between frames in hundredths of a second - note that not all viewers pay much attention to this value. +bool GifBegin(GifWriter* writer, const char* filename, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth = 8, bool dither = false) +{ +#if _MSC_VER >= 1400 + writer->f = 0; + fopen_s(&writer->f, filename, "wb"); +#else + writer->f = fopen(filename, "wb"); +#endif + if (!writer->f) return false; + + writer->firstFrame = true; + + // allocate + writer->oldImage = (uint8_t*)GIF_MALLOC(width*height * 4); + + fputs("GIF89a", writer->f); + + // screen descriptor + fputc(width & 0xff, writer->f); + fputc((width >> 8) & 0xff, writer->f); + fputc(height & 0xff, writer->f); + fputc((height >> 8) & 0xff, writer->f); + + fputc(0xf0, writer->f); // there is an unsorted global color table of 2 entries + fputc(0, writer->f); // background color + fputc(0, writer->f); // pixels are square (we need to specify this because it's 1989) + + // now the "global" palette (really just a dummy palette) + // color 0: black + fputc(0, writer->f); + fputc(0, writer->f); + fputc(0, writer->f); + // color 1: also black + fputc(0, writer->f); + fputc(0, writer->f); + fputc(0, writer->f); + + if (delay != 0) + { + // animation header + fputc(0x21, writer->f); // extension + fputc(0xff, writer->f); // application specific + fputc(11, writer->f); // length 11 + fputs("NETSCAPE2.0", writer->f); // yes, really + fputc(3, writer->f); // 3 bytes of NETSCAPE2.0 data + + fputc(1, writer->f); // JUST BECAUSE + fputc(0, writer->f); // loop infinitely (byte 0) + fputc(0, writer->f); // loop infinitely (byte 1) + + fputc(0, writer->f); // block terminator + } + + return true; +} + +// Writes out a new frame to a GIF in progress. +// The GIFWriter should have been created by GIFBegin. +// AFAIK, it is legal to use different bit depths for different frames of an image - +// this may be handy to save bits in animations that don't change much. +bool GifWriteFrame(GifWriter* writer, const uint8_t* image, uint32_t width, uint32_t height, uint32_t delay, uint8_t bitDepth = 8, bool dither = false) +{ + if (!writer->f) return false; + + const uint8_t* oldImage = writer->firstFrame ? NULL : writer->oldImage; + writer->firstFrame = false; + + GifPalette pal; + GifMakePalette((dither ? NULL : oldImage), image, width, height, bitDepth, dither, &pal); + + if (dither) + GifDitherImage(oldImage, image, writer->oldImage, width, height, &pal); + else + GifThresholdImage(oldImage, image, writer->oldImage, width, height, &pal); + + GifWriteLzwImage(writer->f, writer->oldImage, 0, 0, width, height, delay, &pal); + + return true; +} + +// Writes the EOF code, closes the file handle, and frees temp memory used by a GIF. +// Many if not most viewers will still display a GIF properly if the EOF code is missing, +// but it's still a good idea to write it out. +bool GifEnd(GifWriter* writer) +{ + if (!writer->f) return false; + + fputc(0x3b, writer->f); // end of file + fclose(writer->f); + GIF_FREE(writer->oldImage); + + writer->f = NULL; + writer->oldImage = NULL; + + return true; +} + +#endif \ No newline at end of file diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 5eebadd02f..8b71630c96 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -120,7 +120,7 @@ function onClicked() { // take snapshot (with no notification) Script.setTimeout(function () { - Window.takeSnapshot(false, 1.91); + Window.takeSnapshotAnimated(false, 1.91); }, SNAPSHOT_DELAY); } From 912c9db1c183a3052242f0feb0c4355f945f06ae Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 8 Nov 2016 10:51:04 -0800 Subject: [PATCH 12/50] More progress. Corrupted output GIF. --- interface/src/Application.cpp | 40 ++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a7140403ca..61b0af63c5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5436,15 +5436,19 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); - //QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + QString path; - //if (!format.compare("animated")) - //{ + if (!format.compare("still")) + { + path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + } + else if (!format.compare("animated")) + { QImage frame; GifWriter myGifWriter; char* cstr; - QString path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); path.append(QDir::separator()); path.append("test.gif"); @@ -5452,20 +5456,36 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR cstr = new char[fname.size() + 1]; strcpy(cstr, fname.c_str()); - GifBegin(&myGifWriter, cstr, 1, 1, 0); - uint8_t test[4] = { 0xFF, 0x00, 0x00, 0x00 }; + frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(500); + qDebug() << "image format: " << frame.format(); + uint8_t frameNumBytes = frame.width() * frame.height() * 4; + uint8_t* pixelArray = new uint8_t[frameNumBytes]; + uchar *bits; + + GifBegin(&myGifWriter, cstr, frame.width(), frame.height(), 0); for (uint8_t itr = 0; itr < 30; itr++) { - test[0] = 0xFF / (itr + 1); - //frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(500); + bits = frame.bits(); + for (uint8_t itr2 = 0; itr2 < frameNumBytes; itr2 += 4) + { + pixelArray[itr2 + 3] = (uint8_t)bits[itr2]; + pixelArray[itr2 + 0] = (uint8_t)bits[itr2 + 1]; + pixelArray[itr2 + 1] = (uint8_t)bits[itr2 + 2]; + pixelArray[itr2 + 2] = (uint8_t)bits[itr2 + 3]; + } - GifWriteFrame(&myGifWriter, test, 1, 1, 0); + GifWriteFrame(&myGifWriter, pixelArray, frame.width(), frame.height(), 0); + usleep(USECS_PER_MSEC * 50); // 1/20 sec + // updateHeartbeat() while making the GIF so we don't scare the deadlock watchdog + updateHeartbeat(); + frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(500); } + delete[frameNumBytes] pixelArray; GifEnd(&myGifWriter); - //} + } emit DependencyManager::get()->snapshotTaken(path, notify); }); From 76121a2bcd876ff42f50b48cdb17965a4655168d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 8 Nov 2016 12:12:40 -0800 Subject: [PATCH 13/50] Getting somewheregit add -A! --- interface/src/Application.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 61b0af63c5..097b1092f4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5457,9 +5457,8 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR strcpy(cstr, fname.c_str()); - frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(500); - qDebug() << "image format: " << frame.format(); - uint8_t frameNumBytes = frame.width() * frame.height() * 4; + frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(400); + uint32_t frameNumBytes = frame.width() * frame.height() * 4; uint8_t* pixelArray = new uint8_t[frameNumBytes]; uchar *bits; @@ -5468,19 +5467,19 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR for (uint8_t itr = 0; itr < 30; itr++) { bits = frame.bits(); - for (uint8_t itr2 = 0; itr2 < frameNumBytes; itr2 += 4) + for (uint32_t itr2 = 0; itr2 < frameNumBytes; itr2 += 4) { - pixelArray[itr2 + 3] = (uint8_t)bits[itr2]; - pixelArray[itr2 + 0] = (uint8_t)bits[itr2 + 1]; - pixelArray[itr2 + 1] = (uint8_t)bits[itr2 + 2]; - pixelArray[itr2 + 2] = (uint8_t)bits[itr2 + 3]; + pixelArray[itr2 + 0] = (uint8_t)bits[itr2 + 0]; // R + pixelArray[itr2 + 1] = (uint8_t)bits[itr2 + 1]; // G + pixelArray[itr2 + 2] = (uint8_t)bits[itr2 + 2]; // B + pixelArray[itr2 + 3] = (uint8_t)bits[itr2 + 3]; // Alpha } GifWriteFrame(&myGifWriter, pixelArray, frame.width(), frame.height(), 0); usleep(USECS_PER_MSEC * 50); // 1/20 sec // updateHeartbeat() while making the GIF so we don't scare the deadlock watchdog updateHeartbeat(); - frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(500); + frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(400); } delete[frameNumBytes] pixelArray; From 21d459007579baa634e73f400a2ab9d211a30707 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 8 Nov 2016 13:18:24 -0800 Subject: [PATCH 14/50] Just using usleep won't work --- interface/src/Application.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 097b1092f4..b59f02329b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5463,7 +5463,7 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR uint8_t* pixelArray = new uint8_t[frameNumBytes]; uchar *bits; - GifBegin(&myGifWriter, cstr, frame.width(), frame.height(), 0); + GifBegin(&myGifWriter, cstr, frame.width(), frame.height(), 50); for (uint8_t itr = 0; itr < 30; itr++) { bits = frame.bits(); @@ -5475,7 +5475,7 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR pixelArray[itr2 + 3] = (uint8_t)bits[itr2 + 3]; // Alpha } - GifWriteFrame(&myGifWriter, pixelArray, frame.width(), frame.height(), 0); + GifWriteFrame(&myGifWriter, pixelArray, frame.width(), frame.height(), 50); usleep(USECS_PER_MSEC * 50); // 1/20 sec // updateHeartbeat() while making the GIF so we don't scare the deadlock watchdog updateHeartbeat(); From ee21d1ccc7606df8300bfe094298176edae0b50b Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 8 Nov 2016 14:35:53 -0800 Subject: [PATCH 15/50] IT WORKSgit add -A! Mega amounts of cleanup to do now. --- interface/src/Application.cpp | 75 +++++++++++++++++++---------------- interface/src/Application.h | 1 + 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b59f02329b..9d88ebf116 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -176,6 +176,7 @@ using namespace std; static QTimer locationUpdateTimer; static QTimer identityPacketTimer; static QTimer pingTimer; +static QTimer animatedSnapshotTimer; static const int MAX_CONCURRENT_RESOURCE_DOWNLOADS = 16; @@ -5429,6 +5430,9 @@ void Application::toggleLogDialog() { } } +uint8_t _currentAnimatedSnapshotFrame; +GifWriter _animatedSnapshotGifWriter; + void Application::takeSnapshot(bool notify, const QString& format, float aspectRatio) { postLambdaEvent([notify, format, aspectRatio, this] { QMediaPlayer* player = new QMediaPlayer(); @@ -5441,55 +5445,56 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR if (!format.compare("still")) { path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + emit DependencyManager::get()->snapshotTaken(path, notify); } else if (!format.compare("animated")) { - QImage frame; - GifWriter myGifWriter; char* cstr; - + connect(&animatedSnapshotTimer, &QTimer::timeout, this, &Application::animatedSnapshotTimerCb); + _currentAnimatedSnapshotFrame = 0; + // File path stuff path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); path.append(QDir::separator()); path.append("test.gif"); - string fname = path.toStdString(); cstr = new char[fname.size() + 1]; strcpy(cstr, fname.c_str()); - - - frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(400); - uint32_t frameNumBytes = frame.width() * frame.height() * 4; - - uint8_t* pixelArray = new uint8_t[frameNumBytes]; - uchar *bits; - - GifBegin(&myGifWriter, cstr, frame.width(), frame.height(), 50); - for (uint8_t itr = 0; itr < 30; itr++) - { - bits = frame.bits(); - for (uint32_t itr2 = 0; itr2 < frameNumBytes; itr2 += 4) - { - pixelArray[itr2 + 0] = (uint8_t)bits[itr2 + 0]; // R - pixelArray[itr2 + 1] = (uint8_t)bits[itr2 + 1]; // G - pixelArray[itr2 + 2] = (uint8_t)bits[itr2 + 2]; // B - pixelArray[itr2 + 3] = (uint8_t)bits[itr2 + 3]; // Alpha - } - - GifWriteFrame(&myGifWriter, pixelArray, frame.width(), frame.height(), 50); - usleep(USECS_PER_MSEC * 50); // 1/20 sec - // updateHeartbeat() while making the GIF so we don't scare the deadlock watchdog - updateHeartbeat(); - frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(400); - } - - delete[frameNumBytes] pixelArray; - GifEnd(&myGifWriter); + // Start the GIF + QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(500).convertToFormat(QImage::Format_RGBA8888); + GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), 5); + Application::animatedSnapshotTimerCb(); + animatedSnapshotTimer.start(50); } - - emit DependencyManager::get()->snapshotTaken(path, notify); }); } +void Application::animatedSnapshotTimerCb() +{ + if (_currentAnimatedSnapshotFrame == 30) + { + animatedSnapshotTimer.stop(); + GifEnd(&_animatedSnapshotGifWriter); + QString path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + path.append(QDir::separator()); + path.append("test.gif"); + emit DependencyManager::get()->snapshotTaken(path, false); + return; + } + + QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(500).convertToFormat(QImage::Format_RGBA8888); + uint32_t frameNumBytes = frame.width() * frame.height() * 4; + + uint8_t* pixelArray = new uint8_t[frameNumBytes]; + //uchar *bits; + //bits = frame.bits(); + //memcpy(pixelArray, (uint8_t*)bits, frameNumBytes); + + GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), 5); + _currentAnimatedSnapshotFrame++; + + delete[frameNumBytes] pixelArray; +} + void Application::shareSnapshot(const QString& path) { postLambdaEvent([path] { // not much to do here, everything is done in snapshot code... diff --git a/interface/src/Application.h b/interface/src/Application.h index d1150fb30f..6fd7c4f1b8 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -267,6 +267,7 @@ public: float getAverageSimsPerSecond() const { return _simCounter.rate(); } void takeSnapshot(bool notify, const QString& format = "still", float aspectRatio = 0.0f); + void animatedSnapshotTimerCb(); void shareSnapshot(const QString& filename); model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; } From ed1d087a68f0120fcd5371ee40a413a6bc20de3d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 8 Nov 2016 15:05:19 -0800 Subject: [PATCH 16/50] Starting cleanup procedure... --- interface/src/Application.cpp | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9d88ebf116..97044d5d9e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5432,6 +5432,12 @@ void Application::toggleLogDialog() { uint8_t _currentAnimatedSnapshotFrame; GifWriter _animatedSnapshotGifWriter; +#define SNAPSNOT_ANIMATED_WIDTH (640) +#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (20) +#define SNAPSNOT_ANIMATED_FRAME_DELAY (100/SNAPSNOT_ANIMATED_FRAMERATE_FPS) +#define SNAPSNOT_ANIMATED_DURATION_SECS (3) +#define SNAPSNOT_ANIMATED_NUM_FRAMES (SNAPSNOT_ANIMATED_DURATION_SECS * SNAPSNOT_ANIMATED_FRAMERATE_FPS) + void Application::takeSnapshot(bool notify, const QString& format, float aspectRatio) { postLambdaEvent([notify, format, aspectRatio, this] { @@ -5442,11 +5448,15 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR QString path; + // If this is a still snapshot... if (!format.compare("still")) { + // Get a screenshot, save it, and notify the window scripting + // interface that we've done so - this part of the code has been around for a while path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); emit DependencyManager::get()->snapshotTaken(path, notify); } + // If this is an animated snapshot (GIF)... else if (!format.compare("animated")) { char* cstr; @@ -5460,8 +5470,8 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR cstr = new char[fname.size() + 1]; strcpy(cstr, fname.c_str()); // Start the GIF - QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(500).convertToFormat(QImage::Format_RGBA8888); - GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), 5); + QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); Application::animatedSnapshotTimerCb(); animatedSnapshotTimer.start(50); } @@ -5470,7 +5480,7 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR void Application::animatedSnapshotTimerCb() { - if (_currentAnimatedSnapshotFrame == 30) + if (_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) { animatedSnapshotTimer.stop(); GifEnd(&_animatedSnapshotGifWriter); @@ -5481,15 +5491,11 @@ void Application::animatedSnapshotTimerCb() return; } - QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(500).convertToFormat(QImage::Format_RGBA8888); + QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); uint32_t frameNumBytes = frame.width() * frame.height() * 4; - uint8_t* pixelArray = new uint8_t[frameNumBytes]; - //uchar *bits; - //bits = frame.bits(); - //memcpy(pixelArray, (uint8_t*)bits, frameNumBytes); - GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), 5); + GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); _currentAnimatedSnapshotFrame++; delete[frameNumBytes] pixelArray; From b6dd795b0085cbb1b366e3ef811eb9d368649aa2 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 8 Nov 2016 15:50:39 -0800 Subject: [PATCH 17/50] It crashes, but it's the start of a new architecture --- interface/src/Application.cpp | 60 ++++++++++++++++------------------- interface/src/Application.h | 1 - interface/src/ui/Gif.h | 4 +-- 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 97044d5d9e..9da9bc186a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5430,9 +5430,7 @@ void Application::toggleLogDialog() { } } -uint8_t _currentAnimatedSnapshotFrame; -GifWriter _animatedSnapshotGifWriter; -#define SNAPSNOT_ANIMATED_WIDTH (640) +#define SNAPSNOT_ANIMATED_WIDTH (900) #define SNAPSNOT_ANIMATED_FRAMERATE_FPS (20) #define SNAPSNOT_ANIMATED_FRAME_DELAY (100/SNAPSNOT_ANIMATED_FRAMERATE_FPS) #define SNAPSNOT_ANIMATED_DURATION_SECS (3) @@ -5446,6 +5444,8 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); + GifWriter _animatedSnapshotGifWriter; + uint8_t _currentAnimatedSnapshotFrame; QString path; // If this is a still snapshot... @@ -5460,7 +5460,6 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR else if (!format.compare("animated")) { char* cstr; - connect(&animatedSnapshotTimer, &QTimer::timeout, this, &Application::animatedSnapshotTimerCb); _currentAnimatedSnapshotFrame = 0; // File path stuff path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); @@ -5469,38 +5468,33 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR string fname = path.toStdString(); cstr = new char[fname.size() + 1]; strcpy(cstr, fname.c_str()); - // Start the GIF - QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); - GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); - Application::animatedSnapshotTimerCb(); - animatedSnapshotTimer.start(50); + + connect(&animatedSnapshotTimer, &QTimer::timeout, this, [&] { + if (_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) + { + animatedSnapshotTimer.stop(); + GifEnd(&_animatedSnapshotGifWriter); + emit DependencyManager::get()->snapshotTaken(path, false); + return; + } + + QImage frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + if (_currentAnimatedSnapshotFrame == 0) + { + GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); + } + uint32_t frameNumBytes = frame.width() * frame.height() * 4; + uint8_t* pixelArray = new uint8_t[frameNumBytes]; + + GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); + _currentAnimatedSnapshotFrame++; + + delete[frameNumBytes] pixelArray; + }); + animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY * 10); } }); } - -void Application::animatedSnapshotTimerCb() -{ - if (_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) - { - animatedSnapshotTimer.stop(); - GifEnd(&_animatedSnapshotGifWriter); - QString path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); - path.append(QDir::separator()); - path.append("test.gif"); - emit DependencyManager::get()->snapshotTaken(path, false); - return; - } - - QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); - uint32_t frameNumBytes = frame.width() * frame.height() * 4; - uint8_t* pixelArray = new uint8_t[frameNumBytes]; - - GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); - _currentAnimatedSnapshotFrame++; - - delete[frameNumBytes] pixelArray; -} - void Application::shareSnapshot(const QString& path) { postLambdaEvent([path] { // not much to do here, everything is done in snapshot code... diff --git a/interface/src/Application.h b/interface/src/Application.h index 6fd7c4f1b8..d1150fb30f 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -267,7 +267,6 @@ public: float getAverageSimsPerSecond() const { return _simCounter.rate(); } void takeSnapshot(bool notify, const QString& format = "still", float aspectRatio = 0.0f); - void animatedSnapshotTimerCb(); void shareSnapshot(const QString& filename); model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; } diff --git a/interface/src/ui/Gif.h b/interface/src/ui/Gif.h index 83f094aee8..40ae37e9cb 100644 --- a/interface/src/ui/Gif.h +++ b/interface/src/ui/Gif.h @@ -368,8 +368,8 @@ void GifMakePalette(const uint8_t* lastFrame, const uint8_t* nextFrame, uint32_t GIF_TEMP_FREE(destroyableImage); // add the bottom node for the transparency index - pPal->treeSplit[1 << (bitDepth - 1)] = 0; - pPal->treeSplitElt[1 << (bitDepth - 1)] = 0; + pPal->treeSplit[1i64 << (bitDepth - 1)] = 0; + pPal->treeSplitElt[1i64 << (bitDepth - 1)] = 0; pPal->r[0] = pPal->g[0] = pPal->b[0] = 0; } From 1912ab1467c72a5ed733e23bbe7cd2a9d3b1a04f Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 8 Nov 2016 16:47:20 -0800 Subject: [PATCH 18/50] still crashing, feeling closer --- interface/src/Application.cpp | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9da9bc186a..d22b271037 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5445,22 +5445,19 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR player->play(); GifWriter _animatedSnapshotGifWriter; - uint8_t _currentAnimatedSnapshotFrame; + uint8_t _currentAnimatedSnapshotFrame = 0; QString path; // If this is a still snapshot... if (!format.compare("still")) { - // Get a screenshot, save it, and notify the window scripting - // interface that we've done so - this part of the code has been around for a while + // Get a screenshot and save it. and - this part of the code has been around for a while path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - emit DependencyManager::get()->snapshotTaken(path, notify); } // If this is an animated snapshot (GIF)... else if (!format.compare("animated")) { char* cstr; - _currentAnimatedSnapshotFrame = 0; // File path stuff path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); path.append(QDir::separator()); @@ -5469,7 +5466,7 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR cstr = new char[fname.size() + 1]; strcpy(cstr, fname.c_str()); - connect(&animatedSnapshotTimer, &QTimer::timeout, this, [&] { + connect(&animatedSnapshotTimer, &QTimer::timeout, [&] { if (_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) { animatedSnapshotTimer.stop(); @@ -5483,16 +5480,15 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR { GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); } - uint32_t frameNumBytes = frame.width() * frame.height() * 4; - uint8_t* pixelArray = new uint8_t[frameNumBytes]; GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); _currentAnimatedSnapshotFrame++; - - delete[frameNumBytes] pixelArray; }); animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY * 10); } + + // Notify the window scripting interface that we've taken a Snapshot + emit DependencyManager::get()->snapshotTaken(path, notify); }); } void Application::shareSnapshot(const QString& path) { From e656a4413f9d08ee9ae82bcc2e81b4a58401b007 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 9 Nov 2016 12:33:37 -0800 Subject: [PATCH 19/50] Comments - still crashing... --- interface/src/Application.cpp | 48 ++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d22b271037..6118de20f8 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5444,51 +5444,65 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); - GifWriter _animatedSnapshotGifWriter; - uint8_t _currentAnimatedSnapshotFrame = 0; - QString path; // If this is a still snapshot... if (!format.compare("still")) { - // Get a screenshot and save it. and - this part of the code has been around for a while - path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + // Get a screenshot and save it + QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + // Notify the window scripting interface that we've taken a Snapshot + emit DependencyManager::get()->snapshotTaken(path, notify); } // If this is an animated snapshot (GIF)... else if (!format.compare("animated")) { + GifWriter _animatedSnapshotGifWriter; + uint8_t _currentAnimatedSnapshotFrame = 0; + // File path stuff -- lots of this is temporary + // until I figure out how to save the .GIF to the same location as the still .JPG char* cstr; - // File path stuff - path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); - path.append(QDir::separator()); - path.append("test.gif"); - string fname = path.toStdString(); - cstr = new char[fname.size() + 1]; - strcpy(cstr, fname.c_str()); + QString path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); // Get the desktop + path.append(QDir::separator()); // Add the dir separator to the desktop location + path.append("test.gif"); // Add "test.gif" to the path + string fname = path.toStdString(); // Turn the QString into a regular string + cstr = new char[fname.size() + 1]; // Create a new character array to hold the .GIF file location + strcpy(cstr, fname.c_str()); // Copy the string into a character array - connect(&animatedSnapshotTimer, &QTimer::timeout, [&] { + // Connect the animatedSnapshotTimer QTimer to the lambda slot function + connect(&animatedSnapshotTimer, &QTimer::timeout, [&]() { + // If this is the last frame... if (_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) { + // Stop the snapshot QTimer animatedSnapshotTimer.stop(); + // Write out the end of the GIF GifEnd(&_animatedSnapshotGifWriter); + // Notify the Window Scripting Interface that the snapshot was taken emit DependencyManager::get()->snapshotTaken(path, false); return; } - QImage frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + // Get a screenshot from the display, then scale the screenshot down, + // then convert it to the image format the GIF library needs, + // then save all that to the QImage named "frame" + QImage frame = (qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + + // If this is the first frame... if (_currentAnimatedSnapshotFrame == 0) { + // Write out the header and beginning of the GIF file GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); } + // Write the frame to the gif GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); + // Increment the current snapshot frame count _currentAnimatedSnapshotFrame++; }); + + // Start the animatedSnapshotTimer QTimer animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY * 10); } - - // Notify the window scripting interface that we've taken a Snapshot - emit DependencyManager::get()->snapshotTaken(path, notify); }); } void Application::shareSnapshot(const QString& path) { From c39a8da3b8f3f248a0e34863957cffbd6b0b9baa Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 9 Nov 2016 13:56:57 -0800 Subject: [PATCH 20/50] Getting theregit add -A! --- interface/src/Application.cpp | 26 +++++++++++++++++++------- interface/src/Application.h | 4 ++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6118de20f8..e40f24b50c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -149,7 +149,6 @@ #include "ui/AddressBarDialog.h" #include "ui/AvatarInputs.h" #include "ui/DialogsManager.h" -#include "ui/Gif.h" #include "ui/LoginDialog.h" #include "ui/overlays/Cube3DOverlay.h" #include "ui/Snapshot.h" @@ -5456,8 +5455,17 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR // If this is an animated snapshot (GIF)... else if (!format.compare("animated")) { - GifWriter _animatedSnapshotGifWriter; - uint8_t _currentAnimatedSnapshotFrame = 0; + // If we're in the middle of capturing a GIF... + if (_currentAnimatedSnapshotFrame != 0) + { + // Protect against clobbering it and return immediately. + // (Perhaps with a "snapshot failed" message? + return; + } + + // Reset the current animated snapshot frame + _currentAnimatedSnapshotFrame = 0; + // File path stuff -- lots of this is temporary // until I figure out how to save the .GIF to the same location as the still .JPG char* cstr; @@ -5469,7 +5477,7 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR strcpy(cstr, fname.c_str()); // Copy the string into a character array // Connect the animatedSnapshotTimer QTimer to the lambda slot function - connect(&animatedSnapshotTimer, &QTimer::timeout, [&]() { + connect(&animatedSnapshotTimer, &QTimer::timeout, [&, path, aspectRatio] { // If this is the last frame... if (_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) { @@ -5485,19 +5493,23 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR // Get a screenshot from the display, then scale the screenshot down, // then convert it to the image format the GIF library needs, // then save all that to the QImage named "frame" - QImage frame = (qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + QImage* frame = new QImage(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + *frame = frame->scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); // If this is the first frame... if (_currentAnimatedSnapshotFrame == 0) { // Write out the header and beginning of the GIF file - GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); + GifBegin(&_animatedSnapshotGifWriter, cstr, frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY); } // Write the frame to the gif - GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); + GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame->bits(), frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY); // Increment the current snapshot frame count _currentAnimatedSnapshotFrame++; + + // Free the dynamic memory + delete frame; }); // Start the animatedSnapshotTimer QTimer diff --git a/interface/src/Application.h b/interface/src/Application.h index d1150fb30f..475f1f2f08 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -60,6 +60,7 @@ #include "scripting/DialogsManagerScriptingInterface.h" #include "ui/ApplicationOverlay.h" #include "ui/BandwidthDialog.h" +#include "ui/Gif.h" #include "ui/LodToolsDialog.h" #include "ui/LogDialog.h" #include "ui/OctreeStatsDialog.h" @@ -609,6 +610,9 @@ private: model::SkyboxPointer _defaultSkybox { new ProceduralSkybox() } ; gpu::TexturePointer _defaultSkyboxTexture; gpu::TexturePointer _defaultSkyboxAmbientTexture; + + GifWriter _animatedSnapshotGifWriter; + uint8_t _currentAnimatedSnapshotFrame = 0; }; From 045dfff158a9ee876870fd659521b4fe1ac37ef8 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 9 Nov 2016 15:19:22 -0800 Subject: [PATCH 21/50] It's workinggit add -A! Stack corruption errors? Must fix. --- interface/src/Application.cpp | 16 +- interface/src/Application.h | 2 +- interface/src/ui/Gif.cpp | 755 ++++++++++++++++++++++++++++++++++ interface/src/ui/Gif.h | 705 +------------------------------ 4 files changed, 785 insertions(+), 693 deletions(-) create mode 100644 interface/src/ui/Gif.cpp diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e40f24b50c..0323d2fb1f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5443,7 +5443,6 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); - // If this is a still snapshot... if (!format.compare("still")) { @@ -5477,19 +5476,20 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR strcpy(cstr, fname.c_str()); // Copy the string into a character array // Connect the animatedSnapshotTimer QTimer to the lambda slot function - connect(&animatedSnapshotTimer, &QTimer::timeout, [&, path, aspectRatio] { + connect(&animatedSnapshotTimer, &QTimer::timeout, [=] { // If this is the last frame... - if (_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) + if (qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) { // Stop the snapshot QTimer animatedSnapshotTimer.stop(); // Write out the end of the GIF - GifEnd(&_animatedSnapshotGifWriter); + GifEnd(&(qApp->_animatedSnapshotGifWriter)); // Notify the Window Scripting Interface that the snapshot was taken emit DependencyManager::get()->snapshotTaken(path, false); return; } + // Get a screenshot from the display, then scale the screenshot down, // then convert it to the image format the GIF library needs, // then save all that to the QImage named "frame" @@ -5497,16 +5497,16 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR *frame = frame->scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); // If this is the first frame... - if (_currentAnimatedSnapshotFrame == 0) + if (qApp->_currentAnimatedSnapshotFrame == 0) { // Write out the header and beginning of the GIF file - GifBegin(&_animatedSnapshotGifWriter, cstr, frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY); + GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); } // Write the frame to the gif - GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame->bits(), frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY); + GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame->bits(), frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); // Increment the current snapshot frame count - _currentAnimatedSnapshotFrame++; + qApp->_currentAnimatedSnapshotFrame++; // Free the dynamic memory delete frame; diff --git a/interface/src/Application.h b/interface/src/Application.h index 475f1f2f08..b88728146f 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -612,7 +612,7 @@ private: gpu::TexturePointer _defaultSkyboxAmbientTexture; GifWriter _animatedSnapshotGifWriter; - uint8_t _currentAnimatedSnapshotFrame = 0; + uint8_t _currentAnimatedSnapshotFrame { 0 }; }; diff --git a/interface/src/ui/Gif.cpp b/interface/src/ui/Gif.cpp new file mode 100644 index 0000000000..87716b0fb4 --- /dev/null +++ b/interface/src/ui/Gif.cpp @@ -0,0 +1,755 @@ +// +// gif.c +// by Charlie Tangora +// Public domain. +// Email me : ctangora -at- gmail -dot- com +// +// This file offers a simple, very limited way to create animated GIFs directly in code. +// +// Those looking for particular cleverness are likely to be disappointed; it's pretty +// much a straight-ahead implementation of the GIF format with optional Floyd-Steinberg +// dithering. (It does at least use delta encoding - only the changed portions of each +// frame are saved.) +// +// So resulting files are often quite large. The hope is that it will be handy nonetheless +// as a quick and easily-integrated way for programs to spit out animations. +// +// Only RGBA8 is currently supported as an input format. (The alpha is ignored.) +// +// USAGE: +// Create a GifWriter struct. Pass it to GifBegin() to initialize and write the header. +// Pass subsequent frames to GifWriteFrame(). +// Finally, call GifEnd() to close the file handle and free memory. +// + +#ifndef gif_c +#define gif_c + +#include "Gif.h" + + +int GifIMax(int l, int r) { return l>r ? l : r; } +int GifIMin(int l, int r) { return l(1 << pPal->bitDepth) - 1) + { + int ind = treeRoot - (1 << pPal->bitDepth); + if (ind == kGifTransIndex) return; + + // check whether this color is better than the current winner + int r_err = r - ((int32_t)pPal->r[ind]); + int g_err = g - ((int32_t)pPal->g[ind]); + int b_err = b - ((int32_t)pPal->b[ind]); + int diff = GifIAbs(r_err) + GifIAbs(g_err) + GifIAbs(b_err); + + if (diff < bestDiff) + { + bestInd = ind; + bestDiff = diff; + } + + return; + } + + // take the appropriate color (r, g, or b) for this node of the k-d tree + int comps[3]; comps[0] = r; comps[1] = g; comps[2] = b; + int splitComp = comps[pPal->treeSplitElt[treeRoot]]; + + int splitPos = pPal->treeSplit[treeRoot]; + if (splitPos > splitComp) + { + // check the left subtree + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); + if (bestDiff > splitPos - splitComp) + { + // cannot prove there's not a better value in the right subtree, check that too + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); + } + } + else + { + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); + if (bestDiff > splitComp - splitPos) + { + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); + } + } +} + +void GifSwapPixels(uint8_t* image, int pixA, int pixB) +{ + uint8_t rA = image[pixA * 4]; + uint8_t gA = image[pixA * 4 + 1]; + uint8_t bA = image[pixA * 4 + 2]; + uint8_t aA = image[pixA * 4 + 3]; + + uint8_t rB = image[pixB * 4]; + uint8_t gB = image[pixB * 4 + 1]; + uint8_t bB = image[pixB * 4 + 2]; + uint8_t aB = image[pixA * 4 + 3]; + + image[pixA * 4] = rB; + image[pixA * 4 + 1] = gB; + image[pixA * 4 + 2] = bB; + image[pixA * 4 + 3] = aB; + + image[pixB * 4] = rA; + image[pixB * 4 + 1] = gA; + image[pixB * 4 + 2] = bA; + image[pixB * 4 + 3] = aA; +} + +// just the partition operation from quicksort +int GifPartition(uint8_t* image, const int left, const int right, const int elt, int pivotIndex) +{ + const int pivotValue = image[(pivotIndex)* 4 + elt]; + GifSwapPixels(image, pivotIndex, right - 1); + int storeIndex = left; + bool split = 0; + for (int ii = left; ii neededCenter) + GifPartitionByMedian(image, left, pivotIndex, com, neededCenter); + + if (pivotIndex < neededCenter) + GifPartitionByMedian(image, pivotIndex + 1, right, com, neededCenter); + } +} + +// Builds a palette by creating a balanced k-d tree of all pixels in the image +void GifSplitPalette(uint8_t* image, int numPixels, int firstElt, int lastElt, int splitElt, int splitDist, int treeNode, bool buildForDither, GifPalette* pal) +{ + if (lastElt <= firstElt || numPixels == 0) + return; + + // base case, bottom of the tree + if (lastElt == firstElt + 1) + { + if (buildForDither) + { + // Dithering needs at least one color as dark as anything + // in the image and at least one brightest color - + // otherwise it builds up error and produces strange artifacts + if (firstElt == 1) + { + // special case: the darkest color in the image + uint32_t r = 255, g = 255, b = 255; + for (int ii = 0; iir[firstElt] = r; + pal->g[firstElt] = g; + pal->b[firstElt] = b; + + return; + } + + if (firstElt == (1 << pal->bitDepth) - 1) + { + // special case: the lightest color in the image + uint32_t r = 0, g = 0, b = 0; + for (int ii = 0; iir[firstElt] = r; + pal->g[firstElt] = g; + pal->b[firstElt] = b; + + return; + } + } + + // otherwise, take the average of all colors in this subcube + uint64_t r = 0, g = 0, b = 0; + for (int ii = 0; iir[firstElt] = (uint8_t)r; + pal->g[firstElt] = (uint8_t)g; + pal->b[firstElt] = (uint8_t)b; + + return; + } + + // Find the axis with the largest range + int minR = 255, maxR = 0; + int minG = 255, maxG = 0; + int minB = 255, maxB = 0; + for (int ii = 0; ii maxR) maxR = r; + if (r < minR) minR = r; + + if (g > maxG) maxG = g; + if (g < minG) minG = g; + + if (b > maxB) maxB = b; + if (b < minB) minB = b; + } + + int rRange = maxR - minR; + int gRange = maxG - minG; + int bRange = maxB - minB; + + // and split along that axis. (incidentally, this means this isn't a "proper" k-d tree but I don't know what else to call it) + int splitCom = 1; + if (bRange > gRange) splitCom = 2; + if (rRange > bRange && rRange > gRange) splitCom = 0; + + int subPixelsA = numPixels * (splitElt - firstElt) / (lastElt - firstElt); + int subPixelsB = numPixels - subPixelsA; + + GifPartitionByMedian(image, 0, numPixels, splitCom, subPixelsA); + + pal->treeSplitElt[treeNode] = splitCom; + pal->treeSplit[treeNode] = image[subPixelsA * 4 + splitCom]; + + GifSplitPalette(image, subPixelsA, firstElt, splitElt, splitElt - splitDist, splitDist / 2, treeNode * 2, buildForDither, pal); + GifSplitPalette(image + subPixelsA * 4, subPixelsB, splitElt, lastElt, splitElt + splitDist, splitDist / 2, treeNode * 2 + 1, buildForDither, pal); +} + +// Finds all pixels that have changed from the previous image and +// moves them to the fromt of th buffer. +// This allows us to build a palette optimized for the colors of the +// changed pixels only. +int GifPickChangedPixels(const uint8_t* lastFrame, uint8_t* frame, int numPixels) +{ + int numChanged = 0; + uint8_t* writeIter = frame; + + for (int ii = 0; iibitDepth = bitDepth; + + // SplitPalette is destructive (it sorts the pixels by color) so + // we must create a copy of the image for it to destroy + int imageSize = width*height * 4 * sizeof(uint8_t); + uint8_t* destroyableImage = (uint8_t*)GIF_TEMP_MALLOC(imageSize); + memcpy(destroyableImage, nextFrame, imageSize); + + int numPixels = width*height; + if (lastFrame) + numPixels = GifPickChangedPixels(lastFrame, destroyableImage, numPixels); + + const int lastElt = 1 << bitDepth; + const int splitElt = lastElt / 2; + const int splitDist = splitElt / 2; + + GifSplitPalette(destroyableImage, numPixels, 1, lastElt, splitElt, splitDist, 1, buildForDither, pPal); + + GIF_TEMP_FREE(destroyableImage); + + // add the bottom node for the transparency index + pPal->treeSplit[1i64 << (bitDepth - 1)] = 0; + pPal->treeSplitElt[1i64 << (bitDepth - 1)] = 0; + + pPal->r[0] = pPal->g[0] = pPal->b[0] = 0; +} + +// Implements Floyd-Steinberg dithering, writes palette value to alpha +void GifDitherImage(const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal) +{ + int numPixels = width*height; + + // quantPixels initially holds color*256 for all pixels + // The extra 8 bits of precision allow for sub-single-color error values + // to be propagated + int32_t* quantPixels = (int32_t*)GIF_TEMP_MALLOC(sizeof(int32_t)*numPixels * 4); + + for (int ii = 0; iir[bestInd]) * 256; + int32_t g_err = nextPix[1] - int32_t(pPal->g[bestInd]) * 256; + int32_t b_err = nextPix[2] - int32_t(pPal->b[bestInd]) * 256; + + nextPix[0] = pPal->r[bestInd]; + nextPix[1] = pPal->g[bestInd]; + nextPix[2] = pPal->b[bestInd]; + nextPix[3] = bestInd; + + // Propagate the error to the four adjacent locations + // that we haven't touched yet + int quantloc_7 = (yy*width + xx + 1); + int quantloc_3 = (yy*width + width + xx - 1); + int quantloc_5 = (yy*width + width + xx); + int quantloc_1 = (yy*width + width + xx + 1); + + if (quantloc_7 < numPixels) + { + int32_t* pix7 = quantPixels + 4 * quantloc_7; + pix7[0] += GifIMax(-pix7[0], r_err * 7 / 16); + pix7[1] += GifIMax(-pix7[1], g_err * 7 / 16); + pix7[2] += GifIMax(-pix7[2], b_err * 7 / 16); + } + + if (quantloc_3 < numPixels) + { + int32_t* pix3 = quantPixels + 4 * quantloc_3; + pix3[0] += GifIMax(-pix3[0], r_err * 3 / 16); + pix3[1] += GifIMax(-pix3[1], g_err * 3 / 16); + pix3[2] += GifIMax(-pix3[2], b_err * 3 / 16); + } + + if (quantloc_5 < numPixels) + { + int32_t* pix5 = quantPixels + 4 * quantloc_5; + pix5[0] += GifIMax(-pix5[0], r_err * 5 / 16); + pix5[1] += GifIMax(-pix5[1], g_err * 5 / 16); + pix5[2] += GifIMax(-pix5[2], b_err * 5 / 16); + } + + if (quantloc_1 < numPixels) + { + int32_t* pix1 = quantPixels + 4 * quantloc_1; + pix1[0] += GifIMax(-pix1[0], r_err / 16); + pix1[1] += GifIMax(-pix1[1], g_err / 16); + pix1[2] += GifIMax(-pix1[2], b_err / 16); + } + } + } + + // Copy the palettized result to the output buffer + for (int ii = 0; iir[bestInd]; + outFrame[1] = pPal->g[bestInd]; + outFrame[2] = pPal->b[bestInd]; + outFrame[3] = bestInd; + } + + if (lastFrame) lastFrame += 4; + outFrame += 4; + nextFrame += 4; + } +} + +// insert a single bit +void GifWriteBit(GifBitStatus& stat, uint32_t bit) +{ + bit = bit & 1; + bit = bit << stat.bitIndex; + stat.byte |= bit; + + ++stat.bitIndex; + if (stat.bitIndex > 7) + { + // move the newly-finished byte to the chunk buffer + stat.chunk[stat.chunkIndex++] = stat.byte; + // and start a new byte + stat.bitIndex = 0; + stat.byte = 0; + } +} + +// write all bytes so far to the file +void GifWriteChunk(FILE* f, GifBitStatus& stat) +{ + fputc(stat.chunkIndex, f); + fwrite(stat.chunk, 1, stat.chunkIndex, f); + + stat.bitIndex = 0; + stat.byte = 0; + stat.chunkIndex = 0; +} + +void GifWriteCode(FILE* f, GifBitStatus& stat, uint32_t code, uint32_t length) +{ + for (uint32_t ii = 0; ii> 1; + + if (stat.chunkIndex == 255) + { + GifWriteChunk(f, stat); + } + } +} + +// write a 256-color (8-bit) image palette to the file +void GifWritePalette(const GifPalette* pPal, FILE* f) +{ + fputc(0, f); // first color: transparency + fputc(0, f); + fputc(0, f); + + for (int ii = 1; ii<(1 << pPal->bitDepth); ++ii) + { + uint32_t r = pPal->r[ii]; + uint32_t g = pPal->g[ii]; + uint32_t b = pPal->b[ii]; + + fputc(r, f); + fputc(g, f); + fputc(b, f); + } +} + +// write the image header, LZW-compress and write out the image +void GifWriteLzwImage(FILE* f, uint8_t* image, uint32_t left, uint32_t top, uint32_t width, uint32_t height, uint32_t delay, GifPalette* pPal) +{ + // graphics control extension + fputc(0x21, f); + fputc(0xf9, f); + fputc(0x04, f); + fputc(0x05, f); // leave prev frame in place, this frame has transparency + fputc(delay & 0xff, f); + fputc((delay >> 8) & 0xff, f); + fputc(kGifTransIndex, f); // transparent color index + fputc(0, f); + + fputc(0x2c, f); // image descriptor block + + fputc(left & 0xff, f); // corner of image in canvas space + fputc((left >> 8) & 0xff, f); + fputc(top & 0xff, f); + fputc((top >> 8) & 0xff, f); + + fputc(width & 0xff, f); // width and height of image + fputc((width >> 8) & 0xff, f); + fputc(height & 0xff, f); + fputc((height >> 8) & 0xff, f); + + //fputc(0, f); // no local color table, no transparency + //fputc(0x80, f); // no local color table, but transparency + + fputc(0x80 + pPal->bitDepth - 1, f); // local color table present, 2 ^ bitDepth entries + GifWritePalette(pPal, f); + + const int minCodeSize = pPal->bitDepth; + const uint32_t clearCode = 1 << pPal->bitDepth; + + fputc(minCodeSize, f); // min code size 8 bits + + GifLzwNode* codetree = (GifLzwNode*)GIF_TEMP_MALLOC(sizeof(GifLzwNode) * 4096); + + memset(codetree, 0, sizeof(GifLzwNode) * 4096); + int32_t curCode = -1; + uint32_t codeSize = minCodeSize + 1; + uint32_t maxCode = clearCode + 1; + + GifBitStatus stat; + stat.byte = 0; + stat.bitIndex = 0; + stat.chunkIndex = 0; + + GifWriteCode(f, stat, clearCode, codeSize); // start with a fresh LZW dictionary + + for (uint32_t yy = 0; yy= (1ul << codeSize)) + { + // dictionary entry count has broken a size barrier, + // we need more bits for codes + codeSize++; + } + if (maxCode == 4095) + { + // the dictionary is full, clear it out and begin anew + GifWriteCode(f, stat, clearCode, codeSize); // clear tree + + memset(codetree, 0, sizeof(GifLzwNode) * 4096); + curCode = -1; + codeSize = minCodeSize + 1; + maxCode = clearCode + 1; + } + + curCode = nextValue; + } + } + } + + // compression footer + GifWriteCode(f, stat, curCode, codeSize); + GifWriteCode(f, stat, clearCode, codeSize); + GifWriteCode(f, stat, clearCode + 1, minCodeSize + 1); + + // write out the last partial chunk + while (stat.bitIndex) GifWriteBit(stat, 0); + if (stat.chunkIndex) GifWriteChunk(f, stat); + + fputc(0, f); // image block terminator + + GIF_TEMP_FREE(codetree); +} + +// Creates a gif file. +// The input GIFWriter is assumed to be uninitialized. +// The delay value is the time between frames in hundredths of a second - note that not all viewers pay much attention to this value. +bool GifBegin(GifWriter* writer, const char* filename, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth, bool dither) +{ +#if _MSC_VER >= 1400 + writer->f = 0; + fopen_s(&writer->f, filename, "wb"); +#else + writer->f = fopen(filename, "wb"); +#endif + if (!writer->f) return false; + + writer->firstFrame = true; + + // allocate + writer->oldImage = (uint8_t*)GIF_MALLOC(width*height * 4); + + fputs("GIF89a", writer->f); + + // screen descriptor + fputc(width & 0xff, writer->f); + fputc((width >> 8) & 0xff, writer->f); + fputc(height & 0xff, writer->f); + fputc((height >> 8) & 0xff, writer->f); + + fputc(0xf0, writer->f); // there is an unsorted global color table of 2 entries + fputc(0, writer->f); // background color + fputc(0, writer->f); // pixels are square (we need to specify this because it's 1989) + + // now the "global" palette (really just a dummy palette) + // color 0: black + fputc(0, writer->f); + fputc(0, writer->f); + fputc(0, writer->f); + // color 1: also black + fputc(0, writer->f); + fputc(0, writer->f); + fputc(0, writer->f); + + if (delay != 0) + { + // animation header + fputc(0x21, writer->f); // extension + fputc(0xff, writer->f); // application specific + fputc(11, writer->f); // length 11 + fputs("NETSCAPE2.0", writer->f); // yes, really + fputc(3, writer->f); // 3 bytes of NETSCAPE2.0 data + + fputc(1, writer->f); // JUST BECAUSE + fputc(0, writer->f); // loop infinitely (byte 0) + fputc(0, writer->f); // loop infinitely (byte 1) + + fputc(0, writer->f); // block terminator + } + + return true; +} + +// Writes out a new frame to a GIF in progress. +// The GIFWriter should have been created by GIFBegin. +// AFAIK, it is legal to use different bit depths for different frames of an image - +// this may be handy to save bits in animations that don't change much. +bool GifWriteFrame(GifWriter* writer, const uint8_t* image, uint32_t width, uint32_t height, uint32_t delay, uint8_t bitDepth, bool dither) +{ + if (!writer->f) return false; + + const uint8_t* oldImage = writer->firstFrame ? NULL : writer->oldImage; + writer->firstFrame = false; + + GifPalette pal; + GifMakePalette((dither ? NULL : oldImage), image, width, height, bitDepth, dither, &pal); + + if (dither) + GifDitherImage(oldImage, image, writer->oldImage, width, height, &pal); + else + GifThresholdImage(oldImage, image, writer->oldImage, width, height, &pal); + + GifWriteLzwImage(writer->f, writer->oldImage, 0, 0, width, height, delay, &pal); + + return true; +} + +// Writes the EOF code, closes the file handle, and frees temp memory used by a GIF. +// Many if not most viewers will still display a GIF properly if the EOF code is missing, +// but it's still a good idea to write it out. +bool GifEnd(GifWriter* writer) +{ + if (!writer->f) return false; + + fputc(0x3b, writer->f); // end of file + fclose(writer->f); + GIF_FREE(writer->oldImage); + + writer->f = NULL; + writer->oldImage = NULL; + + return true; +} + +#endif \ No newline at end of file diff --git a/interface/src/ui/Gif.h b/interface/src/ui/Gif.h index 40ae37e9cb..7d3929a3bb 100644 --- a/interface/src/ui/Gif.h +++ b/interface/src/ui/Gif.h @@ -25,15 +25,9 @@ #ifndef gif_h #define gif_h +#include #include // for FILE* #include // for memcpy and bzero -#include // for integer typedefs - -// Define these macros to hook into a custom memory allocator. -// TEMP_MALLOC and TEMP_FREE will only be called in stack fashion - frees in the reverse order of mallocs -// and any temp memory allocated by a function will be freed before it exits. -// MALLOC and FREE are used only by GifBegin and GifEnd respectively (to allocate a buffer the size of the image, which -// is used to find changed pixels for delta-encoding.) #ifndef GIF_TEMP_MALLOC #include @@ -73,453 +67,42 @@ struct GifPalette }; // max, min, and abs functions -int GifIMax(int l, int r) { return l>r ? l : r; } -int GifIMin(int l, int r) { return l(1 << pPal->bitDepth) - 1) - { - int ind = treeRoot - (1 << pPal->bitDepth); - if (ind == kGifTransIndex) return; +void GifGetClosestPaletteColor(GifPalette* pPal, int r, int g, int b, int& bestInd, int& bestDiff, int treeRoot); - // check whether this color is better than the current winner - int r_err = r - ((int32_t)pPal->r[ind]); - int g_err = g - ((int32_t)pPal->g[ind]); - int b_err = b - ((int32_t)pPal->b[ind]); - int diff = GifIAbs(r_err) + GifIAbs(g_err) + GifIAbs(b_err); - - if (diff < bestDiff) - { - bestInd = ind; - bestDiff = diff; - } - - return; - } - - // take the appropriate color (r, g, or b) for this node of the k-d tree - int comps[3]; comps[0] = r; comps[1] = g; comps[2] = b; - int splitComp = comps[pPal->treeSplitElt[treeRoot]]; - - int splitPos = pPal->treeSplit[treeRoot]; - if (splitPos > splitComp) - { - // check the left subtree - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); - if (bestDiff > splitPos - splitComp) - { - // cannot prove there's not a better value in the right subtree, check that too - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); - } - } - else - { - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); - if (bestDiff > splitComp - splitPos) - { - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); - } - } -} - -void GifSwapPixels(uint8_t* image, int pixA, int pixB) -{ - uint8_t rA = image[pixA * 4]; - uint8_t gA = image[pixA * 4 + 1]; - uint8_t bA = image[pixA * 4 + 2]; - uint8_t aA = image[pixA * 4 + 3]; - - uint8_t rB = image[pixB * 4]; - uint8_t gB = image[pixB * 4 + 1]; - uint8_t bB = image[pixB * 4 + 2]; - uint8_t aB = image[pixA * 4 + 3]; - - image[pixA * 4] = rB; - image[pixA * 4 + 1] = gB; - image[pixA * 4 + 2] = bB; - image[pixA * 4 + 3] = aB; - - image[pixB * 4] = rA; - image[pixB * 4 + 1] = gA; - image[pixB * 4 + 2] = bA; - image[pixB * 4 + 3] = aA; -} +void GifSwapPixels(uint8_t* image, int pixA, int pixB); // just the partition operation from quicksort -int GifPartition(uint8_t* image, const int left, const int right, const int elt, int pivotIndex) -{ - const int pivotValue = image[(pivotIndex)* 4 + elt]; - GifSwapPixels(image, pivotIndex, right - 1); - int storeIndex = left; - bool split = 0; - for (int ii = left; ii neededCenter) - GifPartitionByMedian(image, left, pivotIndex, com, neededCenter); - - if (pivotIndex < neededCenter) - GifPartitionByMedian(image, pivotIndex + 1, right, com, neededCenter); - } -} +void GifPartitionByMedian(uint8_t* image, int left, int right, int com, int neededCenter); // Builds a palette by creating a balanced k-d tree of all pixels in the image -void GifSplitPalette(uint8_t* image, int numPixels, int firstElt, int lastElt, int splitElt, int splitDist, int treeNode, bool buildForDither, GifPalette* pal) -{ - if (lastElt <= firstElt || numPixels == 0) - return; - - // base case, bottom of the tree - if (lastElt == firstElt + 1) - { - if (buildForDither) - { - // Dithering needs at least one color as dark as anything - // in the image and at least one brightest color - - // otherwise it builds up error and produces strange artifacts - if (firstElt == 1) - { - // special case: the darkest color in the image - uint32_t r = 255, g = 255, b = 255; - for (int ii = 0; iir[firstElt] = r; - pal->g[firstElt] = g; - pal->b[firstElt] = b; - - return; - } - - if (firstElt == (1 << pal->bitDepth) - 1) - { - // special case: the lightest color in the image - uint32_t r = 0, g = 0, b = 0; - for (int ii = 0; iir[firstElt] = r; - pal->g[firstElt] = g; - pal->b[firstElt] = b; - - return; - } - } - - // otherwise, take the average of all colors in this subcube - uint64_t r = 0, g = 0, b = 0; - for (int ii = 0; iir[firstElt] = (uint8_t)r; - pal->g[firstElt] = (uint8_t)g; - pal->b[firstElt] = (uint8_t)b; - - return; - } - - // Find the axis with the largest range - int minR = 255, maxR = 0; - int minG = 255, maxG = 0; - int minB = 255, maxB = 0; - for (int ii = 0; ii maxR) maxR = r; - if (r < minR) minR = r; - - if (g > maxG) maxG = g; - if (g < minG) minG = g; - - if (b > maxB) maxB = b; - if (b < minB) minB = b; - } - - int rRange = maxR - minR; - int gRange = maxG - minG; - int bRange = maxB - minB; - - // and split along that axis. (incidentally, this means this isn't a "proper" k-d tree but I don't know what else to call it) - int splitCom = 1; - if (bRange > gRange) splitCom = 2; - if (rRange > bRange && rRange > gRange) splitCom = 0; - - int subPixelsA = numPixels * (splitElt - firstElt) / (lastElt - firstElt); - int subPixelsB = numPixels - subPixelsA; - - GifPartitionByMedian(image, 0, numPixels, splitCom, subPixelsA); - - pal->treeSplitElt[treeNode] = splitCom; - pal->treeSplit[treeNode] = image[subPixelsA * 4 + splitCom]; - - GifSplitPalette(image, subPixelsA, firstElt, splitElt, splitElt - splitDist, splitDist / 2, treeNode * 2, buildForDither, pal); - GifSplitPalette(image + subPixelsA * 4, subPixelsB, splitElt, lastElt, splitElt + splitDist, splitDist / 2, treeNode * 2 + 1, buildForDither, pal); -} +void GifSplitPalette(uint8_t* image, int numPixels, int firstElt, int lastElt, int splitElt, int splitDist, int treeNode, bool buildForDither, GifPalette* pal); // Finds all pixels that have changed from the previous image and // moves them to the fromt of th buffer. // This allows us to build a palette optimized for the colors of the // changed pixels only. -int GifPickChangedPixels(const uint8_t* lastFrame, uint8_t* frame, int numPixels) -{ - int numChanged = 0; - uint8_t* writeIter = frame; - - for (int ii = 0; iibitDepth = bitDepth; - - // SplitPalette is destructive (it sorts the pixels by color) so - // we must create a copy of the image for it to destroy - int imageSize = width*height * 4 * sizeof(uint8_t); - uint8_t* destroyableImage = (uint8_t*)GIF_TEMP_MALLOC(imageSize); - memcpy(destroyableImage, nextFrame, imageSize); - - int numPixels = width*height; - if (lastFrame) - numPixels = GifPickChangedPixels(lastFrame, destroyableImage, numPixels); - - const int lastElt = 1 << bitDepth; - const int splitElt = lastElt / 2; - const int splitDist = splitElt / 2; - - GifSplitPalette(destroyableImage, numPixels, 1, lastElt, splitElt, splitDist, 1, buildForDither, pPal); - - GIF_TEMP_FREE(destroyableImage); - - // add the bottom node for the transparency index - pPal->treeSplit[1i64 << (bitDepth - 1)] = 0; - pPal->treeSplitElt[1i64 << (bitDepth - 1)] = 0; - - pPal->r[0] = pPal->g[0] = pPal->b[0] = 0; -} +void GifMakePalette(const uint8_t* lastFrame, const uint8_t* nextFrame, uint32_t width, uint32_t height, uint8_t bitDepth, bool buildForDither, GifPalette* pPal); // Implements Floyd-Steinberg dithering, writes palette value to alpha -void GifDitherImage(const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal) -{ - int numPixels = width*height; - - // quantPixels initially holds color*256 for all pixels - // The extra 8 bits of precision allow for sub-single-color error values - // to be propagated - int32_t* quantPixels = (int32_t*)GIF_TEMP_MALLOC(sizeof(int32_t)*numPixels * 4); - - for (int ii = 0; iir[bestInd]) * 256; - int32_t g_err = nextPix[1] - int32_t(pPal->g[bestInd]) * 256; - int32_t b_err = nextPix[2] - int32_t(pPal->b[bestInd]) * 256; - - nextPix[0] = pPal->r[bestInd]; - nextPix[1] = pPal->g[bestInd]; - nextPix[2] = pPal->b[bestInd]; - nextPix[3] = bestInd; - - // Propagate the error to the four adjacent locations - // that we haven't touched yet - int quantloc_7 = (yy*width + xx + 1); - int quantloc_3 = (yy*width + width + xx - 1); - int quantloc_5 = (yy*width + width + xx); - int quantloc_1 = (yy*width + width + xx + 1); - - if (quantloc_7 < numPixels) - { - int32_t* pix7 = quantPixels + 4 * quantloc_7; - pix7[0] += GifIMax(-pix7[0], r_err * 7 / 16); - pix7[1] += GifIMax(-pix7[1], g_err * 7 / 16); - pix7[2] += GifIMax(-pix7[2], b_err * 7 / 16); - } - - if (quantloc_3 < numPixels) - { - int32_t* pix3 = quantPixels + 4 * quantloc_3; - pix3[0] += GifIMax(-pix3[0], r_err * 3 / 16); - pix3[1] += GifIMax(-pix3[1], g_err * 3 / 16); - pix3[2] += GifIMax(-pix3[2], b_err * 3 / 16); - } - - if (quantloc_5 < numPixels) - { - int32_t* pix5 = quantPixels + 4 * quantloc_5; - pix5[0] += GifIMax(-pix5[0], r_err * 5 / 16); - pix5[1] += GifIMax(-pix5[1], g_err * 5 / 16); - pix5[2] += GifIMax(-pix5[2], b_err * 5 / 16); - } - - if (quantloc_1 < numPixels) - { - int32_t* pix1 = quantPixels + 4 * quantloc_1; - pix1[0] += GifIMax(-pix1[0], r_err / 16); - pix1[1] += GifIMax(-pix1[1], g_err / 16); - pix1[2] += GifIMax(-pix1[2], b_err / 16); - } - } - } - - // Copy the palettized result to the output buffer - for (int ii = 0; iir[bestInd]; - outFrame[1] = pPal->g[bestInd]; - outFrame[2] = pPal->b[bestInd]; - outFrame[3] = bestInd; - } - - if (lastFrame) lastFrame += 4; - outFrame += 4; - nextFrame += 4; - } -} +void GifThresholdImage(const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal); // Simple structure to write out the LZW-compressed portion of the image // one bit at a time @@ -533,47 +116,12 @@ struct GifBitStatus }; // insert a single bit -void GifWriteBit(GifBitStatus& stat, uint32_t bit) -{ - bit = bit & 1; - bit = bit << stat.bitIndex; - stat.byte |= bit; - - ++stat.bitIndex; - if (stat.bitIndex > 7) - { - // move the newly-finished byte to the chunk buffer - stat.chunk[stat.chunkIndex++] = stat.byte; - // and start a new byte - stat.bitIndex = 0; - stat.byte = 0; - } -} +void GifWriteBit(GifBitStatus& stat, uint32_t bit); // write all bytes so far to the file -void GifWriteChunk(FILE* f, GifBitStatus& stat) -{ - fputc(stat.chunkIndex, f); - fwrite(stat.chunk, 1, stat.chunkIndex, f); +void GifWriteChunk(FILE* f, GifBitStatus& stat); - stat.bitIndex = 0; - stat.byte = 0; - stat.chunkIndex = 0; -} - -void GifWriteCode(FILE* f, GifBitStatus& stat, uint32_t code, uint32_t length) -{ - for (uint32_t ii = 0; ii> 1; - - if (stat.chunkIndex == 255) - { - GifWriteChunk(f, stat); - } - } -} +void GifWriteCode(FILE* f, GifBitStatus& stat, uint32_t code, uint32_t length); // The LZW dictionary is a 256-ary tree constructed as the file is encoded, // this is one node @@ -583,137 +131,10 @@ struct GifLzwNode }; // write a 256-color (8-bit) image palette to the file -void GifWritePalette(const GifPalette* pPal, FILE* f) -{ - fputc(0, f); // first color: transparency - fputc(0, f); - fputc(0, f); - - for (int ii = 1; ii<(1 << pPal->bitDepth); ++ii) - { - uint32_t r = pPal->r[ii]; - uint32_t g = pPal->g[ii]; - uint32_t b = pPal->b[ii]; - - fputc(r, f); - fputc(g, f); - fputc(b, f); - } -} +void GifWritePalette(const GifPalette* pPal, FILE* f); // write the image header, LZW-compress and write out the image -void GifWriteLzwImage(FILE* f, uint8_t* image, uint32_t left, uint32_t top, uint32_t width, uint32_t height, uint32_t delay, GifPalette* pPal) -{ - // graphics control extension - fputc(0x21, f); - fputc(0xf9, f); - fputc(0x04, f); - fputc(0x05, f); // leave prev frame in place, this frame has transparency - fputc(delay & 0xff, f); - fputc((delay >> 8) & 0xff, f); - fputc(kGifTransIndex, f); // transparent color index - fputc(0, f); - - fputc(0x2c, f); // image descriptor block - - fputc(left & 0xff, f); // corner of image in canvas space - fputc((left >> 8) & 0xff, f); - fputc(top & 0xff, f); - fputc((top >> 8) & 0xff, f); - - fputc(width & 0xff, f); // width and height of image - fputc((width >> 8) & 0xff, f); - fputc(height & 0xff, f); - fputc((height >> 8) & 0xff, f); - - //fputc(0, f); // no local color table, no transparency - //fputc(0x80, f); // no local color table, but transparency - - fputc(0x80 + pPal->bitDepth - 1, f); // local color table present, 2 ^ bitDepth entries - GifWritePalette(pPal, f); - - const int minCodeSize = pPal->bitDepth; - const uint32_t clearCode = 1 << pPal->bitDepth; - - fputc(minCodeSize, f); // min code size 8 bits - - GifLzwNode* codetree = (GifLzwNode*)GIF_TEMP_MALLOC(sizeof(GifLzwNode) * 4096); - - memset(codetree, 0, sizeof(GifLzwNode) * 4096); - int32_t curCode = -1; - uint32_t codeSize = minCodeSize + 1; - uint32_t maxCode = clearCode + 1; - - GifBitStatus stat; - stat.byte = 0; - stat.bitIndex = 0; - stat.chunkIndex = 0; - - GifWriteCode(f, stat, clearCode, codeSize); // start with a fresh LZW dictionary - - for (uint32_t yy = 0; yy= (1ul << codeSize)) - { - // dictionary entry count has broken a size barrier, - // we need more bits for codes - codeSize++; - } - if (maxCode == 4095) - { - // the dictionary is full, clear it out and begin anew - GifWriteCode(f, stat, clearCode, codeSize); // clear tree - - memset(codetree, 0, sizeof(GifLzwNode) * 4096); - curCode = -1; - codeSize = minCodeSize + 1; - maxCode = clearCode + 1; - } - - curCode = nextValue; - } - } - } - - // compression footer - GifWriteCode(f, stat, curCode, codeSize); - GifWriteCode(f, stat, clearCode, codeSize); - GifWriteCode(f, stat, clearCode + 1, minCodeSize + 1); - - // write out the last partial chunk - while (stat.bitIndex) GifWriteBit(stat, 0); - if (stat.chunkIndex) GifWriteChunk(f, stat); - - fputc(0, f); // image block terminator - - GIF_TEMP_FREE(codetree); -} +void GifWriteLzwImage(FILE* f, uint8_t* image, uint32_t left, uint32_t top, uint32_t width, uint32_t height, uint32_t delay, GifPalette* pPal); struct GifWriter { @@ -725,101 +146,17 @@ struct GifWriter // Creates a gif file. // The input GIFWriter is assumed to be uninitialized. // The delay value is the time between frames in hundredths of a second - note that not all viewers pay much attention to this value. -bool GifBegin(GifWriter* writer, const char* filename, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth = 8, bool dither = false) -{ -#if _MSC_VER >= 1400 - writer->f = 0; - fopen_s(&writer->f, filename, "wb"); -#else - writer->f = fopen(filename, "wb"); -#endif - if (!writer->f) return false; - - writer->firstFrame = true; - - // allocate - writer->oldImage = (uint8_t*)GIF_MALLOC(width*height * 4); - - fputs("GIF89a", writer->f); - - // screen descriptor - fputc(width & 0xff, writer->f); - fputc((width >> 8) & 0xff, writer->f); - fputc(height & 0xff, writer->f); - fputc((height >> 8) & 0xff, writer->f); - - fputc(0xf0, writer->f); // there is an unsorted global color table of 2 entries - fputc(0, writer->f); // background color - fputc(0, writer->f); // pixels are square (we need to specify this because it's 1989) - - // now the "global" palette (really just a dummy palette) - // color 0: black - fputc(0, writer->f); - fputc(0, writer->f); - fputc(0, writer->f); - // color 1: also black - fputc(0, writer->f); - fputc(0, writer->f); - fputc(0, writer->f); - - if (delay != 0) - { - // animation header - fputc(0x21, writer->f); // extension - fputc(0xff, writer->f); // application specific - fputc(11, writer->f); // length 11 - fputs("NETSCAPE2.0", writer->f); // yes, really - fputc(3, writer->f); // 3 bytes of NETSCAPE2.0 data - - fputc(1, writer->f); // JUST BECAUSE - fputc(0, writer->f); // loop infinitely (byte 0) - fputc(0, writer->f); // loop infinitely (byte 1) - - fputc(0, writer->f); // block terminator - } - - return true; -} +bool GifBegin(GifWriter* writer, const char* filename, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth, bool dither); // Writes out a new frame to a GIF in progress. // The GIFWriter should have been created by GIFBegin. // AFAIK, it is legal to use different bit depths for different frames of an image - // this may be handy to save bits in animations that don't change much. -bool GifWriteFrame(GifWriter* writer, const uint8_t* image, uint32_t width, uint32_t height, uint32_t delay, uint8_t bitDepth = 8, bool dither = false) -{ - if (!writer->f) return false; - - const uint8_t* oldImage = writer->firstFrame ? NULL : writer->oldImage; - writer->firstFrame = false; - - GifPalette pal; - GifMakePalette((dither ? NULL : oldImage), image, width, height, bitDepth, dither, &pal); - - if (dither) - GifDitherImage(oldImage, image, writer->oldImage, width, height, &pal); - else - GifThresholdImage(oldImage, image, writer->oldImage, width, height, &pal); - - GifWriteLzwImage(writer->f, writer->oldImage, 0, 0, width, height, delay, &pal); - - return true; -} +bool GifWriteFrame(GifWriter* writer, const uint8_t* image, uint32_t width, uint32_t height, uint32_t delay, uint8_t bitDepth, bool dither); // Writes the EOF code, closes the file handle, and frees temp memory used by a GIF. // Many if not most viewers will still display a GIF properly if the EOF code is missing, // but it's still a good idea to write it out. -bool GifEnd(GifWriter* writer) -{ - if (!writer->f) return false; - - fputc(0x3b, writer->f); // end of file - fclose(writer->f); - GIF_FREE(writer->oldImage); - - writer->f = NULL; - writer->oldImage = NULL; - - return true; -} +bool GifEnd(GifWriter* writer); #endif \ No newline at end of file From 152e0aee177005ea0442635c8e1787f2bd4dc616 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 9 Nov 2016 15:31:12 -0800 Subject: [PATCH 22/50] I think I'm very close to a PR... --- interface/src/Application.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 0323d2fb1f..ac5d3b33f4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5430,7 +5430,7 @@ void Application::toggleLogDialog() { } #define SNAPSNOT_ANIMATED_WIDTH (900) -#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (20) +#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (50) #define SNAPSNOT_ANIMATED_FRAME_DELAY (100/SNAPSNOT_ANIMATED_FRAMERATE_FPS) #define SNAPSNOT_ANIMATED_DURATION_SECS (3) #define SNAPSNOT_ANIMATED_NUM_FRAMES (SNAPSNOT_ANIMATED_DURATION_SECS * SNAPSNOT_ANIMATED_FRAMERATE_FPS) @@ -5480,6 +5480,8 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR // If this is the last frame... if (qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) { + // Reset the current frame number + qApp->_currentAnimatedSnapshotFrame = 0; // Stop the snapshot QTimer animatedSnapshotTimer.stop(); // Write out the end of the GIF @@ -5493,23 +5495,20 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR // Get a screenshot from the display, then scale the screenshot down, // then convert it to the image format the GIF library needs, // then save all that to the QImage named "frame" - QImage* frame = new QImage(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - *frame = frame->scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + QImage frame(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); // If this is the first frame... if (qApp->_currentAnimatedSnapshotFrame == 0) { // Write out the header and beginning of the GIF file - GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); + GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); } // Write the frame to the gif - GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame->bits(), frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); + GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); // Increment the current snapshot frame count qApp->_currentAnimatedSnapshotFrame++; - - // Free the dynamic memory - delete frame; }); // Start the animatedSnapshotTimer QTimer From 0c4c97d3f9e41d3009075868eb265d5b91b62af1 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 9 Nov 2016 16:02:00 -0800 Subject: [PATCH 23/50] GIF speed is wrong. Need to save both still and animated. --- interface/src/Application.cpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ac5d3b33f4..5315a38a08 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5429,10 +5429,11 @@ void Application::toggleLogDialog() { } } -#define SNAPSNOT_ANIMATED_WIDTH (900) +#define SNAPSNOT_ANIMATED_WIDTH (720) #define SNAPSNOT_ANIMATED_FRAMERATE_FPS (50) -#define SNAPSNOT_ANIMATED_FRAME_DELAY (100/SNAPSNOT_ANIMATED_FRAMERATE_FPS) #define SNAPSNOT_ANIMATED_DURATION_SECS (3) + +#define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_FRAMERATE_FPS) #define SNAPSNOT_ANIMATED_NUM_FRAMES (SNAPSNOT_ANIMATED_DURATION_SECS * SNAPSNOT_ANIMATED_FRAMERATE_FPS) @@ -5464,6 +5465,8 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR // Reset the current animated snapshot frame _currentAnimatedSnapshotFrame = 0; + // Record the current timestamp - used to clamp GIF duration + quint64 usecTimestampSnapshot = usecTimestampNow(); // File path stuff -- lots of this is temporary // until I figure out how to save the .GIF to the same location as the still .JPG @@ -5478,7 +5481,8 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR // Connect the animatedSnapshotTimer QTimer to the lambda slot function connect(&animatedSnapshotTimer, &QTimer::timeout, [=] { // If this is the last frame... - if (qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) + if ((qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) || + ((usecTimestampNow() - usecTimestampSnapshot) >= (SNAPSNOT_ANIMATED_DURATION_SECS * USECS_PER_SECOND))) { // Reset the current frame number qApp->_currentAnimatedSnapshotFrame = 0; @@ -5502,17 +5506,19 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR if (qApp->_currentAnimatedSnapshotFrame == 0) { // Write out the header and beginning of the GIF file - GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); + GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); } // Write the frame to the gif - GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); + GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); // Increment the current snapshot frame count qApp->_currentAnimatedSnapshotFrame++; }); - // Start the animatedSnapshotTimer QTimer - animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY * 10); + // Start the animatedSnapshotTimer QTimer - argument for this is in milliseconds + // I'm pretty sure the timer slot is getting called more often than its execution finishes. + // This makes the GIF longer than it's supposed to be, as the duration between frames is longer. + animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); } }); } From d91158f07cfdd85433bc8edaf762e769c3029cb6 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 10 Nov 2016 10:20:54 -0800 Subject: [PATCH 24/50] Small changes, still trying to figure out judder --- interface/src/Application.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5315a38a08..a40910cdc9 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5430,7 +5430,7 @@ void Application::toggleLogDialog() { } #define SNAPSNOT_ANIMATED_WIDTH (720) -#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (50) +#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (30) #define SNAPSNOT_ANIMATED_DURATION_SECS (3) #define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_FRAMERATE_FPS) @@ -5478,6 +5478,9 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR cstr = new char[fname.size() + 1]; // Create a new character array to hold the .GIF file location strcpy(cstr, fname.c_str()); // Copy the string into a character array + // Ensure the snapshot timer is Precise (attempted millisecond precision) + animatedSnapshotTimer.setTimerType(Qt::PreciseTimer); + // Connect the animatedSnapshotTimer QTimer to the lambda slot function connect(&animatedSnapshotTimer, &QTimer::timeout, [=] { // If this is the last frame... @@ -5509,6 +5512,8 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); } + qDebug() << "Frame time: " << usecTimestampNow(); + // Write the frame to the gif GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); // Increment the current snapshot frame count From 1158ae8b3d5555bf2503e687234e4f34dd719c49 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 10 Nov 2016 14:22:03 -0800 Subject: [PATCH 25/50] Much more stability. --- interface/src/Application.cpp | 45 ++++++++++++++++------------------- interface/src/Application.h | 1 + 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a40910cdc9..501eb332be 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -175,7 +175,6 @@ using namespace std; static QTimer locationUpdateTimer; static QTimer identityPacketTimer; static QTimer pingTimer; -static QTimer animatedSnapshotTimer; static const int MAX_CONCURRENT_RESOURCE_DOWNLOADS = 16; @@ -5429,8 +5428,12 @@ void Application::toggleLogDialog() { } } -#define SNAPSNOT_ANIMATED_WIDTH (720) -#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (30) +// If this is "too big" (which depends on PC spec): +// The frame will take too long to pack, the timer slot will +// not execute properly, and the GIF will appear sped-up. +// This is unacceptable and is probably a blocker for release. +#define SNAPSNOT_ANIMATED_WIDTH (500) +#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (50) // This value should divide evenly into 100 #define SNAPSNOT_ANIMATED_DURATION_SECS (3) #define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_FRAMERATE_FPS) @@ -5465,8 +5468,6 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR // Reset the current animated snapshot frame _currentAnimatedSnapshotFrame = 0; - // Record the current timestamp - used to clamp GIF duration - quint64 usecTimestampSnapshot = usecTimestampNow(); // File path stuff -- lots of this is temporary // until I figure out how to save the .GIF to the same location as the still .JPG @@ -5482,22 +5483,7 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR animatedSnapshotTimer.setTimerType(Qt::PreciseTimer); // Connect the animatedSnapshotTimer QTimer to the lambda slot function - connect(&animatedSnapshotTimer, &QTimer::timeout, [=] { - // If this is the last frame... - if ((qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) || - ((usecTimestampNow() - usecTimestampSnapshot) >= (SNAPSNOT_ANIMATED_DURATION_SECS * USECS_PER_SECOND))) - { - // Reset the current frame number - qApp->_currentAnimatedSnapshotFrame = 0; - // Stop the snapshot QTimer - animatedSnapshotTimer.stop(); - // Write out the end of the GIF - GifEnd(&(qApp->_animatedSnapshotGifWriter)); - // Notify the Window Scripting Interface that the snapshot was taken - emit DependencyManager::get()->snapshotTaken(path, false); - return; - } - + connect(&(qApp->animatedSnapshotTimer), &QTimer::timeout, [=] { // Get a screenshot from the display, then scale the screenshot down, // then convert it to the image format the GIF library needs, @@ -5512,17 +5498,26 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); } - qDebug() << "Frame time: " << usecTimestampNow(); - // Write the frame to the gif GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); // Increment the current snapshot frame count qApp->_currentAnimatedSnapshotFrame++; + + // If that was the last frame... + if (qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) + { + // Reset the current frame number + qApp->_currentAnimatedSnapshotFrame = 0; + // Write out the end of the GIF + GifEnd(&(qApp->_animatedSnapshotGifWriter)); + // Notify the Window Scripting Interface that the snapshot was taken + emit DependencyManager::get()->snapshotTaken(path, false); + // Stop the snapshot QTimer + qApp->animatedSnapshotTimer.stop(); + } }); // Start the animatedSnapshotTimer QTimer - argument for this is in milliseconds - // I'm pretty sure the timer slot is getting called more often than its execution finishes. - // This makes the GIF longer than it's supposed to be, as the duration between frames is longer. animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); } }); diff --git a/interface/src/Application.h b/interface/src/Application.h index b88728146f..58f2d263cc 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -611,6 +611,7 @@ private: gpu::TexturePointer _defaultSkyboxTexture; gpu::TexturePointer _defaultSkyboxAmbientTexture; + QTimer animatedSnapshotTimer; GifWriter _animatedSnapshotGifWriter; uint8_t _currentAnimatedSnapshotFrame { 0 }; }; From f726ea546f4a1c9242b62768f27a6e320b0aed45 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 10 Nov 2016 14:59:13 -0800 Subject: [PATCH 26/50] code cleanup; simultaneous gif and still capture --- interface/src/Application.cpp | 119 ++++++++---------- interface/src/Application.h | 3 +- .../scripting/WindowScriptingInterface.cpp | 8 +- .../src/scripting/WindowScriptingInterface.h | 3 +- scripts/system/snapshot.js | 2 +- 5 files changed, 58 insertions(+), 77 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 501eb332be..1226e62944 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2511,7 +2511,7 @@ void Application::keyPressEvent(QKeyEvent* event) { } else if (isOption && !isShifted && !isMeta) { Menu::getInstance()->triggerOption(MenuOption::ScriptEditor); } else if (!isOption && !isShifted && isMeta) { - takeSnapshot(true, "still"); + takeSnapshot(true); } break; @@ -5440,86 +5440,71 @@ void Application::toggleLogDialog() { #define SNAPSNOT_ANIMATED_NUM_FRAMES (SNAPSNOT_ANIMATED_DURATION_SECS * SNAPSNOT_ANIMATED_FRAMERATE_FPS) -void Application::takeSnapshot(bool notify, const QString& format, float aspectRatio) { - postLambdaEvent([notify, format, aspectRatio, this] { +void Application::takeSnapshot(bool notify, float aspectRatio) { + postLambdaEvent([notify, aspectRatio, this] { QMediaPlayer* player = new QMediaPlayer(); QFileInfo inf = QFileInfo(PathUtils::resourcesPath() + "sounds/snap.wav"); player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); - // If this is a still snapshot... - if (!format.compare("still")) + // Get a screenshot and save it + QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + + // If we're in the middle of capturing a GIF... + if (_currentAnimatedSnapshotFrame != 0) { - // Get a screenshot and save it - QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); // Notify the window scripting interface that we've taken a Snapshot emit DependencyManager::get()->snapshotTaken(path, notify); + // Protect against clobbering it and return immediately. + // (Perhaps with a "snapshot failed" message? + return; } - // If this is an animated snapshot (GIF)... - else if (!format.compare("animated")) - { - // If we're in the middle of capturing a GIF... - if (_currentAnimatedSnapshotFrame != 0) + + // Reset the current animated snapshot frame + _currentAnimatedSnapshotFrame = 0; + + // Change the extension of the future GIF saved snapshot file to "gif" + _animatedSnapshotPath = path.replace("jpg", "gif"); + + // Ensure the snapshot timer is Precise (attempted millisecond precision) + animatedSnapshotTimer.setTimerType(Qt::PreciseTimer); + + // Connect the animatedSnapshotTimer QTimer to the lambda slot function + connect(&(qApp->animatedSnapshotTimer), &QTimer::timeout, [=] { + // Get a screenshot from the display, then scale the screenshot down, + // then convert it to the image format the GIF library needs, + // then save all that to the QImage named "frame" + QImage frame(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + + // If this is the first frame... + if (qApp->_currentAnimatedSnapshotFrame == 0) { - // Protect against clobbering it and return immediately. - // (Perhaps with a "snapshot failed" message? - return; + // Write out the header and beginning of the GIF file + GifBegin(&(qApp->_animatedSnapshotGifWriter), qPrintable(qApp->_animatedSnapshotPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10, 8, false); } - // Reset the current animated snapshot frame - _currentAnimatedSnapshotFrame = 0; + // Write the frame to the gif + GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); + // Increment the current snapshot frame count + qApp->_currentAnimatedSnapshotFrame++; - // File path stuff -- lots of this is temporary - // until I figure out how to save the .GIF to the same location as the still .JPG - char* cstr; - QString path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); // Get the desktop - path.append(QDir::separator()); // Add the dir separator to the desktop location - path.append("test.gif"); // Add "test.gif" to the path - string fname = path.toStdString(); // Turn the QString into a regular string - cstr = new char[fname.size() + 1]; // Create a new character array to hold the .GIF file location - strcpy(cstr, fname.c_str()); // Copy the string into a character array + // If that was the last frame... + if (qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) + { + // Reset the current frame number + qApp->_currentAnimatedSnapshotFrame = 0; + // Write out the end of the GIF + GifEnd(&(qApp->_animatedSnapshotGifWriter)); + // Notify the Window Scripting Interface that the snapshot was taken + emit DependencyManager::get()->snapshotTaken(qApp->_animatedSnapshotPath, false); + // Stop the snapshot QTimer + qApp->animatedSnapshotTimer.stop(); + } + }); - // Ensure the snapshot timer is Precise (attempted millisecond precision) - animatedSnapshotTimer.setTimerType(Qt::PreciseTimer); - - // Connect the animatedSnapshotTimer QTimer to the lambda slot function - connect(&(qApp->animatedSnapshotTimer), &QTimer::timeout, [=] { - - // Get a screenshot from the display, then scale the screenshot down, - // then convert it to the image format the GIF library needs, - // then save all that to the QImage named "frame" - QImage frame(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); - - // If this is the first frame... - if (qApp->_currentAnimatedSnapshotFrame == 0) - { - // Write out the header and beginning of the GIF file - GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); - } - - // Write the frame to the gif - GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); - // Increment the current snapshot frame count - qApp->_currentAnimatedSnapshotFrame++; - - // If that was the last frame... - if (qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) - { - // Reset the current frame number - qApp->_currentAnimatedSnapshotFrame = 0; - // Write out the end of the GIF - GifEnd(&(qApp->_animatedSnapshotGifWriter)); - // Notify the Window Scripting Interface that the snapshot was taken - emit DependencyManager::get()->snapshotTaken(path, false); - // Stop the snapshot QTimer - qApp->animatedSnapshotTimer.stop(); - } - }); - - // Start the animatedSnapshotTimer QTimer - argument for this is in milliseconds - animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); - } + // Start the animatedSnapshotTimer QTimer - argument for this is in milliseconds + animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); }); } void Application::shareSnapshot(const QString& path) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 58f2d263cc..2754fdef52 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -267,7 +267,7 @@ public: float getAvatarSimrate() const { return _avatarSimCounter.rate(); } float getAverageSimsPerSecond() const { return _simCounter.rate(); } - void takeSnapshot(bool notify, const QString& format = "still", float aspectRatio = 0.0f); + void takeSnapshot(bool notify, float aspectRatio = 0.0f); void shareSnapshot(const QString& filename); model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; } @@ -614,6 +614,7 @@ private: QTimer animatedSnapshotTimer; GifWriter _animatedSnapshotGifWriter; uint8_t _currentAnimatedSnapshotFrame { 0 }; + QString _animatedSnapshotPath; }; diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 0abdddf0d8..0f9dd698fd 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -199,12 +199,8 @@ void WindowScriptingInterface::copyToClipboard(const QString& text) { QApplication::clipboard()->setText(text); } -void WindowScriptingInterface::takeSnapshotStill(bool notify, float aspectRatio) { - qApp->takeSnapshot(notify, "still", aspectRatio); -} - -void WindowScriptingInterface::takeSnapshotAnimated(bool notify, float aspectRatio) { - qApp->takeSnapshot(notify, "animated", aspectRatio); +void WindowScriptingInterface::takeSnapshot(bool notify, float aspectRatio) { + qApp->takeSnapshot(notify, aspectRatio); } void WindowScriptingInterface::shareSnapshot(const QString& path) { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 6c06b4d60e..f4a89ae221 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -52,8 +52,7 @@ public slots: QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void showAssetServer(const QString& upload = ""); void copyToClipboard(const QString& text); - void takeSnapshotStill(bool notify = true, float aspectRatio = 0.0f); - void takeSnapshotAnimated(bool notify = true, float aspectRatio = 0.0f); + void takeSnapshot(bool notify = true, float aspectRatio = 0.0f); void shareSnapshot(const QString& path); bool isPhysicsEnabled(); diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 8b71630c96..5eebadd02f 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -120,7 +120,7 @@ function onClicked() { // take snapshot (with no notification) Script.setTimeout(function () { - Window.takeSnapshotAnimated(false, 1.91); + Window.takeSnapshot(false, 1.91); }, SNAPSHOT_DELAY); } From fc21cae904fcfe9976a6de31bc1ec6e812c50bf2 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 11 Nov 2016 13:16:01 -0800 Subject: [PATCH 27/50] More stable than ever! Uses external GifCreator lib. Still frame timing issues. --- cmake/externals/GifCreator/CMakeLists.txt | 20 + cmake/modules/FindGifCreator.cmake | 26 + interface/CMakeLists.txt | 4 + interface/src/Application.cpp | 4 +- interface/src/Application.h | 4 +- interface/src/ui/Gif.cpp | 755 ---------------------- interface/src/ui/Gif.h | 162 ----- 7 files changed, 54 insertions(+), 921 deletions(-) create mode 100644 cmake/externals/GifCreator/CMakeLists.txt create mode 100644 cmake/modules/FindGifCreator.cmake delete mode 100644 interface/src/ui/Gif.cpp delete mode 100644 interface/src/ui/Gif.h diff --git a/cmake/externals/GifCreator/CMakeLists.txt b/cmake/externals/GifCreator/CMakeLists.txt new file mode 100644 index 0000000000..f3f4e6d2ad --- /dev/null +++ b/cmake/externals/GifCreator/CMakeLists.txt @@ -0,0 +1,20 @@ +set(EXTERNAL_NAME GifCreator) + +include(ExternalProject) +ExternalProject_Add( + ${EXTERNAL_NAME} + URL https://hifi-public.s3.amazonaws.com/dependencies/GifCreator.zip + URL_MD5 8ac8ef5196f47c658dce784df5ecdb70 + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + LOG_DOWNLOAD 1 +) + +# Hide this external target (for ide users) +set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") + +ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) + +string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) +set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/src/${EXTERNAL_NAME_UPPER} CACHE PATH "List of GifCreator include directories") \ No newline at end of file diff --git a/cmake/modules/FindGifCreator.cmake b/cmake/modules/FindGifCreator.cmake new file mode 100644 index 0000000000..88428cb833 --- /dev/null +++ b/cmake/modules/FindGifCreator.cmake @@ -0,0 +1,26 @@ +# +# FindGifCreator.cmake +# +# Try to find GifCreator include path. +# Once done this will define +# +# GIFCREATOR_INCLUDE_DIRS +# +# Created on 11/10/2016 by Zach Fox +# Copyright 2016 High Fidelity, Inc. +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# + +# setup hints for GifCreator search +include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") +hifi_library_search_hints("GifCreator") + +# locate header +find_path(GIFCREATOR_INCLUDE_DIRS "GifCreator/GifCreator.h" HINTS ${GIFCREATOR_SEARCH_DIRS}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(GIFCREATOR DEFAULT_MSG GIFCREATOR_INCLUDE_DIRS) + +mark_as_advanced(GIFCREATOR_INCLUDE_DIRS GIFCREATOR_SEARCH_DIRS) \ No newline at end of file diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 131c4ee509..56e83a3171 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -351,3 +351,7 @@ if (ANDROID) qt_create_apk() endif () + +add_dependency_external_projects(GifCreator) +find_package(GIFCREATOR REQUIRED) +target_include_directories(${TARGET_NAME} PUBLIC ${GIFCREATOR_INCLUDE_DIRS}) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1226e62944..ba442ede9c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5432,7 +5432,7 @@ void Application::toggleLogDialog() { // The frame will take too long to pack, the timer slot will // not execute properly, and the GIF will appear sped-up. // This is unacceptable and is probably a blocker for release. -#define SNAPSNOT_ANIMATED_WIDTH (500) +#define SNAPSNOT_ANIMATED_WIDTH (300) #define SNAPSNOT_ANIMATED_FRAMERATE_FPS (50) // This value should divide evenly into 100 #define SNAPSNOT_ANIMATED_DURATION_SECS (3) @@ -5490,7 +5490,7 @@ void Application::takeSnapshot(bool notify, float aspectRatio) { qApp->_currentAnimatedSnapshotFrame++; // If that was the last frame... - if (qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) + if (qApp->_currentAnimatedSnapshotFrame >= SNAPSNOT_ANIMATED_NUM_FRAMES) { // Reset the current frame number qApp->_currentAnimatedSnapshotFrame = 0; diff --git a/interface/src/Application.h b/interface/src/Application.h index 2754fdef52..963b4c8072 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -60,7 +61,6 @@ #include "scripting/DialogsManagerScriptingInterface.h" #include "ui/ApplicationOverlay.h" #include "ui/BandwidthDialog.h" -#include "ui/Gif.h" #include "ui/LodToolsDialog.h" #include "ui/LogDialog.h" #include "ui/OctreeStatsDialog.h" @@ -613,7 +613,7 @@ private: QTimer animatedSnapshotTimer; GifWriter _animatedSnapshotGifWriter; - uint8_t _currentAnimatedSnapshotFrame { 0 }; + uint32_t _currentAnimatedSnapshotFrame { 0 }; QString _animatedSnapshotPath; }; diff --git a/interface/src/ui/Gif.cpp b/interface/src/ui/Gif.cpp deleted file mode 100644 index 87716b0fb4..0000000000 --- a/interface/src/ui/Gif.cpp +++ /dev/null @@ -1,755 +0,0 @@ -// -// gif.c -// by Charlie Tangora -// Public domain. -// Email me : ctangora -at- gmail -dot- com -// -// This file offers a simple, very limited way to create animated GIFs directly in code. -// -// Those looking for particular cleverness are likely to be disappointed; it's pretty -// much a straight-ahead implementation of the GIF format with optional Floyd-Steinberg -// dithering. (It does at least use delta encoding - only the changed portions of each -// frame are saved.) -// -// So resulting files are often quite large. The hope is that it will be handy nonetheless -// as a quick and easily-integrated way for programs to spit out animations. -// -// Only RGBA8 is currently supported as an input format. (The alpha is ignored.) -// -// USAGE: -// Create a GifWriter struct. Pass it to GifBegin() to initialize and write the header. -// Pass subsequent frames to GifWriteFrame(). -// Finally, call GifEnd() to close the file handle and free memory. -// - -#ifndef gif_c -#define gif_c - -#include "Gif.h" - - -int GifIMax(int l, int r) { return l>r ? l : r; } -int GifIMin(int l, int r) { return l(1 << pPal->bitDepth) - 1) - { - int ind = treeRoot - (1 << pPal->bitDepth); - if (ind == kGifTransIndex) return; - - // check whether this color is better than the current winner - int r_err = r - ((int32_t)pPal->r[ind]); - int g_err = g - ((int32_t)pPal->g[ind]); - int b_err = b - ((int32_t)pPal->b[ind]); - int diff = GifIAbs(r_err) + GifIAbs(g_err) + GifIAbs(b_err); - - if (diff < bestDiff) - { - bestInd = ind; - bestDiff = diff; - } - - return; - } - - // take the appropriate color (r, g, or b) for this node of the k-d tree - int comps[3]; comps[0] = r; comps[1] = g; comps[2] = b; - int splitComp = comps[pPal->treeSplitElt[treeRoot]]; - - int splitPos = pPal->treeSplit[treeRoot]; - if (splitPos > splitComp) - { - // check the left subtree - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); - if (bestDiff > splitPos - splitComp) - { - // cannot prove there's not a better value in the right subtree, check that too - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); - } - } - else - { - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); - if (bestDiff > splitComp - splitPos) - { - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); - } - } -} - -void GifSwapPixels(uint8_t* image, int pixA, int pixB) -{ - uint8_t rA = image[pixA * 4]; - uint8_t gA = image[pixA * 4 + 1]; - uint8_t bA = image[pixA * 4 + 2]; - uint8_t aA = image[pixA * 4 + 3]; - - uint8_t rB = image[pixB * 4]; - uint8_t gB = image[pixB * 4 + 1]; - uint8_t bB = image[pixB * 4 + 2]; - uint8_t aB = image[pixA * 4 + 3]; - - image[pixA * 4] = rB; - image[pixA * 4 + 1] = gB; - image[pixA * 4 + 2] = bB; - image[pixA * 4 + 3] = aB; - - image[pixB * 4] = rA; - image[pixB * 4 + 1] = gA; - image[pixB * 4 + 2] = bA; - image[pixB * 4 + 3] = aA; -} - -// just the partition operation from quicksort -int GifPartition(uint8_t* image, const int left, const int right, const int elt, int pivotIndex) -{ - const int pivotValue = image[(pivotIndex)* 4 + elt]; - GifSwapPixels(image, pivotIndex, right - 1); - int storeIndex = left; - bool split = 0; - for (int ii = left; ii neededCenter) - GifPartitionByMedian(image, left, pivotIndex, com, neededCenter); - - if (pivotIndex < neededCenter) - GifPartitionByMedian(image, pivotIndex + 1, right, com, neededCenter); - } -} - -// Builds a palette by creating a balanced k-d tree of all pixels in the image -void GifSplitPalette(uint8_t* image, int numPixels, int firstElt, int lastElt, int splitElt, int splitDist, int treeNode, bool buildForDither, GifPalette* pal) -{ - if (lastElt <= firstElt || numPixels == 0) - return; - - // base case, bottom of the tree - if (lastElt == firstElt + 1) - { - if (buildForDither) - { - // Dithering needs at least one color as dark as anything - // in the image and at least one brightest color - - // otherwise it builds up error and produces strange artifacts - if (firstElt == 1) - { - // special case: the darkest color in the image - uint32_t r = 255, g = 255, b = 255; - for (int ii = 0; iir[firstElt] = r; - pal->g[firstElt] = g; - pal->b[firstElt] = b; - - return; - } - - if (firstElt == (1 << pal->bitDepth) - 1) - { - // special case: the lightest color in the image - uint32_t r = 0, g = 0, b = 0; - for (int ii = 0; iir[firstElt] = r; - pal->g[firstElt] = g; - pal->b[firstElt] = b; - - return; - } - } - - // otherwise, take the average of all colors in this subcube - uint64_t r = 0, g = 0, b = 0; - for (int ii = 0; iir[firstElt] = (uint8_t)r; - pal->g[firstElt] = (uint8_t)g; - pal->b[firstElt] = (uint8_t)b; - - return; - } - - // Find the axis with the largest range - int minR = 255, maxR = 0; - int minG = 255, maxG = 0; - int minB = 255, maxB = 0; - for (int ii = 0; ii maxR) maxR = r; - if (r < minR) minR = r; - - if (g > maxG) maxG = g; - if (g < minG) minG = g; - - if (b > maxB) maxB = b; - if (b < minB) minB = b; - } - - int rRange = maxR - minR; - int gRange = maxG - minG; - int bRange = maxB - minB; - - // and split along that axis. (incidentally, this means this isn't a "proper" k-d tree but I don't know what else to call it) - int splitCom = 1; - if (bRange > gRange) splitCom = 2; - if (rRange > bRange && rRange > gRange) splitCom = 0; - - int subPixelsA = numPixels * (splitElt - firstElt) / (lastElt - firstElt); - int subPixelsB = numPixels - subPixelsA; - - GifPartitionByMedian(image, 0, numPixels, splitCom, subPixelsA); - - pal->treeSplitElt[treeNode] = splitCom; - pal->treeSplit[treeNode] = image[subPixelsA * 4 + splitCom]; - - GifSplitPalette(image, subPixelsA, firstElt, splitElt, splitElt - splitDist, splitDist / 2, treeNode * 2, buildForDither, pal); - GifSplitPalette(image + subPixelsA * 4, subPixelsB, splitElt, lastElt, splitElt + splitDist, splitDist / 2, treeNode * 2 + 1, buildForDither, pal); -} - -// Finds all pixels that have changed from the previous image and -// moves them to the fromt of th buffer. -// This allows us to build a palette optimized for the colors of the -// changed pixels only. -int GifPickChangedPixels(const uint8_t* lastFrame, uint8_t* frame, int numPixels) -{ - int numChanged = 0; - uint8_t* writeIter = frame; - - for (int ii = 0; iibitDepth = bitDepth; - - // SplitPalette is destructive (it sorts the pixels by color) so - // we must create a copy of the image for it to destroy - int imageSize = width*height * 4 * sizeof(uint8_t); - uint8_t* destroyableImage = (uint8_t*)GIF_TEMP_MALLOC(imageSize); - memcpy(destroyableImage, nextFrame, imageSize); - - int numPixels = width*height; - if (lastFrame) - numPixels = GifPickChangedPixels(lastFrame, destroyableImage, numPixels); - - const int lastElt = 1 << bitDepth; - const int splitElt = lastElt / 2; - const int splitDist = splitElt / 2; - - GifSplitPalette(destroyableImage, numPixels, 1, lastElt, splitElt, splitDist, 1, buildForDither, pPal); - - GIF_TEMP_FREE(destroyableImage); - - // add the bottom node for the transparency index - pPal->treeSplit[1i64 << (bitDepth - 1)] = 0; - pPal->treeSplitElt[1i64 << (bitDepth - 1)] = 0; - - pPal->r[0] = pPal->g[0] = pPal->b[0] = 0; -} - -// Implements Floyd-Steinberg dithering, writes palette value to alpha -void GifDitherImage(const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal) -{ - int numPixels = width*height; - - // quantPixels initially holds color*256 for all pixels - // The extra 8 bits of precision allow for sub-single-color error values - // to be propagated - int32_t* quantPixels = (int32_t*)GIF_TEMP_MALLOC(sizeof(int32_t)*numPixels * 4); - - for (int ii = 0; iir[bestInd]) * 256; - int32_t g_err = nextPix[1] - int32_t(pPal->g[bestInd]) * 256; - int32_t b_err = nextPix[2] - int32_t(pPal->b[bestInd]) * 256; - - nextPix[0] = pPal->r[bestInd]; - nextPix[1] = pPal->g[bestInd]; - nextPix[2] = pPal->b[bestInd]; - nextPix[3] = bestInd; - - // Propagate the error to the four adjacent locations - // that we haven't touched yet - int quantloc_7 = (yy*width + xx + 1); - int quantloc_3 = (yy*width + width + xx - 1); - int quantloc_5 = (yy*width + width + xx); - int quantloc_1 = (yy*width + width + xx + 1); - - if (quantloc_7 < numPixels) - { - int32_t* pix7 = quantPixels + 4 * quantloc_7; - pix7[0] += GifIMax(-pix7[0], r_err * 7 / 16); - pix7[1] += GifIMax(-pix7[1], g_err * 7 / 16); - pix7[2] += GifIMax(-pix7[2], b_err * 7 / 16); - } - - if (quantloc_3 < numPixels) - { - int32_t* pix3 = quantPixels + 4 * quantloc_3; - pix3[0] += GifIMax(-pix3[0], r_err * 3 / 16); - pix3[1] += GifIMax(-pix3[1], g_err * 3 / 16); - pix3[2] += GifIMax(-pix3[2], b_err * 3 / 16); - } - - if (quantloc_5 < numPixels) - { - int32_t* pix5 = quantPixels + 4 * quantloc_5; - pix5[0] += GifIMax(-pix5[0], r_err * 5 / 16); - pix5[1] += GifIMax(-pix5[1], g_err * 5 / 16); - pix5[2] += GifIMax(-pix5[2], b_err * 5 / 16); - } - - if (quantloc_1 < numPixels) - { - int32_t* pix1 = quantPixels + 4 * quantloc_1; - pix1[0] += GifIMax(-pix1[0], r_err / 16); - pix1[1] += GifIMax(-pix1[1], g_err / 16); - pix1[2] += GifIMax(-pix1[2], b_err / 16); - } - } - } - - // Copy the palettized result to the output buffer - for (int ii = 0; iir[bestInd]; - outFrame[1] = pPal->g[bestInd]; - outFrame[2] = pPal->b[bestInd]; - outFrame[3] = bestInd; - } - - if (lastFrame) lastFrame += 4; - outFrame += 4; - nextFrame += 4; - } -} - -// insert a single bit -void GifWriteBit(GifBitStatus& stat, uint32_t bit) -{ - bit = bit & 1; - bit = bit << stat.bitIndex; - stat.byte |= bit; - - ++stat.bitIndex; - if (stat.bitIndex > 7) - { - // move the newly-finished byte to the chunk buffer - stat.chunk[stat.chunkIndex++] = stat.byte; - // and start a new byte - stat.bitIndex = 0; - stat.byte = 0; - } -} - -// write all bytes so far to the file -void GifWriteChunk(FILE* f, GifBitStatus& stat) -{ - fputc(stat.chunkIndex, f); - fwrite(stat.chunk, 1, stat.chunkIndex, f); - - stat.bitIndex = 0; - stat.byte = 0; - stat.chunkIndex = 0; -} - -void GifWriteCode(FILE* f, GifBitStatus& stat, uint32_t code, uint32_t length) -{ - for (uint32_t ii = 0; ii> 1; - - if (stat.chunkIndex == 255) - { - GifWriteChunk(f, stat); - } - } -} - -// write a 256-color (8-bit) image palette to the file -void GifWritePalette(const GifPalette* pPal, FILE* f) -{ - fputc(0, f); // first color: transparency - fputc(0, f); - fputc(0, f); - - for (int ii = 1; ii<(1 << pPal->bitDepth); ++ii) - { - uint32_t r = pPal->r[ii]; - uint32_t g = pPal->g[ii]; - uint32_t b = pPal->b[ii]; - - fputc(r, f); - fputc(g, f); - fputc(b, f); - } -} - -// write the image header, LZW-compress and write out the image -void GifWriteLzwImage(FILE* f, uint8_t* image, uint32_t left, uint32_t top, uint32_t width, uint32_t height, uint32_t delay, GifPalette* pPal) -{ - // graphics control extension - fputc(0x21, f); - fputc(0xf9, f); - fputc(0x04, f); - fputc(0x05, f); // leave prev frame in place, this frame has transparency - fputc(delay & 0xff, f); - fputc((delay >> 8) & 0xff, f); - fputc(kGifTransIndex, f); // transparent color index - fputc(0, f); - - fputc(0x2c, f); // image descriptor block - - fputc(left & 0xff, f); // corner of image in canvas space - fputc((left >> 8) & 0xff, f); - fputc(top & 0xff, f); - fputc((top >> 8) & 0xff, f); - - fputc(width & 0xff, f); // width and height of image - fputc((width >> 8) & 0xff, f); - fputc(height & 0xff, f); - fputc((height >> 8) & 0xff, f); - - //fputc(0, f); // no local color table, no transparency - //fputc(0x80, f); // no local color table, but transparency - - fputc(0x80 + pPal->bitDepth - 1, f); // local color table present, 2 ^ bitDepth entries - GifWritePalette(pPal, f); - - const int minCodeSize = pPal->bitDepth; - const uint32_t clearCode = 1 << pPal->bitDepth; - - fputc(minCodeSize, f); // min code size 8 bits - - GifLzwNode* codetree = (GifLzwNode*)GIF_TEMP_MALLOC(sizeof(GifLzwNode) * 4096); - - memset(codetree, 0, sizeof(GifLzwNode) * 4096); - int32_t curCode = -1; - uint32_t codeSize = minCodeSize + 1; - uint32_t maxCode = clearCode + 1; - - GifBitStatus stat; - stat.byte = 0; - stat.bitIndex = 0; - stat.chunkIndex = 0; - - GifWriteCode(f, stat, clearCode, codeSize); // start with a fresh LZW dictionary - - for (uint32_t yy = 0; yy= (1ul << codeSize)) - { - // dictionary entry count has broken a size barrier, - // we need more bits for codes - codeSize++; - } - if (maxCode == 4095) - { - // the dictionary is full, clear it out and begin anew - GifWriteCode(f, stat, clearCode, codeSize); // clear tree - - memset(codetree, 0, sizeof(GifLzwNode) * 4096); - curCode = -1; - codeSize = minCodeSize + 1; - maxCode = clearCode + 1; - } - - curCode = nextValue; - } - } - } - - // compression footer - GifWriteCode(f, stat, curCode, codeSize); - GifWriteCode(f, stat, clearCode, codeSize); - GifWriteCode(f, stat, clearCode + 1, minCodeSize + 1); - - // write out the last partial chunk - while (stat.bitIndex) GifWriteBit(stat, 0); - if (stat.chunkIndex) GifWriteChunk(f, stat); - - fputc(0, f); // image block terminator - - GIF_TEMP_FREE(codetree); -} - -// Creates a gif file. -// The input GIFWriter is assumed to be uninitialized. -// The delay value is the time between frames in hundredths of a second - note that not all viewers pay much attention to this value. -bool GifBegin(GifWriter* writer, const char* filename, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth, bool dither) -{ -#if _MSC_VER >= 1400 - writer->f = 0; - fopen_s(&writer->f, filename, "wb"); -#else - writer->f = fopen(filename, "wb"); -#endif - if (!writer->f) return false; - - writer->firstFrame = true; - - // allocate - writer->oldImage = (uint8_t*)GIF_MALLOC(width*height * 4); - - fputs("GIF89a", writer->f); - - // screen descriptor - fputc(width & 0xff, writer->f); - fputc((width >> 8) & 0xff, writer->f); - fputc(height & 0xff, writer->f); - fputc((height >> 8) & 0xff, writer->f); - - fputc(0xf0, writer->f); // there is an unsorted global color table of 2 entries - fputc(0, writer->f); // background color - fputc(0, writer->f); // pixels are square (we need to specify this because it's 1989) - - // now the "global" palette (really just a dummy palette) - // color 0: black - fputc(0, writer->f); - fputc(0, writer->f); - fputc(0, writer->f); - // color 1: also black - fputc(0, writer->f); - fputc(0, writer->f); - fputc(0, writer->f); - - if (delay != 0) - { - // animation header - fputc(0x21, writer->f); // extension - fputc(0xff, writer->f); // application specific - fputc(11, writer->f); // length 11 - fputs("NETSCAPE2.0", writer->f); // yes, really - fputc(3, writer->f); // 3 bytes of NETSCAPE2.0 data - - fputc(1, writer->f); // JUST BECAUSE - fputc(0, writer->f); // loop infinitely (byte 0) - fputc(0, writer->f); // loop infinitely (byte 1) - - fputc(0, writer->f); // block terminator - } - - return true; -} - -// Writes out a new frame to a GIF in progress. -// The GIFWriter should have been created by GIFBegin. -// AFAIK, it is legal to use different bit depths for different frames of an image - -// this may be handy to save bits in animations that don't change much. -bool GifWriteFrame(GifWriter* writer, const uint8_t* image, uint32_t width, uint32_t height, uint32_t delay, uint8_t bitDepth, bool dither) -{ - if (!writer->f) return false; - - const uint8_t* oldImage = writer->firstFrame ? NULL : writer->oldImage; - writer->firstFrame = false; - - GifPalette pal; - GifMakePalette((dither ? NULL : oldImage), image, width, height, bitDepth, dither, &pal); - - if (dither) - GifDitherImage(oldImage, image, writer->oldImage, width, height, &pal); - else - GifThresholdImage(oldImage, image, writer->oldImage, width, height, &pal); - - GifWriteLzwImage(writer->f, writer->oldImage, 0, 0, width, height, delay, &pal); - - return true; -} - -// Writes the EOF code, closes the file handle, and frees temp memory used by a GIF. -// Many if not most viewers will still display a GIF properly if the EOF code is missing, -// but it's still a good idea to write it out. -bool GifEnd(GifWriter* writer) -{ - if (!writer->f) return false; - - fputc(0x3b, writer->f); // end of file - fclose(writer->f); - GIF_FREE(writer->oldImage); - - writer->f = NULL; - writer->oldImage = NULL; - - return true; -} - -#endif \ No newline at end of file diff --git a/interface/src/ui/Gif.h b/interface/src/ui/Gif.h deleted file mode 100644 index 7d3929a3bb..0000000000 --- a/interface/src/ui/Gif.h +++ /dev/null @@ -1,162 +0,0 @@ -// -// gif.h -// by Charlie Tangora -// Public domain. -// Email me : ctangora -at- gmail -dot- com -// -// This file offers a simple, very limited way to create animated GIFs directly in code. -// -// Those looking for particular cleverness are likely to be disappointed; it's pretty -// much a straight-ahead implementation of the GIF format with optional Floyd-Steinberg -// dithering. (It does at least use delta encoding - only the changed portions of each -// frame are saved.) -// -// So resulting files are often quite large. The hope is that it will be handy nonetheless -// as a quick and easily-integrated way for programs to spit out animations. -// -// Only RGBA8 is currently supported as an input format. (The alpha is ignored.) -// -// USAGE: -// Create a GifWriter struct. Pass it to GifBegin() to initialize and write the header. -// Pass subsequent frames to GifWriteFrame(). -// Finally, call GifEnd() to close the file handle and free memory. -// - -#ifndef gif_h -#define gif_h - -#include -#include // for FILE* -#include // for memcpy and bzero - -#ifndef GIF_TEMP_MALLOC -#include -#define GIF_TEMP_MALLOC malloc -#endif - -#ifndef GIF_TEMP_FREE -#include -#define GIF_TEMP_FREE free -#endif - -#ifndef GIF_MALLOC -#include -#define GIF_MALLOC malloc -#endif - -#ifndef GIF_FREE -#include -#define GIF_FREE free -#endif - -const int kGifTransIndex = 0; - -struct GifPalette -{ - uint8_t bitDepth; - - uint8_t r[256]; - uint8_t g[256]; - uint8_t b[256]; - - // k-d tree over RGB space, organized in heap fashion - // i.e. left child of node i is node i*2, right child is node i*2+1 - // nodes 256-511 are implicitly the leaves, containing a color - uint8_t treeSplitElt[255]; - uint8_t treeSplit[255]; -}; - -// max, min, and abs functions -int GifIMax(int l, int r); -int GifIMin(int l, int r); -int GifIAbs(int i); - -// walks the k-d tree to pick the palette entry for a desired color. -// Takes as in/out parameters the current best color and its error - -// only changes them if it finds a better color in its subtree. -// this is the major hotspot in the code at the moment. -void GifGetClosestPaletteColor(GifPalette* pPal, int r, int g, int b, int& bestInd, int& bestDiff, int treeRoot); - -void GifSwapPixels(uint8_t* image, int pixA, int pixB); - -// just the partition operation from quicksort -int GifPartition(uint8_t* image, const int left, const int right, const int elt, int pivotIndex); - -// Perform an incomplete sort, finding all elements above and below the desired median -void GifPartitionByMedian(uint8_t* image, int left, int right, int com, int neededCenter); - -// Builds a palette by creating a balanced k-d tree of all pixels in the image -void GifSplitPalette(uint8_t* image, int numPixels, int firstElt, int lastElt, int splitElt, int splitDist, int treeNode, bool buildForDither, GifPalette* pal); - -// Finds all pixels that have changed from the previous image and -// moves them to the fromt of th buffer. -// This allows us to build a palette optimized for the colors of the -// changed pixels only. -int GifPickChangedPixels(const uint8_t* lastFrame, uint8_t* frame, int numPixels); - -// Creates a palette by placing all the image pixels in a k-d tree and then averaging the blocks at the bottom. -// This is known as the "modified median split" technique -void GifMakePalette(const uint8_t* lastFrame, const uint8_t* nextFrame, uint32_t width, uint32_t height, uint8_t bitDepth, bool buildForDither, GifPalette* pPal); - -// Implements Floyd-Steinberg dithering, writes palette value to alpha -void GifDitherImage(const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal); - -// Picks palette colors for the image using simple thresholding, no dithering -void GifThresholdImage(const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal); - -// Simple structure to write out the LZW-compressed portion of the image -// one bit at a time -struct GifBitStatus -{ - uint8_t bitIndex; // how many bits in the partial byte written so far - uint8_t byte; // current partial byte - - uint32_t chunkIndex; - uint8_t chunk[256]; // bytes are written in here until we have 256 of them, then written to the file -}; - -// insert a single bit -void GifWriteBit(GifBitStatus& stat, uint32_t bit); - -// write all bytes so far to the file -void GifWriteChunk(FILE* f, GifBitStatus& stat); - -void GifWriteCode(FILE* f, GifBitStatus& stat, uint32_t code, uint32_t length); - -// The LZW dictionary is a 256-ary tree constructed as the file is encoded, -// this is one node -struct GifLzwNode -{ - uint16_t m_next[256]; -}; - -// write a 256-color (8-bit) image palette to the file -void GifWritePalette(const GifPalette* pPal, FILE* f); - -// write the image header, LZW-compress and write out the image -void GifWriteLzwImage(FILE* f, uint8_t* image, uint32_t left, uint32_t top, uint32_t width, uint32_t height, uint32_t delay, GifPalette* pPal); - -struct GifWriter -{ - FILE* f; - uint8_t* oldImage; - bool firstFrame; -}; - -// Creates a gif file. -// The input GIFWriter is assumed to be uninitialized. -// The delay value is the time between frames in hundredths of a second - note that not all viewers pay much attention to this value. -bool GifBegin(GifWriter* writer, const char* filename, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth, bool dither); - -// Writes out a new frame to a GIF in progress. -// The GIFWriter should have been created by GIFBegin. -// AFAIK, it is legal to use different bit depths for different frames of an image - -// this may be handy to save bits in animations that don't change much. -bool GifWriteFrame(GifWriter* writer, const uint8_t* image, uint32_t width, uint32_t height, uint32_t delay, uint8_t bitDepth, bool dither); - -// Writes the EOF code, closes the file handle, and frees temp memory used by a GIF. -// Many if not most viewers will still display a GIF properly if the EOF code is missing, -// but it's still a good idea to write it out. -bool GifEnd(GifWriter* writer); - -#endif \ No newline at end of file From 25471d9fae492aced1bd759e1f3052bf0285bf84 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 11 Nov 2016 16:14:18 -0800 Subject: [PATCH 28/50] Still unstable, but maybe better? --- interface/src/Application.cpp | 108 ++++++++++++++++++++-------------- interface/src/Application.h | 3 +- 2 files changed, 66 insertions(+), 45 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ba442ede9c..54039c3fea 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5432,8 +5432,8 @@ void Application::toggleLogDialog() { // The frame will take too long to pack, the timer slot will // not execute properly, and the GIF will appear sped-up. // This is unacceptable and is probably a blocker for release. -#define SNAPSNOT_ANIMATED_WIDTH (300) -#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (50) // This value should divide evenly into 100 +#define SNAPSNOT_ANIMATED_WIDTH (400) +#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (25) // This value should divide evenly into 100 #define SNAPSNOT_ANIMATED_DURATION_SECS (3) #define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_FRAMERATE_FPS) @@ -5451,60 +5451,80 @@ void Application::takeSnapshot(bool notify, float aspectRatio) { QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); // If we're in the middle of capturing a GIF... - if (_currentAnimatedSnapshotFrame != 0) + if (_animatedSnapshotFirstFrameTimestamp != 0) { // Notify the window scripting interface that we've taken a Snapshot emit DependencyManager::get()->snapshotTaken(path, notify); // Protect against clobbering it and return immediately. // (Perhaps with a "snapshot failed" message? - return; } + else + { + // Reset the current animated snapshot frame + qApp->_animatedSnapshotFirstFrameTimestamp = 0; + // Reset the current animated snapshot frame timestamp + qApp->_animatedSnapshotTimestamp = 0; - // Reset the current animated snapshot frame - _currentAnimatedSnapshotFrame = 0; + // Change the extension of the future GIF saved snapshot file to "gif" + qApp->_animatedSnapshotPath = path.replace("jpg", "gif"); - // Change the extension of the future GIF saved snapshot file to "gif" - _animatedSnapshotPath = path.replace("jpg", "gif"); + // Ensure the snapshot timer is Precise (attempted millisecond precision) + qApp->animatedSnapshotTimer.setTimerType(Qt::PreciseTimer); - // Ensure the snapshot timer is Precise (attempted millisecond precision) - animatedSnapshotTimer.setTimerType(Qt::PreciseTimer); + // Connect the animatedSnapshotTimer QTimer to the lambda slot function + connect(&(qApp->animatedSnapshotTimer), &QTimer::timeout, [=] { + // Get a screenshot from the display, then scale the screenshot down, + // then convert it to the image format the GIF library needs, + // then save all that to the QImage named "frame" + QImage frame(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); + frame = frame.convertToFormat(QImage::Format_RGBA8888); - // Connect the animatedSnapshotTimer QTimer to the lambda slot function - connect(&(qApp->animatedSnapshotTimer), &QTimer::timeout, [=] { - // Get a screenshot from the display, then scale the screenshot down, - // then convert it to the image format the GIF library needs, - // then save all that to the QImage named "frame" - QImage frame(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + // If this is the first frame... + if (qApp->_animatedSnapshotTimestamp == 0) + { + // Write out the header and beginning of the GIF file + GifBegin(&(qApp->_animatedSnapshotGifWriter), qPrintable(qApp->_animatedSnapshotPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Write the first to the gif + GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), + (uint8_t*)frame.bits(), + frame.width(), + frame.height(), + SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Record the current frame timestamp + qApp->_animatedSnapshotTimestamp = QDateTime::currentMSecsSinceEpoch(); + qApp->_animatedSnapshotFirstFrameTimestamp = qApp->_animatedSnapshotTimestamp; + } + else + { + // Write the frame to the gif + GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), + (uint8_t*)frame.bits(), + frame.width(), + frame.height(), + round(((float)(QDateTime::currentMSecsSinceEpoch() - qApp->_animatedSnapshotTimestamp)) / 10)); + // Record the current frame timestamp + qApp->_animatedSnapshotTimestamp = QDateTime::currentMSecsSinceEpoch(); - // If this is the first frame... - if (qApp->_currentAnimatedSnapshotFrame == 0) - { - // Write out the header and beginning of the GIF file - GifBegin(&(qApp->_animatedSnapshotGifWriter), qPrintable(qApp->_animatedSnapshotPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10, 8, false); - } + // If that was the last frame... + if ((qApp->_animatedSnapshotTimestamp - qApp->_animatedSnapshotFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) + { + // Reset the current frame timestamp + qApp->_animatedSnapshotTimestamp = 0; + qApp->_animatedSnapshotFirstFrameTimestamp = 0; + // Write out the end of the GIF + GifEnd(&(qApp->_animatedSnapshotGifWriter)); + // Notify the Window Scripting Interface that the snapshot was taken + emit DependencyManager::get()->snapshotTaken(qApp->_animatedSnapshotPath, false); + // Stop the snapshot QTimer + qApp->animatedSnapshotTimer.stop(); + } + } + }); - // Write the frame to the gif - GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); - // Increment the current snapshot frame count - qApp->_currentAnimatedSnapshotFrame++; - - // If that was the last frame... - if (qApp->_currentAnimatedSnapshotFrame >= SNAPSNOT_ANIMATED_NUM_FRAMES) - { - // Reset the current frame number - qApp->_currentAnimatedSnapshotFrame = 0; - // Write out the end of the GIF - GifEnd(&(qApp->_animatedSnapshotGifWriter)); - // Notify the Window Scripting Interface that the snapshot was taken - emit DependencyManager::get()->snapshotTaken(qApp->_animatedSnapshotPath, false); - // Stop the snapshot QTimer - qApp->animatedSnapshotTimer.stop(); - } - }); - - // Start the animatedSnapshotTimer QTimer - argument for this is in milliseconds - animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); + // Start the animatedSnapshotTimer QTimer - argument for this is in milliseconds + qApp->animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); + } }); } void Application::shareSnapshot(const QString& path) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 963b4c8072..092b2d6c1d 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -613,7 +613,8 @@ private: QTimer animatedSnapshotTimer; GifWriter _animatedSnapshotGifWriter; - uint32_t _currentAnimatedSnapshotFrame { 0 }; + quint64 _animatedSnapshotTimestamp{ 0 }; + quint64 _animatedSnapshotFirstFrameTimestamp{ 0 }; QString _animatedSnapshotPath; }; From 8f9ffd2bc5120c84d7c62b4718dfc3f389d24c38 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 14 Nov 2016 10:19:43 -0800 Subject: [PATCH 29/50] Framerate is stable, and GIF duration is correctgit add -A! --- interface/src/Application.cpp | 35 ++++++++++++++++++++--------------- interface/src/Application.h | 5 +++-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 54039c3fea..f2301353b1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5428,12 +5428,11 @@ void Application::toggleLogDialog() { } } -// If this is "too big" (which depends on PC spec): -// The frame will take too long to pack, the timer slot will -// not execute properly, and the GIF will appear sped-up. -// This is unacceptable and is probably a blocker for release. -#define SNAPSNOT_ANIMATED_WIDTH (400) -#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (25) // This value should divide evenly into 100 +// If the snapshot width or the framerate are too high for the +// application to handle, the framerate of the output GIF will drop. +#define SNAPSNOT_ANIMATED_WIDTH (720) +// This value should divide evenly into 100. Snapshot framerate is NOT guaranteed. +#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (25) #define SNAPSNOT_ANIMATED_DURATION_SECS (3) #define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_FRAMERATE_FPS) @@ -5497,15 +5496,6 @@ void Application::takeSnapshot(bool notify, float aspectRatio) { } else { - // Write the frame to the gif - GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), - (uint8_t*)frame.bits(), - frame.width(), - frame.height(), - round(((float)(QDateTime::currentMSecsSinceEpoch() - qApp->_animatedSnapshotTimestamp)) / 10)); - // Record the current frame timestamp - qApp->_animatedSnapshotTimestamp = QDateTime::currentMSecsSinceEpoch(); - // If that was the last frame... if ((qApp->_animatedSnapshotTimestamp - qApp->_animatedSnapshotFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) { @@ -5519,6 +5509,21 @@ void Application::takeSnapshot(bool notify, float aspectRatio) { // Stop the snapshot QTimer qApp->animatedSnapshotTimer.stop(); } + else + { + // Variable used to determine how long the current frame took to pack + qint64 temp = QDateTime::currentMSecsSinceEpoch(); + // Write the frame to the gif + GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), + (uint8_t*)frame.bits(), + frame.width(), + frame.height(), + round(((float)(QDateTime::currentMSecsSinceEpoch() - qApp->_animatedSnapshotTimestamp + qApp->_animatedSnapshotLastWriteFrameDuration)) / 10)); + // Record how long it took for the current frame to pack + qApp->_animatedSnapshotLastWriteFrameDuration = QDateTime::currentMSecsSinceEpoch() - temp; + // Record the current frame timestamp + qApp->_animatedSnapshotTimestamp = QDateTime::currentMSecsSinceEpoch(); + } } }); diff --git a/interface/src/Application.h b/interface/src/Application.h index 092b2d6c1d..49ad9ec03b 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -613,8 +613,9 @@ private: QTimer animatedSnapshotTimer; GifWriter _animatedSnapshotGifWriter; - quint64 _animatedSnapshotTimestamp{ 0 }; - quint64 _animatedSnapshotFirstFrameTimestamp{ 0 }; + qint64 _animatedSnapshotTimestamp { 0 }; + qint64 _animatedSnapshotFirstFrameTimestamp { 0 }; + qint64 _animatedSnapshotLastWriteFrameDuration { 20 }; QString _animatedSnapshotPath; }; From a81289a0d7037b6c60599c9e867af89b4e77f8cd Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 14 Nov 2016 17:41:46 -0800 Subject: [PATCH 30/50] Huge progress today. Buggy behavior with multiple snapshots remains --- interface/src/Application.cpp | 97 +- interface/src/Application.h | 9 +- .../scripting/WindowScriptingInterface.cpp | 4 +- .../src/scripting/WindowScriptingInterface.h | 4 +- interface/src/ui/SnapshotAnimated.cpp | 105 ++ interface/src/ui/SnapshotAnimated.h | 44 + scripts/system/html/SnapshotReview.html | 68 +- scripts/system/html/js/SnapshotReview.js | 47 +- scripts/system/notifications.js | 1028 +++++++++-------- scripts/system/snapshot.js | 7 +- 10 files changed, 735 insertions(+), 678 deletions(-) create mode 100644 interface/src/ui/SnapshotAnimated.cpp create mode 100644 interface/src/ui/SnapshotAnimated.h diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f2301353b1..b21a27977e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -152,6 +152,7 @@ #include "ui/LoginDialog.h" #include "ui/overlays/Cube3DOverlay.h" #include "ui/Snapshot.h" +#include "ui/SnapshotAnimated.h" #include "ui/StandAloneJSConsole.h" #include "ui/Stats.h" #include "ui/UpdateDialog.h" @@ -5428,19 +5429,9 @@ void Application::toggleLogDialog() { } } -// If the snapshot width or the framerate are too high for the -// application to handle, the framerate of the output GIF will drop. -#define SNAPSNOT_ANIMATED_WIDTH (720) -// This value should divide evenly into 100. Snapshot framerate is NOT guaranteed. -#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (25) -#define SNAPSNOT_ANIMATED_DURATION_SECS (3) -#define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_FRAMERATE_FPS) -#define SNAPSNOT_ANIMATED_NUM_FRAMES (SNAPSNOT_ANIMATED_DURATION_SECS * SNAPSNOT_ANIMATED_FRAMERATE_FPS) - - -void Application::takeSnapshot(bool notify, float aspectRatio) { - postLambdaEvent([notify, aspectRatio, this] { +void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio) { + postLambdaEvent([notify, includeAnimated, aspectRatio, this] { QMediaPlayer* player = new QMediaPlayer(); QFileInfo inf = QFileInfo(PathUtils::resourcesPath() + "sounds/snap.wav"); player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); @@ -5449,87 +5440,7 @@ void Application::takeSnapshot(bool notify, float aspectRatio) { // Get a screenshot and save it QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - // If we're in the middle of capturing a GIF... - if (_animatedSnapshotFirstFrameTimestamp != 0) - { - // Notify the window scripting interface that we've taken a Snapshot - emit DependencyManager::get()->snapshotTaken(path, notify); - // Protect against clobbering it and return immediately. - // (Perhaps with a "snapshot failed" message? - } - else - { - // Reset the current animated snapshot frame - qApp->_animatedSnapshotFirstFrameTimestamp = 0; - // Reset the current animated snapshot frame timestamp - qApp->_animatedSnapshotTimestamp = 0; - - // Change the extension of the future GIF saved snapshot file to "gif" - qApp->_animatedSnapshotPath = path.replace("jpg", "gif"); - - // Ensure the snapshot timer is Precise (attempted millisecond precision) - qApp->animatedSnapshotTimer.setTimerType(Qt::PreciseTimer); - - // Connect the animatedSnapshotTimer QTimer to the lambda slot function - connect(&(qApp->animatedSnapshotTimer), &QTimer::timeout, [=] { - // Get a screenshot from the display, then scale the screenshot down, - // then convert it to the image format the GIF library needs, - // then save all that to the QImage named "frame" - QImage frame(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); - frame = frame.convertToFormat(QImage::Format_RGBA8888); - - // If this is the first frame... - if (qApp->_animatedSnapshotTimestamp == 0) - { - // Write out the header and beginning of the GIF file - GifBegin(&(qApp->_animatedSnapshotGifWriter), qPrintable(qApp->_animatedSnapshotPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Write the first to the gif - GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), - (uint8_t*)frame.bits(), - frame.width(), - frame.height(), - SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Record the current frame timestamp - qApp->_animatedSnapshotTimestamp = QDateTime::currentMSecsSinceEpoch(); - qApp->_animatedSnapshotFirstFrameTimestamp = qApp->_animatedSnapshotTimestamp; - } - else - { - // If that was the last frame... - if ((qApp->_animatedSnapshotTimestamp - qApp->_animatedSnapshotFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) - { - // Reset the current frame timestamp - qApp->_animatedSnapshotTimestamp = 0; - qApp->_animatedSnapshotFirstFrameTimestamp = 0; - // Write out the end of the GIF - GifEnd(&(qApp->_animatedSnapshotGifWriter)); - // Notify the Window Scripting Interface that the snapshot was taken - emit DependencyManager::get()->snapshotTaken(qApp->_animatedSnapshotPath, false); - // Stop the snapshot QTimer - qApp->animatedSnapshotTimer.stop(); - } - else - { - // Variable used to determine how long the current frame took to pack - qint64 temp = QDateTime::currentMSecsSinceEpoch(); - // Write the frame to the gif - GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), - (uint8_t*)frame.bits(), - frame.width(), - frame.height(), - round(((float)(QDateTime::currentMSecsSinceEpoch() - qApp->_animatedSnapshotTimestamp + qApp->_animatedSnapshotLastWriteFrameDuration)) / 10)); - // Record how long it took for the current frame to pack - qApp->_animatedSnapshotLastWriteFrameDuration = QDateTime::currentMSecsSinceEpoch() - temp; - // Record the current frame timestamp - qApp->_animatedSnapshotTimestamp = QDateTime::currentMSecsSinceEpoch(); - } - } - }); - - // Start the animatedSnapshotTimer QTimer - argument for this is in milliseconds - qApp->animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); - } + SnapshotAnimated::saveSnapshotAnimated(includeAnimated, path, aspectRatio, qApp, DependencyManager::get()); }); } void Application::shareSnapshot(const QString& path) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 49ad9ec03b..5397420497 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -267,7 +267,7 @@ public: float getAvatarSimrate() const { return _avatarSimCounter.rate(); } float getAverageSimsPerSecond() const { return _simCounter.rate(); } - void takeSnapshot(bool notify, float aspectRatio = 0.0f); + void takeSnapshot(bool notify, bool includeAnimated = false, float aspectRatio = 0.0f); void shareSnapshot(const QString& filename); model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; } @@ -610,13 +610,6 @@ private: model::SkyboxPointer _defaultSkybox { new ProceduralSkybox() } ; gpu::TexturePointer _defaultSkyboxTexture; gpu::TexturePointer _defaultSkyboxAmbientTexture; - - QTimer animatedSnapshotTimer; - GifWriter _animatedSnapshotGifWriter; - qint64 _animatedSnapshotTimestamp { 0 }; - qint64 _animatedSnapshotFirstFrameTimestamp { 0 }; - qint64 _animatedSnapshotLastWriteFrameDuration { 20 }; - QString _animatedSnapshotPath; }; diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 0f9dd698fd..0cb574c1f6 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -199,8 +199,8 @@ void WindowScriptingInterface::copyToClipboard(const QString& text) { QApplication::clipboard()->setText(text); } -void WindowScriptingInterface::takeSnapshot(bool notify, float aspectRatio) { - qApp->takeSnapshot(notify, aspectRatio); +void WindowScriptingInterface::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio) { + qApp->takeSnapshot(notify, includeAnimated, aspectRatio); } void WindowScriptingInterface::shareSnapshot(const QString& path) { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index f4a89ae221..7246dc0927 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -52,7 +52,7 @@ public slots: QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void showAssetServer(const QString& upload = ""); void copyToClipboard(const QString& text); - void takeSnapshot(bool notify = true, float aspectRatio = 0.0f); + void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f); void shareSnapshot(const QString& path); bool isPhysicsEnabled(); @@ -60,7 +60,7 @@ signals: void domainChanged(const QString& domainHostname); void svoImportRequested(const QString& url); void domainConnectionRefused(const QString& reasonMessage, int reasonCode, const QString& extraInfo); - void snapshotTaken(const QString& path, bool notify); + void snapshotTaken(const QString& pathStillSnapshot, const QString& pathAnimatedSnapshot, bool notify); void snapshotShared(const QString& error); private: diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp new file mode 100644 index 0000000000..a9c6394426 --- /dev/null +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -0,0 +1,105 @@ +// +// SnapshotAnimated.cpp +// interface/src/ui +// +// Created by Zach Fox on 11/14/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include + +#include "SnapshotAnimated.h" + +QTimer SnapshotAnimated::snapshotAnimatedTimer; +GifWriter SnapshotAnimated::snapshotAnimatedGifWriter; +qint64 SnapshotAnimated::snapshotAnimatedTimestamp = 0; +qint64 SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; +qint64 SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = 0; + +void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathStillSnapshot, float aspectRatio, Application* app, QSharedPointer dm) { + // If we're not in the middle of capturing an animated snapshot... + if ((snapshotAnimatedFirstFrameTimestamp == 0) && (includeAnimated)) + { + // Define the output location of the animated snapshot + QString pathAnimatedSnapshot(pathStillSnapshot); + pathAnimatedSnapshot.replace("jpg", "gif"); + // Reset the current animated snapshot last frame duration + snapshotAnimatedLastWriteFrameDuration = SNAPSNOT_ANIMATED_INITIAL_WRITE_DURATION_MSEC; + + // Ensure the snapshot timer is Precise (attempted millisecond precision) + snapshotAnimatedTimer.setTimerType(Qt::PreciseTimer); + + // Connect the snapshotAnimatedTimer QTimer to the lambda slot function + QObject::connect(&(snapshotAnimatedTimer), &QTimer::timeout, [=] { + // Get a screenshot from the display, then scale the screenshot down, + // then convert it to the image format the GIF library needs, + // then save all that to the QImage named "frame" + QImage frame(app->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); + frame = frame.convertToFormat(QImage::Format_RGBA8888); + + // If this is the first frame... + if (snapshotAnimatedTimestamp == 0) + { + // Write out the header and beginning of the GIF file + GifBegin(&(snapshotAnimatedGifWriter), qPrintable(pathAnimatedSnapshot), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Write the first to the gif + GifWriteFrame(&(snapshotAnimatedGifWriter), + (uint8_t*)frame.bits(), + frame.width(), + frame.height(), + SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Record the current frame timestamp + snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + snapshotAnimatedFirstFrameTimestamp = snapshotAnimatedTimestamp; + } + else + { + // If that was the last frame... + if ((snapshotAnimatedTimestamp - snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) + { + // Reset the current frame timestamp + snapshotAnimatedTimestamp = 0; + snapshotAnimatedFirstFrameTimestamp = 0; + // Write out the end of the GIF + GifEnd(&(snapshotAnimatedGifWriter)); + // Stop the snapshot QTimer + snapshotAnimatedTimer.stop(); + emit dm->snapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, false); + qDebug() << "still: " << pathStillSnapshot << "anim: " << pathAnimatedSnapshot; + //emit dm->snapshotTaken("C:\\Users\\Zach Fox\\Desktop\\hifi-snap-by-zfox-on-2016-11-14_17-07-33.jpg", "C:\\Users\\Zach Fox\\Desktop\\hifi-snap-by-zfox-on-2016-11-14_17-10-02.gif", false); + } + else + { + // Variable used to determine how long the current frame took to pack + qint64 framePackStartTime = QDateTime::currentMSecsSinceEpoch(); + // Write the frame to the gif + GifWriteFrame(&(snapshotAnimatedGifWriter), + (uint8_t*)frame.bits(), + frame.width(), + frame.height(), + round(((float)(QDateTime::currentMSecsSinceEpoch() - snapshotAnimatedTimestamp + snapshotAnimatedLastWriteFrameDuration)) / 10)); + // Record how long it took for the current frame to pack + snapshotAnimatedLastWriteFrameDuration = QDateTime::currentMSecsSinceEpoch() - framePackStartTime; + // Record the current frame timestamp + snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + } + } + }); + + // Start the snapshotAnimatedTimer QTimer - argument for this is in milliseconds + snapshotAnimatedTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); + } + // If we're already in the middle of capturing an animated snapshot... + else + { + // Just tell the dependency manager that the capture of the still snapshot has taken place. + emit dm->snapshotTaken(pathStillSnapshot, "", false); + } +} diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h new file mode 100644 index 0000000000..ca778341a6 --- /dev/null +++ b/interface/src/ui/SnapshotAnimated.h @@ -0,0 +1,44 @@ +// +// SnapshotAnimated.h +// interface/src/ui +// +// Created by Zach Fox on 11/14/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_SnapshotAnimated_h +#define hifi_SnapshotAnimated_h + +#include +#include +#include +#include +#include "scripting/WindowScriptingInterface.h" + +// If the snapshot width or the framerate are too high for the +// application to handle, the framerate of the output GIF will drop. +#define SNAPSNOT_ANIMATED_WIDTH (720) +// This value should divide evenly into 100. Snapshot framerate is NOT guaranteed. +#define SNAPSNOT_ANIMATED_TARGET_FRAMERATE (25) +#define SNAPSNOT_ANIMATED_DURATION_SECS (3) + +#define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_TARGET_FRAMERATE) +// This is the fudge factor that we add to the *first* GIF frame's "delay" value +#define SNAPSNOT_ANIMATED_INITIAL_WRITE_DURATION_MSEC (20) +#define SNAPSNOT_ANIMATED_NUM_FRAMES (SNAPSNOT_ANIMATED_DURATION_SECS * SNAPSNOT_ANIMATED_TARGET_FRAMERATE) + +class SnapshotAnimated { +private: + static QTimer snapshotAnimatedTimer; + static GifWriter snapshotAnimatedGifWriter; + static qint64 snapshotAnimatedTimestamp; + static qint64 snapshotAnimatedFirstFrameTimestamp; + static qint64 snapshotAnimatedLastWriteFrameDuration; +public: + static void saveSnapshotAnimated(bool includeAnimated, QString stillSnapshotPath, float aspectRatio, Application* app, QSharedPointer dm); +}; + +#endif // hifi_SnapshotAnimated_h diff --git a/scripts/system/html/SnapshotReview.html b/scripts/system/html/SnapshotReview.html index db70a1910b..d37afb180c 100644 --- a/scripts/system/html/SnapshotReview.html +++ b/scripts/system/html/SnapshotReview.html @@ -1,48 +1,48 @@ - + Share - + - +
-
-
- -
-
-
-
-
Would you like to share your pic in the Snapshots feed?
-
- - - - +
+
+ +
+
+
+
+
Would you like to share your pics in the Snapshots feed?
+
+ + + + +
+
+
+ +
+
+
+
+ + + + + + + +
-
-
- -
-
-
- - - - - - - - +
-
-
-
- + diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index a6515df825..ccd70c40b2 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -12,29 +12,30 @@ var paths = [], idCounter = 0, useCheckboxes; function addImage(data) { - var div = document.createElement("DIV"), - input = document.createElement("INPUT"), - label = document.createElement("LABEL"), - img = document.createElement("IMG"), - id = "p" + idCounter++; - function toggle() { data.share = input.checked; } - img.src = data.localPath; - div.appendChild(img); - data.share = true; - if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular. - // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. - label.setAttribute('for', id); // cannot do label.for = - input.id = id; - input.type = "checkbox"; - input.checked = true; - input.addEventListener('change', toggle); - div.class = "property checkbox"; - div.appendChild(input); - div.appendChild(label); + if (data.localPath) { + var div = document.createElement("DIV"), + input = document.createElement("INPUT"), + label = document.createElement("LABEL"), + img = document.createElement("IMG"), + id = "p" + idCounter++; + function toggle() { data.share = input.checked; } + img.src = data.localPath; + div.appendChild(img); + data.share = true; + if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular. + // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. + label.setAttribute('for', id); // cannot do label.for = + input.id = id; + input.type = "checkbox"; + input.checked = (id === "p0"); + input.addEventListener('change', toggle); + div.class = "property checkbox"; + div.appendChild(input); + div.appendChild(label); + } + document.getElementById("snapshot-images").appendChild(div); + paths.push(data); } - document.getElementById("snapshot-images").appendChild(div); - paths.push(data); - } function handleShareButtons(shareMsg) { var openFeed = document.getElementById('openFeed'); @@ -49,7 +50,7 @@ function handleShareButtons(shareMsg) { window.onload = function () { // Something like the following will allow testing in a browser. //addImage({localPath: 'c:/Users/howar/OneDrive/Pictures/hifi-snap-by--on-2016-07-27_12-58-43.jpg'}); - //addImage({localPath: 'http://lorempixel.com/1512/1680'}); + //addImage({ localPath: 'http://lorempixel.com/1512/1680' }); openEventBridge(function () { // Set up a handler for receiving the data, and tell the .js we are ready to receive it. EventBridge.scriptEventReceived.connect(function (message) { diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index d89b532f31..4c16a637bf 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -58,581 +58,583 @@ /* global Script, Controller, Overlays, SoundArray, Quat, Vec3, MyAvatar, Menu, HMD, AudioDevice, LODManager, Settings, Camera */ -(function() { // BEGIN LOCAL_SCOPE +(function () { // BEGIN LOCAL_SCOPE -Script.include("./libraries/soundArray.js"); + Script.include("./libraries/soundArray.js"); -var width = 340.0; //width of notification overlay -var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window -var overlayLocationX = (windowDimensions.x - (width + 20.0)); // positions window 20px from the right of the interface window -var buttonLocationX = overlayLocationX + (width - 28.0); -var locationY = 20.0; // position down from top of interface window -var topMargin = 13.0; -var leftMargin = 10.0; -var textColor = { red: 228, green: 228, blue: 228}; // text color -var backColor = { red: 2, green: 2, blue: 2}; // background color was 38,38,38 -var backgroundAlpha = 0; -var fontSize = 12.0; -var PERSIST_TIME_2D = 10.0; // Time in seconds before notification fades -var PERSIST_TIME_3D = 15.0; -var persistTime = PERSIST_TIME_2D; -var frame = 0; -var ourWidth = Window.innerWidth; -var ourHeight = Window.innerHeight; -var ctrlIsPressed = false; -var ready = true; -var MENU_NAME = 'Tools > Notifications'; -var PLAY_NOTIFICATION_SOUNDS_MENU_ITEM = "Play Notification Sounds"; -var NOTIFICATION_MENU_ITEM_POST = " Notifications"; -var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; -var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; -var lodTextID = false; + var width = 340.0; //width of notification overlay + var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window + var overlayLocationX = (windowDimensions.x - (width + 20.0)); // positions window 20px from the right of the interface window + var buttonLocationX = overlayLocationX + (width - 28.0); + var locationY = 20.0; // position down from top of interface window + var topMargin = 13.0; + var leftMargin = 10.0; + var textColor = { red: 228, green: 228, blue: 228 }; // text color + var backColor = { red: 2, green: 2, blue: 2 }; // background color was 38,38,38 + var backgroundAlpha = 0; + var fontSize = 12.0; + var PERSIST_TIME_2D = 10.0; // Time in seconds before notification fades + var PERSIST_TIME_3D = 15.0; + var persistTime = PERSIST_TIME_2D; + var frame = 0; + var ourWidth = Window.innerWidth; + var ourHeight = Window.innerHeight; + var ctrlIsPressed = false; + var ready = true; + var MENU_NAME = 'Tools > Notifications'; + var PLAY_NOTIFICATION_SOUNDS_MENU_ITEM = "Play Notification Sounds"; + var NOTIFICATION_MENU_ITEM_POST = " Notifications"; + var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; + var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; + var lodTextID = false; -var NotificationType = { - UNKNOWN: 0, - SNAPSHOT: 1, - LOD_WARNING: 2, - CONNECTION_REFUSED: 3, - EDIT_ERROR: 4, - properties: [ - { text: "Snapshot" }, - { text: "Level of Detail" }, - { text: "Connection Refused" }, - { text: "Edit error" } - ], - getTypeFromMenuItem: function(menuItemName) { - if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { - return NotificationType.UNKNOWN; - } - var preMenuItemName = menuItemName.substr(0, menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length); - for (var type in this.properties) { - if (this.properties[type].text === preMenuItemName) { - return parseInt(type) + 1; + var NotificationType = { + UNKNOWN: 0, + SNAPSHOT: 1, + LOD_WARNING: 2, + CONNECTION_REFUSED: 3, + EDIT_ERROR: 4, + properties: [ + { text: "Snapshot" }, + { text: "Level of Detail" }, + { text: "Connection Refused" }, + { text: "Edit error" } + ], + getTypeFromMenuItem: function (menuItemName) { + if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { + return NotificationType.UNKNOWN; } + var preMenuItemName = menuItemName.substr(0, menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length); + for (var type in this.properties) { + if (this.properties[type].text === preMenuItemName) { + return parseInt(type) + 1; + } + } + return NotificationType.UNKNOWN; + }, + getMenuString: function (type) { + return this.properties[type - 1].text + NOTIFICATION_MENU_ITEM_POST; } - return NotificationType.UNKNOWN; - }, - getMenuString: function(type) { - return this.properties[type - 1].text + NOTIFICATION_MENU_ITEM_POST; - } -}; - -var randomSounds = new SoundArray({ localOnly: true }, true); -var numberOfSounds = 2; -for (var i = 1; i <= numberOfSounds; i++) { - randomSounds.addSound(Script.resolvePath("assets/sounds/notification-general"+ i + ".raw")); -} - -var notifications = []; -var buttons = []; -var times = []; -var heights = []; -var myAlpha = []; -var arrays = []; -var isOnHMD = false, - NOTIFICATIONS_3D_DIRECTION = 0.0, // Degrees from avatar orientation. - NOTIFICATIONS_3D_DISTANCE = 0.6, // Horizontal distance from avatar position. - NOTIFICATIONS_3D_ELEVATION = -0.8, // Height of top middle of top notification relative to avatar eyes. - NOTIFICATIONS_3D_YAW = 0.0, // Degrees relative to notifications direction. - NOTIFICATIONS_3D_PITCH = -60.0, // Degrees from vertical. - NOTIFICATION_3D_SCALE = 0.002, // Multiplier that converts 2D overlay dimensions to 3D overlay dimensions. - NOTIFICATION_3D_BUTTON_WIDTH = 40 * NOTIFICATION_3D_SCALE, // Need a little more room for button in 3D. - overlay3DDetails = []; - -// push data from above to the 2 dimensional array -function createArrays(notice, button, createTime, height, myAlpha) { - arrays.push([notice, button, createTime, height, myAlpha]); -} - -// This handles the final dismissal of a notification after fading -function dismiss(firstNoteOut, firstButOut, firstOut) { - if (firstNoteOut == lodTextID) { - lodTextID = false; - } - - Overlays.deleteOverlay(firstNoteOut); - Overlays.deleteOverlay(firstButOut); - notifications.splice(firstOut, 1); - buttons.splice(firstOut, 1); - times.splice(firstOut, 1); - heights.splice(firstOut, 1); - myAlpha.splice(firstOut, 1); - overlay3DDetails.splice(firstOut, 1); -} - -function fadeIn(noticeIn, buttonIn) { - var q = 0, - qFade, - pauseTimer = null; - - pauseTimer = Script.setInterval(function () { - q += 1; - qFade = q / 10.0; - Overlays.editOverlay(noticeIn, { alpha: qFade }); - Overlays.editOverlay(buttonIn, { alpha: qFade }); - if (q >= 9.0) { - Script.clearInterval(pauseTimer); - } - }, 10); -} - -// this fades the notification ready for dismissal, and removes it from the arrays -function fadeOut(noticeOut, buttonOut, arraysOut) { - var r = 9.0, - rFade, - pauseTimer = null; - - pauseTimer = Script.setInterval(function () { - r -= 1; - rFade = r / 10.0; - Overlays.editOverlay(noticeOut, { alpha: rFade }); - Overlays.editOverlay(buttonOut, { alpha: rFade }); - if (r < 0) { - dismiss(noticeOut, buttonOut, arraysOut); - arrays.splice(arraysOut, 1); - ready = true; - Script.clearInterval(pauseTimer); - } - }, 20); -} - -function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) { - // Calculates overlay positions and orientations in avatar coordinates. - var noticeY, - originOffset, - notificationOrientation, - notificationPosition, - buttonPosition; - - // Notification plane positions - noticeY = -y * NOTIFICATION_3D_SCALE - noticeHeight / 2; - notificationPosition = { x: 0, y: noticeY, z: 0 }; - buttonPosition = { x: (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH) / 2, y: noticeY, z: 0.001 }; - - // Rotate plane - notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, - NOTIFICATIONS_3D_DIRECTION + NOTIFICATIONS_3D_YAW, 0); - notificationPosition = Vec3.multiplyQbyV(notificationOrientation, notificationPosition); - buttonPosition = Vec3.multiplyQbyV(notificationOrientation, buttonPosition); - - // Translate plane - originOffset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, NOTIFICATIONS_3D_DIRECTION, 0), - { x: 0, y: 0, z: -NOTIFICATIONS_3D_DISTANCE }); - originOffset.y += NOTIFICATIONS_3D_ELEVATION; - notificationPosition = Vec3.sum(originOffset, notificationPosition); - buttonPosition = Vec3.sum(originOffset, buttonPosition); - - return { - notificationOrientation: notificationOrientation, - notificationPosition: notificationPosition, - buttonPosition: buttonPosition }; -} -// Pushes data to each array and sets up data for 2nd dimension array -// to handle auxiliary data not carried by the overlay class -// specifically notification "heights", "times" of creation, and . -function notify(notice, button, height, imageProperties, image) { - var notificationText, - noticeWidth, - noticeHeight, - positions, - last; + var randomSounds = new SoundArray({ localOnly: true }, true); + var numberOfSounds = 2; + for (var i = 1; i <= numberOfSounds; i++) { + randomSounds.addSound(Script.resolvePath("assets/sounds/notification-general" + i + ".raw")); + } - if (isOnHMD) { - // Calculate 3D values from 2D overlay properties. + var notifications = []; + var buttons = []; + var times = []; + var heights = []; + var myAlpha = []; + var arrays = []; + var isOnHMD = false, + NOTIFICATIONS_3D_DIRECTION = 0.0, // Degrees from avatar orientation. + NOTIFICATIONS_3D_DISTANCE = 0.6, // Horizontal distance from avatar position. + NOTIFICATIONS_3D_ELEVATION = -0.8, // Height of top middle of top notification relative to avatar eyes. + NOTIFICATIONS_3D_YAW = 0.0, // Degrees relative to notifications direction. + NOTIFICATIONS_3D_PITCH = -60.0, // Degrees from vertical. + NOTIFICATION_3D_SCALE = 0.002, // Multiplier that converts 2D overlay dimensions to 3D overlay dimensions. + NOTIFICATION_3D_BUTTON_WIDTH = 40 * NOTIFICATION_3D_SCALE, // Need a little more room for button in 3D. + overlay3DDetails = []; - noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; - noticeHeight = notice.height * NOTIFICATION_3D_SCALE; + // push data from above to the 2 dimensional array + function createArrays(notice, button, createTime, height, myAlpha) { + arrays.push([notice, button, createTime, height, myAlpha]); + } - notice.size = { x: noticeWidth, y: noticeHeight }; - - positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); - - notice.parentID = MyAvatar.sessionUUID; - notice.parentJointIndex = -2; - - if (!image) { - notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; - notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; - notice.bottomMargin = 0; - notice.rightMargin = 0; - notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; - notice.isFacingAvatar = false; - - notificationText = Overlays.addOverlay("text3d", notice); - notifications.push(notificationText); - } else { - notifications.push(Overlays.addOverlay("image3d", notice)); + // This handles the final dismissal of a notification after fading + function dismiss(firstNoteOut, firstButOut, firstOut) { + if (firstNoteOut == lodTextID) { + lodTextID = false; } - button.url = button.imageURL; - button.scale = button.width * NOTIFICATION_3D_SCALE; - button.isFacingAvatar = false; - button.parentID = MyAvatar.sessionUUID; - button.parentJointIndex = -2; + Overlays.deleteOverlay(firstNoteOut); + Overlays.deleteOverlay(firstButOut); + notifications.splice(firstOut, 1); + buttons.splice(firstOut, 1); + times.splice(firstOut, 1); + heights.splice(firstOut, 1); + myAlpha.splice(firstOut, 1); + overlay3DDetails.splice(firstOut, 1); + } - buttons.push((Overlays.addOverlay("image3d", button))); - overlay3DDetails.push({ - notificationOrientation: positions.notificationOrientation, - notificationPosition: positions.notificationPosition, - buttonPosition: positions.buttonPosition, - width: noticeWidth, - height: noticeHeight - }); + function fadeIn(noticeIn, buttonIn) { + var q = 0, + qFade, + pauseTimer = null; + pauseTimer = Script.setInterval(function () { + q += 1; + qFade = q / 10.0; + Overlays.editOverlay(noticeIn, { alpha: qFade }); + Overlays.editOverlay(buttonIn, { alpha: qFade }); + if (q >= 9.0) { + Script.clearInterval(pauseTimer); + } + }, 10); + } - var defaultEyePosition, - avatarOrientation, - notificationPosition, + // this fades the notification ready for dismissal, and removes it from the arrays + function fadeOut(noticeOut, buttonOut, arraysOut) { + var r = 9.0, + rFade, + pauseTimer = null; + + pauseTimer = Script.setInterval(function () { + r -= 1; + rFade = r / 10.0; + Overlays.editOverlay(noticeOut, { alpha: rFade }); + Overlays.editOverlay(buttonOut, { alpha: rFade }); + if (r < 0) { + dismiss(noticeOut, buttonOut, arraysOut); + arrays.splice(arraysOut, 1); + ready = true; + Script.clearInterval(pauseTimer); + } + }, 20); + } + + function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) { + // Calculates overlay positions and orientations in avatar coordinates. + var noticeY, + originOffset, notificationOrientation, + notificationPosition, buttonPosition; - if (isOnHMD && notifications.length > 0) { - // Update 3D overlays to maintain positions relative to avatar - defaultEyePosition = MyAvatar.getDefaultEyePosition(); - avatarOrientation = MyAvatar.orientation; + // Notification plane positions + noticeY = -y * NOTIFICATION_3D_SCALE - noticeHeight / 2; + notificationPosition = { x: 0, y: noticeY, z: 0 }; + buttonPosition = { x: (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH) / 2, y: noticeY, z: 0.001 }; - for (i = 0; i < notifications.length; i += 1) { - notificationPosition = Vec3.sum(defaultEyePosition, - Vec3.multiplyQbyV(avatarOrientation, - overlay3DDetails[i].notificationPosition)); - notificationOrientation = Quat.multiply(avatarOrientation, - overlay3DDetails[i].notificationOrientation); - buttonPosition = Vec3.sum(defaultEyePosition, - Vec3.multiplyQbyV(avatarOrientation, - overlay3DDetails[i].buttonPosition)); - Overlays.editOverlay(notifications[i], { position: notificationPosition, - rotation: notificationOrientation }); - Overlays.editOverlay(buttons[i], { position: buttonPosition, rotation: notificationOrientation }); - } - } + // Rotate plane + notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, + NOTIFICATIONS_3D_DIRECTION + NOTIFICATIONS_3D_YAW, 0); + notificationPosition = Vec3.multiplyQbyV(notificationOrientation, notificationPosition); + buttonPosition = Vec3.multiplyQbyV(notificationOrientation, buttonPosition); - } else { - if (!image) { - notificationText = Overlays.addOverlay("text", notice); - notifications.push((notificationText)); - } else { - notifications.push(Overlays.addOverlay("image", notice)); - } - buttons.push(Overlays.addOverlay("image", button)); + // Translate plane + originOffset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, NOTIFICATIONS_3D_DIRECTION, 0), + { x: 0, y: 0, z: -NOTIFICATIONS_3D_DISTANCE }); + originOffset.y += NOTIFICATIONS_3D_ELEVATION; + notificationPosition = Vec3.sum(originOffset, notificationPosition); + buttonPosition = Vec3.sum(originOffset, buttonPosition); + + return { + notificationOrientation: notificationOrientation, + notificationPosition: notificationPosition, + buttonPosition: buttonPosition + }; } - height = height + 1.0; - heights.push(height); - times.push(new Date().getTime() / 1000); - last = notifications.length - 1; - myAlpha.push(notifications[last].alpha); - createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); - fadeIn(notifications[last], buttons[last]); + // Pushes data to each array and sets up data for 2nd dimension array + // to handle auxiliary data not carried by the overlay class + // specifically notification "heights", "times" of creation, and . + function notify(notice, button, height, imageProperties, image) { + var notificationText, + noticeWidth, + noticeHeight, + positions, + last; - if (imageProperties && !image) { - var imageHeight = notice.width / imageProperties.aspectRatio; - notice = { - x: notice.x, - y: notice.y + height, - width: notice.width, - height: imageHeight, - subImage: { x: 0, y: 0 }, - color: { red: 255, green: 255, blue: 255}, + if (isOnHMD) { + // Calculate 3D values from 2D overlay properties. + + noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; + noticeHeight = notice.height * NOTIFICATION_3D_SCALE; + + notice.size = { x: noticeWidth, y: noticeHeight }; + + positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); + + notice.parentID = MyAvatar.sessionUUID; + notice.parentJointIndex = -2; + + if (!image) { + notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; + notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; + notice.bottomMargin = 0; + notice.rightMargin = 0; + notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; + notice.isFacingAvatar = false; + + notificationText = Overlays.addOverlay("text3d", notice); + notifications.push(notificationText); + } else { + notifications.push(Overlays.addOverlay("image3d", notice)); + } + + button.url = button.imageURL; + button.scale = button.width * NOTIFICATION_3D_SCALE; + button.isFacingAvatar = false; + button.parentID = MyAvatar.sessionUUID; + button.parentJointIndex = -2; + + buttons.push((Overlays.addOverlay("image3d", button))); + overlay3DDetails.push({ + notificationOrientation: positions.notificationOrientation, + notificationPosition: positions.notificationPosition, + buttonPosition: positions.buttonPosition, + width: noticeWidth, + height: noticeHeight + }); + + + var defaultEyePosition, + avatarOrientation, + notificationPosition, + notificationOrientation, + buttonPosition; + + if (isOnHMD && notifications.length > 0) { + // Update 3D overlays to maintain positions relative to avatar + defaultEyePosition = MyAvatar.getDefaultEyePosition(); + avatarOrientation = MyAvatar.orientation; + + for (i = 0; i < notifications.length; i += 1) { + notificationPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, + overlay3DDetails[i].notificationPosition)); + notificationOrientation = Quat.multiply(avatarOrientation, + overlay3DDetails[i].notificationOrientation); + buttonPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, + overlay3DDetails[i].buttonPosition)); + Overlays.editOverlay(notifications[i], { + position: notificationPosition, + rotation: notificationOrientation + }); + Overlays.editOverlay(buttons[i], { position: buttonPosition, rotation: notificationOrientation }); + } + } + + } else { + if (!image) { + notificationText = Overlays.addOverlay("text", notice); + notifications.push((notificationText)); + } else { + notifications.push(Overlays.addOverlay("image", notice)); + } + buttons.push(Overlays.addOverlay("image", button)); + } + + height = height + 1.0; + heights.push(height); + times.push(new Date().getTime() / 1000); + last = notifications.length - 1; + myAlpha.push(notifications[last].alpha); + createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); + fadeIn(notifications[last], buttons[last]); + + if (imageProperties && !image) { + var imageHeight = notice.width / imageProperties.aspectRatio; + notice = { + x: notice.x, + y: notice.y + height, + width: notice.width, + height: imageHeight, + subImage: { x: 0, y: 0 }, + color: { red: 255, green: 255, blue: 255 }, + visible: true, + imageURL: imageProperties.path, + alpha: backgroundAlpha + }; + notify(notice, button, imageHeight, imageProperties, true); + } + + return notificationText; + } + + var CLOSE_NOTIFICATION_ICON = Script.resolvePath("assets/images/close-small-light.svg"); + + // This function creates and sizes the overlays + function createNotification(text, notificationType, imageProperties) { + var count = (text.match(/\n/g) || []).length, + breakPoint = 43.0, // length when new line is added + extraLine = 0, + breaks = 0, + height = 40.0, + stack = 0, + level, + noticeProperties, + bLevel, + buttonProperties, + i; + + if (text.length >= breakPoint) { + breaks = count; + } + extraLine = breaks * 16.0; + for (i = 0; i < heights.length; i += 1) { + stack = stack + heights[i]; + } + + level = (stack + 20.0); + height = height + extraLine; + + noticeProperties = { + x: overlayLocationX, + y: level, + width: width, + height: height, + color: textColor, + backgroundColor: backColor, + alpha: backgroundAlpha, + topMargin: topMargin, + leftMargin: leftMargin, + font: { size: fontSize }, + text: text + }; + + bLevel = level + 12.0; + buttonProperties = { + x: buttonLocationX, + y: bLevel, + width: 10.0, + height: 10.0, + subImage: { x: 0, y: 0, width: 10, height: 10 }, + imageURL: CLOSE_NOTIFICATION_ICON, + color: { red: 255, green: 255, blue: 255 }, visible: true, - imageURL: imageProperties.path, alpha: backgroundAlpha }; - notify(notice, button, imageHeight, imageProperties, true); - } - return notificationText; -} - -var CLOSE_NOTIFICATION_ICON = Script.resolvePath("assets/images/close-small-light.svg"); - -// This function creates and sizes the overlays -function createNotification(text, notificationType, imageProperties) { - var count = (text.match(/\n/g) || []).length, - breakPoint = 43.0, // length when new line is added - extraLine = 0, - breaks = 0, - height = 40.0, - stack = 0, - level, - noticeProperties, - bLevel, - buttonProperties, - i; - - if (text.length >= breakPoint) { - breaks = count; - } - extraLine = breaks * 16.0; - for (i = 0; i < heights.length; i += 1) { - stack = stack + heights[i]; - } - - level = (stack + 20.0); - height = height + extraLine; - - noticeProperties = { - x: overlayLocationX, - y: level, - width: width, - height: height, - color: textColor, - backgroundColor: backColor, - alpha: backgroundAlpha, - topMargin: topMargin, - leftMargin: leftMargin, - font: {size: fontSize}, - text: text - }; - - bLevel = level + 12.0; - buttonProperties = { - x: buttonLocationX, - y: bLevel, - width: 10.0, - height: 10.0, - subImage: { x: 0, y: 0, width: 10, height: 10 }, - imageURL: CLOSE_NOTIFICATION_ICON, - color: { red: 255, green: 255, blue: 255}, - visible: true, - alpha: backgroundAlpha - }; - - if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) && - Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) { - randomSounds.playRandom(); - } - - return notify(noticeProperties, buttonProperties, height, imageProperties); -} - -function deleteNotification(index) { - var notificationTextID = notifications[index]; - if (notificationTextID == lodTextID) { - lodTextID = false; - } - Overlays.deleteOverlay(notificationTextID); - Overlays.deleteOverlay(buttons[index]); - notifications.splice(index, 1); - buttons.splice(index, 1); - times.splice(index, 1); - heights.splice(index, 1); - myAlpha.splice(index, 1); - overlay3DDetails.splice(index, 1); - arrays.splice(index, 1); -} - -// wraps whole word to newline -function stringDivider(str, slotWidth, spaceReplacer) { - var left, right; - - if (str.length > slotWidth && slotWidth > 0) { - left = str.substring(0, slotWidth); - right = str.substring(slotWidth); - return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); - } - return str; -} - -// formats string to add newline every 43 chars -function wordWrap(str) { - return stringDivider(str, 43.0, "\n"); -} - -function update() { - var nextOverlay, - noticeOut, - buttonOut, - arraysOut, - positions, - i, - j, - k; - - if (isOnHMD !== HMD.active) { - while (arrays.length > 0) { - deleteNotification(0); + if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) && + Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) { + randomSounds.playRandom(); } - isOnHMD = !isOnHMD; - persistTime = isOnHMD ? PERSIST_TIME_3D : PERSIST_TIME_2D; - return; + + return notify(noticeProperties, buttonProperties, height, imageProperties); } - frame += 1; - if ((frame % 60.0) === 0) { // only update once a second - locationY = 20.0; - for (i = 0; i < arrays.length; i += 1) { //repositions overlays as others fade - nextOverlay = Overlays.getOverlayAtPoint({ x: overlayLocationX, y: locationY }); - Overlays.editOverlay(notifications[i], { x: overlayLocationX, y: locationY }); - Overlays.editOverlay(buttons[i], { x: buttonLocationX, y: locationY + 12.0 }); - if (isOnHMD) { - positions = calculate3DOverlayPositions(overlay3DDetails[i].width, - overlay3DDetails[i].height, locationY); - overlay3DDetails[i].notificationOrientation = positions.notificationOrientation; - overlay3DDetails[i].notificationPosition = positions.notificationPosition; - overlay3DDetails[i].buttonPosition = positions.buttonPosition; + function deleteNotification(index) { + var notificationTextID = notifications[index]; + if (notificationTextID == lodTextID) { + lodTextID = false; + } + Overlays.deleteOverlay(notificationTextID); + Overlays.deleteOverlay(buttons[index]); + notifications.splice(index, 1); + buttons.splice(index, 1); + times.splice(index, 1); + heights.splice(index, 1); + myAlpha.splice(index, 1); + overlay3DDetails.splice(index, 1); + arrays.splice(index, 1); + } + + // wraps whole word to newline + function stringDivider(str, slotWidth, spaceReplacer) { + var left, right; + + if (str.length > slotWidth && slotWidth > 0) { + left = str.substring(0, slotWidth); + right = str.substring(slotWidth); + return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); + } + return str; + } + + // formats string to add newline every 43 chars + function wordWrap(str) { + return stringDivider(str, 43.0, "\n"); + } + + function update() { + var nextOverlay, + noticeOut, + buttonOut, + arraysOut, + positions, + i, + j, + k; + + if (isOnHMD !== HMD.active) { + while (arrays.length > 0) { + deleteNotification(0); } - locationY = locationY + arrays[i][3]; + isOnHMD = !isOnHMD; + persistTime = isOnHMD ? PERSIST_TIME_3D : PERSIST_TIME_2D; + return; } - } - // This checks the age of the notification and prepares to fade it after 9.0 seconds (var persistTime - 1) - for (i = 0; i < arrays.length; i += 1) { - if (ready) { - j = arrays[i][2]; - k = j + persistTime; - if (k < (new Date().getTime() / 1000)) { - ready = false; - noticeOut = arrays[i][0]; - buttonOut = arrays[i][1]; - arraysOut = i; - fadeOut(noticeOut, buttonOut, arraysOut); + frame += 1; + if ((frame % 60.0) === 0) { // only update once a second + locationY = 20.0; + for (i = 0; i < arrays.length; i += 1) { //repositions overlays as others fade + nextOverlay = Overlays.getOverlayAtPoint({ x: overlayLocationX, y: locationY }); + Overlays.editOverlay(notifications[i], { x: overlayLocationX, y: locationY }); + Overlays.editOverlay(buttons[i], { x: buttonLocationX, y: locationY + 12.0 }); + if (isOnHMD) { + positions = calculate3DOverlayPositions(overlay3DDetails[i].width, + overlay3DDetails[i].height, locationY); + overlay3DDetails[i].notificationOrientation = positions.notificationOrientation; + overlay3DDetails[i].notificationPosition = positions.notificationPosition; + overlay3DDetails[i].buttonPosition = positions.buttonPosition; + } + locationY = locationY + arrays[i][3]; + } + } + + // This checks the age of the notification and prepares to fade it after 9.0 seconds (var persistTime - 1) + for (i = 0; i < arrays.length; i += 1) { + if (ready) { + j = arrays[i][2]; + k = j + persistTime; + if (k < (new Date().getTime() / 1000)) { + ready = false; + noticeOut = arrays[i][0]; + buttonOut = arrays[i][1]; + arraysOut = i; + fadeOut(noticeOut, buttonOut, arraysOut); + } } } } -} -var STARTUP_TIMEOUT = 500, // ms - startingUp = true, - startupTimer = null; + var STARTUP_TIMEOUT = 500, // ms + startingUp = true, + startupTimer = null; -function finishStartup() { - startingUp = false; - Script.clearTimeout(startupTimer); -} + function finishStartup() { + startingUp = false; + Script.clearTimeout(startupTimer); + } -function isStartingUp() { - // Is starting up until get no checks that it is starting up for STARTUP_TIMEOUT - if (startingUp) { - if (startupTimer) { - Script.clearTimeout(startupTimer); + function isStartingUp() { + // Is starting up until get no checks that it is starting up for STARTUP_TIMEOUT + if (startingUp) { + if (startupTimer) { + Script.clearTimeout(startupTimer); + } + startupTimer = Script.setTimeout(finishStartup, STARTUP_TIMEOUT); } - startupTimer = Script.setTimeout(finishStartup, STARTUP_TIMEOUT); - } - return startingUp; -} - -function onDomainConnectionRefused(reason) { - createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); -} - -function onEditError(msg) { - createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); -} - - -function onSnapshotTaken(path, notify) { - if (notify) { - var imageProperties = { - path: "file:///" + path, - aspectRatio: Window.innerWidth / Window.innerHeight - }; - createNotification(wordWrap("Snapshot saved to " + path), NotificationType.SNAPSHOT, imageProperties); - } -} - -// handles mouse clicks on buttons -function mousePressEvent(event) { - var pickRay, - clickedOverlay, - i; - - if (isOnHMD) { - pickRay = Camera.computePickRay(event.x, event.y); - clickedOverlay = Overlays.findRayIntersection(pickRay).overlayID; - } else { - clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + return startingUp; } - for (i = 0; i < buttons.length; i += 1) { - if (clickedOverlay === buttons[i]) { - deleteNotification(i); + function onDomainConnectionRefused(reason) { + createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); + } + + function onEditError(msg) { + createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); + } + + + function onSnapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, notify) { + if (notify) { + var imageProperties = { + path: "file:///" + pathStillSnapshot, + aspectRatio: Window.innerWidth / Window.innerHeight + }; + createNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), NotificationType.SNAPSHOT, imageProperties); } } -} -// Control key remains active only while key is held down -function keyReleaseEvent(key) { - if (key.key === 16777249) { - ctrlIsPressed = false; + // handles mouse clicks on buttons + function mousePressEvent(event) { + var pickRay, + clickedOverlay, + i; + + if (isOnHMD) { + pickRay = Camera.computePickRay(event.x, event.y); + clickedOverlay = Overlays.findRayIntersection(pickRay).overlayID; + } else { + clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + } + + for (i = 0; i < buttons.length; i += 1) { + if (clickedOverlay === buttons[i]) { + deleteNotification(i); + } + } } -} -// Triggers notification on specific key driven events -function keyPressEvent(key) { - if (key.key === 16777249) { - ctrlIsPressed = true; + // Control key remains active only while key is held down + function keyReleaseEvent(key) { + if (key.key === 16777249) { + ctrlIsPressed = false; + } } -} -function setup() { - Menu.addMenu(MENU_NAME); - var checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING); - checked = checked === '' ? true : checked; - Menu.addMenuItem({ - menuName: MENU_NAME, - menuItemName: PLAY_NOTIFICATION_SOUNDS_MENU_ITEM, - isCheckable: true, - isChecked: Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING) - }); - Menu.addSeparator(MENU_NAME, "Play sounds for:"); - for (var type in NotificationType.properties) { - checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + (parseInt(type) + 1)); + // Triggers notification on specific key driven events + function keyPressEvent(key) { + if (key.key === 16777249) { + ctrlIsPressed = true; + } + } + + function setup() { + Menu.addMenu(MENU_NAME); + var checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING); checked = checked === '' ? true : checked; Menu.addMenuItem({ menuName: MENU_NAME, - menuItemName: NotificationType.properties[type].text + NOTIFICATION_MENU_ITEM_POST, + menuItemName: PLAY_NOTIFICATION_SOUNDS_MENU_ITEM, isCheckable: true, - isChecked: checked + isChecked: Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING) }); + Menu.addSeparator(MENU_NAME, "Play sounds for:"); + for (var type in NotificationType.properties) { + checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + (parseInt(type) + 1)); + checked = checked === '' ? true : checked; + Menu.addMenuItem({ + menuName: MENU_NAME, + menuItemName: NotificationType.properties[type].text + NOTIFICATION_MENU_ITEM_POST, + isCheckable: true, + isChecked: checked + }); + } } -} -// When our script shuts down, we should clean up all of our overlays -function scriptEnding() { - for (var i = 0; i < notifications.length; i++) { - Overlays.deleteOverlay(notifications[i]); - Overlays.deleteOverlay(buttons[i]); + // When our script shuts down, we should clean up all of our overlays + function scriptEnding() { + for (var i = 0; i < notifications.length; i++) { + Overlays.deleteOverlay(notifications[i]); + Overlays.deleteOverlay(buttons[i]); + } + Menu.removeMenu(MENU_NAME); } - Menu.removeMenu(MENU_NAME); -} -function menuItemEvent(menuItem) { - if (menuItem === PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) { - Settings.setValue(PLAY_NOTIFICATION_SOUNDS_SETTING, Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM)); - return; + function menuItemEvent(menuItem) { + if (menuItem === PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) { + Settings.setValue(PLAY_NOTIFICATION_SOUNDS_SETTING, Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM)); + return; + } + var notificationType = NotificationType.getTypeFromMenuItem(menuItem); + if (notificationType !== notificationType.UNKNOWN) { + Settings.setValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + notificationType, Menu.isOptionChecked(menuItem)); + } } - var notificationType = NotificationType.getTypeFromMenuItem(menuItem); - if (notificationType !== notificationType.UNKNOWN) { - Settings.setValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + notificationType, Menu.isOptionChecked(menuItem)); - } -} -LODManager.LODDecreased.connect(function() { - var warningText = "\n" + - "Due to the complexity of the content, the \n" + - "level of detail has been decreased. " + - "You can now see: \n" + - LODManager.getLODFeedbackText(); + LODManager.LODDecreased.connect(function () { + var warningText = "\n" + + "Due to the complexity of the content, the \n" + + "level of detail has been decreased. " + + "You can now see: \n" + + LODManager.getLODFeedbackText(); - if (lodTextID === false) { - lodTextID = createNotification(warningText, NotificationType.LOD_WARNING); - } else { - Overlays.editOverlay(lodTextID, { text: warningText }); - } -}); + if (lodTextID === false) { + lodTextID = createNotification(warningText, NotificationType.LOD_WARNING); + } else { + Overlays.editOverlay(lodTextID, { text: warningText }); + } + }); -Controller.keyPressEvent.connect(keyPressEvent); -Controller.mousePressEvent.connect(mousePressEvent); -Controller.keyReleaseEvent.connect(keyReleaseEvent); -Script.update.connect(update); -Script.scriptEnding.connect(scriptEnding); -Menu.menuItemEvent.connect(menuItemEvent); -Window.domainConnectionRefused.connect(onDomainConnectionRefused); -Window.snapshotTaken.connect(onSnapshotTaken); -Window.notifyEditError = onEditError; + Controller.keyPressEvent.connect(keyPressEvent); + Controller.mousePressEvent.connect(mousePressEvent); + Controller.keyReleaseEvent.connect(keyReleaseEvent); + Script.update.connect(update); + Script.scriptEnding.connect(scriptEnding); + Menu.menuItemEvent.connect(menuItemEvent); + Window.domainConnectionRefused.connect(onDomainConnectionRefused); + Window.snapshotTaken.connect(onSnapshotTaken); + Window.notifyEditError = onEditError; -setup(); + setup(); }()); // END LOCAL_SCOPE diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 5eebadd02f..bdb54d313f 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -120,11 +120,11 @@ function onClicked() { // take snapshot (with no notification) Script.setTimeout(function () { - Window.takeSnapshot(false, 1.91); + Window.takeSnapshot(false, true, 1.91); }, SNAPSHOT_DELAY); } -function resetButtons(path, notify) { +function resetButtons(pathStillSnapshot, pathAnimatedSnapshot, notify) { // show overlays if they were on if (resetOverlays) { Menu.setIsOptionChecked("Overlays", true); @@ -141,7 +141,8 @@ function resetButtons(path, notify) { // last element in data array tells dialog whether we can share or not confirmShare([ - { localPath: path }, + { localPath: pathAnimatedSnapshot }, + { localPath: pathStillSnapshot }, { canShare: !!location.placename, openFeedAfterShare: shouldOpenFeedAfterShare() From adcbb0b7606d4a80daaf49a270897e7b7c534fa1 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 14 Nov 2016 18:09:00 -0800 Subject: [PATCH 31/50] Code clarity and potential bugfix --- interface/src/ui/SnapshotAnimated.cpp | 36 +++++++++--------- scripts/system/html/js/SnapshotReview.js | 47 ++++++++++++------------ 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index a9c6394426..80f0077184 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -45,50 +45,50 @@ void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathSt frame = frame.convertToFormat(QImage::Format_RGBA8888); // If this is the first frame... - if (snapshotAnimatedTimestamp == 0) + if (SnapshotAnimated::snapshotAnimatedTimestamp == 0) { // Write out the header and beginning of the GIF file - GifBegin(&(snapshotAnimatedGifWriter), qPrintable(pathAnimatedSnapshot), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + GifBegin(&(SnapshotAnimated::snapshotAnimatedGifWriter), qPrintable(pathAnimatedSnapshot), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); // Write the first to the gif - GifWriteFrame(&(snapshotAnimatedGifWriter), + GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); // Record the current frame timestamp - snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); - snapshotAnimatedFirstFrameTimestamp = snapshotAnimatedTimestamp; + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; } else { // If that was the last frame... - if ((snapshotAnimatedTimestamp - snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) + if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) { - // Reset the current frame timestamp - snapshotAnimatedTimestamp = 0; - snapshotAnimatedFirstFrameTimestamp = 0; - // Write out the end of the GIF - GifEnd(&(snapshotAnimatedGifWriter)); // Stop the snapshot QTimer - snapshotAnimatedTimer.stop(); + SnapshotAnimated::snapshotAnimatedTimer.stop(); + // Reset the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = 0; + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; + // Write out the end of the GIF + GifEnd(&(SnapshotAnimated::snapshotAnimatedGifWriter)); + // Let the dependency manager know that the snapshots have been taken. emit dm->snapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, false); - qDebug() << "still: " << pathStillSnapshot << "anim: " << pathAnimatedSnapshot; - //emit dm->snapshotTaken("C:\\Users\\Zach Fox\\Desktop\\hifi-snap-by-zfox-on-2016-11-14_17-07-33.jpg", "C:\\Users\\Zach Fox\\Desktop\\hifi-snap-by-zfox-on-2016-11-14_17-10-02.gif", false); } + // If that was an intermediate frame... else { // Variable used to determine how long the current frame took to pack qint64 framePackStartTime = QDateTime::currentMSecsSinceEpoch(); // Write the frame to the gif - GifWriteFrame(&(snapshotAnimatedGifWriter), + GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), - round(((float)(QDateTime::currentMSecsSinceEpoch() - snapshotAnimatedTimestamp + snapshotAnimatedLastWriteFrameDuration)) / 10)); + round(((float)(QDateTime::currentMSecsSinceEpoch() - SnapshotAnimated::snapshotAnimatedTimestamp + SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration)) / 10)); // Record how long it took for the current frame to pack - snapshotAnimatedLastWriteFrameDuration = QDateTime::currentMSecsSinceEpoch() - framePackStartTime; + SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = QDateTime::currentMSecsSinceEpoch() - framePackStartTime; // Record the current frame timestamp - snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); } } }); diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index ccd70c40b2..b2b7030b73 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -12,30 +12,31 @@ var paths = [], idCounter = 0, useCheckboxes; function addImage(data) { - if (data.localPath) { - var div = document.createElement("DIV"), - input = document.createElement("INPUT"), - label = document.createElement("LABEL"), - img = document.createElement("IMG"), - id = "p" + idCounter++; - function toggle() { data.share = input.checked; } - img.src = data.localPath; - div.appendChild(img); - data.share = true; - if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular. - // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. - label.setAttribute('for', id); // cannot do label.for = - input.id = id; - input.type = "checkbox"; - input.checked = (id === "p0"); - input.addEventListener('change', toggle); - div.class = "property checkbox"; - div.appendChild(input); - div.appendChild(label); - } - document.getElementById("snapshot-images").appendChild(div); - paths.push(data); + if (!data.localPath) { + return; } + var div = document.createElement("DIV"), + input = document.createElement("INPUT"), + label = document.createElement("LABEL"), + img = document.createElement("IMG"), + id = "p" + idCounter++; + function toggle() { data.share = input.checked; } + img.src = data.localPath; + div.appendChild(img); + data.share = true; + if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular. + // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. + label.setAttribute('for', id); // cannot do label.for = + input.id = id; + input.type = "checkbox"; + input.checked = (id === "p0"); + input.addEventListener('change', toggle); + div.class = "property checkbox"; + div.appendChild(input); + div.appendChild(label); + } + document.getElementById("snapshot-images").appendChild(div); + paths.push(data); } function handleShareButtons(shareMsg) { var openFeed = document.getElementById('openFeed'); From cc8bf0ce6ea1aecfb1ac2da458c9a5ab47595d32 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 15 Nov 2016 11:05:31 -0800 Subject: [PATCH 32/50] BUGS FIXED! getting super close... --- interface/src/ui/SnapshotAnimated.cpp | 98 +++++++++++++++------------ interface/src/ui/SnapshotAnimated.h | 5 +- 2 files changed, 57 insertions(+), 46 deletions(-) diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index 80f0077184..fece5c6da8 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -21,60 +21,51 @@ GifWriter SnapshotAnimated::snapshotAnimatedGifWriter; qint64 SnapshotAnimated::snapshotAnimatedTimestamp = 0; qint64 SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; qint64 SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = 0; +bool SnapshotAnimated::snapshotAnimatedTimerRunning = false; +QString SnapshotAnimated::snapshotAnimatedPath; +QString SnapshotAnimated::snapshotStillPath; -void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathStillSnapshot, float aspectRatio, Application* app, QSharedPointer dm) { +void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathStill, float aspectRatio, Application* app, QSharedPointer dm) { // If we're not in the middle of capturing an animated snapshot... - if ((snapshotAnimatedFirstFrameTimestamp == 0) && (includeAnimated)) + if ((SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) && (includeAnimated)) { - // Define the output location of the animated snapshot - QString pathAnimatedSnapshot(pathStillSnapshot); - pathAnimatedSnapshot.replace("jpg", "gif"); + // Define the output location of the still and animated snapshots. + SnapshotAnimated::snapshotStillPath = pathStill; + SnapshotAnimated::snapshotAnimatedPath = pathStill; + SnapshotAnimated::snapshotAnimatedPath.replace("jpg", "gif"); // Reset the current animated snapshot last frame duration - snapshotAnimatedLastWriteFrameDuration = SNAPSNOT_ANIMATED_INITIAL_WRITE_DURATION_MSEC; + SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = SNAPSNOT_ANIMATED_INITIAL_WRITE_DURATION_MSEC; // Ensure the snapshot timer is Precise (attempted millisecond precision) - snapshotAnimatedTimer.setTimerType(Qt::PreciseTimer); + SnapshotAnimated::snapshotAnimatedTimer.setTimerType(Qt::PreciseTimer); // Connect the snapshotAnimatedTimer QTimer to the lambda slot function - QObject::connect(&(snapshotAnimatedTimer), &QTimer::timeout, [=] { - // Get a screenshot from the display, then scale the screenshot down, - // then convert it to the image format the GIF library needs, - // then save all that to the QImage named "frame" - QImage frame(app->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); - frame = frame.convertToFormat(QImage::Format_RGBA8888); + QObject::connect(&(SnapshotAnimated::snapshotAnimatedTimer), &QTimer::timeout, [=] { + if (SnapshotAnimated::snapshotAnimatedTimerRunning) + { + // Get a screenshot from the display, then scale the screenshot down, + // then convert it to the image format the GIF library needs, + // then save all that to the QImage named "frame" + QImage frame(app->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); + frame = frame.convertToFormat(QImage::Format_RGBA8888); - // If this is the first frame... - if (SnapshotAnimated::snapshotAnimatedTimestamp == 0) - { - // Write out the header and beginning of the GIF file - GifBegin(&(SnapshotAnimated::snapshotAnimatedGifWriter), qPrintable(pathAnimatedSnapshot), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Write the first to the gif - GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), - (uint8_t*)frame.bits(), - frame.width(), - frame.height(), - SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Record the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; - } - else - { - // If that was the last frame... - if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) + // If this is the first frame... + if (SnapshotAnimated::snapshotAnimatedTimestamp == 0) { - // Stop the snapshot QTimer - SnapshotAnimated::snapshotAnimatedTimer.stop(); - // Reset the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = 0; - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; - // Write out the end of the GIF - GifEnd(&(SnapshotAnimated::snapshotAnimatedGifWriter)); - // Let the dependency manager know that the snapshots have been taken. - emit dm->snapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, false); + // Write out the header and beginning of the GIF file + GifBegin(&(SnapshotAnimated::snapshotAnimatedGifWriter), qPrintable(SnapshotAnimated::snapshotAnimatedPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Write the first to the gif + GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), + (uint8_t*)frame.bits(), + frame.width(), + frame.height(), + SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Record the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; } - // If that was an intermediate frame... + // If that was an intermediate or the final frame... else { // Variable used to determine how long the current frame took to pack @@ -89,17 +80,34 @@ void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathSt SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = QDateTime::currentMSecsSinceEpoch() - framePackStartTime; // Record the current frame timestamp SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + // If that was the last frame... + if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) + { + // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE + // that the slot will not be called again in the future. + // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html + SnapshotAnimated::snapshotAnimatedTimer.stop(); + SnapshotAnimated::snapshotAnimatedTimerRunning = false; + // Reset the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = 0; + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; + // Write out the end of the GIF + GifEnd(&(SnapshotAnimated::snapshotAnimatedGifWriter)); + // Let the dependency manager know that the snapshots have been taken. + emit dm->snapshotTaken(SnapshotAnimated::snapshotStillPath, SnapshotAnimated::snapshotAnimatedPath, false); + } } } }); // Start the snapshotAnimatedTimer QTimer - argument for this is in milliseconds - snapshotAnimatedTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); + SnapshotAnimated::snapshotAnimatedTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); + SnapshotAnimated::snapshotAnimatedTimerRunning = true; } // If we're already in the middle of capturing an animated snapshot... else { // Just tell the dependency manager that the capture of the still snapshot has taken place. - emit dm->snapshotTaken(pathStillSnapshot, "", false); + emit dm->snapshotTaken(pathStill, "", false); } } diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h index ca778341a6..d56d294241 100644 --- a/interface/src/ui/SnapshotAnimated.h +++ b/interface/src/ui/SnapshotAnimated.h @@ -37,8 +37,11 @@ private: static qint64 snapshotAnimatedTimestamp; static qint64 snapshotAnimatedFirstFrameTimestamp; static qint64 snapshotAnimatedLastWriteFrameDuration; + static bool snapshotAnimatedTimerRunning; + static QString snapshotAnimatedPath; + static QString snapshotStillPath; public: - static void saveSnapshotAnimated(bool includeAnimated, QString stillSnapshotPath, float aspectRatio, Application* app, QSharedPointer dm); + static void saveSnapshotAnimated(bool includeAnimated, QString pathStill, float aspectRatio, Application* app, QSharedPointer dm); }; #endif // hifi_SnapshotAnimated_h From 5e8afb04c7b2402b54f58acf6b22280ad9ef51aa Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 15 Nov 2016 11:56:13 -0800 Subject: [PATCH 33/50] Potentially fix OSX and Linux build errors? --- cmake/modules/FindGifCreator.cmake | 4 ++-- interface/CMakeLists.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmake/modules/FindGifCreator.cmake b/cmake/modules/FindGifCreator.cmake index 88428cb833..def9f1d131 100644 --- a/cmake/modules/FindGifCreator.cmake +++ b/cmake/modules/FindGifCreator.cmake @@ -6,7 +6,7 @@ # # GIFCREATOR_INCLUDE_DIRS # -# Created on 11/10/2016 by Zach Fox +# Created on 11/15/2016 by Zach Fox # Copyright 2016 High Fidelity, Inc. # # Distributed under the Apache License, Version 2.0. @@ -15,7 +15,7 @@ # setup hints for GifCreator search include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") -hifi_library_search_hints("GifCreator") +hifi_library_search_hints("GIFCREATOR") # locate header find_path(GIFCREATOR_INCLUDE_DIRS "GifCreator/GifCreator.h" HINTS ${GIFCREATOR_SEARCH_DIRS}) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 56e83a3171..e32df6bc62 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -353,5 +353,5 @@ if (ANDROID) endif () add_dependency_external_projects(GifCreator) -find_package(GIFCREATOR REQUIRED) +find_package(GifCreator REQUIRED) target_include_directories(${TARGET_NAME} PUBLIC ${GIFCREATOR_INCLUDE_DIRS}) From ad5c3e6f155936ca1688356a6ae1cb1c8810ae15 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 15 Nov 2016 12:14:22 -0800 Subject: [PATCH 34/50] Decrease animated snapshot resolution --- interface/src/ui/SnapshotAnimated.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h index d56d294241..0c7faa1a8c 100644 --- a/interface/src/ui/SnapshotAnimated.h +++ b/interface/src/ui/SnapshotAnimated.h @@ -20,7 +20,7 @@ // If the snapshot width or the framerate are too high for the // application to handle, the framerate of the output GIF will drop. -#define SNAPSNOT_ANIMATED_WIDTH (720) +#define SNAPSNOT_ANIMATED_WIDTH (480) // This value should divide evenly into 100. Snapshot framerate is NOT guaranteed. #define SNAPSNOT_ANIMATED_TARGET_FRAMERATE (25) #define SNAPSNOT_ANIMATED_DURATION_SECS (3) From 2c0cfdf2419d420127046628cc0d60c9f48c3047 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 15 Nov 2016 13:12:48 -0800 Subject: [PATCH 35/50] Fix build for real? --- cmake/externals/GifCreator/CMakeLists.txt | 2 +- interface/src/Application.h | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cmake/externals/GifCreator/CMakeLists.txt b/cmake/externals/GifCreator/CMakeLists.txt index f3f4e6d2ad..127bdf28f5 100644 --- a/cmake/externals/GifCreator/CMakeLists.txt +++ b/cmake/externals/GifCreator/CMakeLists.txt @@ -17,4 +17,4 @@ set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) -set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/src/${EXTERNAL_NAME_UPPER} CACHE PATH "List of GifCreator include directories") \ No newline at end of file +set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/src/${EXTERNAL_NAME} CACHE PATH "List of GifCreator include directories") \ No newline at end of file diff --git a/interface/src/Application.h b/interface/src/Application.h index 5397420497..8f8b42d66a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -28,7 +28,6 @@ #include #include #include -#include #include #include #include From 20a2d1275ad2abbdb889394a7286b659e5eed00d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 15 Nov 2016 16:26:43 -0800 Subject: [PATCH 36/50] Bugfixes and GIF uploads workinggit add -A! --- interface/src/Application.cpp | 9 +++- interface/src/ui/Snapshot.cpp | 26 ++++++++--- interface/src/ui/SnapshotAnimated.cpp | 57 ++++++++++-------------- interface/src/ui/SnapshotAnimated.h | 3 +- scripts/system/html/js/SnapshotReview.js | 4 +- scripts/system/snapshot.js | 2 +- 6 files changed, 57 insertions(+), 44 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b21a27977e..d71cc12858 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5440,7 +5440,14 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa // Get a screenshot and save it QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - SnapshotAnimated::saveSnapshotAnimated(includeAnimated, path, aspectRatio, qApp, DependencyManager::get()); + // If we're not doing an animated snapshot as well... + if (!includeAnimated) { + // Tell the dependency manager that the capture of the still snapshot has taken place. + emit DependencyManager::get()->snapshotTaken(path, "", notify); + } else { + // Get an animated GIF snapshot and save it + SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, qApp, DependencyManager::get()); + } }); } void Application::shareSnapshot(const QString& path) { diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index 1bf5f5de4e..5df0d4575b 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -51,16 +51,24 @@ SnapshotMetaData* Snapshot::parseSnapshotData(QString snapshotPath) { return NULL; } - QImage shot(snapshotPath); + QUrl url; - // no location data stored - if (shot.text(URL).isEmpty()) { + if (snapshotPath.right(3) == "jpg") { + QImage shot(snapshotPath); + + // no location data stored + if (shot.text(URL).isEmpty()) { + return NULL; + } + + // parsing URL + url = QUrl(shot.text(URL), QUrl::ParsingMode::StrictMode); + } else if (snapshotPath.right(3) == "gif") { + url = QUrl(DependencyManager::get()->currentShareableAddress()); + } else { return NULL; } - // parsing URL - QUrl url = QUrl(shot.text(URL), QUrl::ParsingMode::StrictMode); - SnapshotMetaData* data = new SnapshotMetaData(); data->setURL(url); @@ -156,7 +164,11 @@ void Snapshot::uploadSnapshot(const QString& filename) { file->open(QIODevice::ReadOnly); QHttpPart imagePart; - imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg")); + if (filename.right(3) == "gif") { + imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/gif")); + } else { + imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg")); + } imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"image\"; filename=\"" + file->fileName() + "\"")); imagePart.setBodyDevice(file); diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index fece5c6da8..0d2c6707a5 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -25,10 +25,9 @@ bool SnapshotAnimated::snapshotAnimatedTimerRunning = false; QString SnapshotAnimated::snapshotAnimatedPath; QString SnapshotAnimated::snapshotStillPath; -void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathStill, float aspectRatio, Application* app, QSharedPointer dm) { +void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm) { // If we're not in the middle of capturing an animated snapshot... - if ((SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) && (includeAnimated)) - { + if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { // Define the output location of the still and animated snapshots. SnapshotAnimated::snapshotStillPath = pathStill; SnapshotAnimated::snapshotAnimatedPath = pathStill; @@ -41,33 +40,15 @@ void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathSt // Connect the snapshotAnimatedTimer QTimer to the lambda slot function QObject::connect(&(SnapshotAnimated::snapshotAnimatedTimer), &QTimer::timeout, [=] { - if (SnapshotAnimated::snapshotAnimatedTimerRunning) - { + if (SnapshotAnimated::snapshotAnimatedTimerRunning) { // Get a screenshot from the display, then scale the screenshot down, // then convert it to the image format the GIF library needs, // then save all that to the QImage named "frame" QImage frame(app->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); - frame = frame.convertToFormat(QImage::Format_RGBA8888); + frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); - // If this is the first frame... - if (SnapshotAnimated::snapshotAnimatedTimestamp == 0) - { - // Write out the header and beginning of the GIF file - GifBegin(&(SnapshotAnimated::snapshotAnimatedGifWriter), qPrintable(SnapshotAnimated::snapshotAnimatedPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Write the first to the gif - GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), - (uint8_t*)frame.bits(), - frame.width(), - frame.height(), - SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Record the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; - } - // If that was an intermediate or the final frame... - else - { + // If this is an intermediate or the final frame... + if (SnapshotAnimated::snapshotAnimatedTimestamp > 0) { // Variable used to determine how long the current frame took to pack qint64 framePackStartTime = QDateTime::currentMSecsSinceEpoch(); // Write the frame to the gif @@ -75,14 +56,13 @@ void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathSt (uint8_t*)frame.bits(), frame.width(), frame.height(), - round(((float)(QDateTime::currentMSecsSinceEpoch() - SnapshotAnimated::snapshotAnimatedTimestamp + SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration)) / 10)); - // Record how long it took for the current frame to pack - SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = QDateTime::currentMSecsSinceEpoch() - framePackStartTime; + round(((float)(framePackStartTime - SnapshotAnimated::snapshotAnimatedTimestamp + SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration)) / 10)); // Record the current frame timestamp SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + // Record how long it took for the current frame to pack + SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = SnapshotAnimated::snapshotAnimatedTimestamp - framePackStartTime; // If that was the last frame... - if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) - { + if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_MSEC)) { // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE // that the slot will not be called again in the future. // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html @@ -96,6 +76,19 @@ void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathSt // Let the dependency manager know that the snapshots have been taken. emit dm->snapshotTaken(SnapshotAnimated::snapshotStillPath, SnapshotAnimated::snapshotAnimatedPath, false); } + // If that was the first frame... + } else { + // Write out the header and beginning of the GIF file + GifBegin(&(SnapshotAnimated::snapshotAnimatedGifWriter), qPrintable(SnapshotAnimated::snapshotAnimatedPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Write the first to the gif + GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), + (uint8_t*)frame.bits(), + frame.width(), + frame.height(), + SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Record the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; } } }); @@ -103,10 +96,8 @@ void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathSt // Start the snapshotAnimatedTimer QTimer - argument for this is in milliseconds SnapshotAnimated::snapshotAnimatedTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); SnapshotAnimated::snapshotAnimatedTimerRunning = true; - } // If we're already in the middle of capturing an animated snapshot... - else - { + } else { // Just tell the dependency manager that the capture of the still snapshot has taken place. emit dm->snapshotTaken(pathStill, "", false); } diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h index 0c7faa1a8c..4de7c339dd 100644 --- a/interface/src/ui/SnapshotAnimated.h +++ b/interface/src/ui/SnapshotAnimated.h @@ -24,6 +24,7 @@ // This value should divide evenly into 100. Snapshot framerate is NOT guaranteed. #define SNAPSNOT_ANIMATED_TARGET_FRAMERATE (25) #define SNAPSNOT_ANIMATED_DURATION_SECS (3) +#define SNAPSNOT_ANIMATED_DURATION_MSEC (SNAPSNOT_ANIMATED_DURATION_SECS*1000) #define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_TARGET_FRAMERATE) // This is the fudge factor that we add to the *first* GIF frame's "delay" value @@ -41,7 +42,7 @@ private: static QString snapshotAnimatedPath; static QString snapshotStillPath; public: - static void saveSnapshotAnimated(bool includeAnimated, QString pathStill, float aspectRatio, Application* app, QSharedPointer dm); + static void saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm); }; #endif // hifi_SnapshotAnimated_h diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index b2b7030b73..a1bb350789 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -23,17 +23,19 @@ function addImage(data) { function toggle() { data.share = input.checked; } img.src = data.localPath; div.appendChild(img); - data.share = true; if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular. // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. label.setAttribute('for', id); // cannot do label.for = input.id = id; input.type = "checkbox"; input.checked = (id === "p0"); + data.share = input.checked; input.addEventListener('change', toggle); div.class = "property checkbox"; div.appendChild(input); div.appendChild(label); + } else { + data.share = true; } document.getElementById("snapshot-images").appendChild(div); paths.push(data); diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index bdb54d313f..b4ebb99ef0 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -36,7 +36,7 @@ var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html"); var outstanding; function confirmShare(data) { - var dialog = new OverlayWebWindow('Snapshot Review', SNAPSHOT_REVIEW_URL, 800, 320); + var dialog = new OverlayWebWindow('Snapshot Review', SNAPSHOT_REVIEW_URL, 800, 520); function onMessage(message) { // Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following: // 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.) From 35d075a78fa90266d8ee1ee7cd3e11f04b8eba67 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 17 Nov 2016 09:51:45 -0800 Subject: [PATCH 37/50] Fix notifications.js formatting --- scripts/system/notifications.js | 1022 +++++++++++++++---------------- 1 file changed, 510 insertions(+), 512 deletions(-) diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 4c16a637bf..d2589cb72f 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -58,583 +58,581 @@ /* global Script, Controller, Overlays, SoundArray, Quat, Vec3, MyAvatar, Menu, HMD, AudioDevice, LODManager, Settings, Camera */ -(function () { // BEGIN LOCAL_SCOPE +(function() { // BEGIN LOCAL_SCOPE - Script.include("./libraries/soundArray.js"); +Script.include("./libraries/soundArray.js"); - var width = 340.0; //width of notification overlay - var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window - var overlayLocationX = (windowDimensions.x - (width + 20.0)); // positions window 20px from the right of the interface window - var buttonLocationX = overlayLocationX + (width - 28.0); - var locationY = 20.0; // position down from top of interface window - var topMargin = 13.0; - var leftMargin = 10.0; - var textColor = { red: 228, green: 228, blue: 228 }; // text color - var backColor = { red: 2, green: 2, blue: 2 }; // background color was 38,38,38 - var backgroundAlpha = 0; - var fontSize = 12.0; - var PERSIST_TIME_2D = 10.0; // Time in seconds before notification fades - var PERSIST_TIME_3D = 15.0; - var persistTime = PERSIST_TIME_2D; - var frame = 0; - var ourWidth = Window.innerWidth; - var ourHeight = Window.innerHeight; - var ctrlIsPressed = false; - var ready = true; - var MENU_NAME = 'Tools > Notifications'; - var PLAY_NOTIFICATION_SOUNDS_MENU_ITEM = "Play Notification Sounds"; - var NOTIFICATION_MENU_ITEM_POST = " Notifications"; - var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; - var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; - var lodTextID = false; +var width = 340.0; //width of notification overlay +var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window +var overlayLocationX = (windowDimensions.x - (width + 20.0)); // positions window 20px from the right of the interface window +var buttonLocationX = overlayLocationX + (width - 28.0); +var locationY = 20.0; // position down from top of interface window +var topMargin = 13.0; +var leftMargin = 10.0; +var textColor = { red: 228, green: 228, blue: 228}; // text color +var backColor = { red: 2, green: 2, blue: 2}; // background color was 38,38,38 +var backgroundAlpha = 0; +var fontSize = 12.0; +var PERSIST_TIME_2D = 10.0; // Time in seconds before notification fades +var PERSIST_TIME_3D = 15.0; +var persistTime = PERSIST_TIME_2D; +var frame = 0; +var ourWidth = Window.innerWidth; +var ourHeight = Window.innerHeight; +var ctrlIsPressed = false; +var ready = true; +var MENU_NAME = 'Tools > Notifications'; +var PLAY_NOTIFICATION_SOUNDS_MENU_ITEM = "Play Notification Sounds"; +var NOTIFICATION_MENU_ITEM_POST = " Notifications"; +var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; +var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; +var lodTextID = false; - var NotificationType = { - UNKNOWN: 0, - SNAPSHOT: 1, - LOD_WARNING: 2, - CONNECTION_REFUSED: 3, - EDIT_ERROR: 4, - properties: [ - { text: "Snapshot" }, - { text: "Level of Detail" }, - { text: "Connection Refused" }, - { text: "Edit error" } - ], - getTypeFromMenuItem: function (menuItemName) { - if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { - return NotificationType.UNKNOWN; - } - var preMenuItemName = menuItemName.substr(0, menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length); - for (var type in this.properties) { - if (this.properties[type].text === preMenuItemName) { - return parseInt(type) + 1; - } - } +var NotificationType = { + UNKNOWN: 0, + SNAPSHOT: 1, + LOD_WARNING: 2, + CONNECTION_REFUSED: 3, + EDIT_ERROR: 4, + properties: [ + { text: "Snapshot" }, + { text: "Level of Detail" }, + { text: "Connection Refused" }, + { text: "Edit error" } + ], + getTypeFromMenuItem: function(menuItemName) { + if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { return NotificationType.UNKNOWN; - }, - getMenuString: function (type) { - return this.properties[type - 1].text + NOTIFICATION_MENU_ITEM_POST; } + var preMenuItemName = menuItemName.substr(0, menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length); + for (var type in this.properties) { + if (this.properties[type].text === preMenuItemName) { + return parseInt(type) + 1; + } + } + return NotificationType.UNKNOWN; + }, + getMenuString: function(type) { + return this.properties[type - 1].text + NOTIFICATION_MENU_ITEM_POST; + } +}; + +var randomSounds = new SoundArray({ localOnly: true }, true); +var numberOfSounds = 2; +for (var i = 1; i <= numberOfSounds; i++) { + randomSounds.addSound(Script.resolvePath("assets/sounds/notification-general"+ i + ".raw")); +} + +var notifications = []; +var buttons = []; +var times = []; +var heights = []; +var myAlpha = []; +var arrays = []; +var isOnHMD = false, + NOTIFICATIONS_3D_DIRECTION = 0.0, // Degrees from avatar orientation. + NOTIFICATIONS_3D_DISTANCE = 0.6, // Horizontal distance from avatar position. + NOTIFICATIONS_3D_ELEVATION = -0.8, // Height of top middle of top notification relative to avatar eyes. + NOTIFICATIONS_3D_YAW = 0.0, // Degrees relative to notifications direction. + NOTIFICATIONS_3D_PITCH = -60.0, // Degrees from vertical. + NOTIFICATION_3D_SCALE = 0.002, // Multiplier that converts 2D overlay dimensions to 3D overlay dimensions. + NOTIFICATION_3D_BUTTON_WIDTH = 40 * NOTIFICATION_3D_SCALE, // Need a little more room for button in 3D. + overlay3DDetails = []; + +// push data from above to the 2 dimensional array +function createArrays(notice, button, createTime, height, myAlpha) { + arrays.push([notice, button, createTime, height, myAlpha]); +} + +// This handles the final dismissal of a notification after fading +function dismiss(firstNoteOut, firstButOut, firstOut) { + if (firstNoteOut == lodTextID) { + lodTextID = false; + } + + Overlays.deleteOverlay(firstNoteOut); + Overlays.deleteOverlay(firstButOut); + notifications.splice(firstOut, 1); + buttons.splice(firstOut, 1); + times.splice(firstOut, 1); + heights.splice(firstOut, 1); + myAlpha.splice(firstOut, 1); + overlay3DDetails.splice(firstOut, 1); +} + +function fadeIn(noticeIn, buttonIn) { + var q = 0, + qFade, + pauseTimer = null; + + pauseTimer = Script.setInterval(function () { + q += 1; + qFade = q / 10.0; + Overlays.editOverlay(noticeIn, { alpha: qFade }); + Overlays.editOverlay(buttonIn, { alpha: qFade }); + if (q >= 9.0) { + Script.clearInterval(pauseTimer); + } + }, 10); +} + +// this fades the notification ready for dismissal, and removes it from the arrays +function fadeOut(noticeOut, buttonOut, arraysOut) { + var r = 9.0, + rFade, + pauseTimer = null; + + pauseTimer = Script.setInterval(function () { + r -= 1; + rFade = r / 10.0; + Overlays.editOverlay(noticeOut, { alpha: rFade }); + Overlays.editOverlay(buttonOut, { alpha: rFade }); + if (r < 0) { + dismiss(noticeOut, buttonOut, arraysOut); + arrays.splice(arraysOut, 1); + ready = true; + Script.clearInterval(pauseTimer); + } + }, 20); +} + +function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) { + // Calculates overlay positions and orientations in avatar coordinates. + var noticeY, + originOffset, + notificationOrientation, + notificationPosition, + buttonPosition; + + // Notification plane positions + noticeY = -y * NOTIFICATION_3D_SCALE - noticeHeight / 2; + notificationPosition = { x: 0, y: noticeY, z: 0 }; + buttonPosition = { x: (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH) / 2, y: noticeY, z: 0.001 }; + + // Rotate plane + notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, + NOTIFICATIONS_3D_DIRECTION + NOTIFICATIONS_3D_YAW, 0); + notificationPosition = Vec3.multiplyQbyV(notificationOrientation, notificationPosition); + buttonPosition = Vec3.multiplyQbyV(notificationOrientation, buttonPosition); + + // Translate plane + originOffset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, NOTIFICATIONS_3D_DIRECTION, 0), + { x: 0, y: 0, z: -NOTIFICATIONS_3D_DISTANCE }); + originOffset.y += NOTIFICATIONS_3D_ELEVATION; + notificationPosition = Vec3.sum(originOffset, notificationPosition); + buttonPosition = Vec3.sum(originOffset, buttonPosition); + + return { + notificationOrientation: notificationOrientation, + notificationPosition: notificationPosition, + buttonPosition: buttonPosition }; +} - var randomSounds = new SoundArray({ localOnly: true }, true); - var numberOfSounds = 2; - for (var i = 1; i <= numberOfSounds; i++) { - randomSounds.addSound(Script.resolvePath("assets/sounds/notification-general" + i + ".raw")); - } +// Pushes data to each array and sets up data for 2nd dimension array +// to handle auxiliary data not carried by the overlay class +// specifically notification "heights", "times" of creation, and . +function notify(notice, button, height, imageProperties, image) { + var notificationText, + noticeWidth, + noticeHeight, + positions, + last; - var notifications = []; - var buttons = []; - var times = []; - var heights = []; - var myAlpha = []; - var arrays = []; - var isOnHMD = false, - NOTIFICATIONS_3D_DIRECTION = 0.0, // Degrees from avatar orientation. - NOTIFICATIONS_3D_DISTANCE = 0.6, // Horizontal distance from avatar position. - NOTIFICATIONS_3D_ELEVATION = -0.8, // Height of top middle of top notification relative to avatar eyes. - NOTIFICATIONS_3D_YAW = 0.0, // Degrees relative to notifications direction. - NOTIFICATIONS_3D_PITCH = -60.0, // Degrees from vertical. - NOTIFICATION_3D_SCALE = 0.002, // Multiplier that converts 2D overlay dimensions to 3D overlay dimensions. - NOTIFICATION_3D_BUTTON_WIDTH = 40 * NOTIFICATION_3D_SCALE, // Need a little more room for button in 3D. - overlay3DDetails = []; + if (isOnHMD) { + // Calculate 3D values from 2D overlay properties. - // push data from above to the 2 dimensional array - function createArrays(notice, button, createTime, height, myAlpha) { - arrays.push([notice, button, createTime, height, myAlpha]); - } + noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; + noticeHeight = notice.height * NOTIFICATION_3D_SCALE; - // This handles the final dismissal of a notification after fading - function dismiss(firstNoteOut, firstButOut, firstOut) { - if (firstNoteOut == lodTextID) { - lodTextID = false; + notice.size = { x: noticeWidth, y: noticeHeight }; + + positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); + + notice.parentID = MyAvatar.sessionUUID; + notice.parentJointIndex = -2; + + if (!image) { + notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; + notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; + notice.bottomMargin = 0; + notice.rightMargin = 0; + notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; + notice.isFacingAvatar = false; + + notificationText = Overlays.addOverlay("text3d", notice); + notifications.push(notificationText); + } else { + notifications.push(Overlays.addOverlay("image3d", notice)); } - Overlays.deleteOverlay(firstNoteOut); - Overlays.deleteOverlay(firstButOut); - notifications.splice(firstOut, 1); - buttons.splice(firstOut, 1); - times.splice(firstOut, 1); - heights.splice(firstOut, 1); - myAlpha.splice(firstOut, 1); - overlay3DDetails.splice(firstOut, 1); - } + button.url = button.imageURL; + button.scale = button.width * NOTIFICATION_3D_SCALE; + button.isFacingAvatar = false; + button.parentID = MyAvatar.sessionUUID; + button.parentJointIndex = -2; - function fadeIn(noticeIn, buttonIn) { - var q = 0, - qFade, - pauseTimer = null; + buttons.push((Overlays.addOverlay("image3d", button))); + overlay3DDetails.push({ + notificationOrientation: positions.notificationOrientation, + notificationPosition: positions.notificationPosition, + buttonPosition: positions.buttonPosition, + width: noticeWidth, + height: noticeHeight + }); - pauseTimer = Script.setInterval(function () { - q += 1; - qFade = q / 10.0; - Overlays.editOverlay(noticeIn, { alpha: qFade }); - Overlays.editOverlay(buttonIn, { alpha: qFade }); - if (q >= 9.0) { - Script.clearInterval(pauseTimer); - } - }, 10); - } - // this fades the notification ready for dismissal, and removes it from the arrays - function fadeOut(noticeOut, buttonOut, arraysOut) { - var r = 9.0, - rFade, - pauseTimer = null; - - pauseTimer = Script.setInterval(function () { - r -= 1; - rFade = r / 10.0; - Overlays.editOverlay(noticeOut, { alpha: rFade }); - Overlays.editOverlay(buttonOut, { alpha: rFade }); - if (r < 0) { - dismiss(noticeOut, buttonOut, arraysOut); - arrays.splice(arraysOut, 1); - ready = true; - Script.clearInterval(pauseTimer); - } - }, 20); - } - - function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) { - // Calculates overlay positions and orientations in avatar coordinates. - var noticeY, - originOffset, - notificationOrientation, + var defaultEyePosition, + avatarOrientation, notificationPosition, + notificationOrientation, buttonPosition; - // Notification plane positions - noticeY = -y * NOTIFICATION_3D_SCALE - noticeHeight / 2; - notificationPosition = { x: 0, y: noticeY, z: 0 }; - buttonPosition = { x: (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH) / 2, y: noticeY, z: 0.001 }; + if (isOnHMD && notifications.length > 0) { + // Update 3D overlays to maintain positions relative to avatar + defaultEyePosition = MyAvatar.getDefaultEyePosition(); + avatarOrientation = MyAvatar.orientation; - // Rotate plane - notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, - NOTIFICATIONS_3D_DIRECTION + NOTIFICATIONS_3D_YAW, 0); - notificationPosition = Vec3.multiplyQbyV(notificationOrientation, notificationPosition); - buttonPosition = Vec3.multiplyQbyV(notificationOrientation, buttonPosition); - - // Translate plane - originOffset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, NOTIFICATIONS_3D_DIRECTION, 0), - { x: 0, y: 0, z: -NOTIFICATIONS_3D_DISTANCE }); - originOffset.y += NOTIFICATIONS_3D_ELEVATION; - notificationPosition = Vec3.sum(originOffset, notificationPosition); - buttonPosition = Vec3.sum(originOffset, buttonPosition); - - return { - notificationOrientation: notificationOrientation, - notificationPosition: notificationPosition, - buttonPosition: buttonPosition - }; - } - - // Pushes data to each array and sets up data for 2nd dimension array - // to handle auxiliary data not carried by the overlay class - // specifically notification "heights", "times" of creation, and . - function notify(notice, button, height, imageProperties, image) { - var notificationText, - noticeWidth, - noticeHeight, - positions, - last; - - if (isOnHMD) { - // Calculate 3D values from 2D overlay properties. - - noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; - noticeHeight = notice.height * NOTIFICATION_3D_SCALE; - - notice.size = { x: noticeWidth, y: noticeHeight }; - - positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); - - notice.parentID = MyAvatar.sessionUUID; - notice.parentJointIndex = -2; - - if (!image) { - notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; - notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; - notice.bottomMargin = 0; - notice.rightMargin = 0; - notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; - notice.isFacingAvatar = false; - - notificationText = Overlays.addOverlay("text3d", notice); - notifications.push(notificationText); - } else { - notifications.push(Overlays.addOverlay("image3d", notice)); - } - - button.url = button.imageURL; - button.scale = button.width * NOTIFICATION_3D_SCALE; - button.isFacingAvatar = false; - button.parentID = MyAvatar.sessionUUID; - button.parentJointIndex = -2; - - buttons.push((Overlays.addOverlay("image3d", button))); - overlay3DDetails.push({ - notificationOrientation: positions.notificationOrientation, - notificationPosition: positions.notificationPosition, - buttonPosition: positions.buttonPosition, - width: noticeWidth, - height: noticeHeight - }); - - - var defaultEyePosition, - avatarOrientation, - notificationPosition, - notificationOrientation, - buttonPosition; - - if (isOnHMD && notifications.length > 0) { - // Update 3D overlays to maintain positions relative to avatar - defaultEyePosition = MyAvatar.getDefaultEyePosition(); - avatarOrientation = MyAvatar.orientation; - - for (i = 0; i < notifications.length; i += 1) { - notificationPosition = Vec3.sum(defaultEyePosition, - Vec3.multiplyQbyV(avatarOrientation, - overlay3DDetails[i].notificationPosition)); - notificationOrientation = Quat.multiply(avatarOrientation, - overlay3DDetails[i].notificationOrientation); - buttonPosition = Vec3.sum(defaultEyePosition, - Vec3.multiplyQbyV(avatarOrientation, - overlay3DDetails[i].buttonPosition)); - Overlays.editOverlay(notifications[i], { - position: notificationPosition, - rotation: notificationOrientation - }); - Overlays.editOverlay(buttons[i], { position: buttonPosition, rotation: notificationOrientation }); - } + for (i = 0; i < notifications.length; i += 1) { + notificationPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, + overlay3DDetails[i].notificationPosition)); + notificationOrientation = Quat.multiply(avatarOrientation, + overlay3DDetails[i].notificationOrientation); + buttonPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, + overlay3DDetails[i].buttonPosition)); + Overlays.editOverlay(notifications[i], { position: notificationPosition, + rotation: notificationOrientation }); + Overlays.editOverlay(buttons[i], { position: buttonPosition, rotation: notificationOrientation }); } + } + } else { + if (!image) { + notificationText = Overlays.addOverlay("text", notice); + notifications.push((notificationText)); } else { - if (!image) { - notificationText = Overlays.addOverlay("text", notice); - notifications.push((notificationText)); - } else { - notifications.push(Overlays.addOverlay("image", notice)); - } - buttons.push(Overlays.addOverlay("image", button)); + notifications.push(Overlays.addOverlay("image", notice)); } - - height = height + 1.0; - heights.push(height); - times.push(new Date().getTime() / 1000); - last = notifications.length - 1; - myAlpha.push(notifications[last].alpha); - createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); - fadeIn(notifications[last], buttons[last]); - - if (imageProperties && !image) { - var imageHeight = notice.width / imageProperties.aspectRatio; - notice = { - x: notice.x, - y: notice.y + height, - width: notice.width, - height: imageHeight, - subImage: { x: 0, y: 0 }, - color: { red: 255, green: 255, blue: 255 }, - visible: true, - imageURL: imageProperties.path, - alpha: backgroundAlpha - }; - notify(notice, button, imageHeight, imageProperties, true); - } - - return notificationText; + buttons.push(Overlays.addOverlay("image", button)); } - var CLOSE_NOTIFICATION_ICON = Script.resolvePath("assets/images/close-small-light.svg"); + height = height + 1.0; + heights.push(height); + times.push(new Date().getTime() / 1000); + last = notifications.length - 1; + myAlpha.push(notifications[last].alpha); + createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); + fadeIn(notifications[last], buttons[last]); - // This function creates and sizes the overlays - function createNotification(text, notificationType, imageProperties) { - var count = (text.match(/\n/g) || []).length, - breakPoint = 43.0, // length when new line is added - extraLine = 0, - breaks = 0, - height = 40.0, - stack = 0, - level, - noticeProperties, - bLevel, - buttonProperties, - i; - - if (text.length >= breakPoint) { - breaks = count; - } - extraLine = breaks * 16.0; - for (i = 0; i < heights.length; i += 1) { - stack = stack + heights[i]; - } - - level = (stack + 20.0); - height = height + extraLine; - - noticeProperties = { - x: overlayLocationX, - y: level, - width: width, - height: height, - color: textColor, - backgroundColor: backColor, - alpha: backgroundAlpha, - topMargin: topMargin, - leftMargin: leftMargin, - font: { size: fontSize }, - text: text - }; - - bLevel = level + 12.0; - buttonProperties = { - x: buttonLocationX, - y: bLevel, - width: 10.0, - height: 10.0, - subImage: { x: 0, y: 0, width: 10, height: 10 }, - imageURL: CLOSE_NOTIFICATION_ICON, - color: { red: 255, green: 255, blue: 255 }, + if (imageProperties && !image) { + var imageHeight = notice.width / imageProperties.aspectRatio; + notice = { + x: notice.x, + y: notice.y + height, + width: notice.width, + height: imageHeight, + subImage: { x: 0, y: 0 }, + color: { red: 255, green: 255, blue: 255}, visible: true, + imageURL: imageProperties.path, alpha: backgroundAlpha }; + notify(notice, button, imageHeight, imageProperties, true); + } - if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) && - Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) { - randomSounds.playRandom(); + return notificationText; +} + +var CLOSE_NOTIFICATION_ICON = Script.resolvePath("assets/images/close-small-light.svg"); + +// This function creates and sizes the overlays +function createNotification(text, notificationType, imageProperties) { + var count = (text.match(/\n/g) || []).length, + breakPoint = 43.0, // length when new line is added + extraLine = 0, + breaks = 0, + height = 40.0, + stack = 0, + level, + noticeProperties, + bLevel, + buttonProperties, + i; + + if (text.length >= breakPoint) { + breaks = count; + } + extraLine = breaks * 16.0; + for (i = 0; i < heights.length; i += 1) { + stack = stack + heights[i]; + } + + level = (stack + 20.0); + height = height + extraLine; + + noticeProperties = { + x: overlayLocationX, + y: level, + width: width, + height: height, + color: textColor, + backgroundColor: backColor, + alpha: backgroundAlpha, + topMargin: topMargin, + leftMargin: leftMargin, + font: {size: fontSize}, + text: text + }; + + bLevel = level + 12.0; + buttonProperties = { + x: buttonLocationX, + y: bLevel, + width: 10.0, + height: 10.0, + subImage: { x: 0, y: 0, width: 10, height: 10 }, + imageURL: CLOSE_NOTIFICATION_ICON, + color: { red: 255, green: 255, blue: 255}, + visible: true, + alpha: backgroundAlpha + }; + + if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) && + Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) { + randomSounds.playRandom(); + } + + return notify(noticeProperties, buttonProperties, height, imageProperties); +} + +function deleteNotification(index) { + var notificationTextID = notifications[index]; + if (notificationTextID == lodTextID) { + lodTextID = false; + } + Overlays.deleteOverlay(notificationTextID); + Overlays.deleteOverlay(buttons[index]); + notifications.splice(index, 1); + buttons.splice(index, 1); + times.splice(index, 1); + heights.splice(index, 1); + myAlpha.splice(index, 1); + overlay3DDetails.splice(index, 1); + arrays.splice(index, 1); +} + +// wraps whole word to newline +function stringDivider(str, slotWidth, spaceReplacer) { + var left, right; + + if (str.length > slotWidth && slotWidth > 0) { + left = str.substring(0, slotWidth); + right = str.substring(slotWidth); + return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); + } + return str; +} + +// formats string to add newline every 43 chars +function wordWrap(str) { + return stringDivider(str, 43.0, "\n"); +} + +function update() { + var nextOverlay, + noticeOut, + buttonOut, + arraysOut, + positions, + i, + j, + k; + + if (isOnHMD !== HMD.active) { + while (arrays.length > 0) { + deleteNotification(0); } - - return notify(noticeProperties, buttonProperties, height, imageProperties); + isOnHMD = !isOnHMD; + persistTime = isOnHMD ? PERSIST_TIME_3D : PERSIST_TIME_2D; + return; } - function deleteNotification(index) { - var notificationTextID = notifications[index]; - if (notificationTextID == lodTextID) { - lodTextID = false; - } - Overlays.deleteOverlay(notificationTextID); - Overlays.deleteOverlay(buttons[index]); - notifications.splice(index, 1); - buttons.splice(index, 1); - times.splice(index, 1); - heights.splice(index, 1); - myAlpha.splice(index, 1); - overlay3DDetails.splice(index, 1); - arrays.splice(index, 1); - } - - // wraps whole word to newline - function stringDivider(str, slotWidth, spaceReplacer) { - var left, right; - - if (str.length > slotWidth && slotWidth > 0) { - left = str.substring(0, slotWidth); - right = str.substring(slotWidth); - return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); - } - return str; - } - - // formats string to add newline every 43 chars - function wordWrap(str) { - return stringDivider(str, 43.0, "\n"); - } - - function update() { - var nextOverlay, - noticeOut, - buttonOut, - arraysOut, - positions, - i, - j, - k; - - if (isOnHMD !== HMD.active) { - while (arrays.length > 0) { - deleteNotification(0); + frame += 1; + if ((frame % 60.0) === 0) { // only update once a second + locationY = 20.0; + for (i = 0; i < arrays.length; i += 1) { //repositions overlays as others fade + nextOverlay = Overlays.getOverlayAtPoint({ x: overlayLocationX, y: locationY }); + Overlays.editOverlay(notifications[i], { x: overlayLocationX, y: locationY }); + Overlays.editOverlay(buttons[i], { x: buttonLocationX, y: locationY + 12.0 }); + if (isOnHMD) { + positions = calculate3DOverlayPositions(overlay3DDetails[i].width, + overlay3DDetails[i].height, locationY); + overlay3DDetails[i].notificationOrientation = positions.notificationOrientation; + overlay3DDetails[i].notificationPosition = positions.notificationPosition; + overlay3DDetails[i].buttonPosition = positions.buttonPosition; } - isOnHMD = !isOnHMD; - persistTime = isOnHMD ? PERSIST_TIME_3D : PERSIST_TIME_2D; - return; + locationY = locationY + arrays[i][3]; } + } - frame += 1; - if ((frame % 60.0) === 0) { // only update once a second - locationY = 20.0; - for (i = 0; i < arrays.length; i += 1) { //repositions overlays as others fade - nextOverlay = Overlays.getOverlayAtPoint({ x: overlayLocationX, y: locationY }); - Overlays.editOverlay(notifications[i], { x: overlayLocationX, y: locationY }); - Overlays.editOverlay(buttons[i], { x: buttonLocationX, y: locationY + 12.0 }); - if (isOnHMD) { - positions = calculate3DOverlayPositions(overlay3DDetails[i].width, - overlay3DDetails[i].height, locationY); - overlay3DDetails[i].notificationOrientation = positions.notificationOrientation; - overlay3DDetails[i].notificationPosition = positions.notificationPosition; - overlay3DDetails[i].buttonPosition = positions.buttonPosition; - } - locationY = locationY + arrays[i][3]; - } - } - - // This checks the age of the notification and prepares to fade it after 9.0 seconds (var persistTime - 1) - for (i = 0; i < arrays.length; i += 1) { - if (ready) { - j = arrays[i][2]; - k = j + persistTime; - if (k < (new Date().getTime() / 1000)) { - ready = false; - noticeOut = arrays[i][0]; - buttonOut = arrays[i][1]; - arraysOut = i; - fadeOut(noticeOut, buttonOut, arraysOut); - } + // This checks the age of the notification and prepares to fade it after 9.0 seconds (var persistTime - 1) + for (i = 0; i < arrays.length; i += 1) { + if (ready) { + j = arrays[i][2]; + k = j + persistTime; + if (k < (new Date().getTime() / 1000)) { + ready = false; + noticeOut = arrays[i][0]; + buttonOut = arrays[i][1]; + arraysOut = i; + fadeOut(noticeOut, buttonOut, arraysOut); } } } +} - var STARTUP_TIMEOUT = 500, // ms - startingUp = true, - startupTimer = null; +var STARTUP_TIMEOUT = 500, // ms + startingUp = true, + startupTimer = null; - function finishStartup() { - startingUp = false; - Script.clearTimeout(startupTimer); - } +function finishStartup() { + startingUp = false; + Script.clearTimeout(startupTimer); +} - function isStartingUp() { - // Is starting up until get no checks that it is starting up for STARTUP_TIMEOUT - if (startingUp) { - if (startupTimer) { - Script.clearTimeout(startupTimer); - } - startupTimer = Script.setTimeout(finishStartup, STARTUP_TIMEOUT); +function isStartingUp() { + // Is starting up until get no checks that it is starting up for STARTUP_TIMEOUT + if (startingUp) { + if (startupTimer) { + Script.clearTimeout(startupTimer); } - return startingUp; + startupTimer = Script.setTimeout(finishStartup, STARTUP_TIMEOUT); + } + return startingUp; +} + +function onDomainConnectionRefused(reason) { + createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); +} + +function onEditError(msg) { + createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); +} + + +function onSnapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, notify) { + if (notify) { + var imageProperties = { + path: "file:///" + pathStillSnapshot, + aspectRatio: Window.innerWidth / Window.innerHeight + }; + createNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), NotificationType.SNAPSHOT, imageProperties); + } +} + +// handles mouse clicks on buttons +function mousePressEvent(event) { + var pickRay, + clickedOverlay, + i; + + if (isOnHMD) { + pickRay = Camera.computePickRay(event.x, event.y); + clickedOverlay = Overlays.findRayIntersection(pickRay).overlayID; + } else { + clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); } - function onDomainConnectionRefused(reason) { - createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); - } - - function onEditError(msg) { - createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); - } - - - function onSnapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, notify) { - if (notify) { - var imageProperties = { - path: "file:///" + pathStillSnapshot, - aspectRatio: Window.innerWidth / Window.innerHeight - }; - createNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), NotificationType.SNAPSHOT, imageProperties); + for (i = 0; i < buttons.length; i += 1) { + if (clickedOverlay === buttons[i]) { + deleteNotification(i); } } +} - // handles mouse clicks on buttons - function mousePressEvent(event) { - var pickRay, - clickedOverlay, - i; - - if (isOnHMD) { - pickRay = Camera.computePickRay(event.x, event.y); - clickedOverlay = Overlays.findRayIntersection(pickRay).overlayID; - } else { - clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); - } - - for (i = 0; i < buttons.length; i += 1) { - if (clickedOverlay === buttons[i]) { - deleteNotification(i); - } - } +// Control key remains active only while key is held down +function keyReleaseEvent(key) { + if (key.key === 16777249) { + ctrlIsPressed = false; } +} - // Control key remains active only while key is held down - function keyReleaseEvent(key) { - if (key.key === 16777249) { - ctrlIsPressed = false; - } +// Triggers notification on specific key driven events +function keyPressEvent(key) { + if (key.key === 16777249) { + ctrlIsPressed = true; } +} - // Triggers notification on specific key driven events - function keyPressEvent(key) { - if (key.key === 16777249) { - ctrlIsPressed = true; - } - } - - function setup() { - Menu.addMenu(MENU_NAME); - var checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING); +function setup() { + Menu.addMenu(MENU_NAME); + var checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING); + checked = checked === '' ? true : checked; + Menu.addMenuItem({ + menuName: MENU_NAME, + menuItemName: PLAY_NOTIFICATION_SOUNDS_MENU_ITEM, + isCheckable: true, + isChecked: Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING) + }); + Menu.addSeparator(MENU_NAME, "Play sounds for:"); + for (var type in NotificationType.properties) { + checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + (parseInt(type) + 1)); checked = checked === '' ? true : checked; Menu.addMenuItem({ menuName: MENU_NAME, - menuItemName: PLAY_NOTIFICATION_SOUNDS_MENU_ITEM, + menuItemName: NotificationType.properties[type].text + NOTIFICATION_MENU_ITEM_POST, isCheckable: true, - isChecked: Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING) + isChecked: checked }); - Menu.addSeparator(MENU_NAME, "Play sounds for:"); - for (var type in NotificationType.properties) { - checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + (parseInt(type) + 1)); - checked = checked === '' ? true : checked; - Menu.addMenuItem({ - menuName: MENU_NAME, - menuItemName: NotificationType.properties[type].text + NOTIFICATION_MENU_ITEM_POST, - isCheckable: true, - isChecked: checked - }); - } } +} - // When our script shuts down, we should clean up all of our overlays - function scriptEnding() { - for (var i = 0; i < notifications.length; i++) { - Overlays.deleteOverlay(notifications[i]); - Overlays.deleteOverlay(buttons[i]); - } - Menu.removeMenu(MENU_NAME); +// When our script shuts down, we should clean up all of our overlays +function scriptEnding() { + for (var i = 0; i < notifications.length; i++) { + Overlays.deleteOverlay(notifications[i]); + Overlays.deleteOverlay(buttons[i]); } + Menu.removeMenu(MENU_NAME); +} - function menuItemEvent(menuItem) { - if (menuItem === PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) { - Settings.setValue(PLAY_NOTIFICATION_SOUNDS_SETTING, Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM)); - return; - } - var notificationType = NotificationType.getTypeFromMenuItem(menuItem); - if (notificationType !== notificationType.UNKNOWN) { - Settings.setValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + notificationType, Menu.isOptionChecked(menuItem)); - } +function menuItemEvent(menuItem) { + if (menuItem === PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) { + Settings.setValue(PLAY_NOTIFICATION_SOUNDS_SETTING, Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM)); + return; } + var notificationType = NotificationType.getTypeFromMenuItem(menuItem); + if (notificationType !== notificationType.UNKNOWN) { + Settings.setValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + notificationType, Menu.isOptionChecked(menuItem)); + } +} - LODManager.LODDecreased.connect(function () { - var warningText = "\n" + - "Due to the complexity of the content, the \n" + - "level of detail has been decreased. " + - "You can now see: \n" + - LODManager.getLODFeedbackText(); +LODManager.LODDecreased.connect(function() { + var warningText = "\n" + + "Due to the complexity of the content, the \n" + + "level of detail has been decreased. " + + "You can now see: \n" + + LODManager.getLODFeedbackText(); - if (lodTextID === false) { - lodTextID = createNotification(warningText, NotificationType.LOD_WARNING); - } else { - Overlays.editOverlay(lodTextID, { text: warningText }); - } - }); + if (lodTextID === false) { + lodTextID = createNotification(warningText, NotificationType.LOD_WARNING); + } else { + Overlays.editOverlay(lodTextID, { text: warningText }); + } +}); - Controller.keyPressEvent.connect(keyPressEvent); - Controller.mousePressEvent.connect(mousePressEvent); - Controller.keyReleaseEvent.connect(keyReleaseEvent); - Script.update.connect(update); - Script.scriptEnding.connect(scriptEnding); - Menu.menuItemEvent.connect(menuItemEvent); - Window.domainConnectionRefused.connect(onDomainConnectionRefused); - Window.snapshotTaken.connect(onSnapshotTaken); - Window.notifyEditError = onEditError; +Controller.keyPressEvent.connect(keyPressEvent); +Controller.mousePressEvent.connect(mousePressEvent); +Controller.keyReleaseEvent.connect(keyReleaseEvent); +Script.update.connect(update); +Script.scriptEnding.connect(scriptEnding); +Menu.menuItemEvent.connect(menuItemEvent); +Window.domainConnectionRefused.connect(onDomainConnectionRefused); +Window.snapshotTaken.connect(onSnapshotTaken); +Window.notifyEditError = onEditError; - setup(); +setup(); }()); // END LOCAL_SCOPE From 7f4613f136b282c1f83e869168fe84c848600c32 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 17 Nov 2016 17:39:26 -0800 Subject: [PATCH 38/50] Add checkbox for GIF and length selector --- interface/src/Application.cpp | 2 +- interface/src/ui/PreferencesDialog.cpp | 15 +++++++++++++++ interface/src/ui/SnapshotAnimated.cpp | 5 ++++- interface/src/ui/SnapshotAnimated.h | 3 +++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d71cc12858..82007c4f06 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5441,7 +5441,7 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); // If we're not doing an animated snapshot as well... - if (!includeAnimated) { + if (!includeAnimated || !(SnapshotAnimated::alsoTakeAnimatedSnapshot.get())) { // Tell the dependency manager that the capture of the still snapshot has taken place. emit DependencyManager::get()->snapshotTaken(path, "", notify); } else { diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 7d3261aa78..35af1067d8 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -23,6 +23,7 @@ #include "LODManager.h" #include "Menu.h" #include "Snapshot.h" +#include "SnapshotAnimated.h" #include "UserActivityLogger.h" #include "AmbientOcclusionEffect.h" @@ -83,6 +84,20 @@ void setupPreferences() { auto preference = new BrowsePreference(SNAPSHOTS, "Put my snapshots here", getter, setter); preferences->addPreference(preference); } + { + auto getter = []()->bool { return SnapshotAnimated::alsoTakeAnimatedSnapshot.get(); }; + auto setter = [](bool value) { SnapshotAnimated::alsoTakeAnimatedSnapshot.set(value); }; + preferences->addPreference(new CheckPreference(SNAPSHOTS, "Take Animated GIF Snapshot with HUD Button", getter, setter)); + } + { + auto getter = []()->float { return SnapshotAnimated::snapshotAnimatedDuration.get(); }; + auto setter = [](float value) { SnapshotAnimated::snapshotAnimatedDuration.set(value); }; + auto preference = new SpinnerPreference(SNAPSHOTS, "Animated Snapshot Duration", getter, setter); + preference->setMin(3); + preference->setMax(10); + preference->setStep(1); + preferences->addPreference(preference); + } // Scripts { diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index 0d2c6707a5..70971fd7c9 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -25,6 +25,9 @@ bool SnapshotAnimated::snapshotAnimatedTimerRunning = false; QString SnapshotAnimated::snapshotAnimatedPath; QString SnapshotAnimated::snapshotStillPath; +Setting::Handle SnapshotAnimated::alsoTakeAnimatedSnapshot("alsoTakeAnimatedSnapshot", true); +Setting::Handle SnapshotAnimated::snapshotAnimatedDuration("snapshotAnimatedDuration", SNAPSNOT_ANIMATED_DURATION_SECS); + void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm) { // If we're not in the middle of capturing an animated snapshot... if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { @@ -62,7 +65,7 @@ void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio // Record how long it took for the current frame to pack SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = SnapshotAnimated::snapshotAnimatedTimestamp - framePackStartTime; // If that was the last frame... - if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_MSEC)) { + if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) { // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE // that the slot will not be called again in the future. // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h index 4de7c339dd..5870eb9e35 100644 --- a/interface/src/ui/SnapshotAnimated.h +++ b/interface/src/ui/SnapshotAnimated.h @@ -16,6 +16,7 @@ #include #include #include +#include #include "scripting/WindowScriptingInterface.h" // If the snapshot width or the framerate are too high for the @@ -43,6 +44,8 @@ private: static QString snapshotStillPath; public: static void saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm); + static Setting::Handle alsoTakeAnimatedSnapshot; + static Setting::Handle snapshotAnimatedDuration; }; #endif // hifi_SnapshotAnimated_h From 1e97a69b51d80cf8f440125ddc672e308ce7fedc Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 18 Nov 2016 14:39:09 -0800 Subject: [PATCH 39/50] Massive performance improvements. --- interface/src/ui/SnapshotAnimated.cpp | 152 ++++++++++++++++---------- interface/src/ui/SnapshotAnimated.h | 21 ++-- 2 files changed, 106 insertions(+), 67 deletions(-) diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index 70971fd7c9..6850f43f4e 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -14,16 +14,22 @@ #include #include +#include #include "SnapshotAnimated.h" -QTimer SnapshotAnimated::snapshotAnimatedTimer; -GifWriter SnapshotAnimated::snapshotAnimatedGifWriter; +QTimer* SnapshotAnimated::snapshotAnimatedTimer = NULL; qint64 SnapshotAnimated::snapshotAnimatedTimestamp = 0; qint64 SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; -qint64 SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = 0; bool SnapshotAnimated::snapshotAnimatedTimerRunning = false; QString SnapshotAnimated::snapshotAnimatedPath; QString SnapshotAnimated::snapshotStillPath; +QVector SnapshotAnimated::snapshotAnimatedFrameVector; +QVector SnapshotAnimated::snapshotAnimatedFrameDelayVector; +Application* SnapshotAnimated::app; +float SnapshotAnimated::aspectRatio; +QSharedPointer SnapshotAnimated::snapshotAnimatedDM; +GifWriter SnapshotAnimated::snapshotAnimatedGifWriter; + Setting::Handle SnapshotAnimated::alsoTakeAnimatedSnapshot("alsoTakeAnimatedSnapshot", true); Setting::Handle SnapshotAnimated::snapshotAnimatedDuration("snapshotAnimatedDuration", SNAPSNOT_ANIMATED_DURATION_SECS); @@ -31,77 +37,103 @@ Setting::Handle SnapshotAnimated::snapshotAnimatedDuration("snapshotAnima void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm) { // If we're not in the middle of capturing an animated snapshot... if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { + SnapshotAnimated::snapshotAnimatedTimer = new QTimer(); + SnapshotAnimated::aspectRatio = aspectRatio; + SnapshotAnimated::app = app; + SnapshotAnimated::snapshotAnimatedDM = dm; // Define the output location of the still and animated snapshots. SnapshotAnimated::snapshotStillPath = pathStill; SnapshotAnimated::snapshotAnimatedPath = pathStill; SnapshotAnimated::snapshotAnimatedPath.replace("jpg", "gif"); - // Reset the current animated snapshot last frame duration - SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = SNAPSNOT_ANIMATED_INITIAL_WRITE_DURATION_MSEC; // Ensure the snapshot timer is Precise (attempted millisecond precision) - SnapshotAnimated::snapshotAnimatedTimer.setTimerType(Qt::PreciseTimer); + SnapshotAnimated::snapshotAnimatedTimer->setTimerType(Qt::PreciseTimer); // Connect the snapshotAnimatedTimer QTimer to the lambda slot function - QObject::connect(&(SnapshotAnimated::snapshotAnimatedTimer), &QTimer::timeout, [=] { - if (SnapshotAnimated::snapshotAnimatedTimerRunning) { - // Get a screenshot from the display, then scale the screenshot down, - // then convert it to the image format the GIF library needs, - // then save all that to the QImage named "frame" - QImage frame(app->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); - - // If this is an intermediate or the final frame... - if (SnapshotAnimated::snapshotAnimatedTimestamp > 0) { - // Variable used to determine how long the current frame took to pack - qint64 framePackStartTime = QDateTime::currentMSecsSinceEpoch(); - // Write the frame to the gif - GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), - (uint8_t*)frame.bits(), - frame.width(), - frame.height(), - round(((float)(framePackStartTime - SnapshotAnimated::snapshotAnimatedTimestamp + SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration)) / 10)); - // Record the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); - // Record how long it took for the current frame to pack - SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = SnapshotAnimated::snapshotAnimatedTimestamp - framePackStartTime; - // If that was the last frame... - if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) { - // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE - // that the slot will not be called again in the future. - // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html - SnapshotAnimated::snapshotAnimatedTimer.stop(); - SnapshotAnimated::snapshotAnimatedTimerRunning = false; - // Reset the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = 0; - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; - // Write out the end of the GIF - GifEnd(&(SnapshotAnimated::snapshotAnimatedGifWriter)); - // Let the dependency manager know that the snapshots have been taken. - emit dm->snapshotTaken(SnapshotAnimated::snapshotStillPath, SnapshotAnimated::snapshotAnimatedPath, false); - } - // If that was the first frame... - } else { - // Write out the header and beginning of the GIF file - GifBegin(&(SnapshotAnimated::snapshotAnimatedGifWriter), qPrintable(SnapshotAnimated::snapshotAnimatedPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Write the first to the gif - GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), - (uint8_t*)frame.bits(), - frame.width(), - frame.height(), - SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Record the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; - } - } - }); + QObject::connect((SnapshotAnimated::snapshotAnimatedTimer), &QTimer::timeout, captureFrames); // Start the snapshotAnimatedTimer QTimer - argument for this is in milliseconds - SnapshotAnimated::snapshotAnimatedTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); SnapshotAnimated::snapshotAnimatedTimerRunning = true; + SnapshotAnimated::snapshotAnimatedTimer->start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); // If we're already in the middle of capturing an animated snapshot... } else { // Just tell the dependency manager that the capture of the still snapshot has taken place. emit dm->snapshotTaken(pathStill, "", false); } } + +void SnapshotAnimated::captureFrames() { + if (SnapshotAnimated::snapshotAnimatedTimerRunning) { + // Get a screenshot from the display, then scale the screenshot down, + // then convert it to the image format the GIF library needs, + // then save all that to the QImage named "frame" + QImage frame(SnapshotAnimated::app->getActiveDisplayPlugin()->getScreenshot(SnapshotAnimated::aspectRatio)); + frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); + SnapshotAnimated::snapshotAnimatedFrameVector.append(frame); + + // If that was the first frame... + if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { + // Record the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + // Record the first frame timestamp + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; + SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // If this is an intermediate or the final frame... + } else { + // Push the current frame delay onto the vector + SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(round(((float)(QDateTime::currentMSecsSinceEpoch() - SnapshotAnimated::snapshotAnimatedTimestamp)) / 10)); + // Record the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + + // If that was the last frame... + if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) { + // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE + // that the slot will not be called again in the future. + // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html + SnapshotAnimated::snapshotAnimatedTimer->stop(); + delete SnapshotAnimated::snapshotAnimatedTimer; + SnapshotAnimated::snapshotAnimatedTimerRunning = false; + // Reset the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = 0; + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; + + // Kick off the thread that'll pack the frames into the GIF + QtConcurrent::run(processFrames); + } + } + } +} + +void SnapshotAnimated::processFrames() { + uint32_t width = SnapshotAnimated::snapshotAnimatedFrameVector[0].width(); + uint32_t height = SnapshotAnimated::snapshotAnimatedFrameVector[0].height(); + + // Create the GIF from the temporary files + // Write out the header and beginning of the GIF file + GifBegin( + &(SnapshotAnimated::snapshotAnimatedGifWriter), + qPrintable(SnapshotAnimated::snapshotAnimatedPath), + width, + height, + 1); // "1" means "yes there is a delay" with this GifCreator library. + for (int itr = 0; itr < SnapshotAnimated::snapshotAnimatedFrameVector.size(); itr++) { + // Write each frame to the GIF + GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), + (uint8_t*)SnapshotAnimated::snapshotAnimatedFrameVector[itr].convertToFormat(QImage::Format_RGBA8888).bits(), + width, + height, + SnapshotAnimated::snapshotAnimatedFrameDelayVector[itr]); + } + // Write out the end of the GIF + GifEnd(&(SnapshotAnimated::snapshotAnimatedGifWriter)); + + // Clear out the frame and frame delay vectors. + // Also release the memory not required to store the items. + SnapshotAnimated::snapshotAnimatedFrameVector.clear(); + SnapshotAnimated::snapshotAnimatedFrameVector.squeeze(); + SnapshotAnimated::snapshotAnimatedFrameDelayVector.clear(); + SnapshotAnimated::snapshotAnimatedFrameDelayVector.squeeze(); + + // Let the dependency manager know that the snapshots have been taken. + emit SnapshotAnimated::snapshotAnimatedDM->snapshotTaken(SnapshotAnimated::snapshotStillPath, SnapshotAnimated::snapshotAnimatedPath, false); +} diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h index 5870eb9e35..78b1529ab4 100644 --- a/interface/src/ui/SnapshotAnimated.h +++ b/interface/src/ui/SnapshotAnimated.h @@ -12,6 +12,7 @@ #ifndef hifi_SnapshotAnimated_h #define hifi_SnapshotAnimated_h +#include #include #include #include @@ -28,20 +29,26 @@ #define SNAPSNOT_ANIMATED_DURATION_MSEC (SNAPSNOT_ANIMATED_DURATION_SECS*1000) #define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_TARGET_FRAMERATE) -// This is the fudge factor that we add to the *first* GIF frame's "delay" value -#define SNAPSNOT_ANIMATED_INITIAL_WRITE_DURATION_MSEC (20) -#define SNAPSNOT_ANIMATED_NUM_FRAMES (SNAPSNOT_ANIMATED_DURATION_SECS * SNAPSNOT_ANIMATED_TARGET_FRAMERATE) class SnapshotAnimated { private: - static QTimer snapshotAnimatedTimer; - static GifWriter snapshotAnimatedGifWriter; + static QTimer* snapshotAnimatedTimer; static qint64 snapshotAnimatedTimestamp; static qint64 snapshotAnimatedFirstFrameTimestamp; - static qint64 snapshotAnimatedLastWriteFrameDuration; static bool snapshotAnimatedTimerRunning; - static QString snapshotAnimatedPath; static QString snapshotStillPath; + + static QString snapshotAnimatedPath; + static QVector snapshotAnimatedFrameVector; + static QVector snapshotAnimatedFrameDelayVector; + static QSharedPointer snapshotAnimatedDM; + static Application* app; + static float aspectRatio; + + static GifWriter snapshotAnimatedGifWriter; + + static void captureFrames(); + static void processFrames(); public: static void saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm); static Setting::Handle alsoTakeAnimatedSnapshot; From 143225a74ce56ca46d2a3b208777fc17538166bc Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 18 Nov 2016 14:40:22 -0800 Subject: [PATCH 40/50] add MAC permission table to domain-server and leverage --- .../resources/describe-settings.json | 82 +++++++++++++++-- domain-server/src/DomainGatekeeper.cpp | 40 +++++++-- domain-server/src/DomainGatekeeper.h | 3 +- domain-server/src/DomainServerNodeData.h | 4 + .../src/DomainServerSettingsManager.cpp | 88 ++++++++++++++----- .../src/DomainServerSettingsManager.h | 6 ++ 6 files changed, 189 insertions(+), 34 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 911732fcef..8cd9136895 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -684,6 +684,79 @@ } ] }, + { + "name": "permissions", + "type": "table", + "caption": "Permissions for Specific Users", + "can_add_new_rows": true, + + "groups": [ + { + "label": "User", + "span": 1 + }, + { + "label": "Permissions ?", + "span": 7 + } + ], + + "columns": [ + { + "name": "permissions_id", + "label": "" + }, + { + "name": "id_can_connect", + "label": "Connect", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_adjust_locks", + "label": "Lock / Unlock", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_rez", + "label": "Rez", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_rez_tmp", + "label": "Rez Temporary", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_write_to_asset_server", + "label": "Write Assets", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_connect_past_max_capacity", + "label": "Ignore Max Capacity", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_kick", + "label": "Kick Users", + "type": "checkbox", + "editable": true, + "default": false + } + ] + }, { "name": "ip_permissions", "type": "table", @@ -757,18 +830,17 @@ ] }, { - "name": "permissions", + "name": "mac_permissions", "type": "table", - "caption": "Permissions for Specific Users", + "caption": "Permissions for Users with MAC Addresses", "can_add_new_rows": true, - "groups": [ { - "label": "User", + "label": "MAC Address", "span": 1 }, { - "label": "Permissions ?", + "label": "Permissions ?", "span": 7 } ], diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 051465efd2..f55a2073d1 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -119,15 +119,20 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointersetNodeInterestSet(safeInterestSet); nodeData->setPlaceName(nodeConnection.placeName); + qDebug() << "Allowed connection from node" << uuidStringWithoutCurlyBraces(node->getUUID()) + << "on" << message->getSenderSockAddr() << "with MAC" << nodeConnection.hardwareAddress; + // signal that we just connected a node so the DomainServer can get it a list // and broadcast its presence right away emit connectedNode(node); } else { - qDebug() << "Refusing connection from node at" << message->getSenderSockAddr(); + qDebug() << "Refusing connection from node at" << message->getSenderSockAddr() + << "with hardware address" << nodeConnection.hardwareAddress; } } -NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress) { +NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, + const QHostAddress& senderAddress, const QString& hardwareAddress) { NodePermissions userPerms; userPerms.setAll(false); @@ -144,8 +149,14 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin #ifdef WANT_DEBUG qDebug() << "| user-permissions: unverified or no username for" << userPerms.getID() << ", so:" << userPerms; #endif + if (!hardwareAddress.isEmpty() && _server->_settingsManager.hasPermissionsForMAC(hardwareAddress)) { + // this user comes from a MAC we have in our permissions table, apply those permissions + userPerms = _server->_settingsManager.getPermissionsForMAC(hardwareAddress); - if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) { +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: specific MAC matches, so:" << userPerms; +#endif + } else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) { // this user comes from an IP we have in our permissions table, apply those permissions userPerms = _server->_settingsManager.getPermissionsForIP(senderAddress); @@ -158,6 +169,13 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin userPerms = _server->_settingsManager.getPermissionsForName(verifiedUsername); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific user matches, so:" << userPerms; +#endif + } else if (!hardwareAddress.isEmpty() && _server->_settingsManager.hasPermissionsForMAC(hardwareAddress)) { + // this user comes from a MAC we have in our permissions table, apply those permissions + userPerms = _server->_settingsManager.getPermissionsForMAC(hardwareAddress); + +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: specific MAC matches, so:" << userPerms; #endif } else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) { // this user comes from an IP we have in our permissions table, apply those permissions @@ -255,7 +273,14 @@ void DomainGatekeeper::updateNodePermissions() { // or the public socket if we haven't activated a socket for the node yet HifiSockAddr connectingAddr = node->getActiveSocket() ? *node->getActiveSocket() : node->getPublicSocket(); - userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress()); + QString hardwareAddress; + + DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); + if (nodeData) { + hardwareAddress = nodeData->getHardwareAddress(); + } + + userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress(), hardwareAddress); } node->setPermissions(userPerms); @@ -308,6 +333,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID()); nodeData->setWalletUUID(it->second.getWalletUUID()); nodeData->setNodeVersion(it->second.getNodeVersion()); + nodeData->setHardwareAddress(nodeConnection.hardwareAddress); nodeData->setWasAssigned(true); // cleanup the PendingAssignedNodeData for this assignment now that it's connecting @@ -369,7 +395,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect } } - userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, nodeConnection.senderSockAddr.getAddress()); + userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, nodeConnection.senderSockAddr.getAddress(), + nodeConnection.hardwareAddress); if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", @@ -425,6 +452,9 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect // if we have a username from the connect request, set it on the DomainServerNodeData nodeData->setUsername(username); + // set the hardware address passed in the connect request + nodeData->setHardwareAddress(nodeConnection.hardwareAddress); + // also add an interpolation to DomainServerNodeData so that servers can get username in stats nodeData->addOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, uuidStringWithoutCurlyBraces(newNode->getUUID()), username); diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index b7d2a03af6..b17d0f61a4 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -107,7 +107,8 @@ private: QSet _domainOwnerFriends; // keep track of friends of the domain owner QSet _inFlightGroupMembershipsRequests; // keep track of which we've already asked for - NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress); + NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, + const QHostAddress& senderAddress, const QString& hardwareAddress); void getGroupMemberships(const QString& username); // void getIsGroupMember(const QString& username, const QUuid groupID); diff --git a/domain-server/src/DomainServerNodeData.h b/domain-server/src/DomainServerNodeData.h index f95403c779..ff637844f1 100644 --- a/domain-server/src/DomainServerNodeData.h +++ b/domain-server/src/DomainServerNodeData.h @@ -53,6 +53,9 @@ public: void setNodeVersion(const QString& nodeVersion) { _nodeVersion = nodeVersion; } const QString& getNodeVersion() { return _nodeVersion; } + + void setHardwareAddress(const QString& hardwareAddress) { _hardwareAddress = hardwareAddress; } + const QString& getHardwareAddress() { return _hardwareAddress; } void addOverrideForKey(const QString& key, const QString& value, const QString& overrideValue); void removeOverrideForKey(const QString& key, const QString& value); @@ -81,6 +84,7 @@ private: bool _isAuthenticated = true; NodeSet _nodeInterestSet; QString _nodeVersion; + QString _hardwareAddress; QString _placeName; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index fbc5fd4bd5..ef71c6ea81 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -29,6 +29,8 @@ #include #include +#include "DomainServerNodeData.h" + #include "DomainServerSettingsManager.h" const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json"; @@ -439,6 +441,9 @@ void DomainServerSettingsManager::packPermissions() { // save settings for IP addresses packPermissionsForMap("permissions", _ipPermissions, IP_PERMISSIONS_KEYPATH); + // save settings for MAC addresses + packPermissionsForMap("permissions", _macPermissions, MAC_PERMISSIONS_KEYPATH); + // save settings for groups packPermissionsForMap("permissions", _groupPermissions, GROUP_PERMISSIONS_KEYPATH); @@ -506,6 +511,17 @@ void DomainServerSettingsManager::unpackPermissions() { } }); + needPack |= unpackPermissionsForKeypath(MAC_PERMISSIONS_KEYPATH, &_macPermissions, + [&](NodePermissionsPointer perms){ + // make sure that this permission row is for a valid IP address + if (perms->getKey().first.isEmpty()) { + _macPermissions.remove(perms->getKey()); + + // we removed a row from the MAC permissions, we'll need a re-pack + needPack = true; + } + }); + needPack |= unpackPermissionsForKeypath(GROUP_PERMISSIONS_KEYPATH, &_groupPermissions, [&](NodePermissionsPointer perms){ @@ -558,7 +574,8 @@ void DomainServerSettingsManager::unpackPermissions() { qDebug() << "--------------- permissions ---------------------"; QList> permissionsSets; permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get() - << _groupPermissions.get() << _groupForbiddens.get() << _ipPermissions.get(); + << _groupPermissions.get() << _groupForbiddens.get() + << _ipPermissions.get() << _macPermissions.get(); foreach (auto permissionSet, permissionsSets) { QHashIterator i(permissionSet); while (i.hasNext()) { @@ -653,19 +670,25 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointergetPermissions().getVerifiedUserName(); - bool hadExistingPermissions = false; + bool newPermissions = false; if (!verifiedUsername.isEmpty()) { // if we have a verified user name for this user, we apply the kick to the username // check if there were already permissions - hadExistingPermissions = havePermissionsForName(verifiedUsername); + bool hadPermissions = havePermissionsForName(verifiedUsername); // grab or create permissions for the given username - destinationPermissions = _agentPermissions[matchingNode->getPermissions().getKey()]; + auto userPermissions = _agentPermissions[matchingNode->getPermissions().getKey()]; + + newPermissions = !hadPermissions || userPermissions->can(NodePermissions::Permission::canConnectToDomain); + + // ensure that the connect permission is clear + userPermissions->clear(NodePermissions::Permission::canConnectToDomain); } else { - // otherwise we apply the kick to the IP from active socket for this node - // (falling back to the public socket if not yet active) + // otherwise we apply the kick to the IP from active socket for this node and the MAC address + + // remove connect permissions for the IP (falling back to the public socket if not yet active) auto& kickAddress = matchingNode->getActiveSocket() ? matchingNode->getActiveSocket()->getAddress() : matchingNode->getPublicSocket().getAddress(); @@ -673,32 +696,41 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointercan(NodePermissions::Permission::canConnectToDomain)) { + newPermissions = true; + + ipPermissions->clear(NodePermissions::Permission::canConnectToDomain); + } + + // potentially remove connect permissions for the MAC address + DomainServerNodeData* nodeData = reinterpret_cast(matchingNode->getLinkedData()); + if (nodeData) { + NodePermissionsKey macAddressKey(nodeData->getHardwareAddress(), 0); + + bool hadMACPermissions = hasPermissionsForMAC(nodeData->getHardwareAddress()); + + auto macPermissions = _macPermissions[macAddressKey]; + + if (!hadMACPermissions || macPermissions->can(NodePermissions::Permission::canConnectToDomain)) { + newPermissions = true; + + macPermissions->clear(NodePermissions::Permission::canConnectToDomain); + } + } } - // make sure we didn't already have existing permissions that disallowed connect - if (!hadExistingPermissions - || destinationPermissions->can(NodePermissions::Permission::canConnectToDomain)) { - + if (newPermissions) { qDebug() << "Removing connect permission for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID()) - << "after kick request"; - - // ensure that the connect permission is clear - destinationPermissions->clear(NodePermissions::Permission::canConnectToDomain); + << "after kick request from" << uuidStringWithoutCurlyBraces(sendingNode->getUUID()); // we've changed permissions, time to store them to disk and emit our signal to say they have changed packPermissions(); - - emit updateNodePermissions(); } else { - qWarning() << "Received kick request for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID()) - << "that already did not have permission to connect"; - - // in this case, though we don't expect the node to be connected to the domain, it is - // emit updateNodePermissions so that the DomainGatekeeper kicks it out emit updateNodePermissions(); } @@ -753,6 +785,16 @@ NodePermissions DomainServerSettingsManager::getPermissionsForIP(const QHostAddr return nullPermissions; } +NodePermissions DomainServerSettingsManager::getPermissionsForMAC(const QString& macAddress) const { + NodePermissionsKey macKey = NodePermissionsKey(macAddress, 0); + if (_macPermissions.contains(macKey)) { + return *(_macPermissions[macKey].get()); + } + NodePermissions nullPermissions; + nullPermissions.setAll(false); + return nullPermissions; +} + NodePermissions DomainServerSettingsManager::getPermissionsForGroup(const QString& groupName, QUuid rankID) const { NodePermissionsKey groupRankKey = NodePermissionsKey(groupName, rankID); if (_groupPermissions.contains(groupRankKey)) { diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index c067377ffc..fcc3e9d91d 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -28,6 +28,7 @@ const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json"; const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions"; const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions"; const QString IP_PERMISSIONS_KEYPATH = "security.ip_permissions"; +const QString MAC_PERMISSIONS_KEYPATH = "security.mac_permissions"; const QString GROUP_PERMISSIONS_KEYPATH = "security.group_permissions"; const QString GROUP_FORBIDDENS_KEYPATH = "security.group_forbiddens"; @@ -62,6 +63,10 @@ public: bool hasPermissionsForIP(const QHostAddress& address) const { return _ipPermissions.contains(address.toString(), 0); } NodePermissions getPermissionsForIP(const QHostAddress& address) const; + // these give access to permissions for specific MACs from the domain-server settings page + bool hasPermissionsForMAC(const QString& macAddress) const { return _macPermissions.contains(macAddress, 0); } + NodePermissions getPermissionsForMAC(const QString& macAddress) const; + // these give access to permissions for specific groups from the domain-server settings page bool havePermissionsForGroup(const QString& groupName, QUuid rankID) const { return _groupPermissions.contains(groupName, rankID); @@ -142,6 +147,7 @@ private: NodePermissionsMap _agentPermissions; // specific account-names NodePermissionsMap _ipPermissions; // permissions granted by node IP address + NodePermissionsMap _macPermissions; // permissions granted by node MAC address NodePermissionsMap _groupPermissions; // permissions granted by membership to specific groups NodePermissionsMap _groupForbiddens; // permissions denied due to membership in a specific group From dec4ce79b260221b41b797647901261dafde008d Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 18 Nov 2016 14:42:29 -0800 Subject: [PATCH 41/50] fix comment in mac permission unpack --- domain-server/src/DomainServerSettingsManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index ef71c6ea81..21214ed5f6 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -513,7 +513,7 @@ void DomainServerSettingsManager::unpackPermissions() { needPack |= unpackPermissionsForKeypath(MAC_PERMISSIONS_KEYPATH, &_macPermissions, [&](NodePermissionsPointer perms){ - // make sure that this permission row is for a valid IP address + // make sure that this permission row is for a non-empty hardware if (perms->getKey().first.isEmpty()) { _macPermissions.remove(perms->getKey()); From 3a5a9b359e3b72181b030943b06f4a659eaa4be6 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 18 Nov 2016 15:04:35 -0800 Subject: [PATCH 42/50] Quick bugfix --- interface/src/ui/SnapshotAnimated.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index 6850f43f4e..c8edbfc028 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -87,11 +87,6 @@ void SnapshotAnimated::captureFrames() { // If that was the last frame... if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) { - // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE - // that the slot will not be called again in the future. - // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html - SnapshotAnimated::snapshotAnimatedTimer->stop(); - delete SnapshotAnimated::snapshotAnimatedTimer; SnapshotAnimated::snapshotAnimatedTimerRunning = false; // Reset the current frame timestamp SnapshotAnimated::snapshotAnimatedTimestamp = 0; @@ -99,6 +94,11 @@ void SnapshotAnimated::captureFrames() { // Kick off the thread that'll pack the frames into the GIF QtConcurrent::run(processFrames); + // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE + // that the slot will not be called again in the future. + // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html + SnapshotAnimated::snapshotAnimatedTimer->stop(); + delete SnapshotAnimated::snapshotAnimatedTimer; } } } From a66a1d392eca54e0ec3fdfe0e45ac707d3896cf9 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Nov 2016 15:24:19 -0800 Subject: [PATCH 43/50] hack to avoid crash --- .../src/RenderablePolyVoxEntityItem.cpp | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 81bb5e8b2b..c46a13c5d9 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -551,11 +551,18 @@ void RenderablePolyVoxEntityItem::setZTextureURL(QString zTextureURL) { } } +quint64 start { 0 }; + void RenderablePolyVoxEntityItem::render(RenderArgs* args) { PerformanceTimer perfTimer("RenderablePolyVoxEntityItem::render"); assert(getType() == EntityTypes::PolyVox); Q_ASSERT(args->_batch); + if (start == 0) { + start = usecTimestampNow(); + return; + } + bool voxelDataDirty; bool volDataDirty; withWriteLock([&] { @@ -600,29 +607,34 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { _pipeline = gpu::Pipeline::create(program, state); } - - if (!_vertexFormat) { - auto vf = std::make_shared(); - vf->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); - vf->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 12); - _vertexFormat = vf; - } + if (!_vertexFormat) { + auto vf = std::make_shared(); + vf->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); + vf->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 12); + _vertexFormat = vf; + } gpu::Batch& batch = *args->_batch; batch.setPipeline(_pipeline); Transform transform(voxelToWorldMatrix()); batch.setModelTransform(transform); -// batch.setInputFormat(mesh->getVertexFormat()); - batch.setInputFormat(_vertexFormat); + batch.setInputFormat(_vertexFormat); + + // ok + if (usecTimestampNow() - start < 200000) { + return; + } - // batch.setInputStream(0, mesh->getVertexStream()); batch.setInputBuffer(gpu::Stream::POSITION, mesh->getVertexBuffer()._buffer, 0, sizeof(PolyVox::PositionMaterialNormal)); - /* batch.setInputBuffer(gpu::Stream::NORMAL, mesh->getVertexBuffer()._buffer, - sizeof(float) * 3, - sizeof(PolyVox::PositionMaterialNormal));*/ + + + // crash + // if (usecTimestampNow() - start < 200000) { + // return; + // } batch.setIndexBuffer(gpu::UINT32, mesh->getIndexBuffer()._buffer, 0); From 3278478a60faa8c28080ded01d58f3e3e68fa74b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Nov 2016 15:26:16 -0800 Subject: [PATCH 44/50] cleanups --- .../src/RenderablePolyVoxEntityItem.cpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index c46a13c5d9..b3334b7ed0 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -1170,23 +1170,17 @@ void RenderablePolyVoxEntityItem::getMesh() { auto vertexBuffer = std::make_shared(vecVertices.size() * sizeof(PolyVox::PositionMaterialNormal), (gpu::Byte*)vecVertices.data()); auto vertexBufferPtr = gpu::BufferPointer(vertexBuffer); - // if (vertexBufferPtr->getSize() > sizeof(PolyVox::PositionMaterialNormal)) { - // vertexBufferSize = vertexBufferPtr->getSize() - sizeof(float) * 4; - // normalBufferSize = vertexBufferPtr->getSize() - sizeof(float); - // } gpu::BufferView vertexBufferView(vertexBufferPtr, 0, - vertexBufferPtr->getSize(), - // vertexBufferSize, + vertexBufferPtr->getSize(), sizeof(PolyVox::PositionMaterialNormal), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW)); mesh->setVertexBuffer(vertexBufferView); mesh->addAttribute(gpu::Stream::NORMAL, gpu::BufferView(vertexBufferPtr, sizeof(float) * 3, - vertexBufferPtr->getSize() , - // normalBufferSize, + vertexBufferPtr->getSize() , sizeof(PolyVox::PositionMaterialNormal), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW))); - entity->setMesh(mesh); + entity->setMesh(mesh); }); } From 47062d29f558463fb7ce3cecbb1546dd27624ff7 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Nov 2016 15:27:45 -0800 Subject: [PATCH 45/50] cleanups --- libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index e8656607d7..ee4c3b318f 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -149,7 +149,7 @@ private: // may not match _voxelVolumeSize. model::MeshPointer _mesh; - gpu::Stream::FormatPointer _vertexFormat; + gpu::Stream::FormatPointer _vertexFormat; bool _meshDirty { true }; // does collision-shape need to be recomputed? bool _meshInitialized { false }; From b2a28147c0180eb01bb52b0bca8e0cbb88c39690 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Nov 2016 15:36:29 -0800 Subject: [PATCH 46/50] more time --- .../src/RenderablePolyVoxEntityItem.cpp | 10 ++++------ .../src/RenderablePolyVoxEntityItem.h | 2 ++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index b3334b7ed0..890e3bfdd1 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -551,15 +551,13 @@ void RenderablePolyVoxEntityItem::setZTextureURL(QString zTextureURL) { } } -quint64 start { 0 }; - void RenderablePolyVoxEntityItem::render(RenderArgs* args) { PerformanceTimer perfTimer("RenderablePolyVoxEntityItem::render"); assert(getType() == EntityTypes::PolyVox); Q_ASSERT(args->_batch); - if (start == 0) { - start = usecTimestampNow(); + if (_start == 0) { + _start = usecTimestampNow(); return; } @@ -622,7 +620,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { batch.setInputFormat(_vertexFormat); // ok - if (usecTimestampNow() - start < 200000) { + if (usecTimestampNow() - _start < 600000) { return; } @@ -632,7 +630,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { // crash - // if (usecTimestampNow() - start < 200000) { + // if (usecTimestampNow() - _start < 600000) { // return; // } diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index ee4c3b318f..21533d4e69 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -188,6 +188,8 @@ private: void cacheNeighbors(); void copyUpperEdgesFromNeighbors(); void bonkNeighbors(); + + quint64 _start { 0 }; }; bool inUserBounds(const PolyVox::SimpleVolume* vol, PolyVoxEntityItem::PolyVoxSurfaceStyle surfaceStyle, From 8ee1fac3677a22797db171ec3a232cf25715434e Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 18 Nov 2016 16:33:48 -0800 Subject: [PATCH 47/50] don't try to render until mesh is ready --- .../src/RenderablePolyVoxEntityItem.cpp | 22 +++++-------------- .../src/RenderablePolyVoxEntityItem.h | 2 -- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 890e3bfdd1..e0c068ea6b 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -556,11 +556,6 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { assert(getType() == EntityTypes::PolyVox); Q_ASSERT(args->_batch); - if (_start == 0) { - _start = usecTimestampNow(); - return; - } - bool voxelDataDirty; bool volDataDirty; withWriteLock([&] { @@ -585,6 +580,11 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { voxelVolumeSize = _voxelVolumeSize; }); + if (!mesh || + !mesh->getIndexBuffer()._buffer) { + return; + } + if (!_pipeline) { gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert)); gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag)); @@ -618,22 +618,10 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { Transform transform(voxelToWorldMatrix()); batch.setModelTransform(transform); batch.setInputFormat(_vertexFormat); - - // ok - if (usecTimestampNow() - _start < 600000) { - return; - } - batch.setInputBuffer(gpu::Stream::POSITION, mesh->getVertexBuffer()._buffer, 0, sizeof(PolyVox::PositionMaterialNormal)); - - // crash - // if (usecTimestampNow() - _start < 600000) { - // return; - // } - batch.setIndexBuffer(gpu::UINT32, mesh->getIndexBuffer()._buffer, 0); if (!_xTextureURL.isEmpty() && !_xTexture) { diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 21533d4e69..ee4c3b318f 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -188,8 +188,6 @@ private: void cacheNeighbors(); void copyUpperEdgesFromNeighbors(); void bonkNeighbors(); - - quint64 _start { 0 }; }; bool inUserBounds(const PolyVox::SimpleVolume* vol, PolyVoxEntityItem::PolyVoxSurfaceStyle surfaceStyle, From 3074be7ad04c9c06a0d89e9581fc5e5859f740fd Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 16 Nov 2016 19:27:16 -0800 Subject: [PATCH 48/50] Glow line replacement without geometry shader --- .../hmd/DebugHmdDisplayPlugin.cpp | 4 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 63 ++++++++--- .../display-plugins/hmd/HmdDisplayPlugin.h | 5 +- libraries/render-utils/src/GeometryCache.cpp | 63 +++-------- libraries/render-utils/src/GeometryCache.h | 1 + libraries/render-utils/src/glowLine.slf | 28 +++-- libraries/render-utils/src/glowLine.slg | 102 ------------------ libraries/render-utils/src/glowLine.slv | 42 +++++++- 8 files changed, 121 insertions(+), 187 deletions(-) delete mode 100644 libraries/render-utils/src/glowLine.slg diff --git a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp index fa267e2c68..5a3e5afc86 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp @@ -39,11 +39,11 @@ bool DebugHmdDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _uiModelTransform = DependencyManager::get()->getModelTransform(); _frameInfos[frameIndex] = _currentRenderFrameInfo; - _handPoses[0] = glm::translate(mat4(), vec3(-0.3f, 0.0f, 0.0f)); + _handPoses[0] = glm::translate(mat4(), vec3(0.3f * cosf(secTimestampNow() * 3.0f), -0.3f * sinf(secTimestampNow() * 5.0f), 0.0f)); _handLasers[0].color = vec4(1, 0, 0, 1); _handLasers[0].mode = HandLaserMode::Overlay; - _handPoses[1] = glm::translate(mat4(), vec3(0.3f, 0.0f, 0.0f)); + _handPoses[1] = glm::translate(mat4(), vec3(0.3f * sinf(secTimestampNow() * 3.0f), -0.3f * cosf(secTimestampNow() * 5.0f), 0.0f)); _handLasers[1].color = vec4(0, 1, 1, 1); _handLasers[1].mode = HandLaserMode::Overlay; }); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index c5d7ac5690..4fa1c30815 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -32,6 +31,9 @@ #include "../Logging.h" #include "../CompositorHelper.h" +#include <../render-utils/shaders/render-utils/glowLine_vert.h> +#include <../render-utils/shaders/render-utils/glowLine_frag.h> + static const QString MONO_PREVIEW = "Mono Preview"; static const QString DISABLE_PREVIEW = "Disable Preview"; @@ -47,6 +49,14 @@ static const size_t NUMBER_OF_HANDS = 2; //#define LIVE_SHADER_RELOAD 1 extern glm::vec3 getPoint(float yaw, float pitch); +struct HandLaserData { + vec4 p1; + vec4 p2; + vec4 color; +}; + +static const uint32_t HAND_LASER_UNIFORM_SLOT = 1; + static QString readFile(const QString& filename) { QFile file(filename); file.open(QFile::Text | QFile::ReadOnly); @@ -112,11 +122,25 @@ void HmdDisplayPlugin::internalDeactivate() { void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); _overlayRenderer.build(); - auto geometryCache = DependencyManager::get(); - for (size_t i = 0; i < _geometryIds.size(); ++i) { - _geometryIds[i] = geometryCache->allocateID(); - } - _extraLaserID = geometryCache->allocateID(); + + { + auto state = std::make_shared(); + auto VS = gpu::Shader::createVertex(std::string(glowLine_vert)); + auto PS = gpu::Shader::createPixel(std::string(glowLine_frag)); + auto program = gpu::Shader::createProgram(VS, PS); + state->setCullMode(gpu::State::CULL_NONE); + state->setDepthTest(true, false, gpu::LESS_EQUAL); + state->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("lineData"), HAND_LASER_UNIFORM_SLOT)); + gpu::Shader::makeProgram(*program, slotBindings); + _glowLinePipeline = gpu::Pipeline::create(program, state); + _handLaserUniforms = std::array{ { std::make_shared(), std::make_shared() } }; + _extraLaserUniforms = std::make_shared(); + }; + } void HmdDisplayPlugin::uncustomizeContext() { @@ -131,12 +155,10 @@ void HmdDisplayPlugin::uncustomizeContext() { }); _overlayRenderer = OverlayRenderer(); _previewTexture.reset(); - - auto geometryCache = DependencyManager::get(); - for (size_t i = 0; i < _geometryIds.size(); ++i) { - geometryCache->releaseID(_geometryIds[i]); - } - geometryCache->releaseID(_extraLaserID); + _handLaserUniforms[0].reset(); + _handLaserUniforms[1].reset(); + _extraLaserUniforms.reset(); + _glowLinePipeline.reset(); Parent::uncustomizeContext(); } @@ -683,11 +705,15 @@ void HmdDisplayPlugin::compositeExtra() { return; } - auto geometryCache = DependencyManager::get(); render([&](gpu::Batch& batch) { batch.setFramebuffer(_compositeFramebuffer); + batch.setModelTransform(Transform()); batch.setViewportTransform(ivec4(uvec2(0), _renderTargetSize)); batch.setViewTransform(_currentPresentFrameInfo.presentPose, false); + // Compile the shaders + batch.setPipeline(_glowLinePipeline); + + bilateral::for_each_side([&](bilateral::Side side){ auto index = bilateral::index(side); if (_presentHandPoses[index] == IDENTITY_MATRIX) { @@ -696,13 +722,20 @@ void HmdDisplayPlugin::compositeExtra() { const auto& laser = _presentHandLasers[index]; if (laser.valid()) { const auto& points = _presentHandLaserPoints[index]; - geometryCache->renderGlowLine(batch, points.first, points.second, laser.color, _geometryIds[index]); + _handLaserUniforms[index]->resize(sizeof(HandLaserData)); + _handLaserUniforms[index]->setSubData(0, HandLaserData { vec4(points.first, 1.0f), vec4(points.second, 1.0f), _handLasers[index].color }); + batch.setUniformBuffer(HAND_LASER_UNIFORM_SLOT, _handLaserUniforms[index]); + qDebug() << "Render line " << index; + batch.draw(gpu::TRIANGLE_STRIP, 4, 0); } }); if (_presentExtraLaser.valid()) { const auto& points = _presentExtraLaserPoints; - geometryCache->renderGlowLine(batch, points.first, points.second, _presentExtraLaser.color, _extraLaserID); + _extraLaserUniforms->resize(sizeof(HandLaserData)); + _extraLaserUniforms->setSubData(0, HandLaserData { vec4(points.first, 1.0f), vec4(points.second, 1.0f), _presentExtraLaser.color }); + batch.setUniformBuffer(HAND_LASER_UNIFORM_SLOT, _extraLaserUniforms); + batch.draw(gpu::TRIANGLE_STRIP, 4, 0); } }); } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index 435f547899..aaa6e347e0 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -80,8 +80,6 @@ protected: Transform _presentUiModelTransform; std::array _presentHandLasers; - std::array _geometryIds; - int _extraLaserID; std::array _presentHandPoses; std::array, 2> _presentHandLaserPoints; @@ -120,6 +118,9 @@ private: bool _disablePreviewItemAdded { false }; bool _monoPreview { true }; bool _clearPreviewFlag { false }; + std::array _handLaserUniforms; + gpu::BufferPointer _extraLaserUniforms; + gpu::PipelinePointer _glowLinePipeline; gpu::TexturePointer _previewTexture; glm::vec2 _lastWindowSize; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 3f5dc26db2..a19f1844f0 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -38,7 +38,6 @@ #include "simple_opaque_web_browser_frag.h" #include "simple_transparent_web_browser_frag.h" #include "glowLine_vert.h" -#include "glowLine_geom.h" #include "glowLine_frag.h" #include "grid_frag.h" @@ -1405,6 +1404,7 @@ GeometryCache::BatchItemDetails::~BatchItemDetails() { void GeometryCache::BatchItemDetails::clear() { isCreated = false; + uniformBuffer.reset(); verticesBuffer.reset(); colorBuffer.reset(); streamFormat.reset(); @@ -1593,8 +1593,6 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glowIntensity = 0.0f; #endif - glowIntensity = 0.0f; - if (glowIntensity <= 0) { bindSimpleProgram(batch, false, false, false, true, false); renderLine(batch, p1, p2, color, id); @@ -1602,20 +1600,20 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const } // Compile the shaders + static const uint32_t LINE_DATA_SLOT = 1; static std::once_flag once; std::call_once(once, [&] { auto state = std::make_shared(); auto VS = gpu::Shader::createVertex(std::string(glowLine_vert)); - auto GS = gpu::Shader::createGeometry(std::string(glowLine_geom)); auto PS = gpu::Shader::createPixel(std::string(glowLine_frag)); - auto program = gpu::Shader::createProgram(VS, GS, PS); + auto program = gpu::Shader::createProgram(VS, PS); state->setCullMode(gpu::State::CULL_NONE); state->setDepthTest(true, false, gpu::LESS_EQUAL); state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), render::ShapePipeline::Slot::MAP::NORMAL_FITTING)); + slotBindings.insert(gpu::Shader::Binding(std::string("lineData"), LINE_DATA_SLOT)); gpu::Shader::makeProgram(*program, slotBindings); _glowLinePipeline = gpu::Pipeline::create(program, state); }); @@ -1626,11 +1624,6 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const bool registered = (id != UNKNOWN_ID); BatchItemDetails& details = _registeredLine3DVBOs[id]; - int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); - // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed if (registered && details.isCreated) { Vec3Pair& lastKey = _lastRegisteredLine3D[id]; @@ -1640,47 +1633,25 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const } } - const int FLOATS_PER_VERTEX = 3 + 3; // vertices + normals - const int NUM_POS_COORDS = 3; - const int VERTEX_NORMAL_OFFSET = NUM_POS_COORDS * sizeof(float); - const int vertices = 2; + const int NUM_VERTICES = 4; if (!details.isCreated) { details.isCreated = true; - details.vertices = vertices; - details.vertexSize = FLOATS_PER_VERTEX; + details.uniformBuffer = std::make_shared(); - auto verticesBuffer = std::make_shared(); - auto colorBuffer = std::make_shared(); - auto streamFormat = std::make_shared(); - auto stream = std::make_shared(); + struct LineData { + vec4 p1; + vec4 p2; + vec4 color; + }; - details.verticesBuffer = verticesBuffer; - details.colorBuffer = colorBuffer; - details.streamFormat = streamFormat; - details.stream = stream; - - details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); - details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET); - details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); - - details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride); - details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride); - - const glm::vec3 NORMAL(1.0f, 0.0f, 0.0f); - float vertexBuffer[vertices * FLOATS_PER_VERTEX] = { - p1.x, p1.y, p1.z, NORMAL.x, NORMAL.y, NORMAL.z, - p2.x, p2.y, p2.z, NORMAL.x, NORMAL.y, NORMAL.z }; - - const int NUM_COLOR_SCALARS = 2; - int colors[NUM_COLOR_SCALARS] = { compactColor, compactColor }; - details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); - details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); + LineData lineData { vec4(p1, 1.0f), vec4(p2, 1.0f), color }; + details.uniformBuffer->resize(sizeof(LineData)); + details.uniformBuffer->setSubData(0, lineData); } - // this is what it takes to render a quad - batch.setInputFormat(details.streamFormat); - batch.setInputStream(0, *details.stream); - batch.draw(gpu::LINES, 2, 0); + // The shader requires no vertices, only uniforms. + batch.setUniformBuffer(LINE_DATA_SLOT, details.uniformBuffer); + batch.draw(gpu::TRIANGLE_STRIP, NUM_VERTICES, 0); } void GeometryCache::useSimpleDrawPipeline(gpu::Batch& batch, bool noBlend) { diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 6e6ac89a8f..84dfd8ccc3 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -369,6 +369,7 @@ private: static int population; gpu::BufferPointer verticesBuffer; gpu::BufferPointer colorBuffer; + gpu::BufferPointer uniformBuffer; gpu::Stream::FormatPointer streamFormat; gpu::BufferStreamPointer stream; diff --git a/libraries/render-utils/src/glowLine.slf b/libraries/render-utils/src/glowLine.slf index edebc99c81..c0af97930a 100644 --- a/libraries/render-utils/src/glowLine.slf +++ b/libraries/render-utils/src/glowLine.slf @@ -10,26 +10,24 @@ // layout(location = 0) in vec4 inColor; -layout(location = 1) in vec3 inLineDistance; out vec4 _fragColor; void main(void) { - vec2 d = inLineDistance.xy; - d.y = abs(d.y); - d.x = abs(d.x); - if (d.x > 1.0) { - d.x = (d.x - 1.0) / 0.02; - } else { - d.x = 0.0; - } - float alpha = 1.0 - length(d); - if (alpha <= 0.0) { - discard; - } - alpha = pow(alpha, 10.0); - if (alpha < 0.05) { + // The incoming value actually ranges from -1 to 1, so modify it + // so that it goes from 0 -> 1 -> 0 with the solid alpha being at + // the center of the line + float alpha = 1.0 - abs(inColor.a); + + // Convert from a linear alpha curve to a sharp peaked one + alpha = pow(alpha, 10); + + // Drop everything where the curve falls off to nearly nothing + if (alpha <= 0.05) { discard; } + + // Emit the color _fragColor = vec4(inColor.rgb, alpha); + return; } diff --git a/libraries/render-utils/src/glowLine.slg b/libraries/render-utils/src/glowLine.slg deleted file mode 100644 index 9af8eaa4d0..0000000000 --- a/libraries/render-utils/src/glowLine.slg +++ /dev/null @@ -1,102 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// Created by Bradley Austin Davis on 2016/07/05 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#extension GL_EXT_geometry_shader4 : enable - -<@include gpu/Transform.slh@> -<$declareStandardCameraTransform()$> - -layout(location = 0) in vec4 inColor[]; - -layout(location = 0) out vec4 outColor; -layout(location = 1) out vec3 outLineDistance; - -layout(lines) in; -layout(triangle_strip, max_vertices = 24) out; - -vec3 ndcToEyeSpace(in vec4 v) { - TransformCamera cam = getTransformCamera(); - vec4 u = cam._projectionInverse * v; - return u.xyz / u.w; -} - -vec2 toScreenSpace(in vec4 v) -{ - TransformCamera cam = getTransformCamera(); - vec4 u = cam._projection * cam._view * v; - return u.xy / u.w; -} - -vec3[2] getOrthogonals(in vec3 n, float scale) { - float yDot = abs(dot(n, vec3(0, 1, 0))); - - vec3 result[2]; - if (yDot < 0.9) { - result[0] = normalize(cross(n, vec3(0, 1, 0))); - } else { - result[0] = normalize(cross(n, vec3(1, 0, 0))); - } - // The cross of result[0] and n is orthogonal to both, which are orthogonal to each other - result[1] = cross(result[0], n); - result[0] *= scale; - result[1] *= scale; - return result; -} - - -vec2 orthogonal(vec2 v) { - vec2 result = v.yx; - result.y *= -1.0; - return result; -} - -void main() { - vec2 endpoints[2]; - vec3 eyeSpace[2]; - TransformCamera cam = getTransformCamera(); - for (int i = 0; i < 2; ++i) { - eyeSpace[i] = ndcToEyeSpace(gl_PositionIn[i]); - endpoints[i] = gl_PositionIn[i].xy / gl_PositionIn[i].w; - } - vec2 lineNormal = normalize(endpoints[1] - endpoints[0]); - vec2 lineOrthogonal = orthogonal(lineNormal); - lineNormal *= 0.02; - lineOrthogonal *= 0.02; - - gl_Position = gl_PositionIn[0]; - gl_Position.xy -= lineOrthogonal; - outColor = inColor[0]; - outLineDistance = vec3(-1.02, -1, gl_Position.z); - EmitVertex(); - - gl_Position = gl_PositionIn[0]; - gl_Position.xy += lineOrthogonal; - outColor = inColor[0]; - outLineDistance = vec3(-1.02, 1, gl_Position.z); - EmitVertex(); - - gl_Position = gl_PositionIn[1]; - gl_Position.xy -= lineOrthogonal; - outColor = inColor[1]; - outLineDistance = vec3(1.02, -1, gl_Position.z); - EmitVertex(); - - gl_Position = gl_PositionIn[1]; - gl_Position.xy += lineOrthogonal; - outColor = inColor[1]; - outLineDistance = vec3(1.02, 1, gl_Position.z); - EmitVertex(); - - EndPrimitive(); -} - - - - diff --git a/libraries/render-utils/src/glowLine.slv b/libraries/render-utils/src/glowLine.slv index aa126fe31a..e856edc787 100644 --- a/libraries/render-utils/src/glowLine.slv +++ b/libraries/render-utils/src/glowLine.slv @@ -9,18 +9,50 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -<@include gpu/Inputs.slh@> -<@include gpu/Color.slh@> <@include gpu/Transform.slh@> <$declareStandardTransform()$> +layout(std140) uniform lineData { + vec4 p1; + vec4 p2; + vec4 color; +}; + layout(location = 0) out vec4 _color; void main(void) { - _color = inColor; + _color = color; - // standard transform TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); - <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> + + vec4 p1eye, p2eye; + <$transformModelToEyePos(cam, obj, p1, p1eye)$> + <$transformModelToEyePos(cam, obj, p2, p2eye)$> + p1eye /= p1eye.w; + p2eye /= p2eye.w; + + // Find the line direction + vec3 v1 = normalize(p1eye.xyz - p2eye.xyz); + // Find the vector from the eye to one of the points + vec3 v2 = normalize(p1eye.xyz); + // The orthogonal vector is the cross product of these two + vec3 orthogonal = cross(v1, v2) * 0.02; + + // Deteremine which end to emit based on the vertex id (even / odd) + vec4 eye = (0 == gl_VertexID % 2) ? p1eye : p2eye; + + // Add or subtract the orthogonal vector based on a different vertex ID + // calculation + if (gl_VertexID < 2) { + // Use the alpha channel to store the distance from the center in 'quad space' + _color.a = -1.0; + eye.xyz -= orthogonal; + } else { + _color.a = 1.0; + eye.xyz += orthogonal; + } + + // Finally, put the eyespace vertex into clip space + <$transformEyeToClipPos(cam, eye, gl_Position)$> } \ No newline at end of file From 362976645b764b2516d905fcaf513748c19823d8 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 18 Nov 2016 17:42:18 -0800 Subject: [PATCH 49/50] Fix HMD UI glow lines --- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 18 +++++++++--------- .../src/display-plugins/hmd/HmdDisplayPlugin.h | 1 + 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 4fa1c30815..d01f2407eb 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -55,8 +55,6 @@ struct HandLaserData { vec4 color; }; -static const uint32_t HAND_LASER_UNIFORM_SLOT = 1; - static QString readFile(const QString& filename) { QFile file(filename); file.open(QFile::Text | QFile::ReadOnly); @@ -133,10 +131,13 @@ void HmdDisplayPlugin::customizeContext() { state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("lineData"), HAND_LASER_UNIFORM_SLOT)); - gpu::Shader::makeProgram(*program, slotBindings); + gpu::Shader::makeProgram(*program, gpu::Shader::BindingSet()); _glowLinePipeline = gpu::Pipeline::create(program, state); + for (const auto& buffer : program->getBuffers()) { + if (buffer._name == "lineData") { + _handLaserUniformSlot = buffer._location; + } + } _handLaserUniforms = std::array{ { std::make_shared(), std::make_shared() } }; _extraLaserUniforms = std::make_shared(); }; @@ -704,7 +705,7 @@ void HmdDisplayPlugin::compositeExtra() { if (_presentHandPoses[0] == IDENTITY_MATRIX && _presentHandPoses[1] == IDENTITY_MATRIX && !_presentExtraLaser.valid()) { return; } - + render([&](gpu::Batch& batch) { batch.setFramebuffer(_compositeFramebuffer); batch.setModelTransform(Transform()); @@ -724,8 +725,7 @@ void HmdDisplayPlugin::compositeExtra() { const auto& points = _presentHandLaserPoints[index]; _handLaserUniforms[index]->resize(sizeof(HandLaserData)); _handLaserUniforms[index]->setSubData(0, HandLaserData { vec4(points.first, 1.0f), vec4(points.second, 1.0f), _handLasers[index].color }); - batch.setUniformBuffer(HAND_LASER_UNIFORM_SLOT, _handLaserUniforms[index]); - qDebug() << "Render line " << index; + batch.setUniformBuffer(_handLaserUniformSlot, _handLaserUniforms[index]); batch.draw(gpu::TRIANGLE_STRIP, 4, 0); } }); @@ -734,7 +734,7 @@ void HmdDisplayPlugin::compositeExtra() { const auto& points = _presentExtraLaserPoints; _extraLaserUniforms->resize(sizeof(HandLaserData)); _extraLaserUniforms->setSubData(0, HandLaserData { vec4(points.first, 1.0f), vec4(points.second, 1.0f), _presentExtraLaser.color }); - batch.setUniformBuffer(HAND_LASER_UNIFORM_SLOT, _extraLaserUniforms); + batch.setUniformBuffer(_handLaserUniformSlot, _extraLaserUniforms); batch.draw(gpu::TRIANGLE_STRIP, 4, 0); } }); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index aaa6e347e0..5443403364 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -119,6 +119,7 @@ private: bool _monoPreview { true }; bool _clearPreviewFlag { false }; std::array _handLaserUniforms; + uint32_t _handLaserUniformSlot { 0 }; gpu::BufferPointer _extraLaserUniforms; gpu::PipelinePointer _glowLinePipeline; gpu::TexturePointer _previewTexture; From fe709f51de32f2a78a04e5238e1c34898d3d2079 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Sat, 19 Nov 2016 15:05:53 -0800 Subject: [PATCH 50/50] personal space feature --- assignment-client/src/AvatarAudioTimer.cpp | 1 - assignment-client/src/audio/AudioMixer.cpp | 35 ++- assignment-client/src/audio/AudioMixer.h | 1 + .../src/audio/AudioMixerClientData.cpp | 4 - .../src/audio/AudioMixerClientData.h | 1 + assignment-client/src/avatars/AvatarMixer.cpp | 19 ++ assignment-client/src/avatars/AvatarMixer.h | 1 + .../src/avatars/AvatarMixerClientData.cpp | 13 + .../src/avatars/AvatarMixerClientData.h | 8 + interface/src/ui/PreferencesDialog.cpp | 14 +- libraries/networking/src/Node.cpp | 10 + libraries/networking/src/Node.h | 7 + libraries/networking/src/NodeList.cpp | 22 +- libraries/networking/src/NodeList.h | 13 +- libraries/networking/src/udt/PacketHeaders.h | 3 +- .../src/UsersScriptingInterface.cpp | 24 ++ .../src/UsersScriptingInterface.h | 66 +++++ scripts/defaultScripts.js | 3 +- scripts/system/assets/images/tools/bubble.svg | 275 ++++++++++++++++++ scripts/system/bubble.js | 58 ++++ 20 files changed, 557 insertions(+), 21 deletions(-) create mode 100644 scripts/system/assets/images/tools/bubble.svg create mode 100644 scripts/system/bubble.js diff --git a/assignment-client/src/AvatarAudioTimer.cpp b/assignment-client/src/AvatarAudioTimer.cpp index 77dd61043e..d031b9d9f6 100644 --- a/assignment-client/src/AvatarAudioTimer.cpp +++ b/assignment-client/src/AvatarAudioTimer.cpp @@ -15,7 +15,6 @@ // this should send a signal every 10ms, with pretty good precision. Hardcoding // to 10ms since that's what you'd want for audio. void AvatarAudioTimer::start() { - qDebug() << __FUNCTION__; auto startTime = usecTimestampNow(); quint64 frameCounter = 0; const int TARGET_INTERVAL_USEC = 10000; // 10ms diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index ffd7cc703b..3dba1ce1c2 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -95,7 +95,7 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); packetReceiver.registerListener(PacketType::NodeMuteRequest, this, "handleNodeMuteRequestPacket"); - + packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket"); connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled); } @@ -393,16 +393,26 @@ bool AudioMixer::prepareMixForListeningNode(Node* node) { && !node->isIgnoringNodeWithID(otherNode->getUUID()) && !otherNode->isIgnoringNodeWithID(node->getUUID())) { AudioMixerClientData* otherNodeClientData = (AudioMixerClientData*) otherNode->getLinkedData(); - // enumerate the ARBs attached to the otherNode and add all that should be added to mix - auto streamsCopy = otherNodeClientData->getAudioStreams(); + // check to see if we're ignoring in radius + bool insideIgnoreRadius = false; + if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) { + AudioMixerClientData* otherData = reinterpret_cast(otherNode->getLinkedData()); + AudioMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); + float ignoreRadius = glm::min(node->getIgnoreRadius(), otherNode->getIgnoreRadius()); + if (glm::distance(nodeData->getPosition(), otherData->getPosition()) < ignoreRadius) { + insideIgnoreRadius = true; + } + } - for (auto& streamPair : streamsCopy) { - - auto otherNodeStream = streamPair.second; - - if (*otherNode != *node || otherNodeStream->shouldLoopbackForNode()) { - addStreamToMixForListeningNodeWithStream(*listenerNodeData, *otherNodeStream, otherNode->getUUID(), - *nodeAudioStream); + if (!insideIgnoreRadius) { + // enumerate the ARBs attached to the otherNode and add all that should be added to mix + auto streamsCopy = otherNodeClientData->getAudioStreams(); + for (auto& streamPair : streamsCopy) { + auto otherNodeStream = streamPair.second; + if (*otherNode != *node || otherNodeStream->shouldLoopbackForNode()) { + addStreamToMixForListeningNodeWithStream(*listenerNodeData, *otherNodeStream, otherNode->getUUID(), + *nodeAudioStream); + } } } } @@ -634,11 +644,14 @@ void AudioMixer::handleKillAvatarPacket(QSharedPointer packet, } } - void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { sendingNode->parseIgnoreRequestMessage(packet); } +void AudioMixer::handleRadiusIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { + sendingNode->parseIgnoreRadiusRequestMessage(packet); +} + void AudioMixer::removeHRTFsForFinishedInjector(const QUuid& streamID) { auto injectorClientData = qobject_cast(sender()); if (injectorClientData) { diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index 91eafadd9d..9bf337fe60 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -48,6 +48,7 @@ private slots: void handleNegotiateAudioFormat(QSharedPointer message, SharedNodePointer sendingNode); void handleNodeKilled(SharedNodePointer killedNode); void handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); + void handleRadiusIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleKillAvatarPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleNodeMuteRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 5b8c4aa105..70d6a67b5b 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -365,10 +365,6 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() { } void AudioMixerClientData::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) { - qDebug() << __FUNCTION__ << - "sendingNode:" << *node << - "currentCodec:" << currentCodec << - "receivedCodec:" << recievedCodec; sendSelectAudioFormat(node, currentCodec); } diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index c74461a444..a8b6b6606d 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -89,6 +89,7 @@ public: bool shouldMuteClient() { return _shouldMuteClient; } void setShouldMuteClient(bool shouldMuteClient) { _shouldMuteClient = shouldMuteClient; } + glm::vec3 getPosition() { return getAvatarAudioStream() ? getAvatarAudioStream()->getPosition() : glm::vec3(0); } signals: void injectorStreamFinished(const QUuid& streamIdentifier); diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 2c9fadc7b1..63cda4a4ff 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -46,6 +46,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::AvatarIdentity, this, "handleAvatarIdentityPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); + packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket"); auto nodeList = DependencyManager::get(); connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &AvatarMixer::handlePacketVersionMismatch); @@ -237,6 +238,20 @@ void AvatarMixer::broadcastAvatarData() { || otherNode->isIgnoringNodeWithID(node->getUUID())) { return false; } else { + AvatarMixerClientData* otherData = reinterpret_cast(otherNode->getLinkedData()); + AvatarMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); + // check to see if we're ignoring in radius + if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) { + float ignoreRadius = glm::min(node->getIgnoreRadius(), otherNode->getIgnoreRadius()); + if (glm::distance(nodeData->getPosition(), otherData->getPosition()) < ignoreRadius) { + nodeData->ignoreOther(node, otherNode); + otherData->ignoreOther(otherNode, node); + return false; + } + } + // not close enough to ignore + nodeData->removeFromRadiusIgnoringSet(otherNode->getUUID()); + otherData->removeFromRadiusIgnoringSet(node->getUUID()); return true; } }, @@ -442,6 +457,10 @@ void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer senderNode->parseIgnoreRequestMessage(message); } +void AvatarMixer::handleRadiusIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { + sendingNode->parseIgnoreRadiusRequestMessage(packet); +} + void AvatarMixer::sendStatsPacket() { QJsonObject statsObject; statsObject["average_listeners_last_second"] = (float) _sumListeners / (float) _numStatFrames; diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 6e1d722145..f537cc9244 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -38,6 +38,7 @@ private slots: void handleAvatarIdentityPacket(QSharedPointer message, SharedNodePointer senderNode); void handleKillAvatarPacket(QSharedPointer message); void handleNodeIgnoreRequestPacket(QSharedPointer message, SharedNodePointer senderNode); + void handleRadiusIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); void domainSettingsRequestComplete(); void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID); diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 4b7a696d58..60d03f8930 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -11,6 +11,9 @@ #include +#include +#include + #include "AvatarMixerClientData.h" int AvatarMixerClientData::parseData(ReceivedMessage& message) { @@ -39,6 +42,16 @@ uint16_t AvatarMixerClientData::getLastBroadcastSequenceNumber(const QUuid& node } } +void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointer other) { + if (!isRadiusIgnoring(other->getUUID())) { + addToRadiusIgnoringSet(other->getUUID()); + auto killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID); + killPacket->write(other->getUUID().toRfc4122()); + DependencyManager::get()->sendUnreliablePacket(*killPacket, *self); + _hasReceivedFirstPacketsFrom.erase(other->getUUID()); + } +} + void AvatarMixerClientData::loadJSONStats(QJsonObject& jsonObject) const { jsonObject["display_name"] = _avatar->getDisplayName(); jsonObject["full_rate_distance"] = _fullRateDistance; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 4a816291f4..96bc275a13 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -79,6 +79,13 @@ public: { return _avgOtherAvatarDataRate.getAverageSampleValuePerSecond() / (float) BYTES_PER_KILOBIT; } void loadJSONStats(QJsonObject& jsonObject) const; + + glm::vec3 getPosition() { return _avatar ? _avatar->getPosition() : glm::vec3(0); } + bool isRadiusIgnoring(const QUuid& other) { return _radiusIgnoredOthers.find(other) != _radiusIgnoredOthers.end(); } + void addToRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.insert(other); } + void removeFromRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.erase(other); } + void ignoreOther(SharedNodePointer self, SharedNodePointer other); + private: AvatarSharedPointer _avatar { new AvatarData() }; @@ -99,6 +106,7 @@ private: int _numOutOfOrderSends = 0; SimpleMovingAverage _avgOtherAvatarDataRate; + std::unordered_set _radiusIgnoredOthers; }; #endif // hifi_AvatarMixerClientData_h diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 35af1067d8..dea1c49346 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -32,7 +32,7 @@ void setupPreferences() { auto preferences = DependencyManager::get(); - + auto nodeList = DependencyManager::get(); auto myAvatar = DependencyManager::get()->getMyAvatar(); static const QString AVATAR_BASICS { "Avatar Basics" }; { @@ -68,6 +68,18 @@ void setupPreferences() { auto setter = [=](bool value) { myAvatar->setClearOverlayWhenMoving(value); }; preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Clear overlays when moving", getter, setter)); } + { + auto getter = [=]()->float { return nodeList->getIgnoreRadius(); }; + auto setter = [=](float value) { + nodeList->ignoreNodesInRadius(value, nodeList->getIgnoreRadiusEnabled()); + }; + auto preference = new SpinnerPreference(AVATAR_BASICS, "Personal space bubble radius (default is 1m)", getter, setter); + preference->setMin(0.01f); + preference->setMax(99.9f); + preference->setDecimals(2); + preference->setStep(0.25); + preferences->addPreference(preference); + } // UI { diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 406498b025..36e7cc961b 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -64,6 +64,7 @@ Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket, { // Update socket's object name setType(_type); + _ignoreRadiusEnabled = false; } void Node::setType(char type) { @@ -101,6 +102,15 @@ void Node::addIgnoredNode(const QUuid& otherNodeID) { } } +void Node::parseIgnoreRadiusRequestMessage(QSharedPointer message) { + bool enabled; + float radius; + message->readPrimitive(&enabled); + message->readPrimitive(&radius); + _ignoreRadiusEnabled = enabled; + _ignoreRadius = radius; +} + QDataStream& operator<<(QDataStream& out, const Node& node) { out << node._type; out << node._uuid; diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 18088c6cea..ab8cdb3a41 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -74,10 +74,14 @@ public: void parseIgnoreRequestMessage(QSharedPointer message); void addIgnoredNode(const QUuid& otherNodeID); bool isIgnoringNodeWithID(const QUuid& nodeID) const { return _ignoredNodeIDSet.find(nodeID) != _ignoredNodeIDSet.cend(); } + void parseIgnoreRadiusRequestMessage(QSharedPointer message); friend QDataStream& operator<<(QDataStream& out, const Node& node); friend QDataStream& operator>>(QDataStream& in, Node& node); + bool isIgnoreRadiusEnabled() const { return _ignoreRadiusEnabled; } + float getIgnoreRadius() { return _ignoreRadiusEnabled ? _ignoreRadius.load() : std::numeric_limits::max(); } + private: // privatize copy and assignment operator to disallow Node copying Node(const Node &otherNode); @@ -94,6 +98,9 @@ private: MovingPercentile _clockSkewMovingPercentile; NodePermissions _permissions; tbb::concurrent_unordered_set _ignoredNodeIDSet; + + std::atomic_bool _ignoreRadiusEnabled; + std::atomic _ignoreRadius { 0.0f }; }; Q_DECLARE_METATYPE(Node*) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 361070b306..86b9bc1794 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -750,9 +750,26 @@ bool NodeList::sockAddrBelongsToDomainOrNode(const HifiSockAddr& sockAddr) { return _domainHandler.getSockAddr() == sockAddr || LimitedNodeList::sockAddrBelongsToNode(sockAddr); } +void NodeList::ignoreNodesInRadius(float radiusToIgnore, bool enabled) { + _ignoreRadiusEnabled.set(enabled); + _ignoreRadius.set(radiusToIgnore); + + eachMatchingNode([](const SharedNodePointer& node)->bool { + return (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer); + }, [this](const SharedNodePointer& destinationNode) { + sendIgnoreRadiusStateToNode(destinationNode); + }); +} + +void NodeList::sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode) { + auto ignorePacket = NLPacket::create(PacketType::RadiusIgnoreRequest, sizeof(bool) + sizeof(float), true); + ignorePacket->writePrimitive(_ignoreRadiusEnabled.get()); + ignorePacket->writePrimitive(_ignoreRadius.get()); + sendPacket(std::move(ignorePacket), *destinationNode); +} + void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) { // enumerate the nodes to send a reliable ignore packet to each that can leverage it - if (!nodeID.isNull() && _sessionUUID != nodeID) { eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool { if (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer) { @@ -811,6 +828,9 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { // send this NLPacketList to the new node sendPacketList(std::move(ignorePacketList), *newNode); } + + // also send them the current ignore radius state. + sendIgnoreRadiusStateToNode(newNode); } } diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 4c06a13469..f30283f3c2 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -30,6 +30,7 @@ #include #include +#include #include "DomainHandler.h" #include "LimitedNodeList.h" @@ -70,6 +71,12 @@ public: void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; } + void ignoreNodesInRadius(float radiusToIgnore, bool enabled = true); + float getIgnoreRadius() const { return _ignoreRadius.get(); } + bool getIgnoreRadiusEnabled() const { return _ignoreRadiusEnabled.get(); } + void toggleIgnoreRadius() { ignoreNodesInRadius(getIgnoreRadius(), !getIgnoreRadiusEnabled()); } + void enableIgnoreRadius() { ignoreNodesInRadius(getIgnoreRadius(), true); } + void disableIgnoreRadius() { ignoreNodesInRadius(getIgnoreRadius(), false); } void ignoreNodeBySessionID(const QUuid& nodeID); bool isIgnoringNode(const QUuid& nodeID) const; @@ -101,7 +108,7 @@ signals: void limitOfSilentDomainCheckInsReached(); void receivedDomainServerList(); void ignoredNode(const QUuid& nodeID); - + private slots: void stopKeepalivePingTimer(); void sendPendingDSPathQuery(); @@ -146,6 +153,10 @@ private: mutable QReadWriteLock _ignoredSetLock; tbb::concurrent_unordered_set _ignoredNodeIDs; + void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); + Setting::Handle _ignoreRadiusEnabled { "IgnoreRadiusEnabled", false }; + Setting::Handle _ignoreRadius { "IgnoreRadius", 1.0f }; + #if (PR_BUILD || DEV_BUILD) bool _shouldSendNewerVersion { false }; #endif diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 8d63b972cc..2b17aa7d57 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -100,7 +100,8 @@ public: MoreEntityShapes, NodeKickRequest, NodeMuteRequest, - LAST_PACKET_TYPE = NodeMuteRequest + RadiusIgnoreRequest, + LAST_PACKET_TYPE = RadiusIgnoreRequest }; }; diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index 702368c2b3..c809617995 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -38,3 +38,27 @@ bool UsersScriptingInterface::getCanKick() { // ask the NodeList to return our ability to kick return DependencyManager::get()->getThisNodeCanKick(); } + +void UsersScriptingInterface::toggleIgnoreRadius() { + DependencyManager::get()->toggleIgnoreRadius(); +} + +void UsersScriptingInterface::enableIgnoreRadius() { + DependencyManager::get()->enableIgnoreRadius(); +} + +void UsersScriptingInterface::disableIgnoreRadius() { + DependencyManager::get()->disableIgnoreRadius(); +} + +void UsersScriptingInterface::setIgnoreRadius(float radius, bool enabled) { + DependencyManager::get()->ignoreNodesInRadius(radius, enabled); +} + + float UsersScriptingInterface::getIgnoreRadius() { + return DependencyManager::get()->getIgnoreRadius(); +} + +bool UsersScriptingInterface::getIgnoreRadiusEnabled() { + return DependencyManager::get()->getIgnoreRadiusEnabled(); +} diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 3c98d0a393..07398558e5 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -16,6 +16,9 @@ #include +/**jsdoc +* @namespace Users +*/ class UsersScriptingInterface : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY @@ -26,12 +29,75 @@ public: UsersScriptingInterface(); public slots: + + /**jsdoc + * Ignore another user. + * @function Users.ignore + * @param {nodeID} nodeID The node or session ID of the user you want to ignore. + */ void ignore(const QUuid& nodeID); + + /**jsdoc + * Kick another user. + * @function Users.kick + * @param {nodeID} nodeID The node or session ID of the user you want to kick. + */ void kick(const QUuid& nodeID); + + /**jsdoc + * Mute another user. + * @function Users.mute + * @param {nodeID} nodeID The node or session ID of the user you want to mute. + */ void mute(const QUuid& nodeID); + /**jsdoc + * Returns `true` if the DomainServer will allow this Node/Avatar to make kick + * @function Users.getCanKick + * @return {bool} `true` if the client can kick other users, `false` if not. + */ bool getCanKick(); + /**jsdoc + * Toggle the state of the ignore in radius feature + * @function Users.toggleIgnoreRadius + */ + void toggleIgnoreRadius(); + + /**jsdoc + * Enables the ignore radius feature. + * @function Users.enableIgnoreRadius + */ + void enableIgnoreRadius(); + + /**jsdoc + * Disables the ignore radius feature. + * @function Users.disableIgnoreRadius + */ + void disableIgnoreRadius(); + + /**jsdoc + * sets the parameters for the ignore radius feature. + * @function Users.setIgnoreRadius + * @param {number} radius The radius for the auto ignore in radius feature + * @param {bool} [enabled=true] Whether the ignore in radius feature should be enabled + */ + void setIgnoreRadius(float radius, bool enabled = true); + + /**jsdoc + * Returns the effective radius of the ingore radius feature if it is enabled. + * @function Users.getIgnoreRadius + * @return {number} radius of the ignore feature + */ + float getIgnoreRadius(); + + /**jsdoc + * Returns `true` if the ignore in radius feature is enabled + * @function Users.getIgnoreRadiusEnabled + * @return {bool} `true` if the ignore in radius feature is enabled, `false` if not. + */ + bool getIgnoreRadiusEnabled(); + signals: void canKickChanged(bool canKick); }; diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 4376960ea5..90a77b508d 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -33,7 +33,8 @@ var DEFAULT_SCRIPTS = [ "system/dialTone.js", "system/firstPersonHMD.js", "system/snapshot.js", - "system/help.js" + "system/help.js", + "system/bubble.js" ]; // add a menu item for debugging diff --git a/scripts/system/assets/images/tools/bubble.svg b/scripts/system/assets/images/tools/bubble.svg new file mode 100644 index 0000000000..064b7734a9 --- /dev/null +++ b/scripts/system/assets/images/tools/bubble.svg @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/bubble.js b/scripts/system/bubble.js new file mode 100644 index 0000000000..ba317ecdca --- /dev/null +++ b/scripts/system/bubble.js @@ -0,0 +1,58 @@ +"use strict"; + +// +// bubble.js +// scripts/system/ +// +// Created by Brad Hefta-Gaub on 11/18/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +/* global Toolbars, Script, Users, Overlays, AvatarList, Controller, Camera, getControllerWorldLocation */ + + +(function() { // BEGIN LOCAL_SCOPE + +// grab the toolbar +var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + +var ASSETS_PATH = Script.resolvePath("assets"); +var TOOLS_PATH = Script.resolvePath("assets/images/tools/"); + +function buttonImageURL() { + return TOOLS_PATH + 'bubble.svg'; +} + +var bubbleActive = Users.getIgnoreRadiusEnabled(); + +// setup the mod button and add it to the toolbar +var button = toolbar.addButton({ + objectName: 'bubble', + imageURL: buttonImageURL(), + visible: true, + buttonState: bubbleActive ? 0 : 1, + defaultState: bubbleActive ? 0 : 1, + hoverState: bubbleActive ? 2 : 3, + alpha: 0.9 +}); + + +// handle clicks on the toolbar button +function buttonClicked(){ + Users.toggleIgnoreRadius(); + bubbleActive = Users.getIgnoreRadiusEnabled(); + button.writeProperty('buttonState', bubbleActive ? 0 : 1); + button.writeProperty('defaultState', bubbleActive ? 0 : 1); + button.writeProperty('hoverState', bubbleActive ? 2 : 3); +} + +button.clicked.connect(buttonClicked); + +// cleanup the toolbar button and overlays when script is stopped +Script.scriptEnding.connect(function() { + toolbar.removeButton('bubble'); +}); + +}()); // END LOCAL_SCOPE