QUuid EntityScriptingInterface::editEntity(QUuid id, EntityItemProperties properties) {
    EntityItemID entityID(id);
    // If we have a local entity tree set, then also update it.
    if (!_entityTree) {
        queueEntityMessage(PacketType::EntityEdit, entityID, properties);
        return id;
    }

    bool updatedEntity = false;
    _entityTree->withWriteLock([&] {
        updatedEntity = _entityTree->updateEntity(entityID, properties);
    });


    if (!updatedEntity) {
        return QUuid();
    }

    _entityTree->withReadLock([&] {
        EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
        if (entity) {
            // make sure the properties has a type, so that the encode can know which properties to include
            properties.setType(entity->getType());
            bool hasTerseUpdateChanges = properties.hasTerseUpdateChanges();
            bool hasPhysicsChanges = properties.hasMiscPhysicsChanges() || hasTerseUpdateChanges;
            if (hasPhysicsChanges) {
                auto nodeList = DependencyManager::get<NodeList>();
                const QUuid myNodeID = nodeList->getSessionUUID();

                if (entity->getSimulatorID() == myNodeID) {
                    // we think we already own the simulation, so make sure to send ALL TerseUpdate properties
                    if (hasTerseUpdateChanges) {
                        entity->getAllTerseUpdateProperties(properties);
                    }
                    // TODO: if we knew that ONLY TerseUpdate properties have changed in properties AND the object 
                    // is dynamic AND it is active in the physics simulation then we could chose to NOT queue an update 
                    // and instead let the physics simulation decide when to send a terse update.  This would remove
                    // the "slide-no-rotate" glitch (and typical a double-update) that we see during the "poke rolling
                    // balls" test.  However, even if we solve this problem we still need to provide a "slerp the visible
                    // proxy toward the true physical position" feature to hide the final glitches in the remote watcher's
                    // simulation.

                    if (entity->getSimulationPriority() < SCRIPT_EDIT_SIMULATION_PRIORITY) {
                        // we re-assert our simulation ownership at a higher priority
                        properties.setSimulationOwner(myNodeID,
                            glm::max(entity->getSimulationPriority(), SCRIPT_EDIT_SIMULATION_PRIORITY));
                    }
                } else {
                    // we make a bid for simulation ownership
                    properties.setSimulationOwner(myNodeID, SCRIPT_EDIT_SIMULATION_PRIORITY);
                    entity->flagForOwnership();
                }
            }
            entity->setLastBroadcast(usecTimestampNow());
        }
    });
    queueEntityMessage(PacketType::EntityEdit, entityID, properties);
    return id;
}
void EntityScriptingInterface::deleteEntity(QUuid id) {
    EntityItemID entityID(id);
    bool shouldDelete = true;

    // If we have a local entity tree set, then also update it.
    if (_entityTree) {
        _entityTree->withWriteLock([&] {
            EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
            if (entity) {

                auto dimensions = entity->getDimensions();
                float volume = dimensions.x * dimensions.y * dimensions.z;
                auto density = entity->getDensity();
                auto velocity = entity->getVelocity().length();
                float cost = calculateCost(density * volume, velocity, 0);
                cost *= costMultiplier;

                if (cost > _currentAvatarEnergy) {
                    shouldDelete = false;
                    return;
                } else {
                    //debit the avatar energy and continue
                    emit debitEnergySource(cost);
                }

                if (entity->getLocked()) {
                    shouldDelete = false;
                } else {
                    _entityTree->deleteEntity(entityID);
                }
            }
        });
    }

    // if at this point, we know the id, and we should still delete the entity, send the update to the entity server
    if (shouldDelete) {
        getEntityPacketSender()->queueEraseEntityMessage(entityID);
    }
}
void EntityScriptingInterface::deleteEntity(QUuid id) {
    EntityItemID entityID(id);
    bool shouldDelete = true;

    // If we have a local entity tree set, then also update it.
    if (_entityTree) {
        _entityTree->withWriteLock([&] {
            EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
            if (entity) {
                if (entity->getLocked()) {
                    shouldDelete = false;
                } else {
                    _entityTree->deleteEntity(entityID);
                }
            }
        });
    }

    // if at this point, we know the id, and we should still delete the entity, send the update to the entity server
    if (shouldDelete) {
        getEntityPacketSender()->queueEraseEntityMessage(entityID);
    }
}
Example #4
0
//-------------------------------------------------------------------------------------
thread::TPTask::TPTaskState DataDownload::presentMainThread()
{
	if(error_)
	{
		ERROR_MSG(fmt::format("DataDownload::presentMainThread: proxy({}), downloadID({}), type({}), thread error.\n", 
			entityID(), id(), type()));

		return thread::TPTask::TPTASK_STATE_COMPLETED; 
	}

	uint32 datasize = GAME_PACKET_MAX_SIZE_TCP - sizeof(int16) - sizeof(uint32);

	if(remainSent_ > 0 && currSent_ < remainSent_)
	{
		Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject();

		if(!sentStart_)
		{
			pBundle->newMessage(ClientInterface::onStreamDataStarted);
			(*pBundle) << this->id();
			(*pBundle) << totalBytes_;
			(*pBundle) << descr_;
			(*pBundle) << type();

			sentStart_ = true;
			if(!send(ClientInterface::onStreamDataStarted, pBundle))
			{
				DEBUG_MSG(fmt::format("DataDownload::presentMainThread: proxy({}), downloadID({}), type({}), thread exit.\n",
					entityID(), id(), type()));

				return thread::TPTask::TPTASK_STATE_COMPLETED; 
			}

			return thread::TPTask::TPTASK_STATE_CONTINUE_MAINTHREAD; 
		}

		pBundle->newMessage(ClientInterface::onStreamDataRecv);
		(*pBundle) << id();

		if(remainSent_ - currSent_ > datasize)
		{
			(*pBundle) << datasize;
			(*pBundle).append(getOutStream() + currSent_, datasize);

			currSent_ += datasize;
			totalSentBytes_ += datasize;

			if(!send(ClientInterface::onStreamDataRecv, pBundle))
			{
				DEBUG_MSG(fmt::format("DataDownload::presentMainThread: proxy({}), downloadID({}), type({}), thread exit.\n",
					entityID(), id(), type()));

				error_ = true;
				return thread::TPTask::TPTASK_STATE_COMPLETED; 
			}
		}
		else
		{
			datasize = remainSent_ - currSent_;
			(*pBundle) << datasize;
			(*pBundle).append(getOutStream() + currSent_, datasize);

			if(!send(ClientInterface::onStreamDataRecv, pBundle))
			{
				DEBUG_MSG(fmt::format("DataDownload::presentMainThread: proxy({}), downloadID({}), type({}), thread exit.\n",
					entityID(), id(), type()));

				error_ = true;
				return thread::TPTask::TPTASK_STATE_COMPLETED; 
			}

			totalSentBytes_ += datasize;
			currSent_ = remainSent_;
		}
	}

	if(totalSentBytes_ == totalBytes_)
	{
		DEBUG_MSG(fmt::format("DataDownload::presentMainThread: proxy({0}), downloadID({1}), type({6}), sentBytes={5},{2}/{3} ({4:.2f}%).\n",
			entityID(), id(), totalSentBytes_, this->totalBytes(), 100.0f, datasize, type()));

		pDataDownloads_->onDownloadCompleted(this);

		Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject();


		pBundle->newMessage(ClientInterface::onStreamDataCompleted);
		(*pBundle) << this->id();

		send(ClientInterface::onStreamDataCompleted, pBundle);
		return thread::TPTask::TPTASK_STATE_COMPLETED; 
	}
	
	DEBUG_MSG(fmt::format("DataDownload::presentMainThread: proxy({0}), downloadID({1}), type({6}), sentBytes={5},{2}/{3} ({4:.2f}%).\n",
		entityID(), id(), totalSentBytes_, this->totalBytes(), 
		(((float)totalSentBytes_ / (float)this->totalBytes()) * 100.0f), datasize, type()));

	if(currSent_ == remainSent_)
	{
		DEBUG_MSG(fmt::format("DataDownload::presentMainThread: proxy({}), downloadID({}), type({}), thread-continue.\n",
			entityID(), id(), type()));

		return thread::TPTask::TPTASK_STATE_CONTINUE_CHILDTHREAD; 
	}

	return thread::TPTask::TPTASK_STATE_CONTINUE_MAINTHREAD; 
}
QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& scriptSideProperties) {
    EntityItemProperties properties = scriptSideProperties;

    auto dimensions = properties.getDimensions();
    float volume = dimensions.x * dimensions.y * dimensions.z;
    auto density = properties.getDensity();
    auto newVelocity = properties.getVelocity().length();
    float oldVelocity = { 0.0f };

    EntityItemID entityID(id);
    if (!_entityTree) {
        queueEntityMessage(PacketType::EntityEdit, entityID, properties);

        //if there is no local entity entity tree, no existing velocity, use 0.
        float cost = calculateCost(density * volume, oldVelocity, newVelocity);
        cost *= costMultiplier;

        if (cost > _currentAvatarEnergy) {
            return QUuid();
        } else {
            //debit the avatar energy and continue
            emit debitEnergySource(cost);
        }

        return id;
    }
    // If we have a local entity tree set, then also update it.

    bool updatedEntity = false;
    _entityTree->withWriteLock([&] {
        if (scriptSideProperties.parentRelatedPropertyChanged()) {
            // All of parentID, parentJointIndex, position, rotation are needed to make sense of any of them.
            // If any of these changed, pull any missing properties from the entity.
            EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
            if (!entity) {
                return;
            }
            //existing entity, retrieve old velocity for check down below
            oldVelocity = entity->getVelocity().length();

            if (!scriptSideProperties.parentIDChanged()) {
                properties.setParentID(entity->getParentID());
            }
            if (!scriptSideProperties.parentJointIndexChanged()) {
                properties.setParentJointIndex(entity->getParentJointIndex());
            }
            if (!scriptSideProperties.localPositionChanged() && !scriptSideProperties.positionChanged()) {
                properties.setPosition(entity->getPosition());
            }
            if (!scriptSideProperties.localRotationChanged() && !scriptSideProperties.rotationChanged()) {
                properties.setRotation(entity->getOrientation());
            }
        }
        properties = convertLocationFromScriptSemantics(properties);

        float cost = calculateCost(density * volume, oldVelocity, newVelocity);
        cost *= costMultiplier;

        if (cost > _currentAvatarEnergy) {
            updatedEntity = false;
        } else {
            //debit the avatar energy and continue
            updatedEntity = _entityTree->updateEntity(entityID, properties);
            if (updatedEntity) {
                emit debitEnergySource(cost);
            }
        }
    });

    if (!updatedEntity) {
        return QUuid();
    }

    _entityTree->withReadLock([&] {
        EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
        if (entity) {
            // make sure the properties has a type, so that the encode can know which properties to include
            properties.setType(entity->getType());
            bool hasTerseUpdateChanges = properties.hasTerseUpdateChanges();
            bool hasPhysicsChanges = properties.hasMiscPhysicsChanges() || hasTerseUpdateChanges;
            if (_bidOnSimulationOwnership && hasPhysicsChanges) {
                auto nodeList = DependencyManager::get<NodeList>();
                const QUuid myNodeID = nodeList->getSessionUUID();

                if (entity->getSimulatorID() == myNodeID) {
                    // we think we already own the simulation, so make sure to send ALL TerseUpdate properties
                    if (hasTerseUpdateChanges) {
                        entity->getAllTerseUpdateProperties(properties);
                    }
                    // TODO: if we knew that ONLY TerseUpdate properties have changed in properties AND the object
                    // is dynamic AND it is active in the physics simulation then we could chose to NOT queue an update
                    // and instead let the physics simulation decide when to send a terse update.  This would remove
                    // the "slide-no-rotate" glitch (and typical double-update) that we see during the "poke rolling
                    // balls" test.  However, even if we solve this problem we still need to provide a "slerp the visible
                    // proxy toward the true physical position" feature to hide the final glitches in the remote watcher's
                    // simulation.

                    if (entity->getSimulationPriority() < SCRIPT_POKE_SIMULATION_PRIORITY) {
                        // we re-assert our simulation ownership at a higher priority
                        properties.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY);
                    }
                } else {
                    // we make a bid for simulation ownership
                    properties.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY);
                    entity->pokeSimulationOwnership();
                }
            }
            if (properties.parentRelatedPropertyChanged() && entity->computePuffedQueryAACube()) {
                properties.setQueryAACube(entity->getQueryAACube());
            }
            entity->setLastBroadcast(usecTimestampNow());

            // if we've moved an entity with children, check/update the queryAACube of all descendents and tell the server
            // if they've changed.
            entity->forEachDescendant([&](SpatiallyNestablePointer descendant) {
                if (descendant->getNestableType() == NestableType::Entity) {
                    if (descendant->computePuffedQueryAACube()) {
                        EntityItemPointer entityDescendant = std::static_pointer_cast<EntityItem>(descendant);
                        EntityItemProperties newQueryCubeProperties;
                        newQueryCubeProperties.setQueryAACube(descendant->getQueryAACube());
                        newQueryCubeProperties.setLastEdited(properties.getLastEdited());
                        queueEntityMessage(PacketType::EntityEdit, descendant->getID(), newQueryCubeProperties);
                        entityDescendant->setLastBroadcast(usecTimestampNow());
                    }
                }
            });
        }
    });
    queueEntityMessage(PacketType::EntityEdit, entityID, properties);
    return id;
}
Example #6
0
void EntityTests::entityTreeTests(bool verbose) {

    bool extraVerbose = false;
    int testsTaken = 0;
    int testsPassed = 0;
    int testsFailed = 0;

    if (verbose) {
        qDebug() << "******************************************************************************************";
    }
    
    qDebug() << "EntityTests::entityTreeTests()";

    // Tree, id, and entity properties used in many tests below...
    EntityTree tree;
    QUuid id = QUuid::createUuid();
    EntityItemID entityID(id);
    entityID.isKnownID = false; // this is a temporary workaround to allow local tree entities to be added with known IDs
    EntityItemProperties properties;
    float oneMeter = 1.0f;
    //float halfMeter = oneMeter / 2.0f;
    float halfOfDomain = TREE_SCALE * 0.5f;
    glm::vec3 positionNearOriginInMeters(oneMeter, oneMeter, oneMeter); // when using properties, these are in meter not tree units
    glm::vec3 positionAtCenterInMeters(halfOfDomain, halfOfDomain, halfOfDomain);
    glm::vec3 positionNearOriginInTreeUnits = positionNearOriginInMeters / (float)TREE_SCALE;
    glm::vec3 positionAtCenterInTreeUnits = positionAtCenterInMeters / (float)TREE_SCALE;

    {
        testsTaken++;
        QString testName = "add entity to tree and search";
        if (verbose) {
            qDebug() << "Test" << testsTaken <<":" << qPrintable(testName);
        }
        
        properties.setPosition(positionAtCenterInMeters);
        // TODO: Fix these unit tests.
        //properties.setRadius(halfMeter);
        //properties.setModelURL("http://s3.amazonaws.com/hifi-public/ozan/theater.fbx");

        tree.addEntity(entityID, properties);
        
        float targetRadius = oneMeter * 2.0 / (float)TREE_SCALE; // in tree units
        const EntityItem* foundEntityByRadius = tree.findClosestEntity(positionAtCenterInTreeUnits, targetRadius);
        const EntityItem* foundEntityByID = tree.findEntityByEntityItemID(entityID);
        EntityTreeElement* containingElement = tree.getContainingElement(entityID);
        AACube elementCube = containingElement ? containingElement->getAACube() : AACube();
        
        if (verbose) {
            qDebug() << "foundEntityByRadius=" << foundEntityByRadius;
            qDebug() << "foundEntityByID=" << foundEntityByID;
            qDebug() << "containingElement=" << containingElement;
            qDebug() << "containingElement.box=" 
                << elementCube.getCorner().x * TREE_SCALE << "," 
                << elementCube.getCorner().y * TREE_SCALE << ","
                << elementCube.getCorner().z * TREE_SCALE << ":" 
                << elementCube.getScale() * TREE_SCALE;
            qDebug() << "elementCube.getScale()=" << elementCube.getScale();
            //containingElement->printDebugDetails("containingElement");
        }

        bool passed = foundEntityByRadius && foundEntityByID && (foundEntityByRadius == foundEntityByID);
        if (passed) {
            testsPassed++;
        } else {
            testsFailed++;
            qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName);
        }
    }

    entityID.isKnownID = true; // this is a temporary workaround to allow local tree entities to be added with known IDs

    {
        testsTaken++;
        QString testName = "change position of entity in tree";
        if (verbose) {
            qDebug() << "Test" << testsTaken <<":" << qPrintable(testName);
        }
        
        glm::vec3 newPosition = positionNearOriginInMeters;

        properties.setPosition(newPosition);

        tree.updateEntity(entityID, properties);
        
        float targetRadius = oneMeter * 2.0 / (float)TREE_SCALE; // in tree units
        const EntityItem* foundEntityByRadius = tree.findClosestEntity(positionNearOriginInTreeUnits, targetRadius);
        const EntityItem* foundEntityByID = tree.findEntityByEntityItemID(entityID);
        EntityTreeElement* containingElement = tree.getContainingElement(entityID);
        AACube elementCube = containingElement ? containingElement->getAACube() : AACube();
        
        if (verbose) {
            qDebug() << "foundEntityByRadius=" << foundEntityByRadius;
            qDebug() << "foundEntityByID=" << foundEntityByID;
            qDebug() << "containingElement=" << containingElement;
            qDebug() << "containingElement.box=" 
                << elementCube.getCorner().x * TREE_SCALE << "," 
                << elementCube.getCorner().y * TREE_SCALE << ","
                << elementCube.getCorner().z * TREE_SCALE << ":" 
                << elementCube.getScale() * TREE_SCALE;
            //containingElement->printDebugDetails("containingElement");
        }

        bool passed = foundEntityByRadius && foundEntityByID && (foundEntityByRadius == foundEntityByID);
        if (passed) {
            testsPassed++;
        } else {
            testsFailed++;
            qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName);
        }
    }

    {
        testsTaken++;
        QString testName = "change position of entity in tree back to center";
        if (verbose) {
            qDebug() << "Test" << testsTaken <<":" << qPrintable(testName);
        }
        
        glm::vec3 newPosition = positionAtCenterInMeters;

        properties.setPosition(newPosition);

        tree.updateEntity(entityID, properties);
        
        float targetRadius = oneMeter * 2.0 / (float)TREE_SCALE; // in tree units
        const EntityItem* foundEntityByRadius = tree.findClosestEntity(positionAtCenterInTreeUnits, targetRadius);
        const EntityItem* foundEntityByID = tree.findEntityByEntityItemID(entityID);
        EntityTreeElement* containingElement = tree.getContainingElement(entityID);
        AACube elementCube = containingElement ? containingElement->getAACube() : AACube();
        
        if (verbose) {
            qDebug() << "foundEntityByRadius=" << foundEntityByRadius;
            qDebug() << "foundEntityByID=" << foundEntityByID;
            qDebug() << "containingElement=" << containingElement;
            qDebug() << "containingElement.box=" 
                << elementCube.getCorner().x * TREE_SCALE << "," 
                << elementCube.getCorner().y * TREE_SCALE << ","
                << elementCube.getCorner().z * TREE_SCALE << ":" 
                << elementCube.getScale() * TREE_SCALE;
            //containingElement->printDebugDetails("containingElement");
        }

        bool passed = foundEntityByRadius && foundEntityByID && (foundEntityByRadius == foundEntityByID);
        if (passed) {
            testsPassed++;
        } else {
            testsFailed++;
            qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName);
        }
    }

    {
        testsTaken++;
        const int TEST_ITERATIONS = 1000;
        QString testName = "Performance - findClosestEntity() "+ QString::number(TEST_ITERATIONS) + " times";
        if (verbose) {
            qDebug() << "Test" << testsTaken <<":" << qPrintable(testName);
        }

        float targetRadius = oneMeter * 2.0 / (float)TREE_SCALE; // in tree units
        quint64 start = usecTimestampNow();
        const EntityItem* foundEntityByRadius = NULL;
        for (int i = 0; i < TEST_ITERATIONS; i++) {        
            foundEntityByRadius = tree.findClosestEntity(positionAtCenterInTreeUnits, targetRadius);
        }
        quint64 end = usecTimestampNow();
        
        if (verbose) {
            qDebug() << "foundEntityByRadius=" << foundEntityByRadius;
        }

        bool passed = foundEntityByRadius;
        if (passed) {
            testsPassed++;
        } else {
            testsFailed++;
            qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName);
        }
        float USECS_PER_MSECS = 1000.0f;
        float elapsedInMSecs = (float)(end - start) / USECS_PER_MSECS;
        qDebug() << "TIME - Test" << testsTaken <<":" << qPrintable(testName) << "elapsed=" << elapsedInMSecs << "msecs";
    }

    {
        testsTaken++;
        const int TEST_ITERATIONS = 1000;
        QString testName = "Performance - findEntityByID() "+ QString::number(TEST_ITERATIONS) + " times";
        if (verbose) {
            qDebug() << "Test" << testsTaken <<":" << qPrintable(testName);
        }

        quint64 start = usecTimestampNow();
        const EntityItem* foundEntityByID = NULL;
        for (int i = 0; i < TEST_ITERATIONS; i++) {
            // TODO: does this need to be updated??
            foundEntityByID = tree.findEntityByEntityItemID(entityID);
        }
        quint64 end = usecTimestampNow();
        
        if (verbose) {
            qDebug() << "foundEntityByID=" << foundEntityByID;
        }

        bool passed = foundEntityByID;
        if (passed) {
            testsPassed++;
        } else {
            testsFailed++;
            qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName);
        }
        float USECS_PER_MSECS = 1000.0f;
        float elapsedInMSecs = (float)(end - start) / USECS_PER_MSECS;
        qDebug() << "TIME - Test" << testsTaken <<":" << qPrintable(testName) << "elapsed=" << elapsedInMSecs << "msecs";
    }

    {
        // seed the random number generator so that our tests are reproducible
        srand(0xFEEDBEEF);
    
        testsTaken++;
        const int TEST_ITERATIONS = 1000;
        QString testName = "Performance - add entity to tree " + QString::number(TEST_ITERATIONS) + " times";
        if (verbose) {
            qDebug() << "Test" << testsTaken <<":" << qPrintable(testName);
        }

        int iterationsPassed = 0;
        quint64 totalElapsedAdd = 0;
        quint64 totalElapsedFind = 0;
        for (int i = 0; i < TEST_ITERATIONS; i++) {        
            QUuid id = QUuid::createUuid();// make sure it doesn't collide with previous entity ids
            EntityItemID entityID(id);
            entityID.isKnownID = false; // this is a temporary workaround to allow local tree entities to be added with known IDs

            float randomX = randFloatInRange(1.0f ,(float)TREE_SCALE - 1.0f);
            float randomY = randFloatInRange(1.0f ,(float)TREE_SCALE - 1.0f);
            float randomZ = randFloatInRange(1.0f ,(float)TREE_SCALE - 1.0f);
            glm::vec3 randomPositionInMeters(randomX,randomY,randomZ);
            glm::vec3 randomPositionInTreeUnits = randomPositionInMeters / (float)TREE_SCALE;

            properties.setPosition(randomPositionInMeters);
            
            // TODO: fix these unit tests
            //properties.setRadius(halfMeter);
            //properties.setModelURL("http://s3.amazonaws.com/hifi-public/ozan/theater.fbx");

            if (extraVerbose) {
                qDebug() << "iteration:" << i
                      << "ading entity at x/y/z=" << randomX << "," << randomY << "," << randomZ;
                qDebug() << "before:" << i << "getOctreeElementsCount()=" << tree.getOctreeElementsCount();
            }

            quint64 startAdd = usecTimestampNow();
            tree.addEntity(entityID, properties);
            quint64 endAdd = usecTimestampNow();
            totalElapsedAdd += (endAdd - startAdd);

            if (extraVerbose) {
                qDebug() << "after:" << i << "getOctreeElementsCount()=" << tree.getOctreeElementsCount();
            }

            quint64 startFind = usecTimestampNow();
            float targetRadius = oneMeter * 2.0 / (float)TREE_SCALE; // in tree units
            const EntityItem* foundEntityByRadius = tree.findClosestEntity(randomPositionInTreeUnits, targetRadius);
            const EntityItem* foundEntityByID = tree.findEntityByEntityItemID(entityID);
            quint64 endFind = usecTimestampNow();
            totalElapsedFind += (endFind - startFind);

            EntityTreeElement* containingElement = tree.getContainingElement(entityID);
            AACube elementCube = containingElement ? containingElement->getAACube() : AACube();
            
            bool elementIsBestFit = containingElement->bestFitEntityBounds(foundEntityByID);
            
            if (extraVerbose) {
                qDebug() << "foundEntityByRadius=" << foundEntityByRadius;
                qDebug() << "foundEntityByID=" << foundEntityByID;
                qDebug() << "containingElement=" << containingElement;
                qDebug() << "containingElement.box=" 
                    << elementCube.getCorner().x * TREE_SCALE << "," 
                    << elementCube.getCorner().y * TREE_SCALE << ","
                    << elementCube.getCorner().z * TREE_SCALE << ":" 
                    << elementCube.getScale() * TREE_SCALE;
                qDebug() << "elementCube.getScale()=" << elementCube.getScale();
                //containingElement->printDebugDetails("containingElement");
                qDebug() << "elementIsBestFit=" << elementIsBestFit;
            }
            
            // Every 1000th test, show the size of the tree...
            if (extraVerbose && (i % 1000 == 0)) {
                qDebug() << "after test:" << i << "getOctreeElementsCount()=" << tree.getOctreeElementsCount();
            }

            bool passed = foundEntityByRadius && foundEntityByID && (foundEntityByRadius == foundEntityByID) && elementIsBestFit;
            if (passed) {
              iterationsPassed++;
            } else {
                if (extraVerbose) {
                    qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName) << "iteration:" << i
                          << "foundEntityByRadius=" << foundEntityByRadius << "foundEntityByID=" << foundEntityByID
                          << "x/y/z=" << randomX << "," << randomY << "," << randomZ
                          << "elementIsBestFit=" << elementIsBestFit;
                }
            }
        }

        if (extraVerbose) {
            qDebug() << "getOctreeElementsCount()=" << tree.getOctreeElementsCount();
        }
        
        bool passed = iterationsPassed == TEST_ITERATIONS;
        if (passed) {
            testsPassed++;
        } else {
            testsFailed++;
            qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName);
        }
        float USECS_PER_MSECS = 1000.0f;
        float elapsedInMSecsAdd = (float)(totalElapsedAdd) / USECS_PER_MSECS;
        float elapsedInMSecsFind = (float)(totalElapsedFind) / USECS_PER_MSECS;
        qDebug() << "TIME - Test" << testsTaken <<":" << qPrintable(testName) 
                        << "elapsed Add=" << elapsedInMSecsAdd << "msecs"
                        << "elapsed Find=" << elapsedInMSecsFind << "msecs";
    }

    {
        testsTaken++;
        const int TEST_ITERATIONS = 1000;
        QString testName = "Performance - delete entity from tree " + QString::number(TEST_ITERATIONS) + " times";
        if (verbose) {
            qDebug() << "Test" << testsTaken <<":" << qPrintable(testName);
        }

        int iterationsPassed = 0;
        quint64 totalElapsedDelete = 0;
        quint64 totalElapsedFind = 0;
        for (int i = 0; i < TEST_ITERATIONS; i++) {        
            QUuid id = QUuid::createUuid();// make sure it doesn't collide with previous entity ids
            EntityItemID entityID(id);
            entityID.isKnownID = true; // this is a temporary workaround to allow local tree entities to be added with known IDs

            if (extraVerbose) {
                qDebug() << "before:" << i << "getOctreeElementsCount()=" << tree.getOctreeElementsCount();
            }

            quint64 startDelete = usecTimestampNow();
            tree.deleteEntity(entityID);
            quint64 endDelete = usecTimestampNow();
            totalElapsedDelete += (endDelete - startDelete);

            if (extraVerbose) {
                qDebug() << "after:" << i << "getOctreeElementsCount()=" << tree.getOctreeElementsCount();
            }

            quint64 startFind = usecTimestampNow();
            const EntityItem* foundEntityByID = tree.findEntityByEntityItemID(entityID);
            quint64 endFind = usecTimestampNow();
            totalElapsedFind += (endFind - startFind);

            EntityTreeElement* containingElement = tree.getContainingElement(entityID);
            
            if (extraVerbose) {
                qDebug() << "foundEntityByID=" << foundEntityByID;
                qDebug() << "containingElement=" << containingElement;
            }
            
            // Every 1000th test, show the size of the tree...
            if (extraVerbose && (i % 1000 == 0)) {
                qDebug() << "after test:" << i << "getOctreeElementsCount()=" << tree.getOctreeElementsCount();
            }

            bool passed = foundEntityByID == NULL && containingElement == NULL;
            if (passed) {
              iterationsPassed++;
            } else {
                if (extraVerbose) {
                    qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName) << "iteration:" << i
                          << "foundEntityByID=" << foundEntityByID
                          << "containingElement=" << containingElement;
                }
            }
        }

        if (extraVerbose) {
            qDebug() << "getOctreeElementsCount()=" << tree.getOctreeElementsCount();
        }
        
        bool passed = iterationsPassed == TEST_ITERATIONS;
        if (passed) {
            testsPassed++;
        } else {
            testsFailed++;
            qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName);
        }
        float USECS_PER_MSECS = 1000.0f;
        float elapsedInMSecsDelete = (float)(totalElapsedDelete) / USECS_PER_MSECS;
        float elapsedInMSecsFind = (float)(totalElapsedFind) / USECS_PER_MSECS;
        qDebug() << "TIME - Test" << testsTaken <<":" << qPrintable(testName) 
                        << "elapsed Delete=" << elapsedInMSecsDelete << "msecs"
                        << "elapsed Find=" << elapsedInMSecsFind << "msecs";
    }


    {
        testsTaken++;
        const int TEST_ITERATIONS = 100;
        const int ENTITIES_PER_ITERATION = 10;
        QString testName = "Performance - delete " + QString::number(ENTITIES_PER_ITERATION) 
                            + " entities from tree " + QString::number(TEST_ITERATIONS) + " times";
        if (verbose) {
            qDebug() << "Test" << testsTaken <<":" << qPrintable(testName);
        }

        int iterationsPassed = 0;
        quint64 totalElapsedDelete = 0;
        quint64 totalElapsedFind = 0;
        for (int i = 0; i < TEST_ITERATIONS; i++) {        

            QSet<EntityItemID> entitiesToDelete;
            for (int j = 0; j < ENTITIES_PER_ITERATION; j++) {        
                //uint32_t id = 2 + (i * ENTITIES_PER_ITERATION) + j; // These are the entities we added above
                QUuid id = QUuid::createUuid();// make sure it doesn't collide with previous entity ids
                EntityItemID entityID(id);
                entitiesToDelete << entityID;
            }

            if (extraVerbose) {
                qDebug() << "before:" << i << "getOctreeElementsCount()=" << tree.getOctreeElementsCount();
            }

            quint64 startDelete = usecTimestampNow();
            tree.deleteEntities(entitiesToDelete);
            quint64 endDelete = usecTimestampNow();
            totalElapsedDelete += (endDelete - startDelete);

            if (extraVerbose) {
                qDebug() << "after:" << i << "getOctreeElementsCount()=" << tree.getOctreeElementsCount();
            }

            quint64 startFind = usecTimestampNow();
            for (int j = 0; j < ENTITIES_PER_ITERATION; j++) {        
                //uint32_t id = 2 + (i * ENTITIES_PER_ITERATION) + j; // These are the entities we added above
                QUuid id = QUuid::createUuid();// make sure it doesn't collide with previous entity ids
                EntityItemID entityID(id);
                const EntityItem* foundEntityByID = tree.findEntityByEntityItemID(entityID);
                EntityTreeElement* containingElement = tree.getContainingElement(entityID);

                if (extraVerbose) {
                    qDebug() << "foundEntityByID=" << foundEntityByID;
                    qDebug() << "containingElement=" << containingElement;
                }
                bool passed = foundEntityByID == NULL && containingElement == NULL;
                if (passed) {
                  iterationsPassed++;
                } else {
                    if (extraVerbose) {
                        qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName) << "iteration:" << i
                              << "foundEntityByID=" << foundEntityByID
                              << "containingElement=" << containingElement;
                    }
                }

            }

            quint64 endFind = usecTimestampNow();
            totalElapsedFind += (endFind - startFind);
        }

        if (extraVerbose) {
            qDebug() << "getOctreeElementsCount()=" << tree.getOctreeElementsCount();
        }
        
        bool passed = iterationsPassed == (TEST_ITERATIONS * ENTITIES_PER_ITERATION);
        if (passed) {
            testsPassed++;
        } else {
            testsFailed++;
            qDebug() << "FAILED - Test" << testsTaken <<":" << qPrintable(testName);
        }
        float USECS_PER_MSECS = 1000.0f;
        float elapsedInMSecsDelete = (float)(totalElapsedDelete) / USECS_PER_MSECS;
        float elapsedInMSecsFind = (float)(totalElapsedFind) / USECS_PER_MSECS;
        qDebug() << "TIME - Test" << testsTaken <<":" << qPrintable(testName) 
                        << "elapsed Delete=" << elapsedInMSecsDelete << "msecs"
                        << "elapsed Find=" << elapsedInMSecsFind << "msecs";
    }

    qDebug() << "   tests passed:" << testsPassed << "out of" << testsTaken;
    if (verbose) {
        qDebug() << "******************************************************************************************";
    }
}