// // ModelEntityItem.cpp // libraries/entities/src // // Created by Brad Hefta-Gaub on 12/4/13. // Copyright 2013 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 "EntitiesLogging.h" #include "EntityItemProperties.h" #include "EntityTree.h" #include "EntityTreeElement.h" #include "ResourceCache.h" #include "ModelEntityItem.h" const QString ModelEntityItem::DEFAULT_MODEL_URL = QString(""); const QString ModelEntityItem::DEFAULT_COMPOUND_SHAPE_URL = QString(""); EntityItemPointer ModelEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer entity(new ModelEntityItem(entityID), [](EntityItem* ptr) { ptr->deleteLater(); }); entity->setProperties(properties); return entity; } ModelEntityItem::ModelEntityItem(const EntityItemID& entityItemID) : EntityItem(entityItemID) { _type = EntityTypes::Model; _lastKnownCurrentFrame = -1; _color[0] = _color[1] = _color[2] = 0; } const QString ModelEntityItem::getTextures() const { QReadLocker locker(&_texturesLock); auto textures = _textures; return textures; } void ModelEntityItem::setTextures(const QString& textures) { QWriteLocker locker(&_texturesLock); _textures = textures; } EntityItemProperties ModelEntityItem::getProperties(EntityPropertyFlags desiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getXColor); COPY_ENTITY_PROPERTY_TO_PROPERTIES(modelURL, getModelURL); COPY_ENTITY_PROPERTY_TO_PROPERTIES(compoundShapeURL, getCompoundShapeURL); COPY_ENTITY_PROPERTY_TO_PROPERTIES(textures, getTextures); COPY_ENTITY_PROPERTY_TO_PROPERTIES(shapeType, getShapeType); COPY_ENTITY_PROPERTY_TO_PROPERTIES(jointRotationsSet, getJointRotationsSet); COPY_ENTITY_PROPERTY_TO_PROPERTIES(jointRotations, getJointRotations); COPY_ENTITY_PROPERTY_TO_PROPERTIES(jointTranslationsSet, getJointTranslationsSet); COPY_ENTITY_PROPERTY_TO_PROPERTIES(jointTranslations, getJointTranslations); _animationProperties.getProperties(properties); return properties; } bool ModelEntityItem::setProperties(const EntityItemProperties& properties) { bool somethingChanged = false; somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); SET_ENTITY_PROPERTY_FROM_PROPERTIES(modelURL, setModelURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(compoundShapeURL, setCompoundShapeURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(textures, setTextures); SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, setShapeType); SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointRotationsSet, setJointRotationsSet); SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointRotations, setJointRotations); SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointTranslationsSet, setJointTranslationsSet); SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointTranslations, setJointTranslations); bool somethingChangedInAnimations = _animationProperties.setProperties(properties); if (somethingChangedInAnimations) { _dirtyFlags |= Simulation::DIRTY_UPDATEABLE; } somethingChanged = somethingChanged || somethingChangedInAnimations; if (somethingChanged) { bool wantDebug = false; if (wantDebug) { uint64_t now = usecTimestampNow(); int elapsed = now - getLastEdited(); qCDebug(entities) << "ModelEntityItem::setProperties() AFTER update... edited AGO=" << elapsed << "now=" << now << " getLastEdited()=" << getLastEdited(); } setLastEdited(properties._lastEdited); } return somethingChanged; } int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args, EntityPropertyFlags& propertyFlags, bool overwriteLocalData, bool& somethingChanged) { int bytesRead = 0; const unsigned char* dataAt = data; bool animationPropertiesChanged = false; READ_ENTITY_PROPERTY(PROP_COLOR, rgbColor, setColor); READ_ENTITY_PROPERTY(PROP_MODEL_URL, QString, setModelURL); READ_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); READ_ENTITY_PROPERTY(PROP_TEXTURES, QString, setTextures); int bytesFromAnimation; withWriteLock([&] { // Note: since we've associated our _animationProperties with our _animationLoop, the readEntitySubclassDataFromBuffer() // will automatically read into the animation loop bytesFromAnimation = _animationProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args, propertyFlags, overwriteLocalData, animationPropertiesChanged); }); bytesRead += bytesFromAnimation; dataAt += bytesFromAnimation; READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, setShapeType); if (animationPropertiesChanged) { _dirtyFlags |= Simulation::DIRTY_UPDATEABLE; somethingChanged = true; } READ_ENTITY_PROPERTY(PROP_JOINT_ROTATIONS_SET, QVector, setJointRotationsSet); READ_ENTITY_PROPERTY(PROP_JOINT_ROTATIONS, QVector, setJointRotations); READ_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS_SET, QVector, setJointTranslationsSet); READ_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS, QVector, setJointTranslations); return bytesRead; } // TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time EntityPropertyFlags ModelEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += PROP_MODEL_URL; requestedProperties += PROP_COMPOUND_SHAPE_URL; requestedProperties += PROP_TEXTURES; requestedProperties += PROP_SHAPE_TYPE; requestedProperties += _animationProperties.getEntityProperties(params); requestedProperties += PROP_JOINT_ROTATIONS_SET; requestedProperties += PROP_JOINT_ROTATIONS; requestedProperties += PROP_JOINT_TRANSLATIONS_SET; requestedProperties += PROP_JOINT_TRANSLATIONS; return requestedProperties; } void ModelEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData, EntityPropertyFlags& requestedProperties, EntityPropertyFlags& propertyFlags, EntityPropertyFlags& propertiesDidntFit, int& propertyCount, OctreeElement::AppendState& appendState) const { bool successPropertyFits = true; APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); APPEND_ENTITY_PROPERTY(PROP_MODEL_URL, getModelURL()); APPEND_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, getCompoundShapeURL()); APPEND_ENTITY_PROPERTY(PROP_TEXTURES, getTextures()); withReadLock([&] { _animationProperties.appendSubclassData(packetData, params, entityTreeElementExtraEncodeData, requestedProperties, propertyFlags, propertiesDidntFit, propertyCount, appendState); }); APPEND_ENTITY_PROPERTY(PROP_SHAPE_TYPE, (uint32_t)getShapeType()); APPEND_ENTITY_PROPERTY(PROP_JOINT_ROTATIONS_SET, getJointRotationsSet()); APPEND_ENTITY_PROPERTY(PROP_JOINT_ROTATIONS, getJointRotations()); APPEND_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS_SET, getJointTranslationsSet()); APPEND_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS, getJointTranslations()); } //angus void ModelEntityItem::update(const quint64& now) { //put something here //qCDebug(entities) << "model entity item update" << getName() << " " << getEntityItemID(); { auto currentAnimationProperties = this->getAnimationProperties(); if (_previousAnimationProperties != currentAnimationProperties) { qCDebug(entities) << "this is where the _currentFrame change is handled in the ModelEntityItem.cpp code"; withWriteLock([&] { _previousAnimationProperties = currentAnimationProperties; if ( (currentAnimationProperties.getFirstFrame() != _previousAnimationProperties.getFirstFrame()) || (currentAnimationProperties.getLastFrame() != _previousAnimationProperties.getLastFrame()) || (currentAnimationProperties.getRunning() && !_previousAnimationProperties.getRunning())) { // if (!(currentAnimationProperties.getCurrentFrame() > currentAnimationProperties.getLastFrame()) && !(currentAnimationProperties.getCurrentFrame() < currentAnimationProperties.getFirstFrame())) { // _currentlyPlayingFrame = currentAnimationProperties.getCurrentFrame(); //_endAnim = _currentlyPlayingFrame + ( currentAnimationProperties.getLastFrame() - currentAnimationProperties.getFirstFrame() ); //_lastAnimated = 0; // } qCDebug(entities) << "this is where the _currentFrame change is handled in the ModelEntityItem.cpp code, current frame is: \n\n" << currentAnimationProperties.getCurrentFrame(); setAnimationCurrentFrame(currentAnimationProperties.getFirstFrame()); } else { //else if ( _previousAnimationProperties.getLoop() && !currentAnimationProperties.getLoop()) { // int currentframe_mod_length = (int)(_currentlyPlayingFrame - (int)(glm::floor(currentAnimationProperties.getCurrentFrame()))) % ((int)(glm::floor(currentAnimationProperties.getLastFrame())) - (int)(glm::floor(currentAnimationProperties.getFirstFrame())) + 1); //_endAnim = _currentlyPlayingFrame + ((int)(currentAnimationProperties.getLastFrame()) - (int)(currentAnimationProperties.getFirstFrame())) - (float)currentframe_mod_length; //} setAnimationCurrentFrame(currentAnimationProperties.getCurrentFrame()); } }); qCDebug(entities) << "this is where the _currentFrame change is handled in the ModelEntityItem.cpp code, current frame is: " << currentAnimationProperties.getCurrentFrame(); } //_previousAnimationProperties = currentAnimationProperties; updateFrameCount(); } } bool ModelEntityItem::needsToCallUpdate() const { //put something here //qCDebug(entities) << "needs to call update"; return true; } void ModelEntityItem::updateFrameCount() { if (!_lastAnimated) { _lastAnimated = usecTimestampNow(); return; } auto now = usecTimestampNow(); //this is now getting the time since the server started the animation. //auto interval = now - _currentlyPlayingFrame; auto interval = now - _lastAnimated; _lastAnimated = now; //here we implement the looping animation property //get entity anim props bool isLooping = getAnimationLoop(); int firstFrame = getAnimationFirstFrame(); int lastFrame = getAnimationLastFrame(); bool isHolding = getAnimationHold(); int updatedFrameCount = lastFrame - firstFrame + 1; if (isLooping || (_currentFrame < _previousAnimationProperties.getLastFrame())) { //else advance the current frame. //if hold or not playing don't advance the current frame. //also if the animFrame is outside of first or last frame then don't advance the motion. if (!isHolding && getAnimationIsPlaying() && !(_previousAnimationProperties.getCurrentFrame() > _previousAnimationProperties.getLastFrame()) && !(_previousAnimationProperties.getCurrentFrame() < _previousAnimationProperties.getFirstFrame())) { float deltaTime = (float)interval / (float)USECS_PER_SECOND; _currentFrame += (deltaTime * _previousAnimationProperties.getFPS()); while ((_currentFrame - _previousAnimationProperties.getFirstFrame()) > updatedFrameCount) { _currentFrame -= updatedFrameCount; } qCDebug(entities) << "the frame is now 1 " << _currentFrame; // setAnimationCurrentlyPlayingFrame(_currentlyPlayingFrame); setAnimationCurrentFrame(_currentFrame); } } else { _currentFrame = getAnimationLastFrame(); setAnimationCurrentFrame(_currentFrame); qCDebug(entities) << "the frame is now 2 " << _currentFrame; } } //angus void ModelEntityItem::debugDump() const { qCDebug(entities) << "ModelEntityItem id:" << getEntityItemID(); qCDebug(entities) << " edited ago:" << getEditedAgo(); qCDebug(entities) << " position:" << getPosition(); qCDebug(entities) << " dimensions:" << getDimensions(); qCDebug(entities) << " model URL:" << getModelURL(); qCDebug(entities) << " compound shape URL:" << getCompoundShapeURL(); } void ModelEntityItem::setShapeType(ShapeType type) { withWriteLock([&] { if (type != _shapeType) { if (type == SHAPE_TYPE_STATIC_MESH && _dynamic) { // dynamic and STATIC_MESH are incompatible // since the shape is being set here we clear the dynamic bit _dynamic = false; _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; } _shapeType = type; _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; } }); } ShapeType ModelEntityItem::getShapeType() const { return computeTrueShapeType(); } ShapeType ModelEntityItem::computeTrueShapeType() const { ShapeType type = _shapeType; if (type == SHAPE_TYPE_STATIC_MESH && _dynamic) { // dynamic is incompatible with STATIC_MESH // shouldn't fall in here but just in case --> fall back to COMPOUND type = SHAPE_TYPE_COMPOUND; } if (type == SHAPE_TYPE_COMPOUND && !hasCompoundShapeURL()) { // no compoundURL set --> fall back to SIMPLE_COMPOUND type = SHAPE_TYPE_SIMPLE_COMPOUND; } return type; } void ModelEntityItem::setModelURL(const QString& url) { withWriteLock([&] { if (_modelURL != url) { _modelURL = url; if (_shapeType == SHAPE_TYPE_STATIC_MESH) { _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; } } }); } void ModelEntityItem::setCompoundShapeURL(const QString& url) { withWriteLock([&] { if (_compoundShapeURL.get() != url) { ShapeType oldType = computeTrueShapeType(); _compoundShapeURL.set(url); if (oldType != computeTrueShapeType()) { _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; } } }); } void ModelEntityItem::setAnimationURL(const QString& url) { _dirtyFlags |= Simulation::DIRTY_UPDATEABLE; withWriteLock([&] { _animationProperties.setURL(url); }); } void ModelEntityItem::setAnimationSettings(const QString& value) { // the animations setting is a JSON string that may contain various animation settings. // if it includes fps, currentFrame, or running, those values will be parsed out and // will over ride the regular animation settings QJsonDocument settingsAsJson = QJsonDocument::fromJson(value.toUtf8()); QJsonObject settingsAsJsonObject = settingsAsJson.object(); QVariantMap settingsMap = settingsAsJsonObject.toVariantMap(); if (settingsMap.contains("fps")) { float fps = settingsMap["fps"].toFloat(); setAnimationFPS(fps); } // old settings used frameIndex if (settingsMap.contains("frameIndex")) { float currentFrame = settingsMap["frameIndex"].toFloat(); #ifdef WANT_DEBUG if (!getAnimationURL().isEmpty()) { qCDebug(entities) << "ModelEntityItem::setAnimationSettings() calling setAnimationFrameIndex()..."; qCDebug(entities) << " model URL:" << getModelURL(); qCDebug(entities) << " animation URL:" << getAnimationURL(); qCDebug(entities) << " settings:" << value; qCDebug(entities) << " settingsMap[frameIndex]:" << settingsMap["frameIndex"]; qCDebug(entities" currentFrame: %20.5f", currentFrame); } #endif setAnimationCurrentFrame(currentFrame); } if (settingsMap.contains("running")) { bool running = settingsMap["running"].toBool(); if (running != getAnimationIsPlaying()) { setAnimationIsPlaying(running); } } if (settingsMap.contains("firstFrame")) { float firstFrame = settingsMap["firstFrame"].toFloat(); setAnimationFirstFrame(firstFrame); } if (settingsMap.contains("lastFrame")) { float lastFrame = settingsMap["lastFrame"].toFloat(); setAnimationLastFrame(lastFrame); } if (settingsMap.contains("loop")) { bool loop = settingsMap["loop"].toBool(); setAnimationLoop(loop); } if (settingsMap.contains("hold")) { bool hold = settingsMap["hold"].toBool(); setAnimationHold(hold); } if (settingsMap.contains("allowTranslation")) { bool allowTranslation = settingsMap["allowTranslation"].toBool(); setAnimationAllowTranslation(allowTranslation); } _dirtyFlags |= Simulation::DIRTY_UPDATEABLE; } void ModelEntityItem::setAnimationIsPlaying(bool value) { _dirtyFlags |= Simulation::DIRTY_UPDATEABLE; _animationProperties.setRunning(value); } void ModelEntityItem::setAnimationFPS(float value) { _dirtyFlags |= Simulation::DIRTY_UPDATEABLE; _animationProperties.setFPS(value); } // virtual bool ModelEntityItem::shouldBePhysical() const { return !isDead() && getShapeType() != SHAPE_TYPE_NONE; } void ModelEntityItem::resizeJointArrays(int newSize) { if (newSize < 0) { return; } _jointDataLock.withWriteLock([&] { if (newSize > _localJointData.size()) { _localJointData.resize(newSize); } }); } void ModelEntityItem::setAnimationJointsData(const QVector& jointsData) { resizeJointArrays(jointsData.size()); _jointDataLock.withWriteLock([&] { for (auto index = 0; index < jointsData.size(); ++index) { const auto& newJointData = jointsData[index]; auto& localJointData = _localJointData[index]; if (newJointData.translationSet) { localJointData.joint.translation = newJointData.translation; localJointData.translationDirty = true; } if (newJointData.rotationSet) { localJointData.joint.rotation = newJointData.rotation; localJointData.rotationDirty = true; } } }); } void ModelEntityItem::setJointRotations(const QVector& rotations) { resizeJointArrays(rotations.size()); _jointDataLock.withWriteLock([&] { _jointRotationsExplicitlySet = rotations.size() > 0; for (int index = 0; index < rotations.size(); index++) { auto& jointData = _localJointData[index]; if (jointData.joint.rotationSet) { jointData.joint.rotation = rotations[index]; jointData.rotationDirty = true; } } }); } void ModelEntityItem::setJointRotationsSet(const QVector& rotationsSet) { resizeJointArrays(rotationsSet.size()); _jointDataLock.withWriteLock([&] { _jointRotationsExplicitlySet = rotationsSet.size() > 0; for (int index = 0; index < rotationsSet.size(); index++) { _localJointData[index].joint.rotationSet = rotationsSet[index]; } }); } void ModelEntityItem::setJointTranslations(const QVector& translations) { resizeJointArrays(translations.size()); _jointDataLock.withWriteLock([&] { _jointTranslationsExplicitlySet = translations.size() > 0; for (int index = 0; index < translations.size(); index++) { auto& jointData = _localJointData[index]; if (jointData.joint.translationSet) { jointData.joint.translation = translations[index]; jointData.translationDirty = true; } } }); } void ModelEntityItem::setJointTranslationsSet(const QVector& translationsSet) { resizeJointArrays(translationsSet.size()); _jointDataLock.withWriteLock([&] { _jointTranslationsExplicitlySet = translationsSet.size() > 0; for (int index = 0; index < translationsSet.size(); index++) { _localJointData[index].joint.translationSet = translationsSet[index]; } }); } QVector ModelEntityItem::getJointRotations() const { QVector result; _jointDataLock.withReadLock([&] { if (_jointRotationsExplicitlySet) { result.resize(_localJointData.size()); for (auto i = 0; i < _localJointData.size(); ++i) { result[i] = _localJointData[i].joint.rotation; } } }); return result; } QVector ModelEntityItem::getJointRotationsSet() const { QVector result; _jointDataLock.withReadLock([&] { if (_jointRotationsExplicitlySet) { result.resize(_localJointData.size()); for (auto i = 0; i < _localJointData.size(); ++i) { result[i] = _localJointData[i].joint.rotationSet; } } }); return result; } QVector ModelEntityItem::getJointTranslations() const { QVector result; _jointDataLock.withReadLock([&] { if (_jointTranslationsExplicitlySet) { result.resize(_localJointData.size()); for (auto i = 0; i < _localJointData.size(); ++i) { result[i] = _localJointData[i].joint.translation; } } }); return result; } QVector ModelEntityItem::getJointTranslationsSet() const { QVector result; _jointDataLock.withReadLock([&] { if (_jointTranslationsExplicitlySet) { result.resize(_localJointData.size()); for (auto i = 0; i < _localJointData.size(); ++i) { result[i] = _localJointData[i].joint.translationSet; } } }); return result; } xColor ModelEntityItem::getXColor() const { xColor color = { _color[RED_INDEX], _color[GREEN_INDEX], _color[BLUE_INDEX] }; return color; } bool ModelEntityItem::hasModel() const { return resultWithReadLock([&] { return !_modelURL.isEmpty(); }); } bool ModelEntityItem::hasCompoundShapeURL() const { return !_compoundShapeURL.get().isEmpty(); } QString ModelEntityItem::getModelURL() const { return resultWithReadLock([&] { return _modelURL; }); } QString ModelEntityItem::getCompoundShapeURL() const { return _compoundShapeURL.get(); } void ModelEntityItem::setColor(const rgbColor& value) { withWriteLock([&] { memcpy(_color, value, sizeof(_color)); }); } void ModelEntityItem::setColor(const xColor& value) { withWriteLock([&] { _color[RED_INDEX] = value.red; _color[GREEN_INDEX] = value.green; _color[BLUE_INDEX] = value.blue; }); } // Animation related items... AnimationPropertyGroup ModelEntityItem::getAnimationProperties() const { AnimationPropertyGroup result; withReadLock([&] { result = _animationProperties; }); return result; } bool ModelEntityItem::hasAnimation() const { return resultWithReadLock([&] { return !_animationProperties.getURL().isEmpty(); }); } QString ModelEntityItem::getAnimationURL() const { return resultWithReadLock([&] { return _animationProperties.getURL(); }); } void ModelEntityItem::setAnimationCurrentFrame(float value) { withWriteLock([&] { _animationProperties.setCurrentFrame(value); }); } void ModelEntityItem::setAnimationCurrentlyPlayingFrame(quint64 value) { _dirtyFlags |= Simulation::DIRTY_UPDATEABLE; withWriteLock([&] { _animationProperties.setCurrentlyPlayingFrame(value); }); } void ModelEntityItem::setAnimationLoop(bool loop) { withWriteLock([&] { _animationProperties.setLoop(loop); }); } bool ModelEntityItem::getAnimationLoop() const { return resultWithReadLock([&] { return _animationProperties.getLoop(); }); } void ModelEntityItem::setAnimationHold(bool hold) { withWriteLock([&] { _animationProperties.setHold(hold); }); } bool ModelEntityItem::getAnimationHold() const { return resultWithReadLock([&] { return _animationProperties.getHold(); }); } void ModelEntityItem::setAnimationFirstFrame(float firstFrame) { withWriteLock([&] { _animationProperties.setFirstFrame(firstFrame); }); } float ModelEntityItem::getAnimationFirstFrame() const { return resultWithReadLock([&] { return _animationProperties.getFirstFrame(); }); } void ModelEntityItem::setAnimationLastFrame(float lastFrame) { withWriteLock([&] { _animationProperties.setLastFrame(lastFrame); }); } float ModelEntityItem::getAnimationLastFrame() const { return resultWithReadLock([&] { return _animationProperties.getLastFrame(); }); } //angus change bool ModelEntityItem::getAnimationIsPlaying() const { return resultWithReadLock([&] { return _animationProperties.getRunning(); }); } float ModelEntityItem::getAnimationCurrentFrame() const { return resultWithReadLock([&] { return _animationProperties.getCurrentFrame(); }); } //angus change bool ModelEntityItem::isAnimatingSomething() const { return resultWithReadLock([&] { return !_animationProperties.getURL().isEmpty() && _animationProperties.getRunning() && (_animationProperties.getFPS() != 0.0f); }); } quint64 ModelEntityItem::getCurrentlyPlayingFrame() const { return resultWithReadLock([&] { return _currentlyPlayingFrame; }); } int ModelEntityItem::getLastKnownCurrentFrame() const { return resultWithReadLock([&] { return _lastKnownCurrentFrame; }); } //angus change