diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index d4a83d00f0..75265bdf10 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -123,6 +123,30 @@ void broadcastIdentityPacket() { } } +void broadcastBillboardPacket(const SharedNodePointer& sendingNode) { + AvatarMixerClientData* nodeData = static_cast(sendingNode->getLinkedData()); + QByteArray packet = byteArrayWithPopulatedHeader(PacketTypeAvatarBillboard); + packet.append(sendingNode->getUUID().toRfc4122()); + packet.append(nodeData->getBillboard()); + + NodeList* nodeList = NodeList::getInstance(); + foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { + if (node->getType() == NodeType::Agent && node != sendingNode) { + nodeList->writeDatagram(packet, node); + } + } +} + +void broadcastBillboardPackets() { + foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { + if (node->getLinkedData() && node->getType() == NodeType::Agent) { + AvatarMixerClientData* nodeData = static_cast(node->getLinkedData()); + broadcastBillboardPacket(node); + nodeData->setHasSentBillboardBetweenKeyFrames(false); + } + } +} + void AvatarMixer::nodeKilled(SharedNodePointer killedNode) { if (killedNode->getType() == NodeType::Agent && killedNode->getLinkedData()) { @@ -170,6 +194,23 @@ void AvatarMixer::readPendingDatagrams() { nodeList->broadcastToNodes(identityPacket, NodeSet() << NodeType::Agent); } } + break; + } + case PacketTypeAvatarBillboard: { + + // check if we have a matching node in our list + SharedNodePointer avatarNode = nodeList->sendingNodeForPacket(receivedPacket); + + if (avatarNode && avatarNode->getLinkedData()) { + AvatarMixerClientData* nodeData = static_cast(avatarNode->getLinkedData()); + if (nodeData->hasBillboardChangedAfterParsing(receivedPacket) + && !nodeData->hasSentBillboardBetweenKeyFrames()) { + // this avatar changed their billboard and we haven't sent a packet in this keyframe + broadcastBillboardPacket(avatarNode); + nodeData->setHasSentBillboardBetweenKeyFrames(true); + } + } + break; } case PacketTypeKillAvatar: { nodeList->processKillNode(receivedPacket); @@ -185,6 +226,7 @@ void AvatarMixer::readPendingDatagrams() { } const qint64 AVATAR_IDENTITY_KEYFRAME_MSECS = 5000; +const qint64 AVATAR_BILLBOARD_KEYFRAME_MSECS = 5000; void AvatarMixer::run() { commonInit(AVATAR_MIXER_LOGGING_NAME, NodeType::AvatarMixer); @@ -202,6 +244,9 @@ void AvatarMixer::run() { QElapsedTimer identityTimer; identityTimer.start(); + QElapsedTimer billboardTimer; + billboardTimer.start(); + while (!_isFinished) { QCoreApplication::processEvents(); @@ -219,6 +264,11 @@ void AvatarMixer::run() { // restart the timer so we do it again in AVATAR_IDENTITY_KEYFRAME_MSECS identityTimer.restart(); } + + if (billboardTimer.elapsed() >= AVATAR_BILLBOARD_KEYFRAME_MSECS) { + broadcastBillboardPackets(); + billboardTimer.restart(); + } int usecToSleep = usecTimestamp(&startTime) + (++nextFrame * AVATAR_DATA_SEND_INTERVAL_USECS) - usecTimestampNow(); diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 0261613532..388d6f6488 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -9,7 +9,8 @@ #include "AvatarMixerClientData.h" AvatarMixerClientData::AvatarMixerClientData() : - _hasSentIdentityBetweenKeyFrames(false) + _hasSentIdentityBetweenKeyFrames(false), + _hasSentBillboardBetweenKeyFrames(false) { } diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 8e046d9212..7240288306 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -21,9 +21,15 @@ public: bool hasSentIdentityBetweenKeyFrames() const { return _hasSentIdentityBetweenKeyFrames; } void setHasSentIdentityBetweenKeyFrames(bool hasSentIdentityBetweenKeyFrames) { _hasSentIdentityBetweenKeyFrames = hasSentIdentityBetweenKeyFrames; } + + bool hasSentBillboardBetweenKeyFrames() const { return _hasSentBillboardBetweenKeyFrames; } + void setHasSentBillboardBetweenKeyFrames(bool hasSentBillboardBetweenKeyFrames) + { _hasSentBillboardBetweenKeyFrames = hasSentBillboardBetweenKeyFrames; } + private: bool _hasSentIdentityBetweenKeyFrames; + bool _hasSentBillboardBetweenKeyFrames; }; #endif /* defined(__hifi__AvatarMixerClientData__) */ diff --git a/assignment-client/src/metavoxels/MetavoxelServer.cpp b/assignment-client/src/metavoxels/MetavoxelServer.cpp index 912f5c22ee..fa60d3a88c 100644 --- a/assignment-client/src/metavoxels/MetavoxelServer.cpp +++ b/assignment-client/src/metavoxels/MetavoxelServer.cpp @@ -28,10 +28,6 @@ void MetavoxelServer::applyEdit(const MetavoxelEditMessage& edit) { edit.apply(_data); } -void MetavoxelServer::removeSession(const QUuid& sessionId) { - _sessions.take(sessionId)->deleteLater(); -} - const QString METAVOXEL_SERVER_LOGGING_NAME = "metavoxel-server"; void MetavoxelServer::run() { @@ -40,6 +36,8 @@ void MetavoxelServer::run() { NodeList* nodeList = NodeList::getInstance(); nodeList->addNodeTypeToInterestSet(NodeType::Agent); + connect(nodeList, SIGNAL(nodeAdded(SharedNodePointer)), SLOT(maybeAttachSession(const SharedNodePointer&))); + _lastSend = QDateTime::currentMSecsSinceEpoch(); _sendTimer.start(SEND_INTERVAL); } @@ -53,25 +51,31 @@ void MetavoxelServer::readPendingDatagrams() { while (readAvailableDatagram(receivedPacket, senderSockAddr)) { if (nodeList->packetVersionAndHashMatch(receivedPacket)) { switch (packetTypeForPacket(receivedPacket)) { - case PacketTypeMetavoxelData: { - SharedNodePointer matchingNode = nodeList->sendingNodeForPacket(receivedPacket); - if (matchingNode) { - processData(receivedPacket, matchingNode); - } + case PacketTypeMetavoxelData: + nodeList->findNodeAndUpdateWithDataFromPacket(receivedPacket); break; - } + default: - NodeList::getInstance()->processNodeData(senderSockAddr, receivedPacket); + nodeList->processNodeData(senderSockAddr, receivedPacket); break; } } } } +void MetavoxelServer::maybeAttachSession(const SharedNodePointer& node) { + if (node->getType() == NodeType::Agent) { + QMutexLocker locker(&node->getMutex()); + node->setLinkedData(new MetavoxelSession(this, NodeList::getInstance()->nodeWithUUID(node->getUUID()))); + } +} + void MetavoxelServer::sendDeltas() { // send deltas for all sessions - foreach (MetavoxelSession* session, _sessions) { - session->sendDelta(); + foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { + if (node->getType() == NodeType::Agent) { + static_cast(node->getLinkedData())->sendDelta(); + } } // restart the send timer @@ -82,35 +86,10 @@ void MetavoxelServer::sendDeltas() { _sendTimer.start(qMax(0, 2 * SEND_INTERVAL - elapsed)); } -void MetavoxelServer::processData(const QByteArray& data, const SharedNodePointer& sendingNode) { - // read the session id - int headerPlusIDSize; - QUuid sessionID = readSessionID(data, sendingNode, headerPlusIDSize); - if (sessionID.isNull()) { - return; - } - - // forward to session, creating if necessary - MetavoxelSession*& session = _sessions[sessionID]; - if (!session) { - session = new MetavoxelSession(this, sessionID, QByteArray::fromRawData(data.constData(), headerPlusIDSize), - sendingNode); - } - session->receivedData(data, sendingNode); -} - -MetavoxelSession::MetavoxelSession(MetavoxelServer* server, const QUuid& sessionId, - const QByteArray& datagramHeader, const SharedNodePointer& sendingNode) : - QObject(server), +MetavoxelSession::MetavoxelSession(MetavoxelServer* server, const SharedNodePointer& node) : _server(server), - _sessionId(sessionId), - _sequencer(datagramHeader), - _sendingNode(sendingNode) { - - const int TIMEOUT_INTERVAL = 30 * 1000; - _timeoutTimer.setInterval(TIMEOUT_INTERVAL); - _timeoutTimer.setSingleShot(true); - connect(&_timeoutTimer, SIGNAL(timeout()), SLOT(timedOut())); + _sequencer(byteArrayWithPopulatedHeader(PacketTypeMetavoxelData)), + _node(node) { connect(&_sequencer, SIGNAL(readyToWrite(const QByteArray&)), SLOT(sendData(const QByteArray&))); connect(&_sequencer, SIGNAL(readyToRead(Bitstream&)), SLOT(readPacket(Bitstream&))); @@ -120,19 +99,15 @@ MetavoxelSession::MetavoxelSession(MetavoxelServer* server, const QUuid& session // insert the baseline send record SendRecord record = { 0 }; _sendRecords.append(record); - - qDebug() << "Opened session [sessionId=" << _sessionId << ", sendingNode=" << sendingNode << "]"; } -void MetavoxelSession::receivedData(const QByteArray& data, const SharedNodePointer& sendingNode) { - // reset the timeout timer - _timeoutTimer.start(); +MetavoxelSession::~MetavoxelSession() { +} - // save the most recent sender - _sendingNode = sendingNode; - +int MetavoxelSession::parseData(const QByteArray& packet) { // process through sequencer - _sequencer.receivedDatagram(data); + _sequencer.receivedDatagram(packet); + return packet.size(); } void MetavoxelSession::sendDelta() { @@ -146,13 +121,8 @@ void MetavoxelSession::sendDelta() { _sendRecords.append(record); } -void MetavoxelSession::timedOut() { - qDebug() << "Session timed out [sessionId=" << _sessionId << ", sendingNode=" << _sendingNode << "]"; - _server->removeSession(_sessionId); -} - void MetavoxelSession::sendData(const QByteArray& data) { - NodeList::getInstance()->writeDatagram(data, _sendingNode); + NodeList::getInstance()->writeDatagram(data, _node); } void MetavoxelSession::readPacket(Bitstream& in) { @@ -167,11 +137,7 @@ void MetavoxelSession::clearSendRecordsBefore(int index) { void MetavoxelSession::handleMessage(const QVariant& message) { int userType = message.userType(); - if (userType == CloseSessionMessage::Type) { - qDebug() << "Session closed [sessionId=" << _sessionId << ", sendingNode=" << _sendingNode << "]"; - _server->removeSession(_sessionId); - - } else if (userType == ClientStateMessage::Type) { + if (userType == ClientStateMessage::Type) { ClientStateMessage state = message.value(); _position = state.position; diff --git a/assignment-client/src/metavoxels/MetavoxelServer.h b/assignment-client/src/metavoxels/MetavoxelServer.h index 60fdeca5a5..a7939ee115 100644 --- a/assignment-client/src/metavoxels/MetavoxelServer.h +++ b/assignment-client/src/metavoxels/MetavoxelServer.h @@ -9,12 +9,9 @@ #ifndef __hifi__MetavoxelServer__ #define __hifi__MetavoxelServer__ -#include #include #include -#include -#include #include #include @@ -35,45 +32,38 @@ public: const MetavoxelData& getData() const { return _data; } - void removeSession(const QUuid& sessionId); - virtual void run(); virtual void readPendingDatagrams(); private slots: - + + void maybeAttachSession(const SharedNodePointer& node); void sendDeltas(); private: - void processData(const QByteArray& data, const SharedNodePointer& sendingNode); - QTimer _sendTimer; qint64 _lastSend; - QHash _sessions; - MetavoxelData _data; }; /// Contains the state of a single client session. -class MetavoxelSession : public QObject { +class MetavoxelSession : public NodeData { Q_OBJECT public: - MetavoxelSession(MetavoxelServer* server, const QUuid& sessionId, - const QByteArray& datagramHeader, const SharedNodePointer& sendingNode); + MetavoxelSession(MetavoxelServer* server, const SharedNodePointer& node); + virtual ~MetavoxelSession(); - void receivedData(const QByteArray& data, const SharedNodePointer& sendingNode); + virtual int parseData(const QByteArray& packet); void sendDelta(); private slots: - void timedOut(); - void sendData(const QByteArray& data); void readPacket(Bitstream& in); @@ -91,12 +81,10 @@ private: }; MetavoxelServer* _server; - QUuid _sessionId; - QTimer _timeoutTimer; DatagramSequencer _sequencer; - SharedNodePointer _sendingNode; + SharedNodePointer _node; glm::vec3 _position; diff --git a/cmake/macros/AutoMTC.cmake b/cmake/macros/AutoMTC.cmake index f0c9ebc6a0..f29c3400bb 100644 --- a/cmake/macros/AutoMTC.cmake +++ b/cmake/macros/AutoMTC.cmake @@ -3,20 +3,11 @@ macro(AUTO_MTC TARGET ROOT_DIR) add_subdirectory(${ROOT_DIR}/tools/mtc ${ROOT_DIR}/tools/mtc) endif (NOT TARGET mtc) + set(AUTOMTC_SRC ${TARGET}_automtc.cpp) + file(GLOB INCLUDE_FILES src/*.h) - add_custom_command(OUTPUT ${TARGET}_automtc.cpp COMMAND mtc -o ${TARGET}_automtc.cpp - ${INCLUDE_FILES} DEPENDS mtc ${INCLUDE_FILES}) - - find_package(Qt5Core REQUIRED) - find_package(Qt5Script REQUIRED) - find_package(Qt5Widgets REQUIRED) - - add_library(${TARGET}_automtc STATIC ${TARGET}_automtc.cpp) - - qt5_use_modules(${TARGET}_automtc Core Script Widgets) - - target_link_libraries(${TARGET} ${TARGET}_automtc) + add_custom_command(OUTPUT ${AUTOMTC_SRC} COMMAND mtc -o ${AUTOMTC_SRC} ${INCLUDE_FILES} DEPENDS mtc ${INCLUDE_FILES}) endmacro() diff --git a/examples/bot.js b/examples/bot.js index 6d0abd740e..8ac718e34d 100644 --- a/examples/bot.js +++ b/examples/bot.js @@ -2,7 +2,7 @@ // bot.js // hifi // -// Created by Brad Hefta-Gaub on 2/20/14. +// Created by Stephen Birarda on 2/20/14. // Copyright (c) 2014 HighFidelity, Inc. All rights reserved. // // This is an example script that demonstrates an NPC avatar. @@ -25,10 +25,30 @@ positionZ = getRandomFloat(0, 50); Avatar.position = {x: positionX, y: 0, z: positionZ}; // pick an integer between 1 and 20 for the face model for this bot -botNumber = getRandomInt(1, 20); +botNumber = getRandomInt(1, 100); + +newBodyFilePrefix = "defaultAvatar"; + +if (botNumber <= 20) { + newFaceFilePrefix = "bot" + botNumber; +} else { + + if (botNumber <= 40) { + newFaceFilePrefix = "superhero"; + } else if (botNumber <= 60) { + newFaceFilePrefix = "amber"; + } else if (botNumber <= 80) { + newFaceFilePrefix = "ron"; + } else { + newFaceFilePrefix = "angie"; + } + + newBodyFilePrefix = "bot" + botNumber; +} // set the face model fst using the bot number // there is no need to change the body model - we're using the default -Avatar.faceModelURL = "https://s3-us-west-1.amazonaws.com/highfidelity-public/meshes/bot" + botNumber + ".fst"; +Avatar.faceModelURL = "https://s3-us-west-1.amazonaws.com/highfidelity-public/meshes/" + newFaceFilePrefix + ".fst"; +Avatar.bodyModelURL = "https://s3-us-west-1.amazonaws.com/highfidelity-public/meshes/" + newBodyFilePrefix + ".fst"; Agent.isAvatar = true; \ No newline at end of file diff --git a/examples/editVoxels.js b/examples/editVoxels.js index 81e3000566..80f37a76a3 100644 --- a/examples/editVoxels.js +++ b/examples/editVoxels.js @@ -24,13 +24,19 @@ var ORBIT_RATE_ALTITUDE = 200.0; var ORBIT_RATE_AZIMUTH = 90.0; var PIXELS_PER_EXTRUDE_VOXEL = 16; +var zFightingSizeAdjust = 0.002; // used to adjust preview voxels to prevent z fighting +var previewLineWidth = 1.5; + var oldMode = Camera.getMode(); -var key_alt = false; -var key_shift = false; var isAdding = false; var isExtruding = false; var isOrbiting = false; +var isOrbitingFromTouch = false; +var isPanning = false; +var isPanningFromTouch = false; +var touchPointsToOrbit = 2; // you can change these, but be mindful that on some track pads 2 touch points = right click+drag +var touchPointsToPan = 3; var orbitAzimuth = 0.0; var orbitAltitude = 0.0; var orbitCenter = { x: 0, y: 0, z: 0 }; @@ -46,8 +52,6 @@ var dragStart = { x: 0, y: 0 }; var mouseX = 0; var mouseY = 0; - - // Create a table of the different colors you can choose var colors = new Array(); colors[0] = { red: 237, green: 175, blue: 0 }; @@ -68,6 +72,7 @@ var changeColorSound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelit var clickSound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Switches+and+sliders/toggle+switch+-+medium.raw"); var audioOptions = new AudioInjectionOptions();
 audioOptions.volume = 0.5; +audioOptions.position = Vec3.sum(MyAvatar.position, { x: 0, y: 1, z: 0 } ); // start with audio slightly above the avatar var editToolsOn = false; // starts out off @@ -92,7 +97,7 @@ var linePreviewTop = Overlays.addOverlay("line3d", { color: { red: 255, green: 255, blue: 255}, alpha: 1, visible: false, - lineWidth: 1 + lineWidth: previewLineWidth }); var linePreviewBottom = Overlays.addOverlay("line3d", { @@ -101,7 +106,7 @@ var linePreviewBottom = Overlays.addOverlay("line3d", { color: { red: 255, green: 255, blue: 255}, alpha: 1, visible: false, - lineWidth: 1 + lineWidth: previewLineWidth }); var linePreviewLeft = Overlays.addOverlay("line3d", { @@ -110,7 +115,7 @@ var linePreviewLeft = Overlays.addOverlay("line3d", { color: { red: 255, green: 255, blue: 255}, alpha: 1, visible: false, - lineWidth: 1 + lineWidth: previewLineWidth }); var linePreviewRight = Overlays.addOverlay("line3d", { @@ -119,7 +124,7 @@ var linePreviewRight = Overlays.addOverlay("line3d", { color: { red: 255, green: 255, blue: 255}, alpha: 1, visible: false, - lineWidth: 1 + lineWidth: previewLineWidth }); @@ -326,11 +331,30 @@ var trackLastMouseY = 0; var trackAsDelete = false; var trackAsRecolor = false; var trackAsEyedropper = false; -var trackAsOrbit = false; +var trackAsOrbitOrPan = false; + +var addToolSelected = true; +var deleteToolSelected = false; +var recolorToolSelected = false; +var eyedropperToolSelected = false; +var selectToolSelected = false; + function calculateVoxelFromIntersection(intersection, operation) { //print("calculateVoxelFromIntersection() operation="+operation); var resultVoxel; + + var wantDebug = false; + if (wantDebug) { + print(">>>>> calculateVoxelFromIntersection().... intersection voxel.red/green/blue=" + intersection.voxel.red + ", " + + intersection.voxel.green + ", " + intersection.voxel.blue); + print(" intersection voxel.x/y/z/s=" + intersection.voxel.x + ", " + + intersection.voxel.y + ", " + intersection.voxel.z+ ": " + intersection.voxel.s); + print(" intersection face=" + intersection.face); + print(" intersection distance=" + intersection.distance); + print(" intersection intersection.x/y/z=" + intersection.intersection.x + ", " + + intersection.intersection.y + ", " + intersection.intersection.z); + } var voxelSize; if (pointerVoxelScaleSet) { @@ -339,93 +363,115 @@ function calculateVoxelFromIntersection(intersection, operation) { voxelSize = intersection.voxel.s; } - // first, calculate the enclosed voxel of size voxelSize that the intersection point falls inside of. - // if you have a voxelSize that's smaller than the voxel you're intersecting, this calculation will result - // in the subvoxel that the intersection point falls in - var x = Math.floor(intersection.intersection.x / voxelSize) * voxelSize; - var y = Math.floor(intersection.intersection.y / voxelSize) * voxelSize; - var z = Math.floor(intersection.intersection.z / voxelSize) * voxelSize; + var x; + var y; + var z; + + // if our "target voxel size" is larger than the voxel we intersected with, then we need to find the closest + // ancestor voxel of our target size that contains our intersected voxel. + if (voxelSize > intersection.voxel.s) { + if (wantDebug) { + print("voxelSize > intersection.voxel.s.... choose the larger voxel that encompasses the one selected"); + } + x = Math.floor(intersection.voxel.x / voxelSize) * voxelSize; + y = Math.floor(intersection.voxel.y / voxelSize) * voxelSize; + z = Math.floor(intersection.voxel.z / voxelSize) * voxelSize; + } else { + // otherwise, calculate the enclosed voxel of size voxelSize that the intersection point falls inside of. + // if you have a voxelSize that's smaller than the voxel you're intersecting, this calculation will result + // in the subvoxel that the intersection point falls in, if the target voxelSize matches the intersecting + // voxel this still works and results in returning the intersecting voxel which is what we want + var adjustToCenter = Vec3.multiply(Voxels.getFaceVector(intersection.face), (voxelSize * -0.5)); + if (wantDebug) { + print("adjustToCenter=" + adjustToCenter.x + "," + adjustToCenter.y + "," + adjustToCenter.z); + } + var centerOfIntersectingVoxel = Vec3.sum(intersection.intersection, adjustToCenter); + x = Math.floor(centerOfIntersectingVoxel.x / voxelSize) * voxelSize; + y = Math.floor(centerOfIntersectingVoxel.y / voxelSize) * voxelSize; + z = Math.floor(centerOfIntersectingVoxel.z / voxelSize) * voxelSize; + } resultVoxel = { x: x, y: y, z: z, s: voxelSize }; highlightAt = { x: x, y: y, z: z, s: voxelSize }; + // we only do the "add to the face we're pointing at" adjustment, if the operation is an add + // operation, and the target voxel size is equal to or smaller than the intersecting voxel. + var wantAddAdjust = (operation == "add" && (voxelSize <= intersection.voxel.s)); + if (wantDebug) { + print("wantAddAdjust="+wantAddAdjust); + } + // now we also want to calculate the "edge square" for the face for this voxel if (intersection.face == "MIN_X_FACE") { - highlightAt.x = intersection.voxel.x; - resultVoxel.x = intersection.voxel.x; - if (operation == "add") { + + highlightAt.x = x - zFightingSizeAdjust; + if (wantAddAdjust) { resultVoxel.x -= voxelSize; } - resultVoxel.bottomLeft = {x: highlightAt.x, y: highlightAt.y, z: highlightAt.z }; - resultVoxel.bottomRight = {x: highlightAt.x, y: highlightAt.y, z: highlightAt.z + voxelSize }; - resultVoxel.topLeft = {x: highlightAt.x, y: highlightAt.y + voxelSize, z: highlightAt.z }; - resultVoxel.topRight = {x: highlightAt.x, y: highlightAt.y + voxelSize, z: highlightAt.z + voxelSize }; + resultVoxel.bottomLeft = {x: highlightAt.x, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z + zFightingSizeAdjust }; + resultVoxel.bottomRight = {x: highlightAt.x, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z + voxelSize - zFightingSizeAdjust }; + resultVoxel.topLeft = {x: highlightAt.x, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z + zFightingSizeAdjust }; + resultVoxel.topRight = {x: highlightAt.x, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z + voxelSize - zFightingSizeAdjust }; } else if (intersection.face == "MAX_X_FACE") { - highlightAt.x = intersection.voxel.x + intersection.voxel.s; - resultVoxel.x = intersection.voxel.x + intersection.voxel.s; - if (operation != "add") { - resultVoxel.x -= voxelSize; + + highlightAt.x = x + voxelSize + zFightingSizeAdjust; + if (wantAddAdjust) { + resultVoxel.x += resultVoxel.s; } - resultVoxel.bottomLeft = {x: highlightAt.x, y: highlightAt.y, z: highlightAt.z }; - resultVoxel.bottomRight = {x: highlightAt.x, y: highlightAt.y, z: highlightAt.z + voxelSize }; - resultVoxel.topLeft = {x: highlightAt.x, y: highlightAt.y + voxelSize, z: highlightAt.z }; - resultVoxel.topRight = {x: highlightAt.x, y: highlightAt.y + voxelSize, z: highlightAt.z + voxelSize }; + resultVoxel.bottomRight = {x: highlightAt.x, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z + zFightingSizeAdjust }; + resultVoxel.bottomLeft = {x: highlightAt.x, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z + voxelSize - zFightingSizeAdjust }; + resultVoxel.topRight = {x: highlightAt.x, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z + zFightingSizeAdjust }; + resultVoxel.topLeft = {x: highlightAt.x, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z + voxelSize - zFightingSizeAdjust }; } else if (intersection.face == "MIN_Y_FACE") { - highlightAt.y = intersection.voxel.y; - resultVoxel.y = intersection.voxel.y; - - if (operation == "add") { + highlightAt.y = y - zFightingSizeAdjust; + if (wantAddAdjust) { resultVoxel.y -= voxelSize; } - resultVoxel.bottomLeft = {x: highlightAt.x, y: highlightAt.y, z: highlightAt.z }; - resultVoxel.bottomRight = {x: highlightAt.x + voxelSize , y: highlightAt.y, z: highlightAt.z}; - resultVoxel.topLeft = {x: highlightAt.x, y: highlightAt.y, z: highlightAt.z + voxelSize }; - resultVoxel.topRight = {x: highlightAt.x + voxelSize , y: highlightAt.y, z: highlightAt.z + voxelSize}; + resultVoxel.topRight = {x: highlightAt.x + zFightingSizeAdjust , y: highlightAt.y, z: highlightAt.z + zFightingSizeAdjust }; + resultVoxel.topLeft = {x: highlightAt.x + voxelSize - zFightingSizeAdjust, y: highlightAt.y, z: highlightAt.z + zFightingSizeAdjust }; + resultVoxel.bottomRight = {x: highlightAt.x + zFightingSizeAdjust , y: highlightAt.y, z: highlightAt.z + voxelSize - zFightingSizeAdjust }; + resultVoxel.bottomLeft = {x: highlightAt.x + voxelSize - zFightingSizeAdjust , y: highlightAt.y, z: highlightAt.z + voxelSize - zFightingSizeAdjust }; } else if (intersection.face == "MAX_Y_FACE") { - highlightAt.y = intersection.voxel.y + intersection.voxel.s; - resultVoxel.y = intersection.voxel.y + intersection.voxel.s; - if (operation != "add") { - resultVoxel.y -= voxelSize; + highlightAt.y = y + voxelSize + zFightingSizeAdjust; + if (wantAddAdjust) { + resultVoxel.y += voxelSize; } - resultVoxel.bottomLeft = {x: highlightAt.x, y: highlightAt.y, z: highlightAt.z }; - resultVoxel.bottomRight = {x: highlightAt.x + voxelSize , y: highlightAt.y, z: highlightAt.z}; - resultVoxel.topLeft = {x: highlightAt.x, y: highlightAt.y, z: highlightAt.z + voxelSize }; - resultVoxel.topRight = {x: highlightAt.x + voxelSize , y: highlightAt.y, z: highlightAt.z + voxelSize}; + resultVoxel.bottomRight = {x: highlightAt.x + zFightingSizeAdjust, y: highlightAt.y, z: highlightAt.z + zFightingSizeAdjust }; + resultVoxel.bottomLeft = {x: highlightAt.x + voxelSize - zFightingSizeAdjust, y: highlightAt.y, z: highlightAt.z + zFightingSizeAdjust}; + resultVoxel.topRight = {x: highlightAt.x + zFightingSizeAdjust, y: highlightAt.y, z: highlightAt.z + voxelSize - zFightingSizeAdjust}; + resultVoxel.topLeft = {x: highlightAt.x + voxelSize - zFightingSizeAdjust, y: highlightAt.y, z: highlightAt.z + voxelSize - zFightingSizeAdjust}; } else if (intersection.face == "MIN_Z_FACE") { - highlightAt.z = intersection.voxel.z; - resultVoxel.z = intersection.voxel.z; - - if (operation == "add") { + highlightAt.z = z - zFightingSizeAdjust; + if (wantAddAdjust) { resultVoxel.z -= voxelSize; } - resultVoxel.bottomLeft = {x: highlightAt.x, y: highlightAt.y, z: highlightAt.z }; - resultVoxel.bottomRight = {x: highlightAt.x + voxelSize , y: highlightAt.y, z: highlightAt.z}; - resultVoxel.topLeft = {x: highlightAt.x, y: highlightAt.y + voxelSize, z: highlightAt.z }; - resultVoxel.topRight = {x: highlightAt.x + voxelSize , y: highlightAt.y + voxelSize, z: highlightAt.z}; + resultVoxel.bottomRight = {x: highlightAt.x + zFightingSizeAdjust, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z }; + resultVoxel.bottomLeft = {x: highlightAt.x + voxelSize - zFightingSizeAdjust, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z}; + resultVoxel.topRight = {x: highlightAt.x + zFightingSizeAdjust, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z }; + resultVoxel.topLeft = {x: highlightAt.x + voxelSize - zFightingSizeAdjust, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z}; } else if (intersection.face == "MAX_Z_FACE") { - highlightAt.z = intersection.voxel.z + intersection.voxel.s; - resultVoxel.z = intersection.voxel.z + intersection.voxel.s; - if (operation != "add") { - resultVoxel.z -= voxelSize; + highlightAt.z = z + voxelSize + zFightingSizeAdjust; + if (wantAddAdjust) { + resultVoxel.z += voxelSize; } - resultVoxel.bottomLeft = {x: highlightAt.x, y: highlightAt.y, z: highlightAt.z }; - resultVoxel.bottomRight = {x: highlightAt.x + voxelSize , y: highlightAt.y, z: highlightAt.z}; - resultVoxel.topLeft = {x: highlightAt.x, y: highlightAt.y + voxelSize, z: highlightAt.z }; - resultVoxel.topRight = {x: highlightAt.x + voxelSize , y: highlightAt.y + voxelSize, z: highlightAt.z}; + resultVoxel.bottomLeft = {x: highlightAt.x + zFightingSizeAdjust, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z }; + resultVoxel.bottomRight = {x: highlightAt.x + voxelSize - zFightingSizeAdjust, y: highlightAt.y + zFightingSizeAdjust, z: highlightAt.z}; + resultVoxel.topLeft = {x: highlightAt.x + zFightingSizeAdjust, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z }; + resultVoxel.topRight = {x: highlightAt.x + voxelSize - zFightingSizeAdjust, y: highlightAt.y + voxelSize - zFightingSizeAdjust, z: highlightAt.z}; } @@ -456,35 +502,45 @@ function showPreviewVoxel() { var guidePosition; - if (trackAsDelete) { + if (trackAsDelete || deleteToolSelected) { guidePosition = calculateVoxelFromIntersection(intersection,"delete"); Overlays.editOverlay(voxelPreview, { position: guidePosition, - size: guidePosition.s, + size: guidePosition.s + zFightingSizeAdjust, visible: true, color: { red: 255, green: 0, blue: 0 }, solid: false, alpha: 1 }); - } else if (trackAsRecolor || trackAsEyedropper) { + } else if (selectToolSelected) { + guidePosition = calculateVoxelFromIntersection(intersection,"select"); + Overlays.editOverlay(voxelPreview, { + position: guidePosition, + size: guidePosition.s + zFightingSizeAdjust, + visible: true, + color: { red: 255, green: 255, blue: 0 }, + solid: false, + alpha: 1 + }); + } else if (trackAsRecolor || recolorToolSelected || trackAsEyedropper|| eyedropperToolSelected) { guidePosition = calculateVoxelFromIntersection(intersection,"recolor"); Overlays.editOverlay(voxelPreview, { position: guidePosition, - size: guidePosition.s + 0.002, + size: guidePosition.s + zFightingSizeAdjust, visible: true, color: voxelColor, solid: true, alpha: 0.8 }); - } else if (trackAsOrbit) { + } else if (trackAsOrbitOrPan) { Overlays.editOverlay(voxelPreview, { visible: false }); - } else if (!isExtruding) { + } else if (addToolSelected && !isExtruding) { guidePosition = calculateVoxelFromIntersection(intersection,"add"); Overlays.editOverlay(voxelPreview, { position: guidePosition, - size: guidePosition.s, + size: (guidePosition.s - zFightingSizeAdjust), visible: true, color: voxelColor, solid: true, @@ -508,12 +564,25 @@ function showPreviewLines() { } resultVoxel = calculateVoxelFromIntersection(intersection,""); - Overlays.editOverlay(linePreviewTop, { position: resultVoxel.topLeft, end: resultVoxel.topRight, visible: true }); - Overlays.editOverlay(linePreviewBottom, { position: resultVoxel.bottomLeft, end: resultVoxel.bottomRight, visible: true }); - Overlays.editOverlay(linePreviewLeft, { position: resultVoxel.topLeft, end: resultVoxel.bottomLeft, visible: true }); - Overlays.editOverlay(linePreviewRight, { position: resultVoxel.topRight, end: resultVoxel.bottomRight, visible: true }); - + if (selectToolSelected) { + Overlays.editOverlay(voxelPreview, { + position: resultVoxel, + size: resultVoxel.s + zFightingSizeAdjust, + visible: true, + color: { red: 255, green: 255, blue: 0 }, + lineWidth: previewLineWidth, + solid: false, + alpha: 1 + }); + } else { + Overlays.editOverlay(voxelPreview, { visible: false }); + Overlays.editOverlay(linePreviewTop, { position: resultVoxel.topLeft, end: resultVoxel.topRight, visible: true }); + Overlays.editOverlay(linePreviewBottom, { position: resultVoxel.bottomLeft, end: resultVoxel.bottomRight, visible: true }); + Overlays.editOverlay(linePreviewLeft, { position: resultVoxel.topLeft, end: resultVoxel.bottomLeft, visible: true }); + Overlays.editOverlay(linePreviewRight, { position: resultVoxel.topRight, end: resultVoxel.bottomRight, visible: true }); + } } else { + Overlays.editOverlay(voxelPreview, { visible: false }); Overlays.editOverlay(linePreviewTop, { visible: false }); Overlays.editOverlay(linePreviewBottom, { visible: false }); Overlays.editOverlay(linePreviewLeft, { visible: false }); @@ -533,9 +602,6 @@ function showPreviewGuides() { Overlays.editOverlay(linePreviewRight, { visible: false }); } else { showPreviewLines(); - - // make sure alternative is hidden - Overlays.editOverlay(voxelPreview, { visible: false }); } } else { // make sure all previews are off @@ -548,16 +614,22 @@ function showPreviewGuides() { } function trackMouseEvent(event) { - trackLastMouseX = event.x; - trackLastMouseY = event.y; - trackAsDelete = event.isControl; - trackAsRecolor = event.isShifted; - trackAsEyedropper = event.isMeta; - trackAsOrbit = event.isAlt; - showPreviewGuides(); + if (!trackAsOrbitOrPan) { + trackLastMouseX = event.x; + trackLastMouseY = event.y; + trackAsDelete = event.isControl; + trackAsRecolor = event.isShifted; + trackAsEyedropper = event.isMeta; + trackAsOrbitOrPan = event.isAlt; // TODO: double check this...?? + showPreviewGuides(); + } } function trackKeyPressEvent(event) { + if (!editToolsOn) { + return; + } + if (event.text == "CONTROL") { trackAsDelete = true; moveTools(); @@ -571,41 +643,13 @@ function trackKeyPressEvent(event) { moveTools(); } if (event.text == "ALT") { - trackAsOrbit = true; + trackAsOrbitOrPan = true; moveTools(); } showPreviewGuides(); } function trackKeyReleaseEvent(event) { - if (event.text == "ESC") { - pointerVoxelScaleSet = false; - } - if (event.text == "-") { - thumbX -= thumbDeltaPerStep; - calcScaleFromThumb(thumbX); - } - if (event.text == "+") { - thumbX += thumbDeltaPerStep; - calcScaleFromThumb(thumbX); - } - if (event.text == "CONTROL") { - trackAsDelete = false; - moveTools(); - } - if (event.text == "SHIFT") { - trackAsRecolor = false; - moveTools(); - } - if (event.text == "META") { - trackAsEyedropper = false; - moveTools(); - } - if (event.text == "ALT") { - trackAsOrbit = false; - moveTools(); - } - // on TAB release, toggle our tool state if (event.text == "TAB") { editToolsOn = !editToolsOn; @@ -613,14 +657,108 @@ function trackKeyReleaseEvent(event) { Audio.playSound(clickSound, audioOptions); } - // on F1 toggle the preview mode between cubes and lines - if (event.text == "F1") { - previewAsVoxel = !previewAsVoxel; - } + if (editToolsOn) { + if (event.text == "ESC") { + pointerVoxelScaleSet = false; + } + if (event.text == "-") { + thumbX -= thumbDeltaPerStep; + calcScaleFromThumb(thumbX); + } + if (event.text == "+") { + thumbX += thumbDeltaPerStep; + calcScaleFromThumb(thumbX); + } + if (event.text == "CONTROL") { + trackAsDelete = false; + moveTools(); + } + if (event.text == "SHIFT") { + trackAsRecolor = false; + moveTools(); + } + if (event.text == "META") { + trackAsEyedropper = false; + moveTools(); + } + if (event.text == "ALT") { + trackAsOrbitOrPan = false; + moveTools(); + } + + // on F1 toggle the preview mode between cubes and lines + if (event.text == "F1") { + previewAsVoxel = !previewAsVoxel; + } - showPreviewGuides(); + showPreviewGuides(); + } } +function startOrbitMode(event) { + mouseX = event.x; + mouseY = event.y; + var pickRay = Camera.computePickRay(event.x, event.y); + var intersection = Voxels.findRayIntersection(pickRay); + + // start orbit camera! + var cameraPosition = Camera.getPosition(); + oldMode = Camera.getMode(); + Camera.setMode("independent"); + Camera.keepLookingAt(intersection.intersection); + // get position for initial azimuth, elevation + orbitCenter = intersection.intersection; + var orbitVector = Vec3.subtract(cameraPosition, orbitCenter); + orbitRadius = Vec3.length(orbitVector); + orbitAzimuth = Math.atan2(orbitVector.z, orbitVector.x); + orbitAltitude = Math.asin(orbitVector.y / Vec3.length(orbitVector)); + + //print("startOrbitMode..."); +} + +function handleOrbitingMove(event) { + var cameraOrientation = Camera.getOrientation(); + var origEulers = Quat.safeEulerAngles(cameraOrientation); + var newEulers = fixEulerAngles(Quat.safeEulerAngles(cameraOrientation)); + var dx = event.x - mouseX; + var dy = event.y - mouseY; + orbitAzimuth += dx / ORBIT_RATE_AZIMUTH; + orbitAltitude += dy / ORBIT_RATE_ALTITUDE; + var orbitVector = { x:(Math.cos(orbitAltitude) * Math.cos(orbitAzimuth)) * orbitRadius, + y:Math.sin(orbitAltitude) * orbitRadius, + z:(Math.cos(orbitAltitude) * Math.sin(orbitAzimuth)) * orbitRadius }; + orbitPosition = Vec3.sum(orbitCenter, orbitVector); + Camera.setPosition(orbitPosition); + mouseX = event.x; + mouseY = event.y; + //print("handleOrbitingMove..."); +} + +function endOrbitMode(event) { + var cameraOrientation = Camera.getOrientation(); + MyAvatar.position = Camera.getPosition(); + MyAvatar.headOrientation = cameraOrientation; + Camera.stopLooking(); + Camera.setMode(oldMode); + Camera.setOrientation(cameraOrientation); + //print("endOrbitMode..."); +} + +function startPanMode(event, intersection) { + // start pan camera! + print("handle PAN mode!!!"); +} + +function handlePanMove(event) { + print("PANNING mode!!! "); + //print("isPanning="+isPanning + " inPanningFromTouch="+isPanningFromTouch + " trackAsOrbitOrPan="+trackAsOrbitOrPan); +} + +function endPanMode(event) { + print("ending PAN mode!!!"); +} + + function mousePressEvent(event) { // if our tools are off, then don't do anything @@ -628,29 +766,100 @@ function mousePressEvent(event) { return; } - var clickedOnSwatch = false; - var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); + // Normally, if we're panning or orbiting from touch, ignore these... because our touch takes precedence. + // but In the case of a button="RIGHT" click, we may get some touch messages first, and we actually want to + // cancel any touch mode, and then let the right-click through + if (isOrbitingFromTouch || isPanningFromTouch) { + + // if the user is holding the ALT key AND they are clicking the RIGHT button (or on multi-touch doing a two + // finger touch, then we want to let the new panning behavior take over. + // if it's any other case we still want to bail + if (event.button == "RIGHT" && trackAsOrbitOrPan) { + // cancel our current multitouch operation... + if (isOrbitingFromTouch) { + endOrbitMode(event); + isOrbitingFromTouch = false; + } + if (isPanningFromTouch) { + //print("mousePressEvent... calling endPanMode()"); + endPanMode(event); + isPanningFromTouch = false; + } + // let things fall through + } else { + return; + } + } + + // no clicking on overlays while in panning mode + if (!trackAsOrbitOrPan) { + var clickedOnSomething = false; + var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); + +print("clickedOverlay="+clickedOverlay); - // If the user clicked on the thumb, handle the slider logic - if (clickedOverlay == thumb) { - isMovingSlider = true; - thumbClickOffsetX = event.x - (sliderX + thumbX); // this should be the position of the mouse relative to the thumb - return; // no further processing - } else { - // if the user clicked on one of the color swatches, update the selectedSwatch - for (s = 0; s < numColors; s++) { - if (clickedOverlay == swatches[s]) { - whichColor = s; - moveTools(); - clickedOnSwatch = true; + // If the user clicked on the thumb, handle the slider logic + if (clickedOverlay == thumb) { + isMovingSlider = true; + thumbClickOffsetX = event.x - (sliderX + thumbX); // this should be the position of the mouse relative to the thumb + clickedOnSomething = true; + } else if (clickedOverlay == addTool) { + addToolSelected = true; + deleteToolSelected = false; + recolorToolSelected = false; + eyedropperToolSelected = false; + selectToolSelected = false; + moveTools(); + clickedOnSomething = true; + } else if (clickedOverlay == deleteTool) { + addToolSelected = false; + deleteToolSelected = true; + recolorToolSelected = false; + eyedropperToolSelected = false; + selectToolSelected = false; + moveTools(); + clickedOnSomething = true; + } else if (clickedOverlay == recolorTool) { + addToolSelected = false; + deleteToolSelected = false; + recolorToolSelected = true; + eyedropperToolSelected = false; + selectToolSelected = false; + moveTools(); + clickedOnSomething = true; + } else if (clickedOverlay == eyedropperTool) { + addToolSelected = false; + deleteToolSelected = false; + recolorToolSelected = false; + eyedropperToolSelected = true; + selectToolSelected = false; + moveTools(); + clickedOnSomething = true; + } else if (clickedOverlay == selectTool) { + addToolSelected = false; + deleteToolSelected = false; + recolorToolSelected = false; + eyedropperToolSelected = false; + selectToolSelected = true; + moveTools(); + clickedOnSomething = true; + } else { + // if the user clicked on one of the color swatches, update the selectedSwatch + for (s = 0; s < numColors; s++) { + if (clickedOverlay == swatches[s]) { + whichColor = s; + moveTools(); + clickedOnSomething = true; + break; + } } } - if (clickedOnSwatch) { + if (clickedOnSomething) { return; // no further processing } } - + // TODO: does any of this stuff need to execute if we're panning or orbiting? trackMouseEvent(event); // used by preview support mouseX = event.x; mouseY = event.y; @@ -663,27 +872,23 @@ function mousePressEvent(event) { calcThumbFromScale(intersection.voxel.s); } - if (event.isAlt) { - // start orbit camera! - var cameraPosition = Camera.getPosition(); - oldMode = Camera.getMode(); - Camera.setMode("independent"); - isOrbiting = true; - Camera.keepLookingAt(intersection.intersection); - // get position for initial azimuth, elevation - orbitCenter = intersection.intersection; - var orbitVector = Vec3.subtract(cameraPosition, orbitCenter); - orbitRadius = Vec3.length(orbitVector); - orbitAzimuth = Math.atan2(orbitVector.z, orbitVector.x); - orbitAltitude = Math.asin(orbitVector.y / Vec3.length(orbitVector)); - - } else if (trackAsDelete || (event.isRightButton && !trackAsEyedropper)) { + // Note: touch and mouse events can cross paths, so we want to ignore any mouse events that would + // start a pan or orbit if we're already doing a pan or orbit via touch... + if ((event.isAlt || trackAsOrbitOrPan) && !(isOrbitingFromTouch || isPanningFromTouch)) { + if (event.isLeftButton && !event.isRightButton) { + startOrbitMode(event); + isOrbiting = true; + } else if (event.isRightButton && !event.isLeftButton) { + startPanMode(event); + isPanning = true; + } + } else if (deleteToolSelected || trackAsDelete || (event.isRightButton && !trackAsEyedropper)) { // Delete voxel voxelDetails = calculateVoxelFromIntersection(intersection,"delete"); Voxels.eraseVoxel(voxelDetails.x, voxelDetails.y, voxelDetails.z, voxelDetails.s); Audio.playSound(deleteSound, audioOptions); Overlays.editOverlay(voxelPreview, { visible: false }); - } else if (trackAsEyedropper) { + } else if (eyedropperToolSelected || trackAsEyedropper) { if (whichColor != -1) { colors[whichColor].red = intersection.voxel.red; colors[whichColor].green = intersection.voxel.green; @@ -691,7 +896,7 @@ function mousePressEvent(event) { moveTools(); } - } else if (trackAsRecolor) { + } else if (recolorToolSelected || trackAsRecolor) { // Recolor Voxel voxelDetails = calculateVoxelFromIntersection(intersection,"recolor"); @@ -701,7 +906,7 @@ function mousePressEvent(event) { colors[whichColor].red, colors[whichColor].green, colors[whichColor].blue); Audio.playSound(changeColorSound, audioOptions); Overlays.editOverlay(voxelPreview, { visible: false }); - } else { + } else if (addToolSelected) { // Add voxel on face if (whichColor == -1) { // Copy mode - use clicked voxel color @@ -717,6 +922,7 @@ function mousePressEvent(event) { } voxelDetails = calculateVoxelFromIntersection(intersection,"add"); + Voxels.eraseVoxel(voxelDetails.x, voxelDetails.y, voxelDetails.z, voxelDetails.s); Voxels.setVoxel(voxelDetails.x, voxelDetails.y, voxelDetails.z, voxelDetails.s, newColor.red, newColor.green, newColor.blue); lastVoxelPosition = { x: voxelDetails.x, y: voxelDetails.y, z: voxelDetails.z }; @@ -727,15 +933,13 @@ function mousePressEvent(event) { Overlays.editOverlay(voxelPreview, { visible: false }); dragStart = { x: event.x, y: event.y }; isAdding = true; - } + } } } function keyPressEvent(event) { // if our tools are off, then don't do anything if (editToolsOn) { - key_alt = event.isAlt; - key_shift = event.isShifted; var nVal = parseInt(event.text); if (event.text == "0") { print("Color = Copy"); @@ -760,6 +964,7 @@ function keyPressEvent(event) { red: colors[color].red, green: colors[color].green, blue: colors[color].blue }; + Voxels.eraseVoxel(voxelDetails.x, voxelDetails.y, voxelDetails.z, voxelDetails.s); Voxels.setVoxel(newVoxel.x, newVoxel.y, newVoxel.z, newVoxel.s, newVoxel.red, newVoxel.green, newVoxel.blue); setAudioPosition(); Audio.playSound(addSound, audioOptions); @@ -778,13 +983,70 @@ function keyPressEvent(event) { function keyReleaseEvent(event) { trackKeyReleaseEvent(event); // used by preview support - key_alt = false; - key_shift = false; + + // handle clipboard items + if (selectToolSelected) { + var pickRay = Camera.computePickRay(trackLastMouseX, trackLastMouseY); + var intersection = Voxels.findRayIntersection(pickRay); + selectedVoxel = calculateVoxelFromIntersection(intersection,"select"); + + // Note: this sample uses Alt+ as the key codes for these clipboard items + if ((event.key == 199 || event.key == 67 || event.text == "C" || event.text == "c") && event.isAlt) { + print("the Alt+C key was pressed... copy"); + Clipboard.copyVoxel(selectedVoxel.x, selectedVoxel.y, selectedVoxel.z, selectedVoxel.s); + } + if ((event.key == 8776 || event.key == 88 || event.text == "X" || event.text == "x") && event.isAlt) { + print("the Alt+X key was pressed... cut"); + Clipboard.cutVoxel(selectedVoxel.x, selectedVoxel.y, selectedVoxel.z, selectedVoxel.s); + } + if ((event.key == 8730 || event.key == 86 || event.text == "V" || event.text == "v") && event.isAlt) { + print("the Alt+V key was pressed... paste"); + Clipboard.pasteVoxel(selectedVoxel.x, selectedVoxel.y, selectedVoxel.z, selectedVoxel.s); + } + if (event.text == "DELETE" || event.text == "BACKSPACE") { + print("the DELETE/BACKSPACE key was pressed... delete"); + Clipboard.deleteVoxel(selectedVoxel.x, selectedVoxel.y, selectedVoxel.z, selectedVoxel.s); + } + + if ((event.text == "E" || event.text == "e") && event.isMeta) { + print("the Ctl+E key was pressed... export"); + Clipboard.exportVoxel(selectedVoxel.x, selectedVoxel.y, selectedVoxel.z, selectedVoxel.s); + } + if ((event.text == "I" || event.text == "i") && event.isMeta) { + print("the Ctl+I key was pressed... import"); + Clipboard.importVoxels(); + } + if ((event.key == 78 || event.text == "N" || event.text == "n") && event.isMeta) { + print("the Ctl+N key was pressed, nudging to left 1 meter... nudge"); + Clipboard.nudgeVoxel(selectedVoxel.x, selectedVoxel.y, selectedVoxel.z, selectedVoxel.s, { x: -1, y: 0, z: 0 }); + } + } } function mouseMoveEvent(event) { - if (isMovingSlider) { + if (!editToolsOn) { + return; + } + + // if we're panning or orbiting from touch, ignore these... because our touch takes precedence. + if (isOrbitingFromTouch || isPanningFromTouch) { + return; + } + + // double check that we didn't accidentally miss a pan or orbit click request + if (trackAsOrbitOrPan && !isPanning && !isOrbiting) { + if (event.isLeftButton && !event.isRightButton) { + startOrbitMode(event); + isOrbiting = true; + } + if (!event.isLeftButton && event.isRightButton) { + startPanMode(event); + isPanning = true; + } + } + + if (!trackAsOrbitOrPan && isMovingSlider) { thumbX = (event.x - thumbClickOffsetX) - sliderX; if (thumbX < minThumbX) { thumbX = minThumbX; @@ -795,21 +1057,10 @@ function mouseMoveEvent(event) { calcScaleFromThumb(thumbX); } else if (isOrbiting) { - var cameraOrientation = Camera.getOrientation(); - var origEulers = Quat.safeEulerAngles(cameraOrientation); - var newEulers = fixEulerAngles(Quat.safeEulerAngles(cameraOrientation)); - var dx = event.x - mouseX; - var dy = event.y - mouseY; - orbitAzimuth += dx / ORBIT_RATE_AZIMUTH; - orbitAltitude += dy / ORBIT_RATE_ALTITUDE; - var orbitVector = { x:(Math.cos(orbitAltitude) * Math.cos(orbitAzimuth)) * orbitRadius, - y:Math.sin(orbitAltitude) * orbitRadius, - z:(Math.cos(orbitAltitude) * Math.sin(orbitAzimuth)) * orbitRadius }; - orbitPosition = Vec3.sum(orbitCenter, orbitVector); - Camera.setPosition(orbitPosition); - mouseX = event.x; - mouseY = event.y; - } else if (isAdding) { + handleOrbitingMove(event); + } else if (isPanning) { + handlePanMove(event); + } else if (!trackAsOrbitOrPan && isAdding) { // Watch the drag direction to tell which way to 'extrude' this voxel if (!isExtruding) { var pickRay = Camera.computePickRay(event.x, event.y); @@ -840,6 +1091,7 @@ function mouseMoveEvent(event) { var dy = event.y - mouseY; if (Math.sqrt(dx*dx + dy*dy) > PIXELS_PER_EXTRUDE_VOXEL) { lastVoxelPosition = Vec3.sum(lastVoxelPosition, extrudeDirection); + Voxels.eraseVoxel(voxelDetails.x, voxelDetails.y, voxelDetails.z, voxelDetails.s); Voxels.setVoxel(lastVoxelPosition.x, lastVoxelPosition.y, lastVoxelPosition.z, extrudeScale, lastVoxelColor.red, lastVoxelColor.green, lastVoxelColor.blue); mouseX = event.x; @@ -861,18 +1113,17 @@ function mouseReleaseEvent(event) { if (isMovingSlider) { isMovingSlider = false; } - + if (isOrbiting) { - var cameraOrientation = Camera.getOrientation(); - var eulers = Quat.safeEulerAngles(cameraOrientation); - MyAvatar.position = Camera.getPosition(); - MyAvatar.orientation = cameraOrientation; - Camera.stopLooking(); - Camera.setMode(oldMode); - Camera.setOrientation(cameraOrientation); + endOrbitMode(event); + isOrbiting = false; + } + if (isPanning) { + print("mouseReleaseEvent... calling endPanMode()"); + endPanMode(event); + isPanning = false; } isAdding = false; - isOrbiting = false; isExtruding = false; } @@ -909,13 +1160,15 @@ function moveTools() { eyedropperToolColor = notSelectedColor; selectToolColor = notSelectedColor; - if (trackAsDelete) { + if (trackAsDelete || deleteToolSelected) { deleteToolColor = toolSelectedColor; - } else if (trackAsRecolor) { + } else if (trackAsRecolor || recolorToolSelected) { recolorToolColor = toolSelectedColor; - } else if (trackAsEyedropper) { + } else if (trackAsEyedropper || eyedropperToolSelected) { eyedropperToolColor = toolSelectedColor; - } else if (trackAsOrbit) { + } else if (selectToolSelected) { + selectToolColor = toolSelectedColor; + } else if (trackAsOrbitOrPan) { // nothing gets selected in this case... } else { addToolColor = toolSelectedColor; @@ -951,7 +1204,6 @@ function moveTools() { visible: editToolsOn }); - sliderX = swatchesX + swatchesWidth; sliderY = windowDimensions.y - sliderHeight; Overlays.editOverlay(slider, { x: sliderX, y: sliderY, visible: editToolsOn }); @@ -962,6 +1214,101 @@ function moveTools() { } +function touchBeginEvent(event) { + if (!editToolsOn) { + return; + } + + // if we're already in the middle of orbiting or panning, then ignore these multi-touch events... + if (isOrbiting || isPanning) { + return; + } + + if (event.isAlt || trackAsOrbitOrPan) { + if (event.touchPoints == touchPointsToOrbit) { + // we need to double check that we didn't start an orbit, because the touch events will sometimes + // come in as 2 then 3 touches... + if (isPanningFromTouch) { + print("touchBeginEvent... calling endPanMode()"); + endPanMode(event); + isPanningFromTouch = false; + } + startOrbitMode(event); + isOrbitingFromTouch = true; + } else if (event.touchPoints == touchPointsToPan) { + // we need to double check that we didn't start an orbit, because the touch events will sometimes + // come in as 2 then 3 touches... + if (isOrbitingFromTouch) { + endOrbitMode(event); + isOrbitingFromTouch = false; + } + startPanMode(event); + isPanningFromTouch = true; + } + } +} + +function touchUpdateEvent(event) { + if (!editToolsOn) { + return; + } + + // if we're already in the middle of orbiting or panning, then ignore these multi-touch events... + if (isOrbiting || isPanning) { + return; + } + + if (isOrbitingFromTouch) { + // we need to double check that we didn't start an orbit, because the touch events will sometimes + // come in as 2 then 3 touches... + if (event.touchPoints == touchPointsToPan) { + //print("we now have touchPointsToPan touches... switch to pan..."); + endOrbitMode(event); + isOrbitingFromTouch = false; + startPanMode(event); + isPanningFromTouch = true; + } else { + handleOrbitingMove(event); + } + } + if (isPanningFromTouch) { + //print("touchUpdateEvent... isPanningFromTouch... event.touchPoints=" + event.touchPoints); + // we need to double check that we didn't start an orbit, because the touch events will sometimes + // come in as 2 then 3 touches... + if (event.touchPoints == touchPointsToOrbit) { + //print("we now have touchPointsToOrbit touches... switch to orbit..."); + //print("touchUpdateEvent... calling endPanMode()"); + endPanMode(event); + isPanningFromTouch = false; + startOrbitMode(event); + isOrbitingFromTouch = true; + handleOrbitingMove(event); + } else { + handlePanMove(event); + } + } +} + +function touchEndEvent(event) { + if (!editToolsOn) { + return; + } + + // if we're already in the middle of orbiting or panning, then ignore these multi-touch events... + if (isOrbiting || isPanning) { + return; + } + + if (isOrbitingFromTouch) { + endOrbitMode(event); + isOrbitingFromTouch = false; + } + if (isPanningFromTouch) { + print("touchEndEvent... calling endPanMode()"); + endPanMode(event); + isPanningFromTouch = false; + } +} function update() { var newWindowDimensions = Controller.getViewportDimensions(); @@ -976,6 +1323,12 @@ Controller.mouseReleaseEvent.connect(mouseReleaseEvent); Controller.mouseMoveEvent.connect(mouseMoveEvent); Controller.keyPressEvent.connect(keyPressEvent); Controller.keyReleaseEvent.connect(keyReleaseEvent); +Controller.touchBeginEvent.connect(touchBeginEvent); +Controller.touchUpdateEvent.connect(touchUpdateEvent); +Controller.touchEndEvent.connect(touchEndEvent); +Controller.captureKeyEvents({ text: "+" }); +Controller.captureKeyEvents({ text: "-" }); + function scriptEnding() { Overlays.deleteOverlay(voxelPreview); @@ -991,6 +1344,10 @@ function scriptEnding() { Overlays.deleteOverlay(recolorTool); Overlays.deleteOverlay(eyedropperTool); Overlays.deleteOverlay(selectTool); + Overlays.deleteOverlay(slider); + Overlays.deleteOverlay(thumb); + Controller.releaseKeyEvents({ text: "+" }); + Controller.releaseKeyEvents({ text: "-" }); } Script.scriptEnding.connect(scriptEnding); diff --git a/examples/overlaysExample.js b/examples/overlaysExample.js index b57f82e7e9..d97ec9e8fd 100644 --- a/examples/overlaysExample.js +++ b/examples/overlaysExample.js @@ -156,6 +156,13 @@ var line3d = Overlays.addOverlay("line3d", { lineWidth: 5 }); +// this will display the content of your clipboard at the origin of the domain +var clipboardPreview = Overlays.addOverlay("clipboard", { + position: { x: 0, y: 0, z: 0}, + size: 1 / 32, + visible: true + }); + // When our script shuts down, we should clean up all of our overlays function scriptEnding() { @@ -170,6 +177,7 @@ function scriptEnding() { Overlays.deleteOverlay(solidCube); Overlays.deleteOverlay(sphere); Overlays.deleteOverlay(line3d); + Overlays.deleteOverlay(clipboardPreview); } Script.scriptEnding.connect(scriptEnding); diff --git a/examples/rayPickExample.js b/examples/rayPickExample.js index 39dc910e78..9c34cca1de 100644 --- a/examples/rayPickExample.js +++ b/examples/rayPickExample.js @@ -26,6 +26,12 @@ function mouseMoveEvent(event) { print("intersection distance=" + intersection.distance); print("intersection intersection.x/y/z=" + intersection.intersection.x + ", " + intersection.intersection.y + ", " + intersection.intersection.z); + + // also test the getVoxelAt() api which should find and return same voxel + + var voxelAt = Voxels.getVoxelAt(intersection.voxel.x, intersection.voxel.y, intersection.voxel.z, intersection.voxel.s); + print("voxelAt.x/y/z/s=" + voxelAt.x + ", " + voxelAt.y + ", " + voxelAt.z + ": " + voxelAt.s); + print("voxelAt.red/green/blue=" + voxelAt.red + ", " + voxelAt.green + ", " + voxelAt.blue); } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b5b73d4271..999e8359d7 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -98,6 +98,7 @@ const int MIRROR_VIEW_HEIGHT = 215; const float MIRROR_FULLSCREEN_DISTANCE = 0.35f; const float MIRROR_REARVIEW_DISTANCE = 0.65f; const float MIRROR_REARVIEW_BODY_DISTANCE = 2.3f; +const float MIRROR_FIELD_OF_VIEW = 30.0f; const QString CHECK_VERSION_URL = "http://highfidelity.io/latestVersion.xml"; const QString SKIP_FILENAME = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/hifi.skipversion"; @@ -264,6 +265,11 @@ Application::Application(int& argc, char** argv, timeval &startup_time) : connect(identityPacketTimer, &QTimer::timeout, _myAvatar, &MyAvatar::sendIdentityPacket); identityPacketTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); + // send the billboard packet for our avatar every few seconds + QTimer* billboardPacketTimer = new QTimer(); + connect(billboardPacketTimer, &QTimer::timeout, _myAvatar, &MyAvatar::sendBillboardPacket); + billboardPacketTimer->start(AVATAR_BILLBOARD_PACKET_SEND_INTERVAL_MSECS); + QString cachePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation); _networkAccessManager = new QNetworkAccessManager(this); @@ -536,73 +542,8 @@ void Application::paintGL() { _glowEffect.render(); if (Menu::getInstance()->isOptionChecked(MenuOption::Mirror)) { - - bool eyeRelativeCamera = false; - if (_rearMirrorTools->getZoomLevel() == BODY) { - _mirrorCamera.setDistance(MIRROR_REARVIEW_BODY_DISTANCE * _myAvatar->getScale()); - _mirrorCamera.setTargetPosition(_myAvatar->getChestPosition()); - } else { // HEAD zoom level - _mirrorCamera.setDistance(MIRROR_REARVIEW_DISTANCE * _myAvatar->getScale()); - if (_myAvatar->getSkeletonModel().isActive() && _myAvatar->getHead()->getFaceModel().isActive()) { - // as a hack until we have a better way of dealing with coordinate precision issues, reposition the - // face/body so that the average eye position lies at the origin - eyeRelativeCamera = true; - _mirrorCamera.setTargetPosition(glm::vec3()); - - } else { - _mirrorCamera.setTargetPosition(_myAvatar->getHead()->calculateAverageEyePosition()); - } - } - - _mirrorCamera.setTargetRotation(_myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PIf, 0.0f))); - _mirrorCamera.update(1.0f/_fps); - - // set the bounds of rear mirror view - glViewport(_mirrorViewRect.x(), _glWidget->height() - _mirrorViewRect.y() - _mirrorViewRect.height(), - _mirrorViewRect.width(), _mirrorViewRect.height()); - glScissor(_mirrorViewRect.x(), _glWidget->height() - _mirrorViewRect.y() - _mirrorViewRect.height(), - _mirrorViewRect.width(), _mirrorViewRect.height()); - bool updateViewFrustum = false; - updateProjectionMatrix(_mirrorCamera, updateViewFrustum); - glEnable(GL_SCISSOR_TEST); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - // render rear mirror view - glPushMatrix(); - if (eyeRelativeCamera) { - // save absolute translations - glm::vec3 absoluteSkeletonTranslation = _myAvatar->getSkeletonModel().getTranslation(); - glm::vec3 absoluteFaceTranslation = _myAvatar->getHead()->getFaceModel().getTranslation(); - - // get the eye positions relative to the neck and use them to set the face translation - glm::vec3 leftEyePosition, rightEyePosition; - _myAvatar->getHead()->getFaceModel().setTranslation(glm::vec3()); - _myAvatar->getHead()->getFaceModel().getEyePositions(leftEyePosition, rightEyePosition); - _myAvatar->getHead()->getFaceModel().setTranslation((leftEyePosition + rightEyePosition) * -0.5f); - - // get the neck position relative to the body and use it to set the skeleton translation - glm::vec3 neckPosition; - _myAvatar->getSkeletonModel().setTranslation(glm::vec3()); - _myAvatar->getSkeletonModel().getNeckPosition(neckPosition); - _myAvatar->getSkeletonModel().setTranslation(_myAvatar->getHead()->getFaceModel().getTranslation() - - neckPosition); - - displaySide(_mirrorCamera, true); - - // restore absolute translations - _myAvatar->getSkeletonModel().setTranslation(absoluteSkeletonTranslation); - _myAvatar->getHead()->getFaceModel().setTranslation(absoluteFaceTranslation); - } else { - displaySide(_mirrorCamera, true); - } - glPopMatrix(); - - _rearMirrorTools->render(false); - - // reset Viewport and projection matrix - glViewport(0, 0, _glWidget->width(), _glWidget->height()); - glDisable(GL_SCISSOR_TEST); - updateProjectionMatrix(_myCamera, updateViewFrustum); + renderRearViewMirror(_mirrorViewRect); + } else if (Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { _rearMirrorTools->render(true); } @@ -2039,6 +1980,9 @@ void Application::updateMouseRay() { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateMouseRay()"); + // make sure the frustum is up-to-date + loadViewFrustum(_myCamera, _viewFrustum); + // if the mouse pointer isn't visible, act like it's at the center of the screen float x = 0.5f, y = 0.5f; if (!_mouseHidden) { @@ -2086,11 +2030,12 @@ void Application::updateVisage() { _visage.update(); } -void Application::updateMyAvatarLookAtPosition(glm::vec3& lookAtSpot) { +void Application::updateMyAvatarLookAtPosition() { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateMyAvatarLookAtPosition()"); + glm::vec3 lookAtSpot; if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { lookAtSpot = _myCamera.getPosition(); @@ -2280,20 +2225,21 @@ void Application::updateMetavoxels(float deltaTime) { } void Application::cameraMenuChanged() { + float modeShiftPeriod = (_myCamera.getMode() == CAMERA_MODE_MIRROR) ? 0.0f : 1.0f; if (Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { if (_myCamera.getMode() != CAMERA_MODE_MIRROR) { _myCamera.setMode(CAMERA_MODE_MIRROR); - _myCamera.setModeShiftPeriod(0.00f); + _myCamera.setModeShiftPeriod(0.0f); } } else if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { if (_myCamera.getMode() != CAMERA_MODE_FIRST_PERSON) { _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); - _myCamera.setModeShiftPeriod(1.0f); + _myCamera.setModeShiftPeriod(modeShiftPeriod); } } else { if (_myCamera.getMode() != CAMERA_MODE_THIRD_PERSON) { _myCamera.setMode(CAMERA_MODE_THIRD_PERSON); - _myCamera.setModeShiftPeriod(1.0f); + _myCamera.setModeShiftPeriod(modeShiftPeriod); } } } @@ -2371,13 +2317,10 @@ void Application::update(float deltaTime) { // check what's under the mouse and update the mouse voxel updateMouseRay(); - // Set where I am looking based on my mouse ray (so that other people can see) - glm::vec3 lookAtSpot; - updateFaceshift(); updateVisage(); - _myAvatar->updateLookAtTargetAvatar(lookAtSpot); - updateMyAvatarLookAtPosition(lookAtSpot); + _myAvatar->updateLookAtTargetAvatar(); + updateMyAvatarLookAtPosition(); // Find the voxel we are hovering over, and respond if clicked float distance; @@ -2400,6 +2343,9 @@ void Application::update(float deltaTime) { _particles.update(); // update the particles... _particleCollisionSystem.update(); // collide the particles... + + // let external parties know we're updating + emit simulating(deltaTime); } void Application::updateMyAvatar(float deltaTime) { @@ -2740,6 +2686,24 @@ void Application::setupWorldLight() { glMateriali(GL_FRONT, GL_SHININESS, 96); } +QImage Application::renderAvatarBillboard() { + _textureCache.getPrimaryFramebufferObject()->bind(); + + glDisable(GL_BLEND); + + const int BILLBOARD_SIZE = 64; + renderRearViewMirror(QRect(0, _glWidget->height() - BILLBOARD_SIZE, BILLBOARD_SIZE, BILLBOARD_SIZE), true); + + QImage image(BILLBOARD_SIZE, BILLBOARD_SIZE, QImage::Format_ARGB32); + glReadPixels(0, 0, BILLBOARD_SIZE, BILLBOARD_SIZE, GL_BGRA, GL_UNSIGNED_BYTE, image.bits()); + + glEnable(GL_BLEND); + + _textureCache.getPrimaryFramebufferObject()->release(); + + return image; +} + void Application::displaySide(Camera& whichCamera, bool selfAvatarOnly) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide()"); // transform by eye offset @@ -2933,8 +2897,8 @@ void Application::displaySide(Camera& whichCamera, bool selfAvatarOnly) { } } - bool renderMyHead = (whichCamera.getInterpolatedMode() != CAMERA_MODE_FIRST_PERSON); - _avatarManager.renderAvatars(renderMyHead, selfAvatarOnly); + bool forceRenderMyHead = (whichCamera.getInterpolatedMode() == CAMERA_MODE_MIRROR); + _avatarManager.renderAvatars(forceRenderMyHead, selfAvatarOnly); if (!selfAvatarOnly) { // Render the world box @@ -3657,6 +3621,84 @@ void Application::renderCoverageMapsRecursively(CoverageMap* map) { } } +void Application::renderRearViewMirror(const QRect& region, bool billboard) { + bool eyeRelativeCamera = false; + if (billboard) { + _mirrorCamera.setFieldOfView(BILLBOARD_FIELD_OF_VIEW); + _mirrorCamera.setDistance(BILLBOARD_DISTANCE * _myAvatar->getScale()); + _mirrorCamera.setTargetPosition(_myAvatar->getPosition()); + + } else if (_rearMirrorTools->getZoomLevel() == BODY) { + _mirrorCamera.setFieldOfView(MIRROR_FIELD_OF_VIEW); + _mirrorCamera.setDistance(MIRROR_REARVIEW_BODY_DISTANCE * _myAvatar->getScale()); + _mirrorCamera.setTargetPosition(_myAvatar->getChestPosition()); + + } else { // HEAD zoom level + _mirrorCamera.setFieldOfView(MIRROR_FIELD_OF_VIEW); + _mirrorCamera.setDistance(MIRROR_REARVIEW_DISTANCE * _myAvatar->getScale()); + if (_myAvatar->getSkeletonModel().isActive() && _myAvatar->getHead()->getFaceModel().isActive()) { + // as a hack until we have a better way of dealing with coordinate precision issues, reposition the + // face/body so that the average eye position lies at the origin + eyeRelativeCamera = true; + _mirrorCamera.setTargetPosition(glm::vec3()); + + } else { + _mirrorCamera.setTargetPosition(_myAvatar->getHead()->calculateAverageEyePosition()); + } + } + _mirrorCamera.setAspectRatio((float)region.width() / region.height()); + + _mirrorCamera.setTargetRotation(_myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PIf, 0.0f))); + _mirrorCamera.update(1.0f/_fps); + + // set the bounds of rear mirror view + glViewport(region.x(), _glWidget->height() - region.y() - region.height(), region.width(), region.height()); + glScissor(region.x(), _glWidget->height() - region.y() - region.height(), region.width(), region.height()); + bool updateViewFrustum = false; + updateProjectionMatrix(_mirrorCamera, updateViewFrustum); + glEnable(GL_SCISSOR_TEST); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // render rear mirror view + glPushMatrix(); + if (eyeRelativeCamera) { + // save absolute translations + glm::vec3 absoluteSkeletonTranslation = _myAvatar->getSkeletonModel().getTranslation(); + glm::vec3 absoluteFaceTranslation = _myAvatar->getHead()->getFaceModel().getTranslation(); + + // get the eye positions relative to the neck and use them to set the face translation + glm::vec3 leftEyePosition, rightEyePosition; + _myAvatar->getHead()->getFaceModel().setTranslation(glm::vec3()); + _myAvatar->getHead()->getFaceModel().getEyePositions(leftEyePosition, rightEyePosition); + _myAvatar->getHead()->getFaceModel().setTranslation((leftEyePosition + rightEyePosition) * -0.5f); + + // get the neck position relative to the body and use it to set the skeleton translation + glm::vec3 neckPosition; + _myAvatar->getSkeletonModel().setTranslation(glm::vec3()); + _myAvatar->getSkeletonModel().getNeckPosition(neckPosition); + _myAvatar->getSkeletonModel().setTranslation(_myAvatar->getHead()->getFaceModel().getTranslation() - + neckPosition); + + displaySide(_mirrorCamera, true); + + // restore absolute translations + _myAvatar->getSkeletonModel().setTranslation(absoluteSkeletonTranslation); + _myAvatar->getHead()->getFaceModel().setTranslation(absoluteFaceTranslation); + } else { + displaySide(_mirrorCamera, true); + } + glPopMatrix(); + + if (!billboard) { + _rearMirrorTools->render(false); + } + + // reset Viewport and projection matrix + glViewport(0, 0, _glWidget->width(), _glWidget->height()); + glDisable(GL_SCISSOR_TEST); + updateProjectionMatrix(_myCamera, updateViewFrustum); +} + // renderViewFrustum() // // Description: this will render the view frustum bounds for EITHER the head @@ -4169,7 +4211,9 @@ void Application::reloadAllScripts() { scriptAction->activate(QAction::Trigger); qDebug() << "stopping script..." << scriptAction->text(); } - _activeScripts.clear(); + + // NOTE: we don't need to clear the _activeScripts list because that is handled on script shutdown. + foreach (QString scriptName, reloadList){ qDebug() << "reloading script..." << scriptName; loadScript(scriptName); @@ -4177,7 +4221,7 @@ void Application::reloadAllScripts() { } void Application::removeScriptName(const QString& fileNameString) { - _activeScripts.removeOne(fileNameString); + _activeScripts.removeOne(fileNameString); } void Application::loadScript(const QString& fileNameString) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 4a3a7ac0e4..61d40eac1f 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -95,6 +96,9 @@ static const float NODE_KILLED_BLUE = 0.0f; static const QString SNAPSHOT_EXTENSION = ".jpg"; +static const float BILLBOARD_FIELD_OF_VIEW = 30.0f; +static const float BILLBOARD_DISTANCE = 5.0f; + class Application : public QApplication { Q_OBJECT @@ -183,6 +187,8 @@ public: void setupWorldLight(); + QImage renderAvatarBillboard(); + void displaySide(Camera& whichCamera, bool selfAvatarOnly = false); /// Loads a view matrix that incorporates the specified model translation without the precision issues that can @@ -198,6 +204,8 @@ public: void computeOffAxisFrustum(float& left, float& right, float& bottom, float& top, float& nearVal, float& farVal, glm::vec4& nearClipPlane, glm::vec4& farClipPlane) const; + + VoxelShader& getVoxelShader() { return _voxelShader; } PointShader& getPointShader() { return _pointShader; } FileLogger* getLogger() { return _logger; } @@ -215,6 +223,9 @@ public: signals: + /// Fired when we're simulating; allows external parties to hook in. + void simulating(float deltaTime); + /// Fired when we're rendering in-world interface elements; allows external parties to hook in. void renderingInWorldInterface(); @@ -296,7 +307,7 @@ private: void updateMouseRay(); void updateFaceshift(); void updateVisage(); - void updateMyAvatarLookAtPosition(glm::vec3& lookAtSpot); + void updateMyAvatarLookAtPosition(); void updateHoverVoxels(float deltaTime, float& distance, BoxFace& face); void updateMouseVoxels(float deltaTime, float& distance, BoxFace& face); void updateHandAndTouch(float deltaTime); @@ -328,7 +339,7 @@ private: void displayStats(); void checkStatsClick(); void toggleStatsExpanded(); - void renderAvatars(bool forceRenderHead, bool selfAvatarOnly = false); + void renderRearViewMirror(const QRect& region, bool billboard = false); void renderViewFrustum(ViewFrustum& viewFrustum); void checkBandwidthMeterClick(); diff --git a/interface/src/Camera.cpp b/interface/src/Camera.cpp index 8729ef58b6..be7e7bac50 100644 --- a/interface/src/Camera.cpp +++ b/interface/src/Camera.cpp @@ -123,6 +123,11 @@ void Camera::setModeShiftPeriod (float period) { const float MIN_PERIOD = 0.001f; const float MAX_PERIOD = 3.0f; _modeShiftPeriod = glm::clamp(period, MIN_PERIOD, MAX_PERIOD); + + // if a zero period was requested, we clearly want to snap immediately to the target + if (period == 0.0f) { + update(MIN_PERIOD); + } } void Camera::setMode(CameraMode m) { diff --git a/interface/src/Camera.h b/interface/src/Camera.h index 7b95ce97f1..b5bf6ea0b7 100644 --- a/interface/src/Camera.h +++ b/interface/src/Camera.h @@ -55,6 +55,7 @@ public: const glm::vec3& getPosition() const { return _position; } const glm::quat& getRotation() const { return _rotation; } CameraMode getMode() const { return _mode; } + float getModeShiftPeriod() const { return _modeShiftPeriod; } const glm::vec3& getTargetPosition() const { return _targetPosition; } const glm::quat& getTargetRotation() const { return _targetRotation; } float getFieldOfView() const { return _fieldOfView; } diff --git a/interface/src/DatagramProcessor.cpp b/interface/src/DatagramProcessor.cpp index 9a208b51d1..2de9b40a95 100644 --- a/interface/src/DatagramProcessor.cpp +++ b/interface/src/DatagramProcessor.cpp @@ -94,11 +94,12 @@ void DatagramProcessor::processDatagrams() { break; } case PacketTypeMetavoxelData: - application->_metavoxels.processData(incomingPacket, senderSockAddr); + nodeList->findNodeAndUpdateWithDataFromPacket(incomingPacket); break; case PacketTypeBulkAvatarData: case PacketTypeKillAvatar: - case PacketTypeAvatarIdentity: { + case PacketTypeAvatarIdentity: + case PacketTypeAvatarBillboard: { // update having heard from the avatar-mixer and record the bytes received SharedNodePointer avatarMixer = nodeList->sendingNodeForPacket(incomingPacket); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index e499675880..6b7d02af4a 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -1161,6 +1161,7 @@ void Menu::showMetavoxelEditor() { _MetavoxelEditor = new MetavoxelEditor(); } _MetavoxelEditor->raise(); + _MetavoxelEditor->activateWindow(); } void Menu::audioMuteToggled() { diff --git a/interface/src/MetavoxelSystem.cpp b/interface/src/MetavoxelSystem.cpp index 3ed62cdb14..59a714ece5 100644 --- a/interface/src/MetavoxelSystem.cpp +++ b/interface/src/MetavoxelSystem.cpp @@ -16,21 +16,18 @@ #include "Application.h" #include "MetavoxelSystem.h" +#include "renderer/Model.h" + +REGISTER_META_OBJECT(StaticModelRenderer) ProgramObject MetavoxelSystem::_program; int MetavoxelSystem::_pointScaleLocation; MetavoxelSystem::MetavoxelSystem() : - _pointVisitor(_points), + _simulateVisitor(_points), _buffer(QOpenGLBuffer::VertexBuffer) { } -MetavoxelSystem::~MetavoxelSystem() { - for (QHash::const_iterator it = _clients.begin(); it != _clients.end(); it++) { - delete it.value(); - } -} - void MetavoxelSystem::init() { if (!_program.isLinked()) { switchToResourcesParentIfRequired(); @@ -42,33 +39,39 @@ void MetavoxelSystem::init() { // let the script cache know to use our common access manager ScriptCache::getInstance()->setNetworkAccessManager(Application::getInstance()->getNetworkAccessManager()); } - - NodeList* nodeList = NodeList::getInstance(); - - connect(nodeList, SIGNAL(nodeAdded(SharedNodePointer)), SLOT(nodeAdded(SharedNodePointer))); - connect(nodeList, SIGNAL(nodeKilled(SharedNodePointer)), SLOT(nodeKilled(SharedNodePointer))); - _buffer.setUsagePattern(QOpenGLBuffer::DynamicDraw); _buffer.create(); + + connect(NodeList::getInstance(), SIGNAL(nodeAdded(SharedNodePointer)), SLOT(maybeAttachClient(const SharedNodePointer&))); } void MetavoxelSystem::applyEdit(const MetavoxelEditMessage& edit) { - foreach (MetavoxelClient* client, _clients) { - client->applyEdit(edit); + foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { + if (node->getType() == NodeType::MetavoxelServer) { + QMutexLocker locker(&node->getMutex()); + MetavoxelClient* client = static_cast(node->getLinkedData()); + if (client) { + client->applyEdit(edit); + } + } } } -void MetavoxelSystem::processData(const QByteArray& data, const HifiSockAddr& sender) { - QMetaObject::invokeMethod(this, "receivedData", Q_ARG(const QByteArray&, data), Q_ARG(const HifiSockAddr&, sender)); -} - void MetavoxelSystem::simulate(float deltaTime) { // simulate the clients _points.clear(); - foreach (MetavoxelClient* client, _clients) { - client->simulate(deltaTime, _pointVisitor); + _simulateVisitor.setDeltaTime(deltaTime); + foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { + if (node->getType() == NodeType::MetavoxelServer) { + QMutexLocker locker(&node->getMutex()); + MetavoxelClient* client = static_cast(node->getLinkedData()); + if (client) { + client->simulate(deltaTime); + client->getData().guide(_simulateVisitor); + } + } } - + _buffer.bind(); int bytes = _points.size() * sizeof(Point); if (_buffer.size() < bytes) { @@ -118,53 +121,39 @@ void MetavoxelSystem::render() { _buffer.release(); _program.release(); + + foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { + if (node->getType() == NodeType::MetavoxelServer) { + QMutexLocker locker(&node->getMutex()); + MetavoxelClient* client = static_cast(node->getLinkedData()); + if (client) { + client->getData().guide(_renderVisitor); + } + } + } } -void MetavoxelSystem::nodeAdded(SharedNodePointer node) { +void MetavoxelSystem::maybeAttachClient(const SharedNodePointer& node) { if (node->getType() == NodeType::MetavoxelServer) { - QMetaObject::invokeMethod(this, "addClient", Q_ARG(const SharedNodePointer&, node)); + QMutexLocker locker(&node->getMutex()); + node->setLinkedData(new MetavoxelClient(NodeList::getInstance()->nodeWithUUID(node->getUUID()))); } } -void MetavoxelSystem::nodeKilled(SharedNodePointer node) { - if (node->getType() == NodeType::MetavoxelServer) { - QMetaObject::invokeMethod(this, "removeClient", Q_ARG(const QUuid&, node->getUUID())); - } -} - -void MetavoxelSystem::addClient(const SharedNodePointer& node) { - MetavoxelClient* client = new MetavoxelClient(node); - _clients.insert(node->getUUID(), client); - _clientsBySessionID.insert(client->getSessionID(), client); -} - -void MetavoxelSystem::removeClient(const QUuid& uuid) { - MetavoxelClient* client = _clients.take(uuid); - _clientsBySessionID.remove(client->getSessionID()); - delete client; -} - -void MetavoxelSystem::receivedData(const QByteArray& data, const SharedNodePointer& sendingNode) { - int headerPlusIDSize; - QUuid sessionID = readSessionID(data, sendingNode, headerPlusIDSize); - if (sessionID.isNull()) { - return; - } - MetavoxelClient* client = _clientsBySessionID.value(sessionID); - if (client) { - client->receivedData(data); - } -} - -MetavoxelSystem::PointVisitor::PointVisitor(QVector& points) : - MetavoxelVisitor(QVector() << - AttributeRegistry::getInstance()->getColorAttribute() << - AttributeRegistry::getInstance()->getNormalAttribute(), - QVector()), +MetavoxelSystem::SimulateVisitor::SimulateVisitor(QVector& points) : + SpannerVisitor(QVector() << AttributeRegistry::getInstance()->getSpannersAttribute(), + QVector() << AttributeRegistry::getInstance()->getColorAttribute() << + AttributeRegistry::getInstance()->getNormalAttribute()), _points(points) { } -bool MetavoxelSystem::PointVisitor::visit(MetavoxelInfo& info) { +void MetavoxelSystem::SimulateVisitor::visit(Spanner* spanner) { + spanner->getRenderer()->simulate(_deltaTime); +} + +bool MetavoxelSystem::SimulateVisitor::visit(MetavoxelInfo& info) { + SpannerVisitor::visit(info); + if (!info.isLeaf) { return true; } @@ -179,16 +168,17 @@ bool MetavoxelSystem::PointVisitor::visit(MetavoxelInfo& info) { return false; } -static QByteArray createDatagramHeader(const QUuid& sessionID) { - QByteArray header = byteArrayWithPopulatedHeader(PacketTypeMetavoxelData); - header += sessionID.toRfc4122(); - return header; +MetavoxelSystem::RenderVisitor::RenderVisitor() : + SpannerVisitor(QVector() << AttributeRegistry::getInstance()->getSpannersAttribute()) { +} + +void MetavoxelSystem::RenderVisitor::visit(Spanner* spanner) { + spanner->getRenderer()->render(1.0f); } MetavoxelClient::MetavoxelClient(const SharedNodePointer& node) : _node(node), - _sessionID(QUuid::createUuid()), - _sequencer(createDatagramHeader(_sessionID)) { + _sequencer(byteArrayWithPopulatedHeader(PacketTypeMetavoxelData)) { connect(&_sequencer, SIGNAL(readyToWrite(const QByteArray&)), SLOT(sendData(const QByteArray&))); connect(&_sequencer, SIGNAL(readyToRead(Bitstream&)), SLOT(readPacket(Bitstream&))); @@ -214,22 +204,20 @@ void MetavoxelClient::applyEdit(const MetavoxelEditMessage& edit) { _sequencer.sendHighPriorityMessage(QVariant::fromValue(edit)); } -void MetavoxelClient::simulate(float deltaTime, MetavoxelVisitor& visitor) { +void MetavoxelClient::simulate(float deltaTime) { Bitstream& out = _sequencer.startPacket(); ClientStateMessage state = { Application::getInstance()->getCamera()->getPosition() }; out << QVariant::fromValue(state); _sequencer.endPacket(); - - _data.guide(visitor); } -void MetavoxelClient::receivedData(const QByteArray& data) { +int MetavoxelClient::parseData(const QByteArray& packet) { // process through sequencer - _sequencer.receivedDatagram(data); + QMetaObject::invokeMethod(&_sequencer, "receivedDatagram", Q_ARG(const QByteArray&, packet)); + return packet.size(); } void MetavoxelClient::sendData(const QByteArray& data) { - QMutexLocker locker(&_node->getMutex()); NodeList::getInstance()->writeDatagram(data, _node); } @@ -265,3 +253,47 @@ void MetavoxelClient::handleMessage(const QVariant& message, Bitstream& in) { } } } + +StaticModelRenderer::StaticModelRenderer() : + _model(new Model(this)) { +} + +void StaticModelRenderer::init(Spanner* spanner) { + _model->init(); + + StaticModel* staticModel = static_cast(spanner); + applyTranslation(staticModel->getTranslation()); + applyRotation(staticModel->getRotation()); + applyScale(staticModel->getScale()); + applyURL(staticModel->getURL()); + + connect(spanner, SIGNAL(translationChanged(const glm::vec3&)), SLOT(applyTranslation(const glm::vec3&))); + connect(spanner, SIGNAL(rotationChanged(const glm::vec3&)), SLOT(applyRotation(const glm::vec3&))); + connect(spanner, SIGNAL(scaleChanged(float)), SLOT(applyScale(float))); + connect(spanner, SIGNAL(urlChanged(const QUrl&)), SLOT(applyURL(const QUrl&))); +} + +void StaticModelRenderer::simulate(float deltaTime) { + _model->simulate(deltaTime); +} + +void StaticModelRenderer::render(float alpha) { + _model->render(alpha); +} + +void StaticModelRenderer::applyTranslation(const glm::vec3& translation) { + _model->setTranslation(translation); +} + +void StaticModelRenderer::applyRotation(const glm::vec3& rotation) { + _model->setRotation(glm::quat(glm::radians(rotation))); +} + +void StaticModelRenderer::applyScale(float scale) { + const float SCALE_MULTIPLIER = 0.0006f; + _model->setScale(glm::vec3(scale, scale, scale) * SCALE_MULTIPLIER); +} + +void StaticModelRenderer::applyURL(const QUrl& url) { + _model->setURL(url); +} diff --git a/interface/src/MetavoxelSystem.h b/interface/src/MetavoxelSystem.h index 7bb7935e4d..4364192009 100644 --- a/interface/src/MetavoxelSystem.h +++ b/interface/src/MetavoxelSystem.h @@ -23,7 +23,7 @@ #include "renderer/ProgramObject.h" -class MetavoxelClient; +class Model; /// Renders a metavoxel tree. class MetavoxelSystem : public QObject { @@ -32,28 +32,20 @@ class MetavoxelSystem : public QObject { public: MetavoxelSystem(); - ~MetavoxelSystem(); void init(); void applyEdit(const MetavoxelEditMessage& edit); - void processData(const QByteArray& data, const HifiSockAddr& sender); - void simulate(float deltaTime); void render(); -public slots: +private slots: + + void maybeAttachClient(const SharedNodePointer& node); - void nodeAdded(SharedNodePointer node); - void nodeKilled(SharedNodePointer node); - private: - - Q_INVOKABLE void addClient(const SharedNodePointer& node); - Q_INVOKABLE void removeClient(const QUuid& uuid); - Q_INVOKABLE void receivedData(const QByteArray& data, const SharedNodePointer& sendingNode); - + class Point { public: glm::vec4 vertex; @@ -61,28 +53,35 @@ private: quint8 normal[3]; }; - class PointVisitor : public MetavoxelVisitor { + class SimulateVisitor : public SpannerVisitor { public: - PointVisitor(QVector& points); + SimulateVisitor(QVector& points); + void setDeltaTime(float deltaTime) { _deltaTime = deltaTime; } + virtual void visit(Spanner* spanner); virtual bool visit(MetavoxelInfo& info); private: QVector& _points; + float _deltaTime; + }; + + class RenderVisitor : public SpannerVisitor { + public: + RenderVisitor(); + virtual void visit(Spanner* spanner); }; static ProgramObject _program; static int _pointScaleLocation; QVector _points; - PointVisitor _pointVisitor; + SimulateVisitor _simulateVisitor; + RenderVisitor _renderVisitor; QOpenGLBuffer _buffer; - - QHash _clients; - QHash _clientsBySessionID; }; /// A client session associated with a single server. -class MetavoxelClient : public QObject { +class MetavoxelClient : public NodeData { Q_OBJECT public: @@ -90,13 +89,13 @@ public: MetavoxelClient(const SharedNodePointer& node); virtual ~MetavoxelClient(); - const QUuid& getSessionID() const { return _sessionID; } + MetavoxelData& getData() { return _data; } void applyEdit(const MetavoxelEditMessage& edit); - void simulate(float deltaTime, MetavoxelVisitor& visitor); + void simulate(float deltaTime); - void receivedData(const QByteArray& data); + virtual int parseData(const QByteArray& packet); private slots: @@ -117,7 +116,6 @@ private: }; SharedNodePointer _node; - QUuid _sessionID; DatagramSequencer _sequencer; @@ -126,4 +124,28 @@ private: QList _receiveRecords; }; +/// Renders static models. +class StaticModelRenderer : public SpannerRenderer { + Q_OBJECT + +public: + + Q_INVOKABLE StaticModelRenderer(); + + virtual void init(Spanner* spanner); + virtual void simulate(float deltaTime); + virtual void render(float alpha); + +private slots: + + void applyTranslation(const glm::vec3& translation); + void applyRotation(const glm::vec3& rotation); + void applyScale(float scale); + void applyURL(const QUrl& url); + +private: + + Model* _model; +}; + #endif /* defined(__interface__MetavoxelSystem__) */ diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 1548f83d33..a946a19bd9 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -25,6 +25,7 @@ #include "Physics.h" #include "world.h" #include "devices/OculusManager.h" +#include "renderer/TextureCache.h" #include "ui/TextRenderer.h" using namespace std; @@ -106,6 +107,10 @@ glm::quat Avatar::getWorldAlignedOrientation () const { return computeRotationFromBodyToWorldUp() * getOrientation(); } +float Avatar::getLODDistance() const { + return glm::distance(Application::getInstance()->getCamera()->getPosition(), _position) / _scale; +} + void Avatar::simulate(float deltaTime) { if (_scale != _targetScale) { setScale(_targetScale); @@ -115,9 +120,9 @@ void Avatar::simulate(float deltaTime) { glm::vec3 oldVelocity = getVelocity(); getHand()->simulate(deltaTime, false); + _skeletonModel.setLODDistance(getLODDistance()); _skeletonModel.simulate(deltaTime); Head* head = getHead(); - head->setBodyRotation(glm::vec3(_bodyPitch, _bodyYaw, _bodyRoll)); glm::vec3 headPosition; if (!_skeletonModel.getHeadPosition(headPosition)) { headPosition = _position; @@ -181,7 +186,7 @@ static TextRenderer* textRenderer(TextRendererType type) { return displayNameRenderer; } -void Avatar::render(bool forceRenderHead) { +void Avatar::render() { glm::vec3 toTarget = _position - Application::getInstance()->getAvatar()->getPosition(); float lengthToTarget = glm::length(toTarget); @@ -199,7 +204,7 @@ void Avatar::render(bool forceRenderHead) { getHead()->getFaceModel().renderCollisionProxies(0.7f); } if (Menu::getInstance()->isOptionChecked(MenuOption::Avatars)) { - renderBody(forceRenderHead); + renderBody(); } // render sphere when far away @@ -280,17 +285,73 @@ glm::quat Avatar::computeRotationFromBodyToWorldUp(float proportion) const { return glm::angleAxis(angle * proportion, axis); } -void Avatar::renderBody(bool forceRenderHead) { - // Render the body's voxels and head - glm::vec3 pos = getPosition(); - //printf("Render other at %.3f, %.2f, %.2f\n", pos.x, pos.y, pos.z); - _skeletonModel.render(1.0f); - if (forceRenderHead) { - getHead()->render(1.0f); +void Avatar::renderBody() { + const float BILLBOARD_DISTANCE = 40.0f; + if (!_billboard.isEmpty() && getLODDistance() >= BILLBOARD_DISTANCE) { + renderBillboard(); + return; } + _skeletonModel.render(1.0f); + getHead()->render(1.0f); getHand()->render(false); } +void Avatar::renderBillboard() { + if (!_billboardTexture) { + QImage image = QImage::fromData(_billboard).convertToFormat(QImage::Format_ARGB32); + + _billboardTexture.reset(new Texture()); + glBindTexture(GL_TEXTURE_2D, _billboardTexture->getID()); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width(), image.height(), 1, + GL_BGRA, GL_UNSIGNED_BYTE, image.constBits()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + + } else { + glBindTexture(GL_TEXTURE_2D, _billboardTexture->getID()); + } + + glEnable(GL_ALPHA_TEST); + glAlphaFunc(GL_GREATER, 0.5f); + + glEnable(GL_TEXTURE_2D); + glDisable(GL_LIGHTING); + + glPushMatrix(); + glTranslatef(_position.x, _position.y, _position.z); + + // rotate about vertical to face the camera + glm::quat rotation = getOrientation(); + glm::vec3 cameraVector = glm::inverse(rotation) * (Application::getInstance()->getCamera()->getPosition() - _position); + rotation = rotation * glm::angleAxis(glm::degrees(atan2f(-cameraVector.x, -cameraVector.z)), 0.0f, 1.0f, 0.0f); + glm::vec3 axis = glm::axis(rotation); + glRotatef(glm::angle(rotation), axis.x, axis.y, axis.z); + + // compute the size from the billboard camera parameters and scale + float size = _scale * BILLBOARD_DISTANCE * tanf(glm::radians(BILLBOARD_FIELD_OF_VIEW / 2.0f)); + glScalef(size, size, size); + + glColor3f(1.0f, 1.0f, 1.0f); + + glBegin(GL_QUADS); + glTexCoord2f(0.0f, 0.0f); + glVertex2f(-1.0f, -1.0f); + glTexCoord2f(1.0f, 0.0f); + glVertex2f(1.0f, -1.0f); + glTexCoord2f(1.0f, 1.0f); + glVertex2f(1.0f, 1.0f); + glTexCoord2f(0.0f, 1.0f); + glVertex2f(-1.0f, 1.0f); + glEnd(); + + glPopMatrix(); + + glDisable(GL_TEXTURE_2D); + glEnable(GL_LIGHTING); + glDisable(GL_ALPHA_TEST); + + glBindTexture(GL_TEXTURE_2D, 0); +} + void Avatar::renderDisplayName() { if (_displayName.isEmpty() || _displayNameAlpha == 0.0f) { @@ -501,6 +562,13 @@ void Avatar::setDisplayName(const QString& displayName) { _displayNameBoundingRect = textRenderer(DISPLAYNAME)->metrics().tightBoundingRect(displayName); } +void Avatar::setBillboard(const QByteArray& billboard) { + AvatarData::setBillboard(billboard); + + // clear out any existing billboard texture + _billboardTexture.reset(); +} + int Avatar::parseData(const QByteArray& packet) { // change in position implies movement glm::vec3 oldPosition = _position; diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 6fa0f203e9..be98254696 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -62,6 +63,8 @@ enum ScreenTintLayer { // Grayson as he's building a street around here for demo dinner 2 const glm::vec3 START_LOCATION(0.485f * TREE_SCALE, 0.f, 0.5f * TREE_SCALE); +class Texture; + class Avatar : public AvatarData { Q_OBJECT @@ -71,7 +74,7 @@ public: void init(); void simulate(float deltaTime); - void render(bool forceRenderHead); + void render(); //setters void setDisplayingLookatVectors(bool displayingLookatVectors) { getHead()->setRenderLookatVectors(displayingLookatVectors); } @@ -87,6 +90,9 @@ public: Head* getHead() { return static_cast(_headData); } Hand* getHand() { return static_cast(_handData); } glm::quat getWorldAlignedOrientation() const; + + /// Returns the distance to use as a LOD parameter. + float getLODDistance() const; Node* getOwningAvatarMixer() { return _owningAvatarMixer.data(); } void setOwningAvatarMixer(const QWeakPointer& owningAvatarMixer) { _owningAvatarMixer = owningAvatarMixer; } @@ -114,6 +120,7 @@ public: virtual void setFaceModelURL(const QUrl& faceModelURL); virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); virtual void setDisplayName(const QString& displayName); + virtual void setBillboard(const QByteArray& billboard); void setShowDisplayName(bool showDisplayName); @@ -167,8 +174,10 @@ protected: private: bool _initialized; + QScopedPointer _billboardTexture; - void renderBody(bool forceRenderHead); + void renderBody(); + void renderBillboard(); void renderDisplayName(); }; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index b873b10017..fed6716ffa 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -69,7 +69,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { simulateAvatarFades(deltaTime); } -void AvatarManager::renderAvatars(bool forceRenderHead, bool selfAvatarOnly) { +void AvatarManager::renderAvatars(bool forceRenderMyHead, bool selfAvatarOnly) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::renderAvatars()"); bool renderLookAtVectors = Menu::getInstance()->isOptionChecked(MenuOption::LookAtVectors); @@ -83,16 +83,16 @@ void AvatarManager::renderAvatars(bool forceRenderHead, bool selfAvatarOnly) { avatar->init(); } if (avatar == static_cast(_myAvatar.data())) { - avatar->render(forceRenderHead); + _myAvatar->render(forceRenderMyHead); } else { - avatar->render(true); + avatar->render(); } avatar->setDisplayingLookatVectors(renderLookAtVectors); } renderAvatarFades(); } else { // just render myAvatar - _myAvatar->render(forceRenderHead); + _myAvatar->render(forceRenderMyHead); _myAvatar->setDisplayingLookatVectors(renderLookAtVectors); } } @@ -121,7 +121,7 @@ void AvatarManager::renderAvatarFades() { foreach(const AvatarSharedPointer& fadingAvatar, _avatarFades) { Avatar* avatar = static_cast(fadingAvatar.data()); - avatar->render(false); + avatar->render(); } } @@ -133,6 +133,9 @@ void AvatarManager::processAvatarMixerDatagram(const QByteArray& datagram, const case PacketTypeAvatarIdentity: processAvatarIdentityPacket(datagram); break; + case PacketTypeAvatarBillboard: + processAvatarBillboardPacket(datagram); + break; case PacketTypeKillAvatar: processKillAvatar(datagram); break; @@ -212,6 +215,20 @@ void AvatarManager::processAvatarIdentityPacket(const QByteArray &packet) { } } +void AvatarManager::processAvatarBillboardPacket(const QByteArray& packet) { + int headerSize = numBytesForPacketHeader(packet); + QUuid nodeUUID = QUuid::fromRfc4122(QByteArray::fromRawData(packet.constData() + headerSize, NUM_BYTES_RFC4122_UUID)); + + AvatarSharedPointer matchingAvatar = _avatarHash.value(nodeUUID); + if (matchingAvatar) { + Avatar* avatar = static_cast(matchingAvatar.data()); + QByteArray billboard = packet.mid(headerSize + NUM_BYTES_RFC4122_UUID); + if (avatar->getBillboard() != billboard) { + avatar->setBillboard(billboard); + } + } +} + void AvatarManager::processKillAvatar(const QByteArray& datagram) { // read the node id QUuid nodeUUID = QUuid::fromRfc4122(datagram.mid(numBytesForPacketHeader(datagram), NUM_BYTES_RFC4122_UUID)); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 719c30a177..db24d5bf4e 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -29,7 +29,7 @@ public: MyAvatar* getMyAvatar() { return _myAvatar.data(); } void updateOtherAvatars(float deltaTime); - void renderAvatars(bool forceRenderHead, bool selfAvatarOnly = false); + void renderAvatars(bool forceRenderMyHead, bool selfAvatarOnly = false); void clearOtherAvatars(); @@ -41,6 +41,7 @@ private: void processAvatarDataPacket(const QByteArray& packet, const QWeakPointer& mixerWeakPointer); void processAvatarIdentityPacket(const QByteArray& packet); + void processAvatarBillboardPacket(const QByteArray& packet); void processKillAvatar(const QByteArray& datagram); void simulateAvatarFades(float deltaTime); diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index 5c6100764a..de3f3cd3d8 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -28,7 +28,6 @@ Head::Head(Avatar* owningAvatar) : _gravity(0.0f, -1.0f, 0.0f), _lastLoudness(0.0f), _audioAttack(0.0f), - _bodyRotation(0.0f, 0.0f, 0.0f), _angularVelocity(0,0,0), _renderLookatVectors(false), _saccade(0.0f, 0.0f, 0.0f), @@ -158,6 +157,9 @@ void Head::simulate(float deltaTime, bool isMine) { glm::clamp(sqrt(_averageLoudness * JAW_OPEN_SCALE) - JAW_OPEN_DEAD_ZONE, 0.0f, 1.0f), _blendshapeCoefficients); } + if (!isMine) { + _faceModel.setLODDistance(static_cast(_owningAvatar)->getLODDistance()); + } _faceModel.simulate(deltaTime); // the blend face may have custom eye meshes @@ -180,12 +182,8 @@ void Head::setScale (float scale) { _scale = scale; } -glm::quat Head::getOrientation() const { - return glm::quat(glm::radians(_bodyRotation)) * glm::quat(glm::radians(glm::vec3(_pitch, _yaw, _roll))); -} - glm::quat Head::getTweakedOrientation() const { - return glm::quat(glm::radians(_bodyRotation)) * glm::quat(glm::radians(glm::vec3(getTweakedPitch(), getTweakedYaw(), getTweakedRoll() ))); + return _owningAvatar->getOrientation() * glm::quat(glm::radians(glm::vec3(getTweakedPitch(), getTweakedYaw(), getTweakedRoll() ))); } glm::quat Head::getCameraOrientation () const { diff --git a/interface/src/avatar/Head.h b/interface/src/avatar/Head.h index c88e654d95..39a2f4eeb6 100644 --- a/interface/src/avatar/Head.h +++ b/interface/src/avatar/Head.h @@ -40,13 +40,11 @@ public: void render(float alpha); void setScale(float scale); void setPosition(glm::vec3 position) { _position = position; } - void setBodyRotation(glm::vec3 bodyRotation) { _bodyRotation = bodyRotation; } void setGravity(glm::vec3 gravity) { _gravity = gravity; } void setAverageLoudness(float averageLoudness) { _averageLoudness = averageLoudness; } void setReturnToCenter (bool returnHeadToCenter) { _returnHeadToCenter = returnHeadToCenter; } void setRenderLookatVectors(bool onOff) { _renderLookatVectors = onOff; } - glm::quat getOrientation() const; glm::quat getTweakedOrientation() const; glm::quat getCameraOrientation () const; const glm::vec3& getAngularVelocity() const { return _angularVelocity; } @@ -97,7 +95,6 @@ private: glm::vec3 _gravity; float _lastLoudness; float _audioAttack; - glm::vec3 _bodyRotation; glm::vec3 _angularVelocity; bool _renderLookatVectors; glm::vec3 _saccade; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index cd3594da09..55f18e23e3 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -9,6 +9,8 @@ #include #include +#include + #include #include @@ -61,7 +63,8 @@ MyAvatar::MyAvatar() : _thrustMultiplier(1.0f), _moveTarget(0,0,0), _moveTargetStepCounter(0), - _lookAtTargetAvatar() + _lookAtTargetAvatar(), + _billboardValid(false) { for (int i = 0; i < MAX_DRIVE_KEYS; i++) { _driveKeys[i] = 0.0f; @@ -330,7 +333,6 @@ void MyAvatar::simulate(float deltaTime) { _skeletonModel.simulate(deltaTime); Head* head = getHead(); - head->setBodyRotation(glm::vec3(_bodyPitch, _bodyYaw, _bodyRoll)); glm::vec3 headPosition; if (!_skeletonModel.getHeadPosition(headPosition)) { headPosition = _position; @@ -341,7 +343,9 @@ void MyAvatar::simulate(float deltaTime) { // Zero thrust out now that we've added it to velocity in this frame _thrust = glm::vec3(0, 0, 0); - + + // consider updating our billboard + maybeUpdateBillboard(); } const float MAX_PITCH = 90.0f; @@ -674,7 +678,7 @@ void MyAvatar::orbit(const glm::vec3& position, int deltaX, int deltaY) { setPosition(position + rotation * (getPosition() - position)); } -void MyAvatar::updateLookAtTargetAvatar(glm::vec3 &eyePosition) { +void MyAvatar::updateLookAtTargetAvatar() { Application* applicationInstance = Application::getInstance(); if (!applicationInstance->isMousePressed()) { @@ -688,14 +692,9 @@ void MyAvatar::updateLookAtTargetAvatar(glm::vec3 &eyePosition) { } float distance; if (avatar->findRayIntersection(mouseOrigin, mouseDirection, distance)) { - // rescale to compensate for head embiggening - eyePosition = (avatar->getHead()->calculateAverageEyePosition() - avatar->getHead()->getScalePivot()) * - (avatar->getScale() / avatar->getHead()->getScale()) + avatar->getHead()->getScalePivot(); _lookAtTargetAvatar = avatarPointer; return; - } else { } - } _lookAtTargetAvatar.clear(); } @@ -714,14 +713,25 @@ glm::vec3 MyAvatar::getUprightHeadPosition() const { return _position + getWorldAlignedOrientation() * glm::vec3(0.0f, getPelvisToHeadLength(), 0.0f); } +void MyAvatar::setFaceModelURL(const QUrl& faceModelURL) { + Avatar::setFaceModelURL(faceModelURL); + _billboardValid = false; +} + +void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { + Avatar::setSkeletonModelURL(skeletonModelURL); + _billboardValid = false; +} + void MyAvatar::renderBody(bool forceRenderHead) { // Render the body's voxels and head _skeletonModel.render(1.0f); // Render head so long as the camera isn't inside it - const float RENDER_HEAD_CUTOFF_DISTANCE = 0.10f; + const float RENDER_HEAD_CUTOFF_DISTANCE = 0.40f; Camera* myCamera = Application::getInstance()->getCamera(); - if (forceRenderHead || (glm::length(myCamera->getPosition() - getHead()->calculateAverageEyePosition()) > RENDER_HEAD_CUTOFF_DISTANCE)) { + if (forceRenderHead || (glm::length(myCamera->getPosition() - getHead()->calculateAverageEyePosition()) > + RENDER_HEAD_CUTOFF_DISTANCE * _scale)) { getHead()->render(1.0f); } getHand()->render(true); @@ -1133,6 +1143,20 @@ void MyAvatar::updateChatCircle(float deltaTime) { _position = glm::mix(_position, targetPosition, APPROACH_RATE); } +void MyAvatar::maybeUpdateBillboard() { + if (_billboardValid || !(_skeletonModel.isLoadedWithTextures() && getHead()->getFaceModel().isLoadedWithTextures())) { + return; + } + QImage image = Application::getInstance()->renderAvatarBillboard(); + _billboard.clear(); + QBuffer buffer(&_billboard); + buffer.open(QIODevice::WriteOnly); + image.save(&buffer, "PNG"); + _billboardValid = true; + + sendBillboardPacket(); +} + void MyAvatar::setGravity(glm::vec3 gravity) { _gravity = gravity; getHead()->setGravity(_gravity); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 672843ad94..10156cf130 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -80,9 +80,11 @@ public: void orbit(const glm::vec3& position, int deltaX, int deltaY); AvatarData* getLookAtTargetAvatar() const { return _lookAtTargetAvatar.data(); } - void updateLookAtTargetAvatar(glm::vec3& eyePosition); + void updateLookAtTargetAvatar(); void clearLookAtTargetAvatar(); + virtual void setFaceModelURL(const QUrl& faceModelURL); + virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); public slots: void goHome(); void increaseSize(); @@ -120,6 +122,8 @@ private: glm::vec3 _transmitterPickStart; glm::vec3 _transmitterPickEnd; + bool _billboardValid; + // private methods void renderBody(bool forceRenderHead); void updateThrust(float deltaTime); @@ -130,6 +134,7 @@ private: void applyHardCollision(const glm::vec3& penetration, float elasticity, float damping); void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency); void updateChatCircle(float deltaTime); + void maybeUpdateBillboard(); }; #endif diff --git a/interface/src/renderer/Model.cpp b/interface/src/renderer/Model.cpp index f1916db4d1..e1780ee9f5 100644 --- a/interface/src/renderer/Model.cpp +++ b/interface/src/renderer/Model.cpp @@ -17,8 +17,8 @@ using namespace std; Model::Model(QObject* parent) : QObject(parent), - _pupilDilation(0.0f) -{ + _lodDistance(0.0f), + _pupilDilation(0.0f) { // we may have been created in the network thread, but we live in the main thread moveToThread(Application::getInstance()->thread()); } @@ -46,6 +46,21 @@ void Model::initSkinProgram(ProgramObject& program, Model::SkinLocations& locati program.release(); } +bool Model::isLoadedWithTextures() const { + if (!isActive()) { + return false; + } + foreach (const NetworkMesh& mesh, _geometry->getMeshes()) { + foreach (const NetworkMeshPart& part, mesh.parts) { + if ((part.diffuseTexture && !part.diffuseTexture->isLoaded()) || + (part.normalTexture && !part.normalTexture->isLoaded())) { + return false; + } + } + } + return true; +} + void Model::init() { if (!_program.isLinked()) { switchToResourcesParentIfRequired(); @@ -92,8 +107,7 @@ void Model::reset() { void Model::simulate(float deltaTime) { // update our LOD if (_geometry) { - QSharedPointer geometry = _geometry->getLODOrFallback(glm::distance(_translation, - Application::getInstance()->getCamera()->getPosition()), _lodHysteresis); + QSharedPointer geometry = _geometry->getLODOrFallback(_lodDistance, _lodHysteresis); if (_geometry != geometry) { deleteGeometry(); _dilatedTextures.clear(); @@ -276,7 +290,7 @@ bool Model::render(float alpha) { // render opaque meshes with alpha testing glEnable(GL_ALPHA_TEST); - glAlphaFunc(GL_GREATER, 0.5f); + glAlphaFunc(GL_GREATER, 0.5f * alpha); renderMeshes(alpha, false); @@ -917,7 +931,7 @@ void Model::renderMeshes(float alpha, bool translucent) { if (!mesh.colors.isEmpty()) { glEnableClientState(GL_COLOR_ARRAY); } else { - glColor3f(1.0f, 1.0f, 1.0f); + glColor4f(1.0f, 1.0f, 1.0f, alpha); } if (!mesh.texCoords.isEmpty()) { glEnableClientState(GL_TEXTURE_COORD_ARRAY); diff --git a/interface/src/renderer/Model.h b/interface/src/renderer/Model.h index 28189d0379..cf6e1fea6d 100644 --- a/interface/src/renderer/Model.h +++ b/interface/src/renderer/Model.h @@ -46,6 +46,8 @@ public: bool isActive() const { return _geometry && _geometry->isLoaded(); } + bool isLoadedWithTextures() const; + void init(); void reset(); void simulate(float deltaTime); @@ -54,6 +56,9 @@ public: Q_INVOKABLE void setURL(const QUrl& url, const QUrl& fallback = QUrl()); const QUrl& getURL() const { return _url; } + /// Sets the distance parameter used for LOD computations. + void setLODDistance(float distance) { _lodDistance = distance; } + /// Returns the extents of the model in its bind pose. Extents getBindExtents() const; @@ -228,13 +233,14 @@ private: void renderMeshes(float alpha, bool translucent); QSharedPointer _baseGeometry; ///< reference required to prevent collection of base + float _lodDistance; float _lodHysteresis; float _pupilDilation; std::vector _blendshapeCoefficients; QUrl _url; - + QVector _blendedVertexBufferIDs; QVector > > _dilatedTextures; bool _resetStates; diff --git a/interface/src/renderer/TextureCache.cpp b/interface/src/renderer/TextureCache.cpp index dc6883a5d0..8bfef5a742 100644 --- a/interface/src/renderer/TextureCache.cpp +++ b/interface/src/renderer/TextureCache.cpp @@ -258,9 +258,11 @@ NetworkTexture::NetworkTexture(const QUrl& url, bool normalMap) : _reply(NULL), _attempts(0), _averageColor(1.0f, 1.0f, 1.0f, 1.0f), - _translucent(false) { + _translucent(false), + _loaded(false) { if (!url.isValid()) { + _loaded = true; return; } _request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); @@ -298,6 +300,7 @@ void NetworkTexture::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTo _reply->disconnect(this); _reply->deleteLater(); _reply = NULL; + _loaded = true; QImage image = QImage::fromData(entirety).convertToFormat(QImage::Format_ARGB32); @@ -345,6 +348,8 @@ void NetworkTexture::handleReplyError() { QTimer::singleShot(BASE_DELAY_MS * (int)pow(2.0, _attempts), this, SLOT(makeRequest())); debug << " -- retrying..."; + } else { + _loaded = true; } } diff --git a/interface/src/renderer/TextureCache.h b/interface/src/renderer/TextureCache.h index e560acf6f7..ca7bf67a32 100644 --- a/interface/src/renderer/TextureCache.h +++ b/interface/src/renderer/TextureCache.h @@ -118,6 +118,8 @@ public: NetworkTexture(const QUrl& url, bool normalMap); ~NetworkTexture(); + bool isLoaded() const { return _loaded; } + /// Returns the average color over the entire texture. const glm::vec4& getAverageColor() const { return _averageColor; } @@ -142,6 +144,7 @@ private: int _attempts; glm::vec4 _averageColor; bool _translucent; + bool _loaded; }; /// Caches derived, dilated textures. diff --git a/interface/src/ui/ClipboardOverlay.cpp b/interface/src/ui/ClipboardOverlay.cpp new file mode 100644 index 0000000000..7eee318ec9 --- /dev/null +++ b/interface/src/ui/ClipboardOverlay.cpp @@ -0,0 +1,50 @@ +// +// ClipboardOverlay.cpp +// hifi +// +// Created by Clément Brisset on 2/20/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include + +#include "ClipboardOverlay.h" +#include "../Application.h" + +static int lastVoxelCount = 0; + +ClipboardOverlay::ClipboardOverlay() { +} + +ClipboardOverlay::~ClipboardOverlay() { +} + +void ClipboardOverlay::render() { + if (!_visible) { + return; // do nothing if we're not visible + } + + VoxelSystem* voxelSystem = Application::getInstance()->getSharedVoxelSystem(); + VoxelTree* clipboard = Application::getInstance()->getClipboard(); + if (voxelSystem->getTree() != clipboard) { + voxelSystem->changeTree(clipboard); + } + + glPushMatrix(); + glTranslatef(_position.x, _position.y, _position.z); + glScalef(_size, _size, _size); + + // We only force the redraw when the clipboard content has changed + if (lastVoxelCount != clipboard->getOctreeElementsCount()) { + voxelSystem->forceRedrawEntireTree(); + lastVoxelCount = clipboard->getOctreeElementsCount(); + } + + voxelSystem->render(); + + glPopMatrix(); +} \ No newline at end of file diff --git a/interface/src/ui/ClipboardOverlay.h b/interface/src/ui/ClipboardOverlay.h new file mode 100644 index 0000000000..49daa73c20 --- /dev/null +++ b/interface/src/ui/ClipboardOverlay.h @@ -0,0 +1,25 @@ +// +// ClipboardOverlay.h +// hifi +// +// Created by Clément Brisset on 2/20/14. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__ClipboardOverlay__ +#define __interface__ClipboardOverlay__ + +#include "Volume3DOverlay.h" + +class ClipboardOverlay : public Volume3DOverlay { + Q_OBJECT + +public: + ClipboardOverlay(); + ~ClipboardOverlay(); + + virtual void render(); +}; + + +#endif /* defined(__interface__ClipboardOverlay__) */ diff --git a/interface/src/ui/MetavoxelEditor.cpp b/interface/src/ui/MetavoxelEditor.cpp index c97832d791..832c6b5d39 100644 --- a/interface/src/ui/MetavoxelEditor.cpp +++ b/interface/src/ui/MetavoxelEditor.cpp @@ -29,7 +29,7 @@ enum GridPlane { const glm::vec2 INVALID_VECTOR(FLT_MAX, FLT_MAX); MetavoxelEditor::MetavoxelEditor() : - QDialog(Application::getInstance()->getGLWidget()) { + QWidget(Application::getInstance()->getGLWidget(), Qt::Tool | Qt::WindowStaysOnTopHint) { setWindowTitle("Metavoxel Editor"); setAttribute(Qt::WA_DeleteOnClose); @@ -45,12 +45,19 @@ MetavoxelEditor::MetavoxelEditor() : attributeGroup->setLayout(attributeLayout); attributeLayout->addWidget(_attributes = new QListWidget()); - connect(_attributes, SIGNAL(itemSelectionChanged()), SLOT(updateValueEditor())); + connect(_attributes, SIGNAL(itemSelectionChanged()), SLOT(selectedAttributeChanged())); + + QHBoxLayout* attributeButtonLayout = new QHBoxLayout(); + attributeLayout->addLayout(attributeButtonLayout); QPushButton* newAttribute = new QPushButton("New..."); - attributeLayout->addWidget(newAttribute); + attributeButtonLayout->addWidget(newAttribute); connect(newAttribute, SIGNAL(clicked()), SLOT(createNewAttribute())); + attributeButtonLayout->addWidget(_deleteAttribute = new QPushButton("Delete")); + _deleteAttribute->setEnabled(false); + connect(_deleteAttribute, SIGNAL(clicked()), SLOT(deleteSelectedAttribute())); + QFormLayout* formLayout = new QFormLayout(); topLayout->addLayout(formLayout); @@ -74,6 +81,9 @@ MetavoxelEditor::MetavoxelEditor() : alignGridPosition(); centerGridPosition(); + formLayout->addRow("Tool:", _toolBox = new QComboBox()); + connect(_toolBox, SIGNAL(currentIndexChanged(int)), SLOT(updateTool())); + _value = new QGroupBox(); _value->setTitle("Value"); topLayout->addWidget(_value); @@ -82,16 +92,22 @@ MetavoxelEditor::MetavoxelEditor() : _value->setLayout(valueLayout); valueLayout->addWidget(_valueArea = new QScrollArea()); + _valueArea->setMinimumHeight(200); _valueArea->setWidgetResizable(true); + addTool(new BoxSetTool(this)); + addTool(new GlobalSetTool(this)); + addTool(new InsertSpannerTool(this)); + addTool(new RemoveSpannerTool(this)); + addTool(new ClearSpannersTool(this)); + updateAttributes(); + connect(Application::getInstance(), SIGNAL(simulating(float)), SLOT(simulate(float))); connect(Application::getInstance(), SIGNAL(renderingInWorldInterface()), SLOT(render())); Application::getInstance()->getGLWidget()->installEventFilter(this); - resetState(); - show(); if (_gridProgram.isLinked()) { @@ -102,60 +118,79 @@ MetavoxelEditor::MetavoxelEditor() : _gridProgram.link(); } -bool MetavoxelEditor::eventFilter(QObject* watched, QEvent* event) { - switch (_state) { - case HOVERING_STATE: - if (event->type() == QEvent::MouseButtonPress && _startPosition != INVALID_VECTOR) { - _state = DRAGGING_STATE; - return true; - } - break; - - case DRAGGING_STATE: - if (event->type() == QEvent::MouseButtonRelease) { - _state = RAISING_STATE; - return true; - } - break; - - case RAISING_STATE: - if (event->type() == QEvent::MouseButtonPress) { - if (_height != 0) { - // find the start and end corners in X/Y - float base = _gridPosition->value(); - float top = base + _height; - glm::quat rotation = getGridRotation(); - glm::vec3 start = rotation * glm::vec3(glm::min(_startPosition, _endPosition), glm::min(base, top)); - float spacing = getGridSpacing(); - glm::vec3 end = rotation * glm::vec3(glm::max(_startPosition, _endPosition) + - glm::vec2(spacing, spacing), glm::max(base, top)); - - // find the minimum and maximum extents after rotation - applyValue(glm::min(start, end), glm::max(start, end)); - } - resetState(); - return true; - } - break; - } - return false; +QString MetavoxelEditor::getSelectedAttribute() const { + QList selectedItems = _attributes->selectedItems(); + return selectedItems.isEmpty() ? QString() : selectedItems.first()->text(); } -void MetavoxelEditor::updateValueEditor() { +double MetavoxelEditor::getGridSpacing() const { + return pow(2.0, _gridSpacing->value()); +} + +double MetavoxelEditor::getGridPosition() const { + return _gridPosition->value(); +} + +glm::quat MetavoxelEditor::getGridRotation() const { + // for simplicity, we handle the other two planes by rotating them onto X/Y and performing computation there + switch (_gridPlane->currentIndex()) { + case GRID_PLANE_XY: + return glm::quat(); + + case GRID_PLANE_XZ: + return glm::angleAxis(-90.0f, 1.0f, 0.0f, 0.0f); + + case GRID_PLANE_YZ: + default: + return glm::angleAxis(90.0f, 0.0f, 1.0f, 0.0f); + } +} + +QVariant MetavoxelEditor::getValue() const { + QWidget* editor = _valueArea->widget(); + return editor ? editor->metaObject()->userProperty().read(editor) : QVariant(); +} + +void MetavoxelEditor::detachValue() { + SharedObjectEditor* editor = qobject_cast(_valueArea->widget()); + if (editor) { + editor->detachObject(); + } +} + +bool MetavoxelEditor::eventFilter(QObject* watched, QEvent* event) { + // pass along to the active tool + MetavoxelTool* tool = getActiveTool(); + return tool && tool->eventFilter(watched, event); +} + +void MetavoxelEditor::selectedAttributeChanged() { + _toolBox->clear(); + QString selected = getSelectedAttribute(); if (selected.isNull()) { + _deleteAttribute->setEnabled(false); + _toolBox->setEnabled(false); _value->setVisible(false); return; } + _deleteAttribute->setEnabled(true); + _toolBox->setEnabled(true); + + AttributePointer attribute = AttributeRegistry::getInstance()->getAttribute(selected); + foreach (MetavoxelTool* tool, _tools) { + if (tool->appliesTo(attribute)) { + _toolBox->addItem(tool->objectName(), QVariant::fromValue(tool)); + } + } _value->setVisible(true); if (_valueArea->widget()) { delete _valueArea->widget(); } - - AttributePointer attribute = AttributeRegistry::getInstance()->getAttribute(selected); QWidget* editor = attribute->createEditor(); if (editor) { + editor->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); _valueArea->setWidget(editor); } } @@ -173,6 +208,10 @@ void MetavoxelEditor::createNewAttribute() { QLineEdit name; form.addRow("Name:", &name); + SharedObjectEditor editor(&Attribute::staticMetaObject, false); + editor.setObject(new QRgbAttribute()); + layout.addWidget(&editor); + QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); dialog.connect(&buttons, SIGNAL(accepted()), SLOT(accept())); dialog.connect(&buttons, SIGNAL(rejected()), SLOT(reject())); @@ -183,11 +222,19 @@ void MetavoxelEditor::createNewAttribute() { return; } QString nameText = name.text().trimmed(); - AttributeRegistry::getInstance()->registerAttribute(new QRgbAttribute(nameText)); + SharedObjectPointer attribute = editor.getObject(); + attribute->setObjectName(nameText); + AttributeRegistry::getInstance()->registerAttribute(attribute.staticCast()); updateAttributes(nameText); } +void MetavoxelEditor::deleteSelectedAttribute() { + AttributeRegistry::getInstance()->deregisterAttribute(getSelectedAttribute()); + _attributes->selectionModel()->clear(); + updateAttributes(); +} + void MetavoxelEditor::centerGridPosition() { const float CENTER_OFFSET = 0.625f; float eyePosition = (glm::inverse(getGridRotation()) * Application::getInstance()->getCamera()->getPosition()).z - @@ -203,13 +250,24 @@ void MetavoxelEditor::alignGridPosition() { _gridPosition->setValue(step * floor(_gridPosition->value() / step)); } -void MetavoxelEditor::render() { - QString selected = getSelectedAttribute(); - if (selected.isNull()) { - resetState(); - return; +void MetavoxelEditor::updateTool() { + MetavoxelTool* active = getActiveTool(); + foreach (MetavoxelTool* tool, _tools) { + tool->setVisible(tool == active); } + _value->setVisible(active && active->getUsesValue()); +} +void MetavoxelEditor::simulate(float deltaTime) { + MetavoxelTool* tool = getActiveTool(); + if (tool) { + tool->simulate(deltaTime); + } +} + +const float GRID_BRIGHTNESS = 0.5f; + +void MetavoxelEditor::render() { glDisable(GL_LIGHTING); glDepthMask(GL_FALSE); @@ -219,11 +277,110 @@ void MetavoxelEditor::render() { glm::vec3 axis = glm::axis(rotation); glRotatef(glm::angle(rotation), axis.x, axis.y, axis.z); + MetavoxelTool* tool = getActiveTool(); + if (tool) { + tool->render(); + } + + glLineWidth(1.0f); + + // center the grid around the camera position on the plane + glm::vec3 rotated = glm::inverse(rotation) * Application::getInstance()->getCamera()->getPosition(); + float spacing = getGridSpacing(); + const int GRID_DIVISIONS = 300; + glTranslatef(spacing * (floorf(rotated.x / spacing) - GRID_DIVISIONS / 2), + spacing * (floorf(rotated.y / spacing) - GRID_DIVISIONS / 2), _gridPosition->value()); + + float scale = GRID_DIVISIONS * spacing; + glScalef(scale, scale, scale); + + glColor3f(GRID_BRIGHTNESS, GRID_BRIGHTNESS, GRID_BRIGHTNESS); + + _gridProgram.bind(); + + Application::getInstance()->getGeometryCache()->renderGrid(GRID_DIVISIONS, GRID_DIVISIONS); + + _gridProgram.release(); + + glPopMatrix(); + + glEnable(GL_LIGHTING); + glDepthMask(GL_TRUE); +} + +void MetavoxelEditor::addTool(MetavoxelTool* tool) { + _tools.append(tool); + layout()->addWidget(tool); +} + +void MetavoxelEditor::updateAttributes(const QString& select) { + // remember the selection in order to preserve it + QString selected = select.isNull() ? getSelectedAttribute() : select; + _attributes->clear(); + + // sort the names for consistent ordering + QList names = AttributeRegistry::getInstance()->getAttributes().keys(); + qSort(names); + + foreach (const QString& name, names) { + QListWidgetItem* item = new QListWidgetItem(name); + _attributes->addItem(item); + if (name == selected || selected.isNull()) { + item->setSelected(true); + selected = name; + } + } +} + +MetavoxelTool* MetavoxelEditor::getActiveTool() const { + int index = _toolBox->currentIndex(); + return (index == -1) ? NULL : static_cast(_toolBox->itemData(index).value()); +} + +ProgramObject MetavoxelEditor::_gridProgram; + +MetavoxelTool::MetavoxelTool(MetavoxelEditor* editor, const QString& name, bool usesValue) : + _editor(editor), + _usesValue(usesValue) { + + QVBoxLayout* layout = new QVBoxLayout(); + setLayout(layout); + + setObjectName(name); + setVisible(false); +} + +bool MetavoxelTool::appliesTo(const AttributePointer& attribute) const { + // shared object sets are a special case + return !attribute->inherits("SharedObjectSetAttribute"); +} + +void MetavoxelTool::simulate(float deltaTime) { + // nothing by default +} + +void MetavoxelTool::render() { + // nothing by default +} + +BoxSetTool::BoxSetTool(MetavoxelEditor* editor) : + MetavoxelTool(editor, "Set Value (Box)") { + + resetState(); +} + +void BoxSetTool::render() { + QString selected = _editor->getSelectedAttribute(); + if (selected.isNull()) { + resetState(); + return; + } + glm::quat rotation = _editor->getGridRotation(); glm::quat inverseRotation = glm::inverse(rotation); glm::vec3 rayOrigin = inverseRotation * Application::getInstance()->getMouseRayOrigin(); glm::vec3 rayDirection = inverseRotation * Application::getInstance()->getMouseRayDirection(); - float spacing = getGridSpacing(); - float position = _gridPosition->value(); + float spacing = _editor->getGridSpacing(); + float position = _editor->getGridPosition(); if (_state == RAISING_STATE) { // find the plane at the mouse position, orthogonal to the plane, facing the eye position glLineWidth(4.0f); @@ -256,7 +413,6 @@ void MetavoxelEditor::render() { resetState(); } - const float GRID_BRIGHTNESS = 0.5f; if (_startPosition != INVALID_VECTOR) { glm::vec2 minimum = glm::min(_startPosition, _endPosition); glm::vec2 maximum = glm::max(_startPosition, _endPosition); @@ -268,7 +424,7 @@ void MetavoxelEditor::render() { glTranslatef(0.5f, 0.5f, 0.5f); if (_state != HOVERING_STATE) { const float BOX_ALPHA = 0.25f; - QColor color = getValue().value(); + QColor color = _editor->getValue().value(); if (color.isValid()) { glColor4f(color.redF(), color.greenF(), color.blueF(), BOX_ALPHA); } else { @@ -281,97 +437,171 @@ void MetavoxelEditor::render() { glColor3f(GRID_BRIGHTNESS, GRID_BRIGHTNESS, GRID_BRIGHTNESS); glutWireCube(1.0); - glPopMatrix(); - - } else { - glColor3f(GRID_BRIGHTNESS, GRID_BRIGHTNESS, GRID_BRIGHTNESS); - } - - glLineWidth(1.0f); - - // center the grid around the camera position on the plane - glm::vec3 rotated = inverseRotation * Application::getInstance()->getCamera()->getPosition(); - const int GRID_DIVISIONS = 300; - glTranslatef(spacing * (floorf(rotated.x / spacing) - GRID_DIVISIONS / 2), - spacing * (floorf(rotated.y / spacing) - GRID_DIVISIONS / 2), position); - - float scale = GRID_DIVISIONS * spacing; - glScalef(scale, scale, scale); - - _gridProgram.bind(); - - Application::getInstance()->getGeometryCache()->renderGrid(GRID_DIVISIONS, GRID_DIVISIONS); - - _gridProgram.release(); - - glPopMatrix(); - - glEnable(GL_LIGHTING); - glDepthMask(GL_TRUE); -} - -void MetavoxelEditor::updateAttributes(const QString& select) { - // remember the selection in order to preserve it - QString selected = select.isNull() ? getSelectedAttribute() : select; - _attributes->clear(); - - // sort the names for consistent ordering - QList names = AttributeRegistry::getInstance()->getAttributes().keys(); - qSort(names); - - foreach (const QString& name, names) { - QListWidgetItem* item = new QListWidgetItem(name); - _attributes->addItem(item); - if (name == selected || selected.isNull()) { - item->setSelected(true); - selected = name; - } + glPopMatrix(); } } -QString MetavoxelEditor::getSelectedAttribute() const { - QList selectedItems = _attributes->selectedItems(); - return selectedItems.isEmpty() ? QString() : selectedItems.first()->text(); -} - -double MetavoxelEditor::getGridSpacing() const { - return pow(2.0, _gridSpacing->value()); -} - -glm::quat MetavoxelEditor::getGridRotation() const { - // for simplicity, we handle the other two planes by rotating them onto X/Y and performing computation there - switch (_gridPlane->currentIndex()) { - case GRID_PLANE_XY: - return glm::quat(); +bool BoxSetTool::eventFilter(QObject* watched, QEvent* event) { + switch (_state) { + case HOVERING_STATE: + if (event->type() == QEvent::MouseButtonPress && _startPosition != INVALID_VECTOR) { + _state = DRAGGING_STATE; + return true; + } + break; - case GRID_PLANE_XZ: - return glm::angleAxis(-90.0f, 1.0f, 0.0f, 0.0f); + case DRAGGING_STATE: + if (event->type() == QEvent::MouseButtonRelease) { + _state = RAISING_STATE; + return true; + } + break; - case GRID_PLANE_YZ: - default: - return glm::angleAxis(90.0f, 0.0f, 1.0f, 0.0f); + case RAISING_STATE: + if (event->type() == QEvent::MouseButtonPress) { + if (_height != 0) { + // find the start and end corners in X/Y + float base = _editor->getGridPosition(); + float top = base + _height; + glm::quat rotation = _editor->getGridRotation(); + glm::vec3 start = rotation * glm::vec3(glm::min(_startPosition, _endPosition), glm::min(base, top)); + float spacing = _editor->getGridSpacing(); + glm::vec3 end = rotation * glm::vec3(glm::max(_startPosition, _endPosition) + + glm::vec2(spacing, spacing), glm::max(base, top)); + + // find the minimum and maximum extents after rotation + applyValue(glm::min(start, end), glm::max(start, end)); + } + resetState(); + return true; + } + break; } + return false; } -void MetavoxelEditor::resetState() { +void BoxSetTool::resetState() { _state = HOVERING_STATE; _startPosition = INVALID_VECTOR; _height = 0.0f; } -void MetavoxelEditor::applyValue(const glm::vec3& minimum, const glm::vec3& maximum) { - AttributePointer attribute = AttributeRegistry::getInstance()->getAttribute(getSelectedAttribute()); +void BoxSetTool::applyValue(const glm::vec3& minimum, const glm::vec3& maximum) { + AttributePointer attribute = AttributeRegistry::getInstance()->getAttribute(_editor->getSelectedAttribute()); if (!attribute) { return; } - OwnedAttributeValue value(attribute, attribute->createFromVariant(getValue())); - MetavoxelEditMessage edit = { { minimum, maximum }, getGridSpacing(), value }; - Application::getInstance()->getMetavoxels()->applyEdit(edit); + OwnedAttributeValue value(attribute, attribute->createFromVariant(_editor->getValue())); + MetavoxelEditMessage message = { QVariant::fromValue(BoxSetEdit(Box(minimum, maximum), + _editor->getGridSpacing(), value)) }; + Application::getInstance()->getMetavoxels()->applyEdit(message); } -QVariant MetavoxelEditor::getValue() const { - QWidget* editor = _valueArea->widget(); - return editor ? editor->metaObject()->userProperty().read(editor) : QVariant(); +GlobalSetTool::GlobalSetTool(MetavoxelEditor* editor) : + MetavoxelTool(editor, "Set Value (Global)") { + + QPushButton* button = new QPushButton("Apply"); + layout()->addWidget(button); + connect(button, SIGNAL(clicked()), SLOT(apply())); } -ProgramObject MetavoxelEditor::_gridProgram; +void GlobalSetTool::apply() { + AttributePointer attribute = AttributeRegistry::getInstance()->getAttribute(_editor->getSelectedAttribute()); + if (!attribute) { + return; + } + OwnedAttributeValue value(attribute, attribute->createFromVariant(_editor->getValue())); + MetavoxelEditMessage message = { QVariant::fromValue(GlobalSetEdit(value)) }; + Application::getInstance()->getMetavoxels()->applyEdit(message); +} + +InsertSpannerTool::InsertSpannerTool(MetavoxelEditor* editor) : + MetavoxelTool(editor, "Insert Spanner") { + + QPushButton* button = new QPushButton("Insert"); + layout()->addWidget(button); + connect(button, SIGNAL(clicked()), SLOT(insert())); +} + +void InsertSpannerTool::simulate(float deltaTime) { + SharedObjectPointer spanner = _editor->getValue().value(); + static_cast(spanner.data())->getRenderer()->simulate(deltaTime); +} + +void InsertSpannerTool::render() { + _editor->detachValue(); + Spanner* spanner = static_cast(_editor->getValue().value().data()); + Transformable* transformable = qobject_cast(spanner); + if (transformable) { + // find the intersection of the mouse ray with the grid and place the transformable there + glm::quat rotation = _editor->getGridRotation(); + glm::quat inverseRotation = glm::inverse(rotation); + glm::vec3 rayOrigin = inverseRotation * Application::getInstance()->getMouseRayOrigin(); + glm::vec3 rayDirection = inverseRotation * Application::getInstance()->getMouseRayDirection(); + float position = _editor->getGridPosition(); + float distance = (position - rayOrigin.z) / rayDirection.z; + + transformable->setTranslation(rotation * glm::vec3(glm::vec2(rayOrigin + rayDirection * distance), position)); + } + const float SPANNER_ALPHA = 0.25f; + spanner->getRenderer()->render(SPANNER_ALPHA); +} + +bool InsertSpannerTool::appliesTo(const AttributePointer& attribute) const { + return attribute->inherits("SharedObjectSetAttribute"); +} + +bool InsertSpannerTool::eventFilter(QObject* watched, QEvent* event) { + if (event->type() == QEvent::MouseButtonPress) { + insert(); + return true; + } + return false; +} + +void InsertSpannerTool::insert() { + AttributePointer attribute = AttributeRegistry::getInstance()->getAttribute(_editor->getSelectedAttribute()); + if (!attribute) { + return; + } + SharedObjectPointer spanner = _editor->getValue().value(); + MetavoxelEditMessage message = { QVariant::fromValue(InsertSpannerEdit(attribute, spanner)) }; + Application::getInstance()->getMetavoxels()->applyEdit(message); +} + +RemoveSpannerTool::RemoveSpannerTool(MetavoxelEditor* editor) : + MetavoxelTool(editor, "Remove Spanner", false) { +} + +bool RemoveSpannerTool::appliesTo(const AttributePointer& attribute) const { + return attribute->inherits("SharedObjectSetAttribute"); +} + +bool RemoveSpannerTool::eventFilter(QObject* watched, QEvent* event) { + if (event->type() == QEvent::MouseButtonPress) { + + return true; + } + return false; +} + +ClearSpannersTool::ClearSpannersTool(MetavoxelEditor* editor) : + MetavoxelTool(editor, "Clear Spanners", false) { + + QPushButton* button = new QPushButton("Clear"); + layout()->addWidget(button); + connect(button, SIGNAL(clicked()), SLOT(clear())); +} + +bool ClearSpannersTool::appliesTo(const AttributePointer& attribute) const { + return attribute->inherits("SharedObjectSetAttribute"); +} + +void ClearSpannersTool::clear() { + AttributePointer attribute = AttributeRegistry::getInstance()->getAttribute(_editor->getSelectedAttribute()); + if (!attribute) { + return; + } + MetavoxelEditMessage message = { QVariant::fromValue(ClearSpannersEdit(attribute)) }; + Application::getInstance()->getMetavoxels()->applyEdit(message); +} diff --git a/interface/src/ui/MetavoxelEditor.h b/interface/src/ui/MetavoxelEditor.h index 9024837757..5b580129a9 100644 --- a/interface/src/ui/MetavoxelEditor.h +++ b/interface/src/ui/MetavoxelEditor.h @@ -9,7 +9,8 @@ #ifndef __interface__MetavoxelEditor__ #define __interface__MetavoxelEditor__ -#include +#include +#include #include "renderer/ProgramObject.h" @@ -17,55 +18,174 @@ class QComboBox; class QDoubleSpinBox; class QGroupBox; class QListWidget; +class QPushButton; class QScrollArea; +class MetavoxelTool; + /// Allows editing metavoxels. -class MetavoxelEditor : public QDialog { +class MetavoxelEditor : public QWidget { Q_OBJECT public: MetavoxelEditor(); + QString getSelectedAttribute() const; + + double getGridSpacing() const; + double getGridPosition() const; + glm::quat getGridRotation() const; + + QVariant getValue() const; + void detachValue(); + virtual bool eventFilter(QObject* watched, QEvent* event); private slots: - void updateValueEditor(); + void selectedAttributeChanged(); void createNewAttribute(); + void deleteSelectedAttribute(); void centerGridPosition(); void alignGridPosition(); + void updateTool(); + void simulate(float deltaTime); void render(); private: - void updateAttributes(const QString& select = QString()); - QString getSelectedAttribute() const; - double getGridSpacing() const; - glm::quat getGridRotation() const; - void resetState(); - void applyValue(const glm::vec3& minimum, const glm::vec3& maximum); - QVariant getValue() const; + void addTool(MetavoxelTool* tool); + void updateAttributes(const QString& select = QString()); + MetavoxelTool* getActiveTool() const; QListWidget* _attributes; + QPushButton* _deleteAttribute; + QComboBox* _gridPlane; QDoubleSpinBox* _gridSpacing; QDoubleSpinBox* _gridPosition; + + QList _tools; + QComboBox* _toolBox; + QGroupBox* _value; QScrollArea* _valueArea; + static ProgramObject _gridProgram; +}; + +/// Base class for editor tools. +class MetavoxelTool : public QWidget { + Q_OBJECT + +public: + + MetavoxelTool(MetavoxelEditor* editor, const QString& name, bool usesValue = true); + + bool getUsesValue() const { return _usesValue; } + + virtual bool appliesTo(const AttributePointer& attribute) const; + + virtual void simulate(float deltaTime); + + /// Renders the tool's interface, if any. + virtual void render(); + +protected: + + MetavoxelEditor* _editor; + bool _usesValue; +}; + +/// Allows setting the value of a region by dragging out a box. +class BoxSetTool : public MetavoxelTool { + Q_OBJECT + +public: + + BoxSetTool(MetavoxelEditor* editor); + + virtual void render(); + + virtual bool eventFilter(QObject* watched, QEvent* event); + +private: + + void resetState(); + void applyValue(const glm::vec3& minimum, const glm::vec3& maximum); + enum State { HOVERING_STATE, DRAGGING_STATE, RAISING_STATE }; State _state; glm::vec2 _mousePosition; ///< the position of the mouse in rotated space - glm::vec2 _startPosition; ///< the first corner of the selection base glm::vec2 _endPosition; ///< the second corner of the selection base float _height; ///< the selection height +}; + +/// Allows setting the value across the entire space. +class GlobalSetTool : public MetavoxelTool { + Q_OBJECT + +public: - static ProgramObject _gridProgram; + GlobalSetTool(MetavoxelEditor* editor); + +private slots: + + void apply(); +}; + +/// Allows inserting a spanner into the scene. +class InsertSpannerTool : public MetavoxelTool { + Q_OBJECT + +public: + + InsertSpannerTool(MetavoxelEditor* editor); + + virtual void simulate(float deltaTime); + + virtual void render(); + + virtual bool appliesTo(const AttributePointer& attribute) const; + + virtual bool eventFilter(QObject* watched, QEvent* event); + +private slots: + + void insert(); +}; + +/// Allows removing a spanner from the scene. +class RemoveSpannerTool : public MetavoxelTool { + Q_OBJECT + +public: + + RemoveSpannerTool(MetavoxelEditor* editor); + + virtual bool appliesTo(const AttributePointer& attribute) const; + + virtual bool eventFilter(QObject* watched, QEvent* event); +}; + +/// Allows removing all spanners from the scene. +class ClearSpannersTool : public MetavoxelTool { + Q_OBJECT + +public: + + ClearSpannersTool(MetavoxelEditor* editor); + + virtual bool appliesTo(const AttributePointer& attribute) const; + +private slots: + + void clear(); }; #endif /* defined(__interface__MetavoxelEditor__) */ diff --git a/interface/src/ui/Overlays.cpp b/interface/src/ui/Overlays.cpp index c35c4fc5ec..84944332f1 100644 --- a/interface/src/ui/Overlays.cpp +++ b/interface/src/ui/Overlays.cpp @@ -12,7 +12,7 @@ #include "Overlays.h" #include "Sphere3DOverlay.h" #include "TextOverlay.h" - +#include "ClipboardOverlay.h" unsigned int Overlays::_nextOverlayID = 1; @@ -73,6 +73,12 @@ unsigned int Overlays::addOverlay(const QString& type, const QScriptValue& prope thisOverlay->setProperties(properties); created = true; is3D = true; + } else if (type == "clipboard") { + thisOverlay = new ClipboardOverlay(); + thisOverlay->init(_parent); + thisOverlay->setProperties(properties); + created = true; + is3D = true; } if (created) { diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 3c50f2622c..2cb719c446 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -305,6 +305,15 @@ QByteArray AvatarData::identityByteArray() { return identityData; } +bool AvatarData::hasBillboardChangedAfterParsing(const QByteArray& packet) { + QByteArray newBillboard = packet.mid(numBytesForPacketHeader(packet)); + if (newBillboard == _billboard) { + return false; + } + _billboard = newBillboard; + return true; +} + void AvatarData::setFaceModelURL(const QUrl& faceModelURL) { _faceModelURL = faceModelURL.isEmpty() ? DEFAULT_HEAD_MODEL_URL : faceModelURL; @@ -323,6 +332,12 @@ void AvatarData::setDisplayName(const QString& displayName) { qDebug() << "Changing display name for avatar to" << displayName; } +void AvatarData::setBillboard(const QByteArray& billboard) { + _billboard = billboard; + + qDebug() << "Changing billboard for avatar."; +} + void AvatarData::setClampedTargetScale(float targetScale) { targetScale = glm::clamp(targetScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE); @@ -344,3 +359,10 @@ void AvatarData::sendIdentityPacket() { NodeList::getInstance()->broadcastToNodes(identityPacket, NodeSet() << NodeType::AvatarMixer); } + +void AvatarData::sendBillboardPacket() { + QByteArray billboardPacket = byteArrayWithPopulatedHeader(PacketTypeAvatarBillboard); + billboardPacket.append(_billboard); + + NodeList::getInstance()->broadcastToNodes(billboardPacket, NodeSet() << NodeType::AvatarMixer); +} diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 07c774d5e6..c5cd72c184 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -29,6 +29,7 @@ typedef unsigned long long quint64; #include #include +#include #include #include #include @@ -54,6 +55,7 @@ static const float MIN_AVATAR_SCALE = .005f; const float MAX_AUDIO_LOUDNESS = 1000.0; // close enough for mouth animation const int AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS = 1000; +const int AVATAR_BILLBOARD_PACKET_SEND_INTERVAL_MSECS = 5000; const QUrl DEFAULT_HEAD_MODEL_URL = QUrl("http://public.highfidelity.io/meshes/defaultAvatar_head.fst"); const QUrl DEFAULT_BODY_MODEL_URL = QUrl("http://public.highfidelity.io/meshes/defaultAvatar_body.fst"); @@ -78,6 +80,7 @@ class AvatarData : public NodeData { Q_PROPERTY(QString chatMessage READ getQStringChatMessage WRITE setChatMessage) Q_PROPERTY(glm::quat orientation READ getOrientation WRITE setOrientation) + Q_PROPERTY(glm::quat headOrientation READ getHeadOrientation WRITE setHeadOrientation) Q_PROPERTY(float headPitch READ getHeadPitch WRITE setHeadPitch) Q_PROPERTY(float audioLoudness READ getAudioLoudness WRITE setAudioLoudness) @@ -109,6 +112,9 @@ public: glm::quat getOrientation() const { return glm::quat(glm::radians(glm::vec3(_bodyPitch, _bodyYaw, _bodyRoll))); } void setOrientation(const glm::quat& orientation); + glm::quat getHeadOrientation() const { return _headData->getOrientation(); } + void setHeadOrientation(const glm::quat& orientation) { _headData->setOrientation(orientation); } + // access to Head().set/getMousePitch float getHeadPitch() const { return _headData->getPitch(); } void setHeadPitch(float value) { _headData->setPitch(value); }; @@ -151,6 +157,8 @@ public: bool hasIdentityChangedAfterParsing(const QByteArray& packet); QByteArray identityByteArray(); + bool hasBillboardChangedAfterParsing(const QByteArray& packet); + const QUrl& getFaceModelURL() const { return _faceModelURL; } QString getFaceModelURLString() const { return _faceModelURL.toString(); } const QUrl& getSkeletonModelURL() const { return _skeletonModelURL; } @@ -159,6 +167,9 @@ public: virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); virtual void setDisplayName(const QString& displayName); + virtual void setBillboard(const QByteArray& billboard); + const QByteArray& getBillboard() const { return _billboard; } + QString getFaceModelURLFromScript() const { return _faceModelURL.toString(); } void setFaceModelURLFromScript(const QString& faceModelString) { setFaceModelURL(faceModelString); } @@ -169,6 +180,7 @@ public: public slots: void sendIdentityPacket(); + void sendBillboardPacket(); protected: glm::vec3 _position; @@ -204,6 +216,8 @@ protected: float _displayNameTargetAlpha; float _displayNameAlpha; + QByteArray _billboard; + private: // privatize the copy constructor and assignment operator so they cannot be called AvatarData(const AvatarData&); diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index 62e8276bd3..68a5c2c826 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -6,6 +6,11 @@ // Copyright (c) 2013 High Fidelity, Inc. All rights reserved. // +#include + +#include + +#include "AvatarData.h" #include "HeadData.h" HeadData::HeadData(AvatarData* owningAvatar) : @@ -26,6 +31,24 @@ HeadData::HeadData(AvatarData* owningAvatar) : } +glm::quat HeadData::getOrientation() const { + return _owningAvatar->getOrientation() * glm::quat(glm::radians(glm::vec3(_pitch, _yaw, _roll))); +} + +void HeadData::setOrientation(const glm::quat& orientation) { + // rotate body about vertical axis + glm::quat bodyOrientation = _owningAvatar->getOrientation(); + glm::vec3 newFront = glm::inverse(bodyOrientation) * (orientation * IDENTITY_FRONT); + bodyOrientation = bodyOrientation * glm::angleAxis(glm::degrees(atan2f(-newFront.x, -newFront.z)), 0.0f, 1.0f, 0.0f); + _owningAvatar->setOrientation(bodyOrientation); + + // the rest goes to the head + glm::vec3 eulers = safeEulerAngles(glm::inverse(bodyOrientation) * orientation); + _pitch = eulers.x; + _yaw = eulers.y; + _roll = eulers.z; +} + void HeadData::addYaw(float yaw) { setYaw(_yaw + yaw); } diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index 04d5fe5b46..618de89b31 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -13,6 +13,7 @@ #include #include +#include const float MIN_HEAD_YAW = -110; const float MAX_HEAD_YAW = 110; @@ -42,6 +43,9 @@ public: float getRoll() const { return _roll; } void setRoll(float roll) { _roll = glm::clamp(roll, MIN_HEAD_ROLL, MAX_HEAD_ROLL); } + glm::quat getOrientation() const; + void setOrientation(const glm::quat& orientation); + float getAudioLoudness() const { return _audioLoudness; } void setAudioLoudness(float audioLoudness) { _audioLoudness = audioLoudness; } diff --git a/libraries/metavoxels/CMakeLists.txt b/libraries/metavoxels/CMakeLists.txt index 989ed6d4a7..491d537b1a 100644 --- a/libraries/metavoxels/CMakeLists.txt +++ b/libraries/metavoxels/CMakeLists.txt @@ -11,12 +11,12 @@ set(TARGET_NAME metavoxels) find_package(Qt5Network REQUIRED) find_package(Qt5Widgets REQUIRED) -include(${MACRO_DIR}/SetupHifiLibrary.cmake) -setup_hifi_library(${TARGET_NAME}) - include(${MACRO_DIR}/AutoMTC.cmake) auto_mtc(${TARGET_NAME} ${ROOT_DIR}) +include(${MACRO_DIR}/SetupHifiLibrary.cmake) +setup_hifi_library(${TARGET_NAME} ${AUTOMTC_SRC}) + qt5_use_modules(${TARGET_NAME} Network Script Widgets) include(${MACRO_DIR}/IncludeGLM.cmake) diff --git a/libraries/metavoxels/src/AttributeRegistry.cpp b/libraries/metavoxels/src/AttributeRegistry.cpp index 9595d96e1f..bd83987666 100644 --- a/libraries/metavoxels/src/AttributeRegistry.cpp +++ b/libraries/metavoxels/src/AttributeRegistry.cpp @@ -13,6 +13,7 @@ REGISTER_META_OBJECT(QRgbAttribute) REGISTER_META_OBJECT(SharedObjectAttribute) +REGISTER_META_OBJECT(SharedObjectSetAttribute) AttributeRegistry* AttributeRegistry::getInstance() { static AttributeRegistry registry; @@ -22,6 +23,7 @@ AttributeRegistry* AttributeRegistry::getInstance() { AttributeRegistry::AttributeRegistry() : _guideAttribute(registerAttribute(new SharedObjectAttribute("guide", &MetavoxelGuide::staticMetaObject, SharedObjectPointer(new DefaultMetavoxelGuide())))), + _spannersAttribute(registerAttribute(new SharedObjectSetAttribute("spanners", &Spanner::staticMetaObject))), _colorAttribute(registerAttribute(new QRgbAttribute("color"))), _normalAttribute(registerAttribute(new QRgbAttribute("normal", qRgb(0, 127, 0)))) { } @@ -53,6 +55,10 @@ AttributePointer AttributeRegistry::registerAttribute(AttributePointer attribute return pointer; } +void AttributeRegistry::deregisterAttribute(const QString& name) { + _attributes.remove(name); +} + QScriptValue AttributeRegistry::getAttribute(QScriptContext* context, QScriptEngine* engine) { return engine->newQObject(getInstance()->getAttribute(context->argument(0).toString()).data(), QScriptEngine::QtOwnership, QScriptEngine::PreferExistingWrapperObject); @@ -223,3 +229,29 @@ QWidget* SharedObjectAttribute::createEditor(QWidget* parent) const { editor->setObject(_defaultValue); return editor; } + +SharedObjectSetAttribute::SharedObjectSetAttribute(const QString& name, const QMetaObject* metaObject) : + InlineAttribute(name), + _metaObject(metaObject) { +} + +void SharedObjectSetAttribute::read(Bitstream& in, void*& value, bool isLeaf) const { + in >> *((SharedObjectSet*)&value); +} + +void SharedObjectSetAttribute::write(Bitstream& out, void* value, bool isLeaf) const { + out << decodeInline(value); +} + +bool SharedObjectSetAttribute::merge(void*& parent, void* children[]) const { + for (int i = 0; i < MERGE_COUNT; i++) { + if (!decodeInline(children[i]).isEmpty()) { + return false; + } + } + return true; +} + +QWidget* SharedObjectSetAttribute::createEditor(QWidget* parent) const { + return new SharedObjectEditor(_metaObject, parent); +} diff --git a/libraries/metavoxels/src/AttributeRegistry.h b/libraries/metavoxels/src/AttributeRegistry.h index 56cff7eeb4..2e029f8201 100644 --- a/libraries/metavoxels/src/AttributeRegistry.h +++ b/libraries/metavoxels/src/AttributeRegistry.h @@ -24,7 +24,7 @@ class QScriptValue; class Attribute; -typedef QSharedPointer AttributePointer; +typedef SharedObjectPointerTemplate AttributePointer; /// Maintains information about metavoxel attribute types. class AttributeRegistry { @@ -48,15 +48,21 @@ public: /// attribute AttributePointer registerAttribute(AttributePointer attribute); + /// Deregisters an attribute. + void deregisterAttribute(const QString& name); + /// Retrieves an attribute by name. AttributePointer getAttribute(const QString& name) const { return _attributes.value(name); } /// Returns a reference to the attribute hash. const QHash& getAttributes() const { return _attributes; } - /// Returns a reference to the standard PolymorphicDataPointer "guide" attribute. + /// Returns a reference to the standard SharedObjectPointer "guide" attribute. const AttributePointer& getGuideAttribute() const { return _guideAttribute; } + /// Returns a reference to the standard SharedObjectSet "spanners" attribute. + const AttributePointer& getSpannersAttribute() const { return _spannersAttribute; } + /// Returns a reference to the standard QRgb "color" attribute. const AttributePointer& getColorAttribute() const { return _colorAttribute; } @@ -69,6 +75,7 @@ private: QHash _attributes; AttributePointer _guideAttribute; + AttributePointer _spannersAttribute; AttributePointer _colorAttribute; AttributePointer _normalAttribute; }; @@ -141,7 +148,7 @@ public: }; /// Represents a registered attribute. -class Attribute : public QObject { +class Attribute : public SharedObject { Q_OBJECT public: @@ -260,7 +267,8 @@ class SharedObjectAttribute : public InlineAttribute { public: - Q_INVOKABLE SharedObjectAttribute(const QString& name = QString(), const QMetaObject* metaObject = NULL, + Q_INVOKABLE SharedObjectAttribute(const QString& name = QString(), + const QMetaObject* metaObject = &SharedObject::staticMetaObject, const SharedObjectPointer& defaultValue = SharedObjectPointer()); virtual void read(Bitstream& in, void*& value, bool isLeaf) const; @@ -277,4 +285,28 @@ private: const QMetaObject* _metaObject; }; +/// An attribute that takes the form of a set of shared objects. +class SharedObjectSetAttribute : public InlineAttribute { + Q_OBJECT + Q_PROPERTY(const QMetaObject* metaObject MEMBER _metaObject) + +public: + + Q_INVOKABLE SharedObjectSetAttribute(const QString& name = QString(), + const QMetaObject* metaObject = &SharedObject::staticMetaObject); + + const QMetaObject* getMetaObject() const { return _metaObject; } + + virtual void read(Bitstream& in, void*& value, bool isLeaf) const; + virtual void write(Bitstream& out, void* value, bool isLeaf) const; + + virtual bool merge(void*& parent, void* children[]) const; + + virtual QWidget* createEditor(QWidget* parent = NULL) const; + +private: + + const QMetaObject* _metaObject; +}; + #endif /* defined(__interface__AttributeRegistry__) */ diff --git a/libraries/metavoxels/src/Bitstream.cpp b/libraries/metavoxels/src/Bitstream.cpp index 262f2df7f5..919bc4cbc8 100644 --- a/libraries/metavoxels/src/Bitstream.cpp +++ b/libraries/metavoxels/src/Bitstream.cpp @@ -68,8 +68,8 @@ IDStreamer& IDStreamer::operator>>(int& value) { int Bitstream::registerMetaObject(const char* className, const QMetaObject* metaObject) { getMetaObjects().insert(className, metaObject); - // register it as a subclass of all of its superclasses - for (const QMetaObject* superClass = metaObject->superClass(); superClass != NULL; superClass = superClass->superClass()) { + // register it as a subclass of itself and all of its superclasses + for (const QMetaObject* superClass = metaObject; superClass != NULL; superClass = superClass->superClass()) { getMetaObjectSubClasses().insert(superClass, metaObject); } return 0; @@ -81,6 +81,10 @@ int Bitstream::registerTypeStreamer(int type, TypeStreamer* streamer) { return 0; } +const QMetaObject* Bitstream::getMetaObject(const QByteArray& className) { + return getMetaObjects().value(className); +} + QList Bitstream::getMetaObjectSubClasses(const QMetaObject* metaObject) { return getMetaObjectSubClasses().values(metaObject); } @@ -351,17 +355,7 @@ Bitstream& Bitstream::operator>>(QObject*& object) { object = NULL; return *this; } - object = metaObject->newInstance(); - for (int i = 0; i < metaObject->propertyCount(); i++) { - QMetaProperty property = metaObject->property(i); - if (!property.isStored(object)) { - continue; - } - const TypeStreamer* streamer = getTypeStreamers().value(property.userType()); - if (streamer) { - property.write(object, streamer->read(*this)); - } - } + readProperties(object = metaObject->newInstance()); return *this; } @@ -472,13 +466,34 @@ Bitstream& Bitstream::operator>(QScriptString& string) { } Bitstream& Bitstream::operator<(const SharedObjectPointer& object) { - return *this << object.data(); + if (!object) { + return *this << (int)0; + } + return *this << object->getID() << (QObject*)object.data(); } Bitstream& Bitstream::operator>(SharedObjectPointer& object) { - QObject* rawObject; - *this >> rawObject; - object = static_cast(rawObject); + int id; + *this >> id; + if (id == 0) { + object = SharedObjectPointer(); + return *this; + } + QPointer& pointer = _transientSharedObjects[id]; + if (pointer) { + const QMetaObject* metaObject; + _metaObjectStreamer >> metaObject; + if (metaObject != pointer->metaObject()) { + qWarning() << "Class mismatch: " << pointer->metaObject()->className() << metaObject->className(); + } + readProperties(pointer.data()); + + } else { + QObject* rawObject; + *this >> rawObject; + pointer = static_cast(rawObject); + } + object = static_cast(pointer.data()); return *this; } @@ -488,6 +503,20 @@ void Bitstream::clearSharedObject() { emit sharedObjectCleared(_sharedObjectStreamer.takePersistentID(object)); } +void Bitstream::readProperties(QObject* object) { + const QMetaObject* metaObject = object->metaObject(); + for (int i = 0; i < metaObject->propertyCount(); i++) { + QMetaProperty property = metaObject->property(i); + if (!property.isStored(object)) { + continue; + } + const TypeStreamer* streamer = getTypeStreamers().value(property.userType()); + if (streamer) { + property.write(object, streamer->read(*this)); + } + } +} + QHash& Bitstream::getMetaObjects() { static QHash metaObjects; return metaObjects; diff --git a/libraries/metavoxels/src/Bitstream.h b/libraries/metavoxels/src/Bitstream.h index cc776a742a..c06c4c3b5f 100644 --- a/libraries/metavoxels/src/Bitstream.h +++ b/libraries/metavoxels/src/Bitstream.h @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -31,7 +32,7 @@ class Bitstream; class OwnedAttributeValue; class TypeStreamer; -typedef QSharedPointer AttributePointer; +typedef SharedObjectPointerTemplate AttributePointer; /// Streams integer identifiers that conform to the following pattern: each ID encountered in the stream is either one that /// has been sent (received) before, or is one more than the highest previously encountered ID (starting at zero). This allows @@ -196,6 +197,9 @@ public: /// \return zero; the function only returns a value so that it can be used in static initialization static int registerTypeStreamer(int type, TypeStreamer* streamer); + /// Returns the meta-object registered under the supplied class name, if any. + static const QMetaObject* getMetaObject(const QByteArray& className); + /// Returns the list of registered subclasses for the supplied meta-object. static QList getMetaObjectSubClasses(const QMetaObject* metaObject); @@ -266,6 +270,9 @@ public: template Bitstream& operator<<(const QList& list); template Bitstream& operator>>(QList& list); + template Bitstream& operator<<(const QSet& set); + template Bitstream& operator>>(QSet& set); + template Bitstream& operator<<(const QHash& hash); template Bitstream& operator>>(QHash& hash); @@ -311,6 +318,8 @@ private slots: void clearSharedObject(); private: + + void readProperties(QObject* object); QDataStream& _underlying; quint8 _byte; @@ -322,6 +331,8 @@ private: RepeatedValueStreamer _scriptStringStreamer; RepeatedValueStreamer _sharedObjectStreamer; + QHash > _transientSharedObjects; + static QHash& getMetaObjects(); static QMultiHash& getMetaObjectSubClasses(); static QHash& getTypeStreamers(); @@ -348,6 +359,27 @@ template inline Bitstream& Bitstream::operator>>(QList& list) { return *this; } +template inline Bitstream& Bitstream::operator<<(const QSet& set) { + *this << set.size(); + foreach (const T& entry, set) { + *this << entry; + } + return *this; +} + +template inline Bitstream& Bitstream::operator>>(QSet& set) { + int size; + *this >> size; + set.clear(); + set.reserve(size); + for (int i = 0; i < size; i++) { + T entry; + *this >> entry; + set.insert(entry); + } + return *this; +} + template inline Bitstream& Bitstream::operator<<(const QHash& hash) { *this << hash.size(); for (typename QHash::const_iterator it = hash.constBegin(); it != hash.constEnd(); it++) { diff --git a/libraries/metavoxels/src/DatagramSequencer.cpp b/libraries/metavoxels/src/DatagramSequencer.cpp index fcbe6b5e87..0c7b8ce8ab 100644 --- a/libraries/metavoxels/src/DatagramSequencer.cpp +++ b/libraries/metavoxels/src/DatagramSequencer.cpp @@ -42,6 +42,7 @@ DatagramSequencer::DatagramSequencer(const QByteArray& datagramHeader, QObject* _outgoingDatagramStream.setByteOrder(QDataStream::LittleEndian); connect(&_outputStream, SIGNAL(sharedObjectCleared(int)), SLOT(sendClearSharedObjectMessage(int))); + connect(this, SIGNAL(receivedHighPriorityMessage(const QVariant&)), SLOT(handleHighPriorityMessage(const QVariant&))); memcpy(_outgoingDatagram.data(), datagramHeader.constData(), _datagramHeaderSize); } @@ -182,7 +183,7 @@ void DatagramSequencer::receivedDatagram(const QByteArray& datagram) { QVariant data; _inputStream >> data; if ((int)i >= _receivedHighPriorityMessages) { - handleHighPriorityMessage(data); + emit receivedHighPriorityMessage(data); } } _receivedHighPriorityMessages = highPriorityMessageCount; @@ -208,9 +209,22 @@ void DatagramSequencer::receivedDatagram(const QByteArray& datagram) { } void DatagramSequencer::sendClearSharedObjectMessage(int id) { - // for now, high priority - ClearSharedObjectMessage message = { id }; - sendHighPriorityMessage(QVariant::fromValue(message)); + // send it low-priority unless the channel has messages disabled + ReliableChannel* channel = getReliableOutputChannel(); + if (channel->getMessagesEnabled()) { + ClearMainChannelSharedObjectMessage message = { id }; + channel->sendMessage(QVariant::fromValue(message)); + + } else { + ClearSharedObjectMessage message = { id }; + sendHighPriorityMessage(QVariant::fromValue(message)); + } +} + +void DatagramSequencer::handleHighPriorityMessage(const QVariant& data) { + if (data.userType() == ClearSharedObjectMessage::Type) { + _inputStream.clearSharedObject(data.value().id); + } } void DatagramSequencer::sendRecordAcknowledged(const SendRecord& record) { @@ -303,15 +317,6 @@ void DatagramSequencer::sendPacket(const QByteArray& packet, const QVector().id); - - } else { - emit receivedHighPriorityMessage(data); - } -} - const int INITIAL_CIRCULAR_BUFFER_CAPACITY = 16; CircularBuffer::CircularBuffer(QObject* parent) : @@ -343,16 +348,32 @@ void CircularBuffer::remove(int length) { } QByteArray CircularBuffer::readBytes(int offset, int length) const { - // write in up to two segments + QByteArray bytes(length, 0); + readBytes(offset, length, bytes.data()); + return bytes; +} + +void CircularBuffer::readBytes(int offset, int length, char* data) const { + // read in up to two segments QByteArray array; int start = (_position + offset) % _data.size(); int firstSegment = qMin(length, _data.size() - start); - array.append(_data.constData() + start, firstSegment); + memcpy(data, _data.constData() + start, firstSegment); int secondSegment = length - firstSegment; if (secondSegment > 0) { - array.append(_data.constData(), secondSegment); + memcpy(data + firstSegment, _data.constData(), secondSegment); + } +} + +void CircularBuffer::writeBytes(int offset, int length, const char* data) { + // write in up to two segments + int start = (_position + offset) % _data.size(); + int firstSegment = qMin(length, _data.size() - start); + memcpy(_data.data() + start, data, firstSegment); + int secondSegment = length - firstSegment; + if (secondSegment > 0) { + memcpy(_data.data(), data + firstSegment, secondSegment); } - return array; } void CircularBuffer::writeToStream(int offset, int length, QDataStream& out) const { @@ -561,7 +582,14 @@ int ReliableChannel::getBytesAvailable() const { } void ReliableChannel::sendMessage(const QVariant& message) { + // write a placeholder for the length, then fill it in when we know what it is + int placeholder = _buffer.pos(); + _dataStream << (quint32)0; _bitstream << message; + _bitstream.flush(); + + quint32 length = _buffer.pos() - placeholder; + _buffer.writeBytes(placeholder, sizeof(quint32), (const char*)&length); } void ReliableChannel::sendClearSharedObjectMessage(int id) { @@ -569,6 +597,16 @@ void ReliableChannel::sendClearSharedObjectMessage(int id) { sendMessage(QVariant::fromValue(message)); } +void ReliableChannel::handleMessage(const QVariant& message) { + if (message.userType() == ClearSharedObjectMessage::Type) { + _bitstream.clearSharedObject(message.value().id); + + } else if (message.userType() == ClearMainChannelSharedObjectMessage::Type) { + static_cast(parent())->_inputStream.clearSharedObject( + message.value().id); + } +} + ReliableChannel::ReliableChannel(DatagramSequencer* sequencer, int index, bool output) : QObject(sequencer), _index(index), @@ -576,12 +614,14 @@ ReliableChannel::ReliableChannel(DatagramSequencer* sequencer, int index, bool o _bitstream(_dataStream), _priority(1.0f), _offset(0), - _writePosition(0) { + _writePosition(0), + _messagesEnabled(true) { _buffer.open(output ? QIODevice::WriteOnly : QIODevice::ReadOnly); _dataStream.setByteOrder(QDataStream::LittleEndian); connect(&_bitstream, SIGNAL(sharedObjectCleared(int)), SLOT(sendClearSharedObjectMessage(int))); + connect(this, SIGNAL(receivedMessage(const QVariant&)), SLOT(handleMessage(const QVariant&))); } void ReliableChannel::writeData(QDataStream& out, int bytes, QVector& spans) { @@ -688,10 +728,32 @@ void ReliableChannel::readData(QDataStream& in) { readSome = true; } } + if (!readSome) { + return; + } - // let listeners know that there's data to read - if (readSome) { - emit _buffer.readyRead(); + forever { + // if we're expecting a message, peek into the buffer to see if we have the whole thing. + // if so, read it in, handle it, and loop back around in case there are more + if (_messagesEnabled) { + quint32 available = _buffer.bytesAvailable(); + if (available >= sizeof(quint32)) { + quint32 length; + _buffer.readBytes(_buffer.pos(), sizeof(quint32), (char*)&length); + if (available >= length) { + _dataStream.skipRawData(sizeof(quint32)); + QVariant message; + _bitstream >> message; + _bitstream.reset(); + emit receivedMessage(message); + continue; + } + } + // otherwise, just let whoever's listening know that data is available + } else { + emit _buffer.readyRead(); + } + break; } // prune any read data from the buffer diff --git a/libraries/metavoxels/src/DatagramSequencer.h b/libraries/metavoxels/src/DatagramSequencer.h index 27a4f05379..13bdf4c7bf 100644 --- a/libraries/metavoxels/src/DatagramSequencer.h +++ b/libraries/metavoxels/src/DatagramSequencer.h @@ -70,7 +70,7 @@ public: /// Processes a datagram received from the other party, emitting readyToRead when the entire packet /// has been successfully assembled. - void receivedDatagram(const QByteArray& datagram); + Q_INVOKABLE void receivedDatagram(const QByteArray& datagram); signals: @@ -94,6 +94,7 @@ signals: private slots: void sendClearSharedObjectMessage(int id); + void handleHighPriorityMessage(const QVariant& data); private: @@ -133,8 +134,6 @@ private: /// readyToWrite) as necessary. void sendPacket(const QByteArray& packet, const QVector& spans); - void handleHighPriorityMessage(const QVariant& data); - QList _sendRecords; QList _receiveRecords; @@ -185,6 +184,12 @@ public: /// Reads part of the data from the buffer. QByteArray readBytes(int offset, int length) const; + /// Reads part of the data from the buffer. + void readBytes(int offset, int length, char* data) const; + + /// Writes to part of the data in the buffer. + void writeBytes(int offset, int length, const char* data); + /// Writes part of the buffer to the supplied stream. void writeToStream(int offset, int length, QDataStream& out) const; @@ -267,12 +272,22 @@ public: int getBytesAvailable() const; + /// Sets whether we expect to write/read framed messages. + void setMessagesEnabled(bool enabled) { _messagesEnabled = enabled; } + bool getMessagesEnabled() const { return _messagesEnabled; } + + /// Sends a framed message on this channel. void sendMessage(const QVariant& message); +signals: + + void receivedMessage(const QVariant& message); + private slots: void sendClearSharedObjectMessage(int id); - + void handleMessage(const QVariant& message); + private: friend class DatagramSequencer; @@ -297,6 +312,7 @@ private: int _offset; int _writePosition; SpanList _acknowledged; + bool _messagesEnabled; }; #endif /* defined(__interface__DatagramSequencer__) */ diff --git a/libraries/metavoxels/src/MetavoxelData.cpp b/libraries/metavoxels/src/MetavoxelData.cpp index b0e24405e6..5665e70f91 100644 --- a/libraries/metavoxels/src/MetavoxelData.cpp +++ b/libraries/metavoxels/src/MetavoxelData.cpp @@ -18,6 +18,8 @@ REGISTER_META_OBJECT(MetavoxelGuide) REGISTER_META_OBJECT(DefaultMetavoxelGuide) REGISTER_META_OBJECT(ScriptedMetavoxelGuide) REGISTER_META_OBJECT(ThrobbingMetavoxelGuide) +REGISTER_META_OBJECT(Spanner) +REGISTER_META_OBJECT(StaticModel) MetavoxelData::MetavoxelData() : _size(1.0f) { } @@ -43,17 +45,19 @@ MetavoxelData& MetavoxelData::operator=(const MetavoxelData& other) { Box MetavoxelData::getBounds() const { float halfSize = _size * 0.5f; - Box bounds = { glm::vec3(-halfSize, -halfSize, -halfSize), glm::vec3(halfSize, halfSize, halfSize) }; - return bounds; + return Box(glm::vec3(-halfSize, -halfSize, -halfSize), glm::vec3(halfSize, halfSize, halfSize)); } void MetavoxelData::guide(MetavoxelVisitor& visitor) { + // let the visitor know we're about to begin a tour + visitor.prepare(); + // start with the root values/defaults (plus the guide attribute) const QVector& inputs = visitor.getInputs(); const QVector& outputs = visitor.getOutputs(); MetavoxelVisitation firstVisitation = { NULL, visitor, QVector(inputs.size() + 1), QVector(outputs.size()), { glm::vec3(_size, _size, _size) * -0.5f, _size, - QVector(inputs.size() + 1), QVector(outputs.size()) } }; + QVector(inputs.size() + 1), QVector(outputs.size()) } }; for (int i = 0; i < inputs.size(); i++) { MetavoxelNode* node = _roots.value(inputs.at(i)); firstVisitation.inputNodes[i] = node; @@ -70,7 +74,7 @@ void MetavoxelData::guide(MetavoxelVisitor& visitor) { static_cast(firstVisitation.info.inputValues.last().getInlineValue< SharedObjectPointer>().data())->guide(firstVisitation); for (int i = 0; i < outputs.size(); i++) { - AttributeValue& value = firstVisitation.info.outputValues[i]; + OwnedAttributeValue& value = firstVisitation.info.outputValues[i]; if (!value.getAttribute()) { continue; } @@ -88,6 +92,103 @@ void MetavoxelData::guide(MetavoxelVisitor& visitor) { } } +class InsertVisitor : public MetavoxelVisitor { +public: + + InsertVisitor(const AttributePointer& attribute, const Box& bounds, float granularity, const SharedObjectPointer& object); + + virtual bool visit(MetavoxelInfo& info); + +private: + + const AttributePointer& _attribute; + const Box& _bounds; + float _longestSide; + const SharedObjectPointer& _object; +}; + +InsertVisitor::InsertVisitor(const AttributePointer& attribute, const Box& bounds, + float granularity, const SharedObjectPointer& object) : + MetavoxelVisitor(QVector() << attribute, QVector() << attribute), + _attribute(attribute), + _bounds(bounds), + _longestSide(qMax(bounds.getLongestSide(), granularity)), + _object(object) { +} + +bool InsertVisitor::visit(MetavoxelInfo& info) { + if (!info.getBounds().intersects(_bounds)) { + return false; + } + if (info.size > _longestSide) { + return true; + } + SharedObjectSet set = info.inputValues.at(0).getInlineValue(); + set.insert(_object); + info.outputValues[0] = AttributeValue(_attribute, encodeInline(set)); + return false; +} + +void MetavoxelData::insert(const AttributePointer& attribute, const Box& bounds, + float granularity, const SharedObjectPointer& object) { + // expand to fit the entire bounds + while (!getBounds().contains(bounds)) { + expand(); + } + InsertVisitor visitor(attribute, bounds, granularity, object); + guide(visitor); +} + +class RemoveVisitor : public MetavoxelVisitor { +public: + + RemoveVisitor(const AttributePointer& attribute, const Box& bounds, float granularity, const SharedObjectPointer& object); + + virtual bool visit(MetavoxelInfo& info); + +private: + + const AttributePointer& _attribute; + const Box& _bounds; + float _longestSide; + const SharedObjectPointer& _object; +}; + +RemoveVisitor::RemoveVisitor(const AttributePointer& attribute, const Box& bounds, + float granularity, const SharedObjectPointer& object) : + MetavoxelVisitor(QVector() << attribute, QVector() << attribute), + _attribute(attribute), + _bounds(bounds), + _longestSide(qMax(bounds.getLongestSide(), granularity)), + _object(object) { +} + +bool RemoveVisitor::visit(MetavoxelInfo& info) { + if (!info.getBounds().intersects(_bounds)) { + return false; + } + if (info.size > _longestSide) { + return true; + } + SharedObjectSet set = info.inputValues.at(0).getInlineValue(); + set.remove(_object); + info.outputValues[0] = AttributeValue(_attribute, encodeInline(set)); + return false; +} + +void MetavoxelData::remove(const AttributePointer& attribute, const Box& bounds, + float granularity, const SharedObjectPointer& object) { + RemoveVisitor visitor(attribute, bounds, granularity, object); + guide(visitor); +} + +void MetavoxelData::clear(const AttributePointer& attribute) { + MetavoxelNode* node = _roots.take(attribute); + if (node) { + node->decrementReferenceCount(attribute); + } +} + const int X_MAXIMUM_FLAG = 1; const int Y_MAXIMUM_FLAG = 2; const int Z_MAXIMUM_FLAG = 4; @@ -432,6 +533,32 @@ MetavoxelVisitor::MetavoxelVisitor(const QVector& inputs, cons MetavoxelVisitor::~MetavoxelVisitor() { } +void MetavoxelVisitor::prepare() { + // nothing by default +} + +SpannerVisitor::SpannerVisitor(const QVector& spannerInputs, const QVector& inputs, + const QVector& outputs) : + MetavoxelVisitor(inputs + spannerInputs, outputs), + _spannerInputCount(spannerInputs.size()) { +} + +void SpannerVisitor::prepare() { + Spanner::incrementVisit(); +} + +bool SpannerVisitor::visit(MetavoxelInfo& info) { + for (int i = _inputs.size() - _spannerInputCount; i < _inputs.size(); i++) { + foreach (const SharedObjectPointer& object, info.inputValues.at(i).getInlineValue()) { + Spanner* spanner = static_cast(object.data()); + if (spanner->testAndSetVisited()) { + visit(spanner); + } + } + } + return !info.isLeaf; +} + DefaultMetavoxelGuide::DefaultMetavoxelGuide() { } @@ -439,7 +566,7 @@ void DefaultMetavoxelGuide::guide(MetavoxelVisitation& visitation) { visitation.info.isLeaf = visitation.allInputNodesLeaves(); bool keepGoing = visitation.visitor.visit(visitation.info); for (int i = 0; i < visitation.outputNodes.size(); i++) { - AttributeValue& value = visitation.info.outputValues[i]; + OwnedAttributeValue& value = visitation.info.outputValues[i]; if (!value.getAttribute()) { continue; } @@ -457,7 +584,7 @@ void DefaultMetavoxelGuide::guide(MetavoxelVisitation& visitation) { MetavoxelVisitation nextVisitation = { &visitation, visitation.visitor, QVector(visitation.inputNodes.size()), QVector(visitation.outputNodes.size()), { glm::vec3(), visitation.info.size * 0.5f, QVector(visitation.inputNodes.size()), - QVector(visitation.outputNodes.size()) } }; + QVector(visitation.outputNodes.size()) } }; for (int i = 0; i < MetavoxelNode::CHILD_COUNT; i++) { for (int j = 0; j < visitation.inputNodes.size(); j++) { MetavoxelNode* node = visitation.inputNodes.at(j); @@ -478,12 +605,12 @@ void DefaultMetavoxelGuide::guide(MetavoxelVisitation& visitation) { static_cast(nextVisitation.info.inputValues.last().getInlineValue< SharedObjectPointer>().data())->guide(nextVisitation); for (int j = 0; j < nextVisitation.outputNodes.size(); j++) { - AttributeValue& value = nextVisitation.info.outputValues[j]; + OwnedAttributeValue& value = nextVisitation.info.outputValues[j]; if (!value.getAttribute()) { continue; } // replace the child - AttributeValue& parentValue = visitation.info.outputValues[j]; + OwnedAttributeValue& parentValue = visitation.info.outputValues[j]; if (!parentValue.getAttribute()) { // shallow-copy the parent node on first change parentValue = value; @@ -511,7 +638,7 @@ void DefaultMetavoxelGuide::guide(MetavoxelVisitation& visitation) { } } for (int i = 0; i < visitation.outputNodes.size(); i++) { - AttributeValue& value = visitation.info.outputValues[i]; + OwnedAttributeValue& value = visitation.info.outputValues[i]; if (value.getAttribute()) { MetavoxelNode* node = visitation.outputNodes.at(i); node->mergeChildren(value.getAttribute()); @@ -674,3 +801,96 @@ AttributeValue MetavoxelVisitation::getInheritedOutputValue(int index) const { return AttributeValue(visitor.getOutputs().at(index)); } +const float DEFAULT_GRANULARITY = 0.01f; + +Spanner::Spanner() : + _granularity(DEFAULT_GRANULARITY), + _lastVisit(0), + _renderer(NULL) { +} + +void Spanner::setBounds(const Box& bounds) { + if (_bounds == bounds) { + return; + } + emit boundsWillChange(); + emit boundsChanged(_bounds = bounds); +} + +bool Spanner::testAndSetVisited() { + if (_lastVisit == _visit) { + return false; + } + _lastVisit = _visit; + return true; +} + +SpannerRenderer* Spanner::getRenderer() { + if (!_renderer) { + QByteArray className = getRendererClassName(); + const QMetaObject* metaObject = Bitstream::getMetaObject(className); + if (!metaObject) { + qDebug() << "Unknown class name:" << className; + metaObject = &SpannerRenderer::staticMetaObject; + } + _renderer = static_cast(metaObject->newInstance()); + _renderer->setParent(this); + _renderer->init(this); + } + return _renderer; +} + +QByteArray Spanner::getRendererClassName() const { + return "SpannerRendererer"; +} + +int Spanner::_visit = 0; + +SpannerRenderer::SpannerRenderer() { +} + +void SpannerRenderer::init(Spanner* spanner) { + // nothing by default +} + +void SpannerRenderer::simulate(float deltaTime) { + // nothing by default +} + +void SpannerRenderer::render(float alpha) { + // nothing by default +} + +Transformable::Transformable() : _scale(1.0f) { +} + +void Transformable::setTranslation(const glm::vec3& translation) { + if (_translation != translation) { + emit translationChanged(_translation = translation); + } +} + +void Transformable::setRotation(const glm::vec3& rotation) { + if (_rotation != rotation) { + emit rotationChanged(_rotation = rotation); + } +} + +void Transformable::setScale(float scale) { + if (_scale != scale) { + emit scaleChanged(_scale = scale); + } +} + +StaticModel::StaticModel() { +} + +void StaticModel::setURL(const QUrl& url) { + if (_url != url) { + emit urlChanged(_url = url); + } +} + +QByteArray StaticModel::getRendererClassName() const { + return "StaticModelRenderer"; +} diff --git a/libraries/metavoxels/src/MetavoxelData.h b/libraries/metavoxels/src/MetavoxelData.h index 51cdd6cf64..85bdd54938 100644 --- a/libraries/metavoxels/src/MetavoxelData.h +++ b/libraries/metavoxels/src/MetavoxelData.h @@ -28,6 +28,8 @@ class MetavoxelNode; class MetavoxelVisitation; class MetavoxelVisitor; class NetworkValue; +class Spanner; +class SpannerRenderer; /// The base metavoxel representation shared between server and client. class MetavoxelData { @@ -45,7 +47,13 @@ public: /// Applies the specified visitor to the contained voxels. void guide(MetavoxelVisitor& visitor); + + void insert(const AttributePointer& attribute, const Box& bounds, float granularity, const SharedObjectPointer& object); + void remove(const AttributePointer& attribute, const Box& bounds, float granularity, const SharedObjectPointer& object); + + void clear(const AttributePointer& attribute); + /// Expands the tree, increasing its capacity in all dimensions. void expand(); @@ -121,15 +129,18 @@ public: glm::vec3 minimum; ///< the minimum extent of the area covered by the voxel float size; ///< the size of the voxel in all dimensions QVector inputValues; - QVector outputValues; + QVector outputValues; bool isLeaf; + + Box getBounds() const { return Box(minimum, minimum + glm::vec3(size, size, size)); } }; /// Interface for visitors to metavoxels. class MetavoxelVisitor { public: - MetavoxelVisitor(const QVector& inputs, const QVector& outputs); + MetavoxelVisitor(const QVector& inputs, + const QVector& outputs = QVector()); virtual ~MetavoxelVisitor(); /// Returns a reference to the list of input attributes desired. @@ -138,6 +149,9 @@ public: /// Returns a reference to the list of output attributes provided. const QVector& getOutputs() const { return _outputs; } + /// Prepares for a new tour of the metavoxel data. + virtual void prepare(); + /// Visits a metavoxel. /// \param info the metavoxel data /// \return if true, continue descending; if false, stop @@ -151,6 +165,25 @@ protected: typedef QSharedPointer MetavoxelVisitorPointer; +/// Interface for visitors to spanners. +class SpannerVisitor : public MetavoxelVisitor { +public: + + SpannerVisitor(const QVector& spannerInputs, + const QVector& inputs = QVector(), + const QVector& outputs = QVector()); + + /// Visits a spanner. + virtual void visit(Spanner* spanner) = 0; + + virtual void prepare(); + virtual bool visit(MetavoxelInfo& info); + +protected: + + int _spannerInputCount; +}; + /// Interface for objects that guide metavoxel visitors. class MetavoxelGuide : public SharedObject { Q_OBJECT @@ -242,4 +275,121 @@ public: AttributeValue getInheritedOutputValue(int index) const; }; +/// An object that spans multiple octree cells. +class Spanner : public SharedObject { + Q_OBJECT + Q_PROPERTY(Box bounds MEMBER _bounds WRITE setBounds NOTIFY boundsChanged DESIGNABLE false) + Q_PROPERTY(float granularity MEMBER _granularity DESIGNABLE false) + +public: + + /// Increments the value of the global visit counter. + static void incrementVisit() { _visit++; } + + Spanner(); + + void setBounds(const Box& bounds); + const Box& getBounds() const { return _bounds; } + + void setGranularity(float granularity) { _granularity = granularity; } + float getGranularity() const { return _granularity; } + + /// Checks whether we've visited this object on the current traversal. If we have, returns false. + /// If we haven't, sets the last visit identifier and returns true. + bool testAndSetVisited(); + + /// Returns a pointer to the renderer, creating it if necessary. + SpannerRenderer* getRenderer(); + +signals: + + void boundsWillChange(); + void boundsChanged(const Box& bounds); + +protected: + + /// Returns the name of the class to instantiate in order to render this spanner. + virtual QByteArray getRendererClassName() const; + +private: + + Box _bounds; + float _granularity; + int _lastVisit; ///< the identifier of the last visit + SpannerRenderer* _renderer; + + static int _visit; ///< the global visit counter +}; + +/// Base class for objects that can render spanners. +class SpannerRenderer : public QObject { + Q_OBJECT + +public: + + Q_INVOKABLE SpannerRenderer(); + + virtual void init(Spanner* spanner); + virtual void simulate(float deltaTime); + virtual void render(float alpha); +}; + +/// An object with a 3D transform. +class Transformable : public Spanner { + Q_OBJECT + Q_PROPERTY(glm::vec3 translation MEMBER _translation WRITE setTranslation NOTIFY translationChanged) + Q_PROPERTY(glm::vec3 rotation MEMBER _rotation WRITE setRotation NOTIFY rotationChanged) + Q_PROPERTY(float scale MEMBER _scale WRITE setScale NOTIFY scaleChanged) + +public: + + Transformable(); + + void setTranslation(const glm::vec3& translation); + const glm::vec3& getTranslation() const { return _translation; } + + void setRotation(const glm::vec3& rotation); + const glm::vec3& getRotation() const { return _rotation; } + + void setScale(float scale); + float getScale() const { return _scale; } + +signals: + + void translationChanged(const glm::vec3& translation); + void rotationChanged(const glm::vec3& rotation); + void scaleChanged(float scale); + +private: + + glm::vec3 _translation; + glm::vec3 _rotation; + float _scale; +}; + +/// A static 3D model loaded from the network. +class StaticModel : public Transformable { + Q_OBJECT + Q_PROPERTY(QUrl url MEMBER _url WRITE setURL NOTIFY urlChanged) + +public: + + Q_INVOKABLE StaticModel(); + + void setURL(const QUrl& url); + const QUrl& getURL() const { return _url; } + +signals: + + void urlChanged(const QUrl& url); + +protected: + + virtual QByteArray getRendererClassName() const; + +private: + + QUrl _url; +}; + #endif /* defined(__interface__MetavoxelData__) */ diff --git a/libraries/metavoxels/src/MetavoxelMessages.cpp b/libraries/metavoxels/src/MetavoxelMessages.cpp index 380df4cac1..c66ce61ee9 100644 --- a/libraries/metavoxels/src/MetavoxelMessages.cpp +++ b/libraries/metavoxels/src/MetavoxelMessages.cpp @@ -9,24 +9,35 @@ #include "MetavoxelData.h" #include "MetavoxelMessages.h" -class EditVisitor : public MetavoxelVisitor { +void MetavoxelEditMessage::apply(MetavoxelData& data) const { + static_cast(edit.data())->apply(data); +} + +MetavoxelEdit::~MetavoxelEdit() { +} + +BoxSetEdit::BoxSetEdit(const Box& region, float granularity, const OwnedAttributeValue& value) : + region(region), granularity(granularity), value(value) { +} + +class BoxSetEditVisitor : public MetavoxelVisitor { public: - EditVisitor(const MetavoxelEditMessage& edit); + BoxSetEditVisitor(const BoxSetEdit& edit); virtual bool visit(MetavoxelInfo& info); private: - const MetavoxelEditMessage& _edit; + const BoxSetEdit& _edit; }; -EditVisitor::EditVisitor(const MetavoxelEditMessage& edit) : +BoxSetEditVisitor::BoxSetEditVisitor(const BoxSetEdit& edit) : MetavoxelVisitor(QVector(), QVector() << edit.value.getAttribute()), _edit(edit) { } -bool EditVisitor::visit(MetavoxelInfo& info) { +bool BoxSetEditVisitor::visit(MetavoxelInfo& info) { // find the intersection between volume and voxel glm::vec3 minimum = glm::max(info.minimum, _edit.region.minimum); glm::vec3 maximum = glm::min(info.minimum + glm::vec3(info.size, info.size, info.size), _edit.region.maximum); @@ -48,12 +59,69 @@ bool EditVisitor::visit(MetavoxelInfo& info) { return true; // subdivide } -void MetavoxelEditMessage::apply(MetavoxelData& data) const { +void BoxSetEdit::apply(MetavoxelData& data) const { // expand to fit the entire edit while (!data.getBounds().contains(region)) { data.expand(); } - EditVisitor visitor(*this); + BoxSetEditVisitor visitor(*this); data.guide(visitor); } + +GlobalSetEdit::GlobalSetEdit(const OwnedAttributeValue& value) : + value(value) { +} + +class GlobalSetEditVisitor : public MetavoxelVisitor { +public: + + GlobalSetEditVisitor(const GlobalSetEdit& edit); + + virtual bool visit(MetavoxelInfo& info); + +private: + + const GlobalSetEdit& _edit; +}; + +GlobalSetEditVisitor::GlobalSetEditVisitor(const GlobalSetEdit& edit) : + MetavoxelVisitor(QVector(), QVector() << edit.value.getAttribute()), + _edit(edit) { +} + +bool GlobalSetEditVisitor::visit(MetavoxelInfo& info) { + info.outputValues[0] = _edit.value; + return false; // entirely contained +} + +void GlobalSetEdit::apply(MetavoxelData& data) const { + GlobalSetEditVisitor visitor(*this); + data.guide(visitor); +} + +InsertSpannerEdit::InsertSpannerEdit(const AttributePointer& attribute, const SharedObjectPointer& spanner) : + attribute(attribute), + spanner(spanner) { +} + +void InsertSpannerEdit::apply(MetavoxelData& data) const { + Spanner* spanner = static_cast(this->spanner.data()); + data.insert(attribute, spanner->getBounds(), spanner->getGranularity(), this->spanner); +} + +RemoveSpannerEdit::RemoveSpannerEdit(const AttributePointer& attribute, int id) : + attribute(attribute), + id(id) { +} + +void RemoveSpannerEdit::apply(MetavoxelData& data) const { +} + +ClearSpannersEdit::ClearSpannersEdit(const AttributePointer& attribute) : + attribute(attribute) { +} + +void ClearSpannersEdit::apply(MetavoxelData& data) const { + data.clear(attribute); +} diff --git a/libraries/metavoxels/src/MetavoxelMessages.h b/libraries/metavoxels/src/MetavoxelMessages.h index c3cc78c5bc..d6f07c2966 100644 --- a/libraries/metavoxels/src/MetavoxelMessages.h +++ b/libraries/metavoxels/src/MetavoxelMessages.h @@ -32,6 +32,17 @@ public: DECLARE_STREAMABLE_METATYPE(ClearSharedObjectMessage) +/// Clears the mapping for a shared object on the main channel (as opposed to the one on which the message was sent). +class ClearMainChannelSharedObjectMessage { + STREAMABLE + +public: + + STREAM int id; +}; + +DECLARE_STREAMABLE_METATYPE(ClearMainChannelSharedObjectMessage) + /// A message containing the state of a client. class ClientStateMessage { STREAMABLE @@ -56,13 +67,101 @@ class MetavoxelEditMessage { public: - STREAM Box region; - STREAM float granularity; - STREAM OwnedAttributeValue value; + STREAM QVariant edit; void apply(MetavoxelData& data) const; }; DECLARE_STREAMABLE_METATYPE(MetavoxelEditMessage) +/// Abstract base class for edits. +class MetavoxelEdit { +public: + + virtual ~MetavoxelEdit(); + + virtual void apply(MetavoxelData& data) const = 0; +}; + +/// An edit that sets the region within a box to a value. +class BoxSetEdit : public MetavoxelEdit { + STREAMABLE + +public: + + STREAM Box region; + STREAM float granularity; + STREAM OwnedAttributeValue value; + + BoxSetEdit(const Box& region = Box(), float granularity = 0.0f, + const OwnedAttributeValue& value = OwnedAttributeValue()); + + virtual void apply(MetavoxelData& data) const; +}; + +DECLARE_STREAMABLE_METATYPE(BoxSetEdit) + +/// An edit that sets the entire tree to a value. +class GlobalSetEdit : public MetavoxelEdit { + STREAMABLE + +public: + + STREAM OwnedAttributeValue value; + + GlobalSetEdit(const OwnedAttributeValue& value = OwnedAttributeValue()); + + virtual void apply(MetavoxelData& data) const; +}; + +DECLARE_STREAMABLE_METATYPE(GlobalSetEdit) + +/// An edit that inserts a spanner into the tree. +class InsertSpannerEdit : public MetavoxelEdit { + STREAMABLE + +public: + + STREAM AttributePointer attribute; + STREAM SharedObjectPointer spanner; + + InsertSpannerEdit(const AttributePointer& attribute = AttributePointer(), + const SharedObjectPointer& spanner = SharedObjectPointer()); + + virtual void apply(MetavoxelData& data) const; +}; + +DECLARE_STREAMABLE_METATYPE(InsertSpannerEdit) + +/// An edit that removes a spanner from the tree. +class RemoveSpannerEdit : public MetavoxelEdit { + STREAMABLE + +public: + + STREAM AttributePointer attribute; + STREAM int id; + + RemoveSpannerEdit(const AttributePointer& attribute = AttributePointer(), int id = 0); + + virtual void apply(MetavoxelData& data) const; +}; + +DECLARE_STREAMABLE_METATYPE(RemoveSpannerEdit) + +/// An edit that clears all spanners from the tree. +class ClearSpannersEdit : public MetavoxelEdit { + STREAMABLE + +public: + + STREAM AttributePointer attribute; + + ClearSpannersEdit(const AttributePointer& attribute = AttributePointer()); + + virtual void apply(MetavoxelData& data) const; +}; + +DECLARE_STREAMABLE_METATYPE(ClearSpannersEdit) + #endif /* defined(__interface__MetavoxelMessages__) */ diff --git a/libraries/metavoxels/src/MetavoxelUtil.cpp b/libraries/metavoxels/src/MetavoxelUtil.cpp index d6b42b11fb..2e93fb1804 100644 --- a/libraries/metavoxels/src/MetavoxelUtil.cpp +++ b/libraries/metavoxels/src/MetavoxelUtil.cpp @@ -17,12 +17,10 @@ #include #include #include +#include #include #include -#include -#include - #include "MetavoxelUtil.h" #include "ScriptCache.h" @@ -53,6 +51,7 @@ public: DoubleEditor::DoubleEditor(QWidget* parent) : QDoubleSpinBox(parent) { setMinimum(-FLT_MAX); + setMaximum(FLT_MAX); } DelegatingItemEditorFactory::DelegatingItemEditorFactory() : @@ -104,12 +103,24 @@ static QItemEditorCreatorBase* createDoubleEditorCreator() { return creator; } +static QItemEditorCreatorBase* createQMetaObjectEditorCreator() { + QItemEditorCreatorBase* creator = new LazyItemEditorCreator(); + getItemEditorFactory()->registerEditor(qMetaTypeId(), creator); + return creator; +} + static QItemEditorCreatorBase* createQColorEditorCreator() { QItemEditorCreatorBase* creator = new LazyItemEditorCreator(); getItemEditorFactory()->registerEditor(qMetaTypeId(), creator); return creator; } +static QItemEditorCreatorBase* createQUrlEditorCreator() { + QItemEditorCreatorBase* creator = new LazyItemEditorCreator(); + getItemEditorFactory()->registerEditor(qMetaTypeId(), creator); + return creator; +} + static QItemEditorCreatorBase* createVec3EditorCreator() { QItemEditorCreatorBase* creator = new LazyItemEditorCreator(); getItemEditorFactory()->registerEditor(qMetaTypeId(), creator); @@ -123,36 +134,57 @@ static QItemEditorCreatorBase* createParameterizedURLEditorCreator() { } static QItemEditorCreatorBase* doubleEditorCreator = createDoubleEditorCreator(); +static QItemEditorCreatorBase* qMetaObjectEditorCreator = createQMetaObjectEditorCreator(); static QItemEditorCreatorBase* qColorEditorCreator = createQColorEditorCreator(); +static QItemEditorCreatorBase* qUrlEditorCreator = createQUrlEditorCreator(); static QItemEditorCreatorBase* vec3EditorCreator = createVec3EditorCreator(); static QItemEditorCreatorBase* parameterizedURLEditorCreator = createParameterizedURLEditorCreator(); -QUuid readSessionID(const QByteArray& data, const SharedNodePointer& sendingNode, int& headerPlusIDSize) { - // get the header size - int headerSize = numBytesForPacketHeader(data); - - // read the session id - const int UUID_BYTES = 16; - headerPlusIDSize = headerSize + UUID_BYTES; - if (data.size() < headerPlusIDSize) { - qWarning() << "Metavoxel data too short [size=" << data.size() << ", sendingNode=" << sendingNode << "]\n"; - return QUuid(); - } - return QUuid::fromRfc4122(QByteArray::fromRawData(data.constData() + headerSize, UUID_BYTES)); -} - QByteArray signal(const char* signature) { static QByteArray prototype = SIGNAL(dummyMethod()); QByteArray signal = prototype; return signal.replace("dummyMethod()", signature); } +Box::Box(const glm::vec3& minimum, const glm::vec3& maximum) : + minimum(minimum), maximum(maximum) { +} + bool Box::contains(const Box& other) const { return other.minimum.x >= minimum.x && other.maximum.x <= maximum.x && other.minimum.y >= minimum.y && other.maximum.y <= maximum.y && other.minimum.z >= minimum.z && other.maximum.z <= maximum.z; } +bool Box::intersects(const Box& other) const { + return other.maximum.x >= minimum.x && other.minimum.x <= maximum.x && + other.maximum.y >= minimum.y && other.minimum.y <= maximum.y && + other.maximum.z >= minimum.z && other.minimum.z <= maximum.z; +} + +QMetaObjectEditor::QMetaObjectEditor(QWidget* parent) : QWidget(parent) { + QVBoxLayout* layout = new QVBoxLayout(); + layout->setContentsMargins(QMargins()); + layout->setAlignment(Qt::AlignTop); + setLayout(layout); + layout->addWidget(_box = new QComboBox()); + connect(_box, SIGNAL(currentIndexChanged(int)), SLOT(updateMetaObject())); + + foreach (const QMetaObject* metaObject, Bitstream::getMetaObjectSubClasses(&SharedObject::staticMetaObject)) { + _box->addItem(metaObject->className(), QVariant::fromValue(metaObject)); + } +} + +void QMetaObjectEditor::setMetaObject(const QMetaObject* metaObject) { + _metaObject = metaObject; + _box->setCurrentIndex(_metaObject ? _box->findText(_metaObject->className()) : -1); +} + +void QMetaObjectEditor::updateMetaObject() { + int index = _box->currentIndex(); + emit metaObjectChanged(_metaObject = (index == -1) ? NULL : _box->itemData(index).value()); +} + QColorEditor::QColorEditor(QWidget* parent) : QWidget(parent) { QVBoxLayout* layout = new QVBoxLayout(); layout->setContentsMargins(QMargins()); @@ -176,6 +208,36 @@ void QColorEditor::selectColor() { } } +QUrlEditor::QUrlEditor(QWidget* parent) : + QComboBox(parent) { + + setEditable(true); + setInsertPolicy(InsertAtTop); + + // populate initial URL list from settings + addItems(QSettings().value("editorURLs").toStringList()); + + connect(this, SIGNAL(activated(const QString&)), SLOT(updateURL(const QString&))); + connect(model(), SIGNAL(rowsInserted(const QModelIndex&,int,int)), SLOT(updateSettings())); +} + +void QUrlEditor::setURL(const QUrl& url) { + setCurrentText((_url = url).toString()); +} + +void QUrlEditor::updateURL(const QString& text) { + emit urlChanged(_url = text); +} + +void QUrlEditor::updateSettings() { + QStringList urls; + const int MAX_STORED_URLS = 10; + for (int i = 0, size = qMin(MAX_STORED_URLS, count()); i < size; i++) { + urls.append(itemText(i)); + } + QSettings().setValue("editorURLs", urls); +} + Vec3Editor::Vec3Editor(QWidget* parent) : QWidget(parent) { QHBoxLayout* layout = new QHBoxLayout(); layout->setContentsMargins(QMargins()); @@ -200,7 +262,8 @@ void Vec3Editor::updateVector() { QDoubleSpinBox* Vec3Editor::createComponentBox() { QDoubleSpinBox* box = new QDoubleSpinBox(); box->setMinimum(-FLT_MAX); - box->setMaximumWidth(100); + box->setMaximum(FLT_MAX); + box->setMinimumWidth(50); connect(box, SIGNAL(valueChanged(double)), SLOT(updateVector())); return box; } @@ -252,8 +315,9 @@ ParameterizedURLEditor::ParameterizedURLEditor(QWidget* parent) : lineContainer->setLayout(lineLayout); lineLayout->setContentsMargins(QMargins()); - lineLayout->addWidget(_line = new QLineEdit(), 1); - connect(_line, SIGNAL(textChanged(const QString&)), SLOT(updateURL())); + lineLayout->addWidget(&_urlEditor, 1); + connect(&_urlEditor, SIGNAL(urlChanged(const QUrl&)), SLOT(updateURL())); + connect(&_urlEditor, SIGNAL(urlChanged(const QUrl&)), SLOT(updateParameters())); QPushButton* refresh = new QPushButton("..."); connect(refresh, SIGNAL(clicked(bool)), SLOT(updateParameters())); @@ -261,8 +325,7 @@ ParameterizedURLEditor::ParameterizedURLEditor(QWidget* parent) : } void ParameterizedURLEditor::setURL(const ParameterizedURL& url) { - _url = url; - _line->setText(url.getURL().toString()); + _urlEditor.setURL((_url = url).getURL()); updateParameters(); } @@ -279,7 +342,7 @@ void ParameterizedURLEditor::updateURL() { widget->property("parameterName").toString()), widgetProperty.read(widget)); } } - emit urlChanged(_url = ParameterizedURL(_line->text(), parameters)); + emit urlChanged(_url = ParameterizedURL(_urlEditor.getURL(), parameters)); if (_program) { _program->disconnect(this); } diff --git a/libraries/metavoxels/src/MetavoxelUtil.h b/libraries/metavoxels/src/MetavoxelUtil.h index 7cfedd7a62..4b3fb43523 100644 --- a/libraries/metavoxels/src/MetavoxelUtil.h +++ b/libraries/metavoxels/src/MetavoxelUtil.h @@ -10,29 +10,21 @@ #define __interface__MetavoxelUtil__ #include +#include #include #include -#include #include -#include #include #include "Bitstream.h" class QByteArray; class QDoubleSpinBox; -class QLineEdit; class QPushButton; -class HifiSockAddr; class NetworkProgram; -/// Reads and returns the session ID from a datagram. -/// \param[out] headerPlusIDSize the size of the header (including the session ID) within the data -/// \return the session ID, or a null ID if invalid (in which case a warning will be logged) -QUuid readSessionID(const QByteArray& data, const SharedNodePointer& sendingNode, int& headerPlusIDSize); - /// Performs the runtime equivalent of Qt's SIGNAL macro, which is to attach a prefix to the signature. QByteArray signal(const char* signature); @@ -45,11 +37,44 @@ public: STREAM glm::vec3 minimum; STREAM glm::vec3 maximum; + Box(const glm::vec3& minimum = glm::vec3(), const glm::vec3& maximum = glm::vec3()); + bool contains(const Box& other) const; + + bool intersects(const Box& other) const; + + float getLongestSide() const { return qMax(qMax(maximum.x - minimum.x, maximum.y - minimum.y), maximum.z - minimum.z); } }; DECLARE_STREAMABLE_METATYPE(Box) +/// Editor for meta-object values. +class QMetaObjectEditor : public QWidget { + Q_OBJECT + Q_PROPERTY(const QMetaObject* metaObject MEMBER _metaObject WRITE setMetaObject NOTIFY metaObjectChanged USER true) + +public: + + QMetaObjectEditor(QWidget* parent); + +signals: + + void metaObjectChanged(const QMetaObject* metaObject); + +public slots: + + void setMetaObject(const QMetaObject* metaObject); + +private slots: + + void updateMetaObject(); + +private: + + QComboBox* _box; + const QMetaObject* _metaObject; +}; + /// Editor for color values. class QColorEditor : public QWidget { Q_OBJECT @@ -77,6 +102,32 @@ private: QColor _color; }; +/// Editor for URL values. +class QUrlEditor : public QComboBox { + Q_OBJECT + Q_PROPERTY(QUrl url READ getURL WRITE setURL NOTIFY urlChanged USER true) + +public: + + QUrlEditor(QWidget* parent = NULL); + + void setURL(const QUrl& url); + const QUrl& getURL() { return _url; } + +signals: + + void urlChanged(const QUrl& url); + +private slots: + + void updateURL(const QString& text); + void updateSettings(); + +private: + + QUrl _url; +}; + /// Editor for vector values. class Vec3Editor : public QWidget { Q_OBJECT @@ -170,7 +221,7 @@ private: ParameterizedURL _url; QSharedPointer _program; - QLineEdit* _line; + QUrlEditor _urlEditor; }; #endif /* defined(__interface__MetavoxelUtil__) */ diff --git a/libraries/metavoxels/src/SharedObject.cpp b/libraries/metavoxels/src/SharedObject.cpp index f97e285bcf..7e10068afe 100644 --- a/libraries/metavoxels/src/SharedObject.cpp +++ b/libraries/metavoxels/src/SharedObject.cpp @@ -16,7 +16,9 @@ #include "MetavoxelUtil.h" #include "SharedObject.h" -SharedObject::SharedObject() : _referenceCount(0) { +REGISTER_META_OBJECT(SharedObject) + +SharedObject::SharedObject() : _id(++_lastID), _referenceCount(0) { } void SharedObject::incrementReferenceCount() { @@ -72,63 +74,17 @@ bool SharedObject::equals(const SharedObject* other) const { return true; } -SharedObjectPointer::SharedObjectPointer(SharedObject* data) : _data(data) { - if (_data) { - _data->incrementReferenceCount(); +void SharedObject::dump(QDebug debug) const { + debug << this; + const QMetaObject* metaObject = this->metaObject(); + for (int i = 0; i < metaObject->propertyCount(); i++) { + debug << metaObject->property(i).name() << metaObject->property(i).read(this); } } -SharedObjectPointer::SharedObjectPointer(const SharedObjectPointer& other) : _data(other._data) { - if (_data) { - _data->incrementReferenceCount(); - } -} +int SharedObject::_lastID = 0; -SharedObjectPointer::~SharedObjectPointer() { - if (_data) { - _data->decrementReferenceCount(); - } -} - -void SharedObjectPointer::detach() { - if (_data && _data->getReferenceCount() > 1) { - _data->decrementReferenceCount(); - (_data = _data->clone())->incrementReferenceCount(); - } -} - -void SharedObjectPointer::reset() { - if (_data) { - _data->decrementReferenceCount(); - } - _data = NULL; -} - -SharedObjectPointer& SharedObjectPointer::operator=(SharedObject* data) { - if (_data) { - _data->decrementReferenceCount(); - } - if ((_data = data)) { - _data->incrementReferenceCount(); - } - return *this; -} - -SharedObjectPointer& SharedObjectPointer::operator=(const SharedObjectPointer& other) { - if (_data) { - _data->decrementReferenceCount(); - } - if ((_data = other._data)) { - _data->incrementReferenceCount(); - } - return *this; -} - -uint qHash(const SharedObjectPointer& pointer, uint seed) { - return qHash(pointer.data(), seed); -} - -SharedObjectEditor::SharedObjectEditor(const QMetaObject* metaObject, QWidget* parent) : QWidget(parent) { +SharedObjectEditor::SharedObjectEditor(const QMetaObject* metaObject, bool nullable, QWidget* parent) : QWidget(parent) { QVBoxLayout* layout = new QVBoxLayout(); layout->setAlignment(Qt::AlignTop); setLayout(layout); @@ -137,11 +93,17 @@ SharedObjectEditor::SharedObjectEditor(const QMetaObject* metaObject, QWidget* p layout->addLayout(form); form->addRow("Type:", _type = new QComboBox()); - _type->addItem("(none)"); + if (nullable) { + _type->addItem("(none)"); + } foreach (const QMetaObject* metaObject, Bitstream::getMetaObjectSubClasses(metaObject)) { - _type->addItem(metaObject->className(), QVariant::fromValue(metaObject)); + // add add constructable subclasses + if (metaObject->constructorCount() > 0) { + _type->addItem(metaObject->className(), QVariant::fromValue(metaObject)); + } } connect(_type, SIGNAL(currentIndexChanged(int)), SLOT(updateType())); + updateType(); } void SharedObjectEditor::setObject(const SharedObjectPointer& object) { @@ -158,6 +120,22 @@ void SharedObjectEditor::setObject(const SharedObjectPointer& object) { } } +void SharedObjectEditor::detachObject() { + SharedObject* oldObject = _object.data(); + if (!_object.detach()) { + return; + } + oldObject->disconnect(this); + const QMetaObject* metaObject = _object->metaObject(); + + QFormLayout* form = static_cast(layout()->itemAt(1)); + for (int i = 0; i < form->rowCount(); i++) { + QWidget* widget = form->itemAt(i, QFormLayout::FieldRole)->widget(); + QMetaProperty property = metaObject->property(widget->property("propertyIndex").toInt()); + connect(_object.data(), signal(property.notifySignal().methodSignature()), SLOT(updateProperty())); + } +} + const QMetaObject* getOwningAncestor(const QMetaObject* metaObject, int propertyIndex) { while (propertyIndex < metaObject->propertyOffset()) { metaObject = metaObject->superClass(); @@ -178,19 +156,26 @@ void SharedObjectEditor::updateType() { } delete form; } + QObject* oldObject = static_cast(_object.data()); + const QMetaObject* oldMetaObject = NULL; + if (oldObject) { + oldMetaObject = oldObject->metaObject(); + oldObject->disconnect(this); + } const QMetaObject* metaObject = _type->itemData(_type->currentIndex()).value(); if (metaObject == NULL) { _object.reset(); return; } - QObject* oldObject = static_cast(_object.data()); - const QMetaObject* oldMetaObject = oldObject ? oldObject->metaObject() : NULL; QObject* newObject = metaObject->newInstance(); QFormLayout* form = new QFormLayout(); static_cast(layout())->addLayout(form); for (int i = QObject::staticMetaObject.propertyCount(); i < metaObject->propertyCount(); i++) { QMetaProperty property = metaObject->property(i); + if (!property.isDesignable()) { + continue; + } if (oldMetaObject && i < oldMetaObject->propertyCount() && getOwningAncestor(metaObject, i) == getOwningAncestor(oldMetaObject, i)) { // copy the state of the shared ancestry @@ -207,6 +192,10 @@ void SharedObjectEditor::updateType() { if (widgetProperty.hasNotifySignal()) { connect(widget, signal(widgetProperty.notifySignal().methodSignature()), SLOT(propertyChanged())); } + if (property.hasNotifySignal()) { + widget->setProperty("notifySignalIndex", property.notifySignalIndex()); + connect(newObject, signal(property.notifySignal().methodSignature()), SLOT(updateProperty())); + } } } _object = static_cast(newObject); @@ -219,10 +208,23 @@ void SharedObjectEditor::propertyChanged() { if (widget != sender()) { continue; } - _object.detach(); + detachObject(); QObject* object = _object.data(); QMetaProperty property = object->metaObject()->property(widget->property("propertyIndex").toInt()); QByteArray valuePropertyName = QItemEditorFactory::defaultFactory()->valuePropertyName(property.userType()); property.write(object, widget->property(valuePropertyName)); } } + +void SharedObjectEditor::updateProperty() { + QFormLayout* form = static_cast(layout()->itemAt(1)); + for (int i = 0; i < form->rowCount(); i++) { + QWidget* widget = form->itemAt(i, QFormLayout::FieldRole)->widget(); + if (widget->property("notifySignalIndex").toInt() != senderSignalIndex()) { + continue; + } + QMetaProperty property = _object->metaObject()->property(widget->property("propertyIndex").toInt()); + QByteArray valuePropertyName = QItemEditorFactory::defaultFactory()->valuePropertyName(property.userType()); + widget->setProperty(valuePropertyName, property.read(_object.data())); + } +} diff --git a/libraries/metavoxels/src/SharedObject.h b/libraries/metavoxels/src/SharedObject.h index e439c4c7f0..9895073e44 100644 --- a/libraries/metavoxels/src/SharedObject.h +++ b/libraries/metavoxels/src/SharedObject.h @@ -11,7 +11,9 @@ #include #include +#include #include +#include class QComboBox; @@ -21,7 +23,9 @@ class SharedObject : public QObject { public: - SharedObject(); + Q_INVOKABLE SharedObject(); + + int getID() { return _id; } int getReferenceCount() const { return _referenceCount; } void incrementReferenceCount(); @@ -33,6 +37,9 @@ public: /// Tests this object for equality with another. virtual bool equals(const SharedObject* other) const; + // Dumps the contents of this object to the debug output. + virtual void dump(QDebug debug = QDebug(QtDebugMsg)) const; + signals: /// Emitted when the reference count drops to one. @@ -40,62 +47,133 @@ signals: private: + int _id; int _referenceCount; + + static int _lastID; }; /// A pointer to a shared object. -class SharedObjectPointer { +template class SharedObjectPointerTemplate { public: - SharedObjectPointer(SharedObject* data = NULL); - SharedObjectPointer(const SharedObjectPointer& other); - ~SharedObjectPointer(); + SharedObjectPointerTemplate(T* data = NULL); + SharedObjectPointerTemplate(const SharedObjectPointerTemplate& other); + ~SharedObjectPointerTemplate(); - SharedObject* data() { return _data; } - const SharedObject* data() const { return _data; } - const SharedObject* constData() const { return _data; } + T* data() const { return _data; } + + /// "Detaches" this object, making a new copy if its reference count is greater than one. + bool detach(); - void detach(); - - void swap(SharedObjectPointer& other) { qSwap(_data, other._data); } + void swap(SharedObjectPointerTemplate& other) { qSwap(_data, other._data); } void reset(); - operator SharedObject*() { return _data; } - operator const SharedObject*() const { return _data; } - bool operator!() const { return !_data; } - - bool operator!=(const SharedObjectPointer& other) const { return _data != other._data; } - - SharedObject& operator*() { return *_data; } - const SharedObject& operator*() const { return *_data; } - - SharedObject* operator->() { return _data; } - const SharedObject* operator->() const { return _data; } - - SharedObjectPointer& operator=(SharedObject* data); - SharedObjectPointer& operator=(const SharedObjectPointer& other); - - bool operator==(const SharedObjectPointer& other) const { return _data == other._data; } - -private: + operator T*() const { return _data; } + T& operator*() const { return *_data; } + T* operator->() const { return _data; } - SharedObject* _data; + template SharedObjectPointerTemplate staticCast() const; + + SharedObjectPointerTemplate& operator=(T* data); + SharedObjectPointerTemplate& operator=(const SharedObjectPointerTemplate& other); + + bool operator==(const SharedObjectPointerTemplate& other) const { return _data == other._data; } + bool operator!=(const SharedObjectPointerTemplate& other) const { return _data != other._data; } + +private: + + T* _data; }; +template inline SharedObjectPointerTemplate::SharedObjectPointerTemplate(T* data) : _data(data) { + if (_data) { + _data->incrementReferenceCount(); + } +} + +template inline SharedObjectPointerTemplate::SharedObjectPointerTemplate(const SharedObjectPointerTemplate& other) : + _data(other._data) { + + if (_data) { + _data->incrementReferenceCount(); + } +} + +template inline SharedObjectPointerTemplate::~SharedObjectPointerTemplate() { + if (_data) { + _data->decrementReferenceCount(); + } +} + +template inline bool SharedObjectPointerTemplate::detach() { + if (_data && _data->getReferenceCount() > 1) { + _data->decrementReferenceCount(); + (_data = _data->clone())->incrementReferenceCount(); + return true; + } + return false; +} + +template inline void SharedObjectPointerTemplate::reset() { + if (_data) { + _data->decrementReferenceCount(); + } + _data = NULL; +} + +template template inline SharedObjectPointerTemplate SharedObjectPointerTemplate::staticCast() const { + return SharedObjectPointerTemplate(static_cast(_data)); +} + +template inline SharedObjectPointerTemplate& SharedObjectPointerTemplate::operator=(T* data) { + if (_data) { + _data->decrementReferenceCount(); + } + if ((_data = data)) { + _data->incrementReferenceCount(); + } + return *this; +} + +template inline SharedObjectPointerTemplate& SharedObjectPointerTemplate::operator=( + const SharedObjectPointerTemplate& other) { + if (_data) { + _data->decrementReferenceCount(); + } + if ((_data = other._data)) { + _data->incrementReferenceCount(); + } + return *this; +} + +template uint qHash(const SharedObjectPointerTemplate& pointer, uint seed = 0) { + return qHash(pointer.data(), seed); +} + +typedef SharedObjectPointerTemplate SharedObjectPointer; + Q_DECLARE_METATYPE(SharedObjectPointer) -uint qHash(const SharedObjectPointer& pointer, uint seed = 0); +typedef QSet SharedObjectSet; + +Q_DECLARE_METATYPE(SharedObjectSet) /// Allows editing shared object instances. class SharedObjectEditor : public QWidget { Q_OBJECT - Q_PROPERTY(SharedObjectPointer object MEMBER _object WRITE setObject USER true) + Q_PROPERTY(SharedObjectPointer object READ getObject WRITE setObject USER true) public: - SharedObjectEditor(const QMetaObject* metaObject, QWidget* parent); + SharedObjectEditor(const QMetaObject* metaObject, bool nullable = true, QWidget* parent = NULL); + + const SharedObjectPointer& getObject() const { return _object; } + + /// "Detaches" the object pointer, copying it if anyone else is holding a reference. + void detachObject(); public slots: @@ -105,6 +183,7 @@ private slots: void updateType(); void propertyChanged(); + void updateProperty(); private: diff --git a/libraries/shared/src/PacketHeaders.h b/libraries/shared/src/PacketHeaders.h index 79e87f3bf7..075598d2a9 100644 --- a/libraries/shared/src/PacketHeaders.h +++ b/libraries/shared/src/PacketHeaders.h @@ -54,6 +54,7 @@ enum PacketType { PacketTypeParticleAddResponse, PacketTypeMetavoxelData, PacketTypeAvatarIdentity, + PacketTypeAvatarBillboard, PacketTypeDomainConnectRequest, PacketTypeDomainServerAuthRequest }; diff --git a/libraries/voxels/src/VoxelsScriptingInterface.cpp b/libraries/voxels/src/VoxelsScriptingInterface.cpp index c0468ccca6..db61460d69 100644 --- a/libraries/voxels/src/VoxelsScriptingInterface.cpp +++ b/libraries/voxels/src/VoxelsScriptingInterface.cpp @@ -12,14 +12,45 @@ void VoxelsScriptingInterface::queueVoxelAdd(PacketType addPacketType, VoxelDeta getVoxelPacketSender()->queueVoxelEditMessages(addPacketType, 1, &addVoxelDetails); } +VoxelDetail VoxelsScriptingInterface::getVoxelAt(float x, float y, float z, float scale) { + // setup a VoxelDetail struct with the data + VoxelDetail result = {0,0,0,0,0,0,0}; + + if (_tree) { + _tree->lockForRead(); + + VoxelTreeElement* voxel = static_cast(_tree->getOctreeElementAt(x / (float)TREE_SCALE, y / (float)TREE_SCALE, + z / (float)TREE_SCALE, scale / (float)TREE_SCALE)); + _tree->unlock(); + if (voxel) { + // Note: these need to be in voxel space because the VoxelDetail -> js converter will upscale + result.x = voxel->getCorner().x; + result.y = voxel->getCorner().y; + result.z = voxel->getCorner().z; + result.s = voxel->getScale(); + result.red = voxel->getColor()[RED_INDEX]; + result.green = voxel->getColor()[GREEN_INDEX]; + result.blue = voxel->getColor()[BLUE_INDEX]; + } + } + return result; +} + void VoxelsScriptingInterface::setVoxelNonDestructive(float x, float y, float z, float scale, uchar red, uchar green, uchar blue) { // setup a VoxelDetail struct with the data VoxelDetail addVoxelDetail = {x / (float)TREE_SCALE, y / (float)TREE_SCALE, z / (float)TREE_SCALE, scale / (float)TREE_SCALE, red, green, blue}; - // queue the packet + // queue the add packet queueVoxelAdd(PacketTypeVoxelSet, addVoxelDetail); + + // handle the local tree also... + if (_tree) { + _tree->lockForWrite(); + _tree->createVoxel(addVoxelDetail.x, addVoxelDetail.y, addVoxelDetail.z, addVoxelDetail.s, red, green, blue, false); + _tree->unlock(); + } } void VoxelsScriptingInterface::setVoxel(float x, float y, float z, float scale, @@ -30,6 +61,13 @@ void VoxelsScriptingInterface::setVoxel(float x, float y, float z, float scale, // queue the destructive add queueVoxelAdd(PacketTypeVoxelSetDestructive, addVoxelDetail); + + // handle the local tree also... + if (_tree) { + _tree->lockForWrite(); + _tree->createVoxel(addVoxelDetail.x, addVoxelDetail.y, addVoxelDetail.z, addVoxelDetail.s, red, green, blue, true); + _tree->unlock(); + } } void VoxelsScriptingInterface::eraseVoxel(float x, float y, float z, float scale) { @@ -39,6 +77,13 @@ void VoxelsScriptingInterface::eraseVoxel(float x, float y, float z, float scale scale / (float)TREE_SCALE, 0, 0, 0}; getVoxelPacketSender()->queueVoxelEditMessages(PacketTypeVoxelErase, 1, &deleteVoxelDetail); + + // handle the local tree also... + if (_tree) { + _tree->lockForWrite(); + _tree->deleteVoxelAt(deleteVoxelDetail.x, deleteVoxelDetail.y, deleteVoxelDetail.z, deleteVoxelDetail.s); + _tree->unlock(); + } } @@ -64,3 +109,21 @@ RayToVoxelIntersectionResult VoxelsScriptingInterface::findRayIntersection(const } return result; } + +glm::vec3 VoxelsScriptingInterface::getFaceVector(const QString& face) { + if (face == "MIN_X_FACE") { + return glm::vec3(-1, 0, 0); + } else if (face == "MAX_X_FACE") { + return glm::vec3(1, 0, 0); + } else if (face == "MIN_Y_FACE") { + return glm::vec3(0, -1, 0); + } else if (face == "MAX_Y_FACE") { + return glm::vec3(0, 1, 0); + } else if (face == "MIN_Z_FACE") { + return glm::vec3(0, 0, -1); + } else if (face == "MAX_Z_FACE") { + return glm::vec3(0, 0, 1); + } + return glm::vec3(0, 0, 0); //error case +} + diff --git a/libraries/voxels/src/VoxelsScriptingInterface.h b/libraries/voxels/src/VoxelsScriptingInterface.h index f87e8d0a4c..fb0bd6ab03 100644 --- a/libraries/voxels/src/VoxelsScriptingInterface.h +++ b/libraries/voxels/src/VoxelsScriptingInterface.h @@ -30,6 +30,14 @@ public: void setVoxelTree(VoxelTree* tree) { _tree = tree; } public slots: + + /// checks the local voxel tree for a voxel at the specified location and scale + /// \param x the x-coordinate of the voxel (in meter units) + /// \param y the y-coordinate of the voxel (in meter units) + /// \param z the z-coordinate of the voxel (in meter units) + /// \param scale the scale of the voxel (in meter units) + VoxelDetail getVoxelAt(float x, float y, float z, float scale); + /// queues the creation of a voxel which will be sent by calling process on the PacketSender /// \param x the x-coordinate of the voxel (in meter units) /// \param y the y-coordinate of the voxel (in meter units) @@ -60,6 +68,9 @@ public slots: /// If the scripting context has visible voxels, this will determine a ray intersection RayToVoxelIntersectionResult findRayIntersection(const PickRay& ray); + /// returns a voxel space axis aligned vector for the face, useful in doing voxel math + glm::vec3 getFaceVector(const QString& face); + private: void queueVoxelAdd(PacketType addPacketType, VoxelDetail& addVoxelDetails); VoxelTree* _tree; diff --git a/tests/metavoxels/CMakeLists.txt b/tests/metavoxels/CMakeLists.txt index 416f398470..9d21dd2a44 100644 --- a/tests/metavoxels/CMakeLists.txt +++ b/tests/metavoxels/CMakeLists.txt @@ -12,12 +12,12 @@ find_package(Qt5Network REQUIRED) find_package(Qt5Script REQUIRED) find_package(Qt5Widgets REQUIRED) -include(${MACRO_DIR}/SetupHifiProject.cmake) -setup_hifi_project(${TARGET_NAME} TRUE) - include(${MACRO_DIR}/AutoMTC.cmake) auto_mtc(${TARGET_NAME} ${ROOT_DIR}) +include(${MACRO_DIR}/SetupHifiProject.cmake) +setup_hifi_project(${TARGET_NAME} TRUE ${AUTOMTC_SRC}) + qt5_use_modules(${TARGET_NAME} Network Script Widgets) #include glm diff --git a/tests/metavoxels/src/MetavoxelTests.cpp b/tests/metavoxels/src/MetavoxelTests.cpp index 15d7463742..530f7e3108 100644 --- a/tests/metavoxels/src/MetavoxelTests.cpp +++ b/tests/metavoxels/src/MetavoxelTests.cpp @@ -22,10 +22,10 @@ static int highPriorityMessagesSent = 0; static int highPriorityMessagesReceived = 0; static int unreliableMessagesSent = 0; static int unreliableMessagesReceived = 0; +static int reliableMessagesSent = 0; +static int reliableMessagesReceived = 0; static int streamedBytesSent = 0; static int streamedBytesReceived = 0; -static int lowPriorityStreamedBytesSent = 0; -static int lowPriorityStreamedBytesReceived = 0; bool MetavoxelTests::run() { @@ -51,9 +51,8 @@ bool MetavoxelTests::run() { qDebug() << "Sent" << highPriorityMessagesSent << "high priority messages, received" << highPriorityMessagesReceived; qDebug() << "Sent" << unreliableMessagesSent << "unreliable messages, received" << unreliableMessagesReceived; + qDebug() << "Sent" << reliableMessagesSent << "reliable messages, received" << reliableMessagesReceived; qDebug() << "Sent" << streamedBytesSent << "streamed bytes, received" << streamedBytesReceived; - qDebug() << "Sent" << lowPriorityStreamedBytesSent << "low-priority streamed bytes, received" << - lowPriorityStreamedBytesReceived; qDebug() << "Sent" << datagramsSent << "datagrams, received" << datagramsReceived; qDebug() << "All tests passed!"; @@ -77,24 +76,31 @@ static QByteArray createRandomBytes() { Endpoint::Endpoint(const QByteArray& datagramHeader) : _sequencer(new DatagramSequencer(datagramHeader, this)), - _highPriorityMessagesToSend(0.0f) { + _highPriorityMessagesToSend(0.0f), + _reliableMessagesToSend(0.0f) { connect(_sequencer, SIGNAL(readyToWrite(const QByteArray&)), SLOT(sendDatagram(const QByteArray&))); connect(_sequencer, SIGNAL(readyToRead(Bitstream&)), SLOT(readMessage(Bitstream&))); connect(_sequencer, SIGNAL(receivedHighPriorityMessage(const QVariant&)), SLOT(handleHighPriorityMessage(const QVariant&))); - connect(&_sequencer->getReliableInputChannel()->getBuffer(), SIGNAL(readyRead()), SLOT(readReliableChannel())); - connect(&_sequencer->getReliableInputChannel(1)->getBuffer(), SIGNAL(readyRead()), SLOT(readLowPriorityReliableChannel())); + + connect(_sequencer->getReliableInputChannel(), SIGNAL(receivedMessage(const QVariant&)), + SLOT(handleReliableMessage(const QVariant&))); + + ReliableChannel* secondInput = _sequencer->getReliableInputChannel(1); + secondInput->setMessagesEnabled(false); + connect(&secondInput->getBuffer(), SIGNAL(readyRead()), SLOT(readReliableChannel())); // enqueue a large amount of data in a low-priority channel ReliableChannel* output = _sequencer->getReliableOutputChannel(1); output->setPriority(0.25f); - const int MIN_LOW_PRIORITY_DATA = 100000; - const int MAX_LOW_PRIORITY_DATA = 200000; - QByteArray bytes = createRandomBytes(MIN_LOW_PRIORITY_DATA, MAX_LOW_PRIORITY_DATA); - _lowPriorityDataStreamed.append(bytes); + output->setMessagesEnabled(false); + const int MIN_STREAM_BYTES = 100000; + const int MAX_STREAM_BYTES = 200000; + QByteArray bytes = createRandomBytes(MIN_STREAM_BYTES, MAX_STREAM_BYTES); + _dataStreamed.append(bytes); output->getBuffer().write(bytes); - lowPriorityStreamedBytesSent += bytes.size(); + streamedBytesSent += bytes.size(); } static QVariant createRandomMessage() { @@ -160,13 +166,17 @@ bool Endpoint::simulate(int iterationNumber) { _highPriorityMessagesToSend -= 1.0f; } - // stream some random data - const int MIN_BYTES_TO_STREAM = 10; - const int MAX_BYTES_TO_STREAM = 100; - QByteArray bytes = createRandomBytes(MIN_BYTES_TO_STREAM, MAX_BYTES_TO_STREAM); - _dataStreamed.append(bytes); - streamedBytesSent += bytes.size(); - _sequencer->getReliableOutputChannel()->getDataStream().writeRawData(bytes.constData(), bytes.size()); + // and some number of reliable messages + const float MIN_RELIABLE_MESSAGES = 0.0f; + const float MAX_RELIABLE_MESSAGES = 4.0f; + _reliableMessagesToSend += randFloatInRange(MIN_RELIABLE_MESSAGES, MAX_RELIABLE_MESSAGES); + while (_reliableMessagesToSend >= 1.0f) { + QVariant message = createRandomMessage(); + _reliableMessagesSent.append(message); + _sequencer->getReliableOutputChannel()->sendMessage(message); + reliableMessagesSent++; + _reliableMessagesToSend -= 1.0f; + } // send a packet try { @@ -243,8 +253,19 @@ void Endpoint::readMessage(Bitstream& in) { throw QString("Received unsent/already sent unreliable message."); } +void Endpoint::handleReliableMessage(const QVariant& message) { + if (_other->_reliableMessagesSent.isEmpty()) { + throw QString("Received unsent/already sent reliable message."); + } + QVariant sentMessage = _other->_reliableMessagesSent.takeFirst(); + if (!messagesEqual(message, sentMessage)) { + throw QString("Sent/received reliable message mismatch."); + } + reliableMessagesReceived++; +} + void Endpoint::readReliableChannel() { - CircularBuffer& buffer = _sequencer->getReliableInputChannel()->getBuffer(); + CircularBuffer& buffer = _sequencer->getReliableInputChannel(1)->getBuffer(); QByteArray bytes = buffer.read(buffer.bytesAvailable()); if (_other->_dataStreamed.size() < bytes.size()) { throw QString("Received unsent/already sent streamed data."); @@ -256,17 +277,3 @@ void Endpoint::readReliableChannel() { } streamedBytesReceived += bytes.size(); } - -void Endpoint::readLowPriorityReliableChannel() { - CircularBuffer& buffer = _sequencer->getReliableInputChannel(1)->getBuffer(); - QByteArray bytes = buffer.read(buffer.bytesAvailable()); - if (_other->_lowPriorityDataStreamed.size() < bytes.size()) { - throw QString("Received unsent/already sent low-priority streamed data."); - } - QByteArray compare = _other->_lowPriorityDataStreamed.readBytes(0, bytes.size()); - _other->_lowPriorityDataStreamed.remove(bytes.size()); - if (compare != bytes) { - throw QString("Sent/received low-priority streamed data mismatch."); - } - lowPriorityStreamedBytesReceived += bytes.size(); -} diff --git a/tests/metavoxels/src/MetavoxelTests.h b/tests/metavoxels/src/MetavoxelTests.h index b73f7eb07e..f19870ac15 100644 --- a/tests/metavoxels/src/MetavoxelTests.h +++ b/tests/metavoxels/src/MetavoxelTests.h @@ -48,9 +48,9 @@ private slots: void sendDatagram(const QByteArray& datagram); void handleHighPriorityMessage(const QVariant& message); void readMessage(Bitstream& in); + void handleReliableMessage(const QVariant& message); void readReliableChannel(); - void readLowPriorityReliableChannel(); - + private: DatagramSequencer* _sequencer; @@ -59,8 +59,9 @@ private: float _highPriorityMessagesToSend; QVariantList _highPriorityMessagesSent; QList _unreliableMessagesSent; + float _reliableMessagesToSend; + QVariantList _reliableMessagesSent; CircularBuffer _dataStreamed; - CircularBuffer _lowPriorityDataStreamed; }; /// A simple test message. @@ -88,7 +89,7 @@ public: DECLARE_STREAMABLE_METATYPE(TestMessageB) // A test message that demonstrates inheritance and composition. -class TestMessageC : public TestMessageA { +class TestMessageC : STREAM public TestMessageA { STREAMABLE public: diff --git a/tools/mtc/src/main.cpp b/tools/mtc/src/main.cpp index 050fe0e418..248c2ddd2d 100644 --- a/tools/mtc/src/main.cpp +++ b/tools/mtc/src/main.cpp @@ -121,7 +121,7 @@ void generateOutput (QTextStream& out, const QList& streamables) { out << " &&\n"; out << " "; } - out << "static_cast<" << base << "&>(first) == static_cast<" << base << "&>(second)"; + out << "static_cast(first) == static_cast(second)"; first = false; } foreach (const QString& field, str.fields) { @@ -147,7 +147,7 @@ void generateOutput (QTextStream& out, const QList& streamables) { out << " ||\n"; out << " "; } - out << "static_cast<" << base << "&>(first) != static_cast<" << base << "&>(second)"; + out << "static_cast(first) != static_cast(second)"; first = false; } foreach (const QString& field, str.fields) {