From b0f27d5b3a5d79aeb165d220db938a7a4a5b5427 Mon Sep 17 00:00:00 2001
From: Rainer Kartmann <rainer.kartmann@kit.edu>
Date: Wed, 21 Apr 2021 17:54:40 +0200
Subject: [PATCH] Improve/fix handling of attachment and updates

---
 .../ObjectPoseObserver/ObjectPoseObserver.cpp |  21 +-
 .../ObjectPoseObserver/ObjectPoseObserver.h   |   3 +-
 .../ObjectPoseObserver/detail/Data.cpp        | 383 +++++++++++-------
 .../ObjectPoseObserver/detail/Data.h          |  75 +++-
 .../ObjectPoseObserver/detail/Visu.cpp        |  11 +-
 .../ObjectPoseObserverInterface.ice           |  17 +-
 6 files changed, 320 insertions(+), 190 deletions(-)

diff --git a/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.cpp b/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.cpp
index 30b7449f0..de71bb557 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.cpp
+++ b/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.cpp
@@ -407,10 +407,11 @@ namespace armarx
         return data.detachObjectFromRobotNode(input);
     }
 
-    objpose::DetachAllObjectsFromRobotNodesOutput ObjectPoseObserver::detachAllObjectsFromRobotNodes(const Ice::Current&)
+    objpose::DetachAllObjectsFromRobotNodesOutput ObjectPoseObserver::detachAllObjectsFromRobotNodes(
+        const objpose::DetachAllObjectsFromRobotNodesInput& input, const Ice::Current&)
     {
         std::scoped_lock lock(dataMutex);
-        return data.detachAllObjectsFromRobotNodes();
+        return data.detachAllObjectsFromRobotNodes(input);
     }
 
 
@@ -492,10 +493,15 @@ namespace armarx
         using namespace armarx::RemoteGui::Client;
 
         tab.visu.setup(this->visu);
+        tab.data.setup(this->data);
         tab.decay.setup(this->data.decay);
         tab.robotHead.setup(this->robotHead);
 
-        VBoxLayout root = {tab.visu.group, tab.decay.group, tab.robotHead.group, VSpacer()};
+        VBoxLayout root =
+        {
+            tab.visu.group, tab.data.group, tab.decay.group, tab.robotHead.group,
+            VSpacer()
+        };
         RemoteGui_createTab(getName(), root, &tab);
     }
 
@@ -506,14 +512,15 @@ namespace armarx
             std::scoped_lock lock(visuMutex);
             tab.visu.update(this->visu);
         }
-        {
-            std::scoped_lock lock(robotHeadMutex);
-            tab.robotHead.update(this->robotHead);
-        }
         {
             std::scoped_lock lock(dataMutex);
+            tab.data.update(this->data);
             tab.decay.update(this->data.decay);
         }
+        {
+            std::scoped_lock lock(robotHeadMutex);
+            tab.robotHead.update(this->robotHead);
+        }
     }
 
     void ObjectPoseObserver::Calibration::defineProperties(PropertyDefinitionsPtr defs, const std::string& prefix)
diff --git a/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.h b/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.h
index c079245f7..c0a7025dc 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.h
+++ b/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.h
@@ -103,7 +103,7 @@ namespace armarx
 
         objpose::AttachObjectToRobotNodeOutput attachObjectToRobotNode(const objpose::AttachObjectToRobotNodeInput& input, ICE_CURRENT_ARG) override;
         objpose::DetachObjectFromRobotNodeOutput detachObjectFromRobotNode(const objpose::DetachObjectFromRobotNodeInput& input, ICE_CURRENT_ARG) override;
-        objpose::DetachAllObjectsFromRobotNodesOutput detachAllObjectsFromRobotNodes(ICE_CURRENT_ARG) override;
+        objpose::DetachAllObjectsFromRobotNodesOutput detachAllObjectsFromRobotNodes(const objpose::DetachAllObjectsFromRobotNodesInput& input, ICE_CURRENT_ARG) override;
 
         objpose::AgentFramesSeq getAttachableFrames(ICE_CURRENT_ARG) override;
 
@@ -174,6 +174,7 @@ namespace armarx
         struct RemoteGuiTab : RemoteGui::Client::Tab
         {
             objpose::observer::Visu::RemoteGui visu;
+            objpose::observer::Data::RemoteGui data;
             objpose::observer::Decay::RemoteGui decay;
             objpose::observer::RobotHeadMovement::RemoteGui robotHead;
         };
diff --git a/source/RobotAPI/components/ObjectPoseObserver/detail/Data.cpp b/source/RobotAPI/components/ObjectPoseObserver/detail/Data.cpp
index 88080c61e..f98b4083c 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/detail/Data.cpp
+++ b/source/RobotAPI/components/ObjectPoseObserver/detail/Data.cpp
@@ -8,6 +8,7 @@
 #include <RobotAPI/libraries/ArmarXObjects/ice_conversions.h>
 #include <RobotAPI/libraries/aron/converter/eigen/EigenConverter.h>
 #include <RobotAPI/libraries/armem/client/Writer.h>
+#include <RobotAPI/libraries/armem/core/Visitor.h>
 
 #include <ArmarXCore/core/time/TimeUtil.h>
 
@@ -24,12 +25,43 @@ namespace armarx::objpose::observer
         memoryToIceAdapter(memoryToIceAdapter)
     {
         memory.name() = defaultMemoryName;
+
+        oobbCache.setFetchFn([this](const ObjectID & id) -> std::optional<simox::OrientedBoxf>
+        {
+            // Try to get OOBB from repository.
+            if (std::optional<ObjectInfo> objectInfo = objectFinder.findObject(id))
+            {
+                try
+                {
+                    return objectInfo->loadOOBB();
+                }
+                catch (const std::ios_base::failure& e)
+                {
+                    // Give up - no OOBB information.
+                    ARMARX_WARNING << "Could not get OOBB of object " << id << ".\n- " << e.what();
+                    return std::nullopt;
+                }
+            }
+            else
+            {
+                return std::nullopt;
+            }
+        });
+
+        classNameToDatasetCache.setFetchFn([this](const std::string & className)
+        {
+            std::optional<ObjectInfo> objectInfo = objectFinder.findObject(className);
+            return objectInfo ? objectInfo->dataset() : "";
+        });
     }
 
     void Data::defineProperties(armarx::PropertyDefinitionsPtr defs, const std::string& prefix)
     {
         defs->optional(memory.name(), "mem.MemoryName", "Name of this memory server.");
         defs->optional(maxHistorySize, "mem.MaxHistorySize", "Maximal size of object poses history (-1 for infinite).");
+        defs->optional(discardSnapshotsWhileAttached, "mem.DiscardSnapshotsWhileAttached",
+                       "If true, no new snapshots are stored while an object is attached to a robot node.\n"
+                       "If false, new snapshots are stored, but the attachment is kept in the new snapshots.");
 
         decay.defineProperties(defs, prefix + "decay.");
     }
@@ -48,14 +80,6 @@ namespace armarx::objpose::observer
     {
         CommitStats stats;
 
-        // This stays empty on the first report.
-        const armem::CoreSegment& coreSegment = getCoreSegment();
-        armem::ProviderSegment const* providerSegment = nullptr;
-        if (coreSegment.hasProviderSegment(providerName))
-        {
-            providerSegment = &coreSegment.getProviderSegment(providerName);
-        }
-
         // Build new poses.
         objpose::ObjectPoseSeq newObjectPoses;
         stats.numUpdated = 0;
@@ -63,48 +87,59 @@ namespace armarx::objpose::observer
         {
             const IceUtil::Time timestamp = IceUtil::Time::microSeconds(provided.timestampMicroSeconds);
 
-            // Check whether we have an old pose for this object.
+            // Check whether we have an old snapshot for this object.
             std::optional<objpose::ObjectPose> previousPose;
-            const std::string entityName = armarx::fromIce(provided.objectID).str();
-            if (providerSegment && providerSegment->hasEntity(entityName))
+            const armem::Entity* entity = findObjectEntity(armarx::fromIce(provided.objectID), providerName);
+            if (entity)
             {
-                const armem::Entity& entity = providerSegment->getEntity(entityName);
-                const objpose::arondto::ObjectPose data = objpose::observer::Data::getLatestInstanceData(entity);
+                const objpose::arondto::ObjectPose data = objpose::observer::Data::getLatestInstanceData(*entity);
 
                 previousPose = objpose::ObjectPose();
                 objpose::fromAron(data, *previousPose);
             }
 
+            bool discard = false;
             if (discardUpdatesUntil && timestamp < discardUpdatesUntil.value())
             {
-                if (previousPose)
+                // Dicard updates temporarily (e.g. due to head movement).
+                discard = true;
+            }
+            else if (previousPose)
+            {
+                if (discardSnapshotsWhileAttached && previousPose->attachment)
                 {
-                    // Keep the old one
-                    newObjectPoses.push_back(*previousPose);
+                    // Discard update due to active attachemnt.
+                    discard = true;
                 }
-                else
+                else if (timestamp == previousPose->timestamp)
                 {
-                    // Discard the new pose.
-                    // ARMARX_IMPORTANT << "Ignoring update of object " << provided.objectID << " because robot head is moved.\n"
-                    //                  << "timestamp " << timestamp << " < " << robotHead.discard.updatesUntil;
+                    // Discard update as it is not new.
+                    discard = true;
                 }
             }
-            else if (previousPose && timestamp == previousPose->timestamp)
-            {
-                // Keep the old one.
-                newObjectPoses.push_back(*previousPose);
-            }
-            else
+
+            if (!discard)
             {
+                // Update the entity.
                 stats.numUpdated++;
+
                 objpose::ObjectPose& newPose = newObjectPoses.emplace_back();
                 newPose.fromProvidedPose(provided, robot);
+
+                if (previousPose && previousPose->attachment)
+                {
+                    // Keep current attachment.
+                    ARMARX_CHECK(!discardSnapshotsWhileAttached);
+                    newPose.attachment = previousPose->attachment;
+                }
+
                 if (newPose.objectID.dataset().empty())
                 {
-                    // Try to find the data set. (It might be good to cache this.)
-                    if (std::optional<ObjectInfo> objectInfo = objectFinder.findObject(newPose.objectID))
+                    // Try to find the data set.
+                    const std::string dataset = classNameToDatasetCache.get(newPose.objectID.className());
+                    if (!dataset.empty())
                     {
-                        newPose.objectID = { objectInfo->dataset(), newPose.objectID.className(), newPose.objectID.instanceName() };
+                        newPose.objectID = { dataset, newPose.objectID.className(), newPose.objectID.instanceName() };
                     }
                 }
                 if (!provided.localOOBB)
@@ -165,8 +200,9 @@ namespace armarx::objpose::observer
 
     ObjectPoseSeq Data::getObjectPoses(IceUtil::Time now)
     {
-        ObjectPoseSeq latestObjectPoses = getLatestObjectPoses();
-        return updateAndFilterObjectPoses(latestObjectPoses, now);
+        ObjectPoseSeq objectPoses = getLatestObjectPoses();
+        updateObjectPoses(objectPoses, now);
+        return filterObjectPoses(objectPoses);
     }
 
 
@@ -176,7 +212,8 @@ namespace armarx::objpose::observer
     {
         ARMARX_CHECK_NOT_NULL(coreSegment);
         ObjectPoseSeq objectPoses = getLatestObjectPoses(coreSegment->getProviderSegment(providerName));
-        return updateAndFilterObjectPoses(objectPoses, now);
+        updateObjectPoses(objectPoses, now);
+        return filterObjectPoses(objectPoses);
     }
 
     armem::Entity* Data::findObjectEntity(const ObjectID& objectID, const std::string& providerName)
@@ -192,14 +229,31 @@ namespace armarx::objpose::observer
                     return &prov.getEntity(entityID);
                 }
             }
-
-            throw armem::error::MissingEntry("entity", entityID.entityName,
-                                             "any provider segment of core segment", coreSegment->name());
+            return nullptr;
         }
         else
         {
             entityID.providerSegmentName = providerName;
-            return &coreSegment->getEntity(entityID);
+            if (coreSegment->hasProviderSegment(providerName))
+            {
+                armem::ProviderSegment& prov = coreSegment->getProviderSegment(providerName);
+                return prov.hasEntity(entityID.entityName) ? &prov.getEntity(entityID) : nullptr;
+            }
+            else
+            {
+                return nullptr;
+            }
+        }
+    }
+
+
+    void Data::updateObjectPoses(ObjectPoseSeq& objectPoses, IceUtil::Time now)
+    {
+        bool agentSynchronized = false;
+
+        for (ObjectPose& objectPose : objectPoses)
+        {
+            updateObjectPose(objectPose, now, robot, agentSynchronized);
         }
     }
 
@@ -232,23 +286,27 @@ namespace armarx::objpose::observer
     }
 
 
+    ObjectPoseSeq Data::filterObjectPoses(const ObjectPoseSeq& objectPoses) const
+    {
+        ObjectPoseSeq result;
+        for (const ObjectPose& objectPose : objectPoses)
+        {
+            if (!(decay.enabled && objectPose.confidence < decay.removeObjectsBelowConfidence))
+            {
+                result.push_back(objectPose);
+            }
+        }
+        return result;
+    }
+
+
     void Data::updateAttachement(
         ObjectPose& objectPose, VirtualRobot::RobotPtr agent, bool& synchronized) const
     {
         if (!objectPose.attachment)
         {
-            // Fetch attachment info from internal data structure.
-            auto it = attachments.find(std::make_pair(objectPose.providerName, objectPose.objectID));
-            if (it != attachments.end())
-            {
-                // Store it in the objectPose.
-                objectPose.attachment = it->second;
-            }
-            else
-            {
-                // No attachment, nothing to do.
-                return;
-            }
+            // No attachment, nothing to do.
+            return;
         }
         ARMARX_CHECK(objectPose.attachment);
 
@@ -266,22 +324,6 @@ namespace armarx::objpose::observer
         return getLatestObjectPoses(*coreSegment);
     }
 
-    ObjectPoseSeq Data::updateAndFilterObjectPoses(ObjectPoseSeq& objectPoses, IceUtil::Time now) const
-    {
-        bool agentSynchronized = false;
-
-        ObjectPoseSeq result;
-        for (ObjectPose& objectPose : objectPoses)
-        {
-            updateObjectPose(objectPose, now, robot, agentSynchronized);
-            if (!(decay.enabled && objectPose.confidence < decay.removeObjectsBelowConfidence))
-            {
-                result.push_back(objectPose);
-            }
-        }
-        return result;
-    }
-
     ObjectPoseSeq Data::getLatestObjectPoses(const armem::CoreSegment& coreSeg)
     {
         ObjectPoseSeq result;
@@ -349,30 +391,9 @@ namespace armarx::objpose::observer
         return data;
     }
 
-
     std::optional<simox::OrientedBoxf> Data::getObjectOOBB(const ObjectID& id)
     {
-        return oobbCache.get(id, [this](const ObjectID & id) -> std::optional<simox::OrientedBoxf>
-        {
-            // Try to get OOBB from repository.
-            if (std::optional<ObjectInfo> objectInfo = objectFinder.findObject(id))
-            {
-                try
-                {
-                    return objectInfo->loadOOBB();
-                }
-                catch (const std::ios_base::failure& e)
-                {
-                    // Give up - no OOBB information.
-                    ARMARX_WARNING << "Could not get OOBB of object " << id << ".\n- " << e.what();
-                    return std::nullopt;
-                }
-            }
-            else
-            {
-                return std::nullopt;
-            }
-        });
+        return oobbCache.get(id);
     }
 
     ProviderInfo Data::getProviderInfo(const std::string& providerName)
@@ -457,7 +478,6 @@ namespace armarx::objpose::observer
                 info.poseInFrame = framed.toEigen();
             }
         }
-        this->attachments[std::make_pair(data.providerName, objectID)] = info;
 
         // Store attachment in new entity snapshot.
         {
@@ -486,41 +506,28 @@ namespace armarx::objpose::observer
         return output;
     }
 
-    DetachObjectFromRobotNodeOutput Data::detachObjectFromRobotNode(const DetachObjectFromRobotNodeInput& input)
+    DetachObjectFromRobotNodeOutput Data::detachObjectFromRobotNode(
+        const DetachObjectFromRobotNodeInput& input)
     {
         const armem::Time now = armem::Time::now();
 
         ObjectID objectID = armarx::fromIce(input.objectID);
         std::string providerName = input.providerName;
 
-        std::optional<ObjectAttachmentInfo> attachment;
+        std::optional<objpose::arondto::ObjectAttachmentInfo> attachment;
         {
             // Remove from latest pose (if it was cached).
             // Find object pose (provider name can be empty).
-            armem::Entity* objectEntity = this->findObjectEntity(objectID, input.providerName);
-            if (objectEntity)
+            armem::Entity* entity = this->findObjectEntity(objectID, input.providerName);
+            if (entity)
             {
-                ARMARX_CHECK_GREATER_EQUAL(objectEntity->size(), 1);
-                armem::EntitySnapshot& snapshot = objectEntity->getLatestSnapshot();
-
-                ARMARX_CHECK_EQUAL(snapshot.size(), 1);
-                armem::EntityInstance& instance = snapshot.getInstance(0);
-
-                arondto::ObjectPose data;
-                data.fromAron(instance.data());
-
-                // Store non-attached pose in new snapshot.
+                const arondto::ObjectPose data = getLatestInstanceData(*entity);
+                if (data.attachmentValid)
                 {
-                    armem::EntityUpdate update;
-                    update.entityID = objectEntity->id();
-                    update.timeCreated = now;
-                    {
-                        arondto::ObjectPose updated = data;
-                        updated.attachmentValid = false;
-                        toAron(updated.attachment, ObjectAttachmentInfo{});
-                        update.instancesData = { updated.toAron() };
-                    }
-                    objectEntity->update(update);
+                    attachment = data.attachment;
+
+                    // Store non-attached pose in new snapshot.
+                    storeDetachedSnapshot(*entity, data, now, input.commitAttachedPose);
                 }
 
                 if (providerName.empty())
@@ -528,31 +535,6 @@ namespace armarx::objpose::observer
                     providerName = data.providerName;
                 }
             }
-
-            // Remove from attachment map.
-            if (input.providerName.size() > 0)
-            {
-                auto it = this->attachments.find(std::make_pair(input.providerName, objectID));
-                if (it != this->attachments.end())
-                {
-                    attachment = it->second;
-                    this->attachments.erase(it);
-                }
-            }
-            else
-            {
-                // Search for entry with matching object ID.
-                for (auto it = this->attachments.begin(); it != this->attachments.end(); ++it)
-                {
-                    const ObjectID& id = it->first.second;
-                    if (id == objectID)
-                    {
-                        attachment = it->second;
-                        this->attachments.erase(it);
-                        break;
-                    }
-                }
-            }
         }
 
         DetachObjectFromRobotNodeOutput output;
@@ -571,38 +553,125 @@ namespace armarx::objpose::observer
         return output;
     }
 
-    DetachAllObjectsFromRobotNodesOutput Data::detachAllObjectsFromRobotNodes()
+
+    struct DetachVisitor : public armem::Visitor
+    {
+        Data& owner;
+        armem::Time now;
+        bool commitAttachedPose;
+
+        int numDetached = 0;
+
+        DetachVisitor(Data& owner, armem::Time now, bool commitAttachedPose) :
+            owner(owner), now(now), commitAttachedPose(commitAttachedPose)
+        {
+        }
+
+        virtual bool visitEnter(armem::Entity& entity) override;
+    };
+
+    bool DetachVisitor::visitEnter(armem::Entity& entity)
+    {
+        const arondto::ObjectPose data = owner.getLatestInstanceData(entity);
+        if (data.attachmentValid)
+        {
+            numDetached++;
+            // Store non-attached pose in new snapshot.
+            owner.storeDetachedSnapshot(entity, data, now, commitAttachedPose);
+        }
+
+        return false; // Stop descending.
+    }
+
+
+    DetachAllObjectsFromRobotNodesOutput Data::detachAllObjectsFromRobotNodes(
+        const DetachAllObjectsFromRobotNodesInput& input)
     {
+        ARMARX_CHECK_NOT_NULL(coreSegment);
+
+        const armem::Time now = armem::Time::now();
+
+        DetachVisitor visitor(*this, now, input.commitAttachedPose);
+        visitor.applyTo(*coreSegment);
+
         DetachAllObjectsFromRobotNodesOutput output;
-        output.numDetached = int(this->attachments.size());
+        output.numDetached = visitor.numDetached;
 
-        // Clear attachment map.
-        this->attachments.clear();
+        ARMARX_INFO << "Detached all objects (" << output.numDetached << ") from robot nodes.";
 
-        // Remove from poses (if it was cached).
-        for (auto& [_, prov] : *coreSegment)
+        return output;
+    }
+
+    void Data::storeDetachedSnapshot(
+        armem::Entity& entity,
+        const arondto::ObjectPose& data,
+        armem::Time now,
+        bool commitAttachedPose)
+    {
+        armem::EntityUpdate update;
+        update.entityID = entity.id();
+        update.timeCreated = now;
         {
-            for (auto& [_, entity] : prov)
+            arondto::ObjectPose updated;
+            if (commitAttachedPose && data.attachmentValid)
             {
-                if (!entity.empty())
-                {
-                    armem::EntitySnapshot& snapshot = entity.getLatestSnapshot();
-                    if (!snapshot.empty())
-                    {
-                        armem::EntityInstance& instance = snapshot.getInstance(0);
+                ObjectPose objectPose;
+                fromAron(data, objectPose);
 
-                        arondto::ObjectPose dto;
-                        dto.fromAron(instance.data());
-                        dto.attachmentValid = false;
-                        instance.setData(dto.toAron());
-                    }
-                }
+                bool agentSynchronized = false;
+                updateAttachement(objectPose, robot, agentSynchronized);
+
+                objectPose.attachment = std::nullopt;
+                toAron(updated, objectPose);
             }
+            else
+            {
+                updated = data;
+                updated.attachmentValid = false;
+                toAron(updated.attachment, ObjectAttachmentInfo{});
+            }
+
+            update.instancesData = { updated.toAron() };
         }
+        entity.update(update);
+    }
 
-        ARMARX_INFO << "Detached all objects (" << output.numDetached << ") from robot nodes.";
 
-        return output;
+
+    void Data::RemoteGui::setup(const Data& data)
+    {
+        using namespace armarx::RemoteGui::Client;
+
+        maxHistorySize.setValue(std::max(1, int(data.maxHistorySize)));
+        maxHistorySize.setRange(1, 1e6);
+        infiniteHistory.setValue(data.maxHistorySize == -1);
+        discardSnapshotsWhileAttached.setValue(data.discardSnapshotsWhileAttached);
+
+        GridLayout grid;
+        int row = 0;
+        grid.add(Label("Max History Size"), {row, 0}).add(maxHistorySize, {row, 1});
+        row++;
+        grid.add(Label("Infinite History Size"), {row, 0}).add(infiniteHistory, {row, 1});
+        row++;
+        grid.add(Label("Discard Snapshots while attached"), {row, 0}).add(discardSnapshotsWhileAttached, {row, 1});
+        row++;
+
+        group.setLabel("Data");
+        group.addChild(grid);
+    }
+
+    void Data::RemoteGui::update(Data& data)
+    {
+        if (infiniteHistory.hasValueChanged() || maxHistorySize.hasValueChanged())
+        {
+            data.maxHistorySize = infiniteHistory.getValue() ? -1 : maxHistorySize.getValue();
+            if (data.coreSegment)
+            {
+                data.coreSegment->setMaxHistorySize(long(data.maxHistorySize));
+            }
+        }
+
+        data.discardSnapshotsWhileAttached = discardSnapshotsWhileAttached.getValue();
     }
 
 }
diff --git a/source/RobotAPI/components/ObjectPoseObserver/detail/Data.h b/source/RobotAPI/components/ObjectPoseObserver/detail/Data.h
index 99e5926fd..95e6cc78a 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/detail/Data.h
+++ b/source/RobotAPI/components/ObjectPoseObserver/detail/Data.h
@@ -73,22 +73,9 @@ namespace armarx::objpose::observer
 
         AttachObjectToRobotNodeOutput attachObjectToRobotNode(const AttachObjectToRobotNodeInput& input);
         DetachObjectFromRobotNodeOutput detachObjectFromRobotNode(const DetachObjectFromRobotNodeInput& input);
-        DetachAllObjectsFromRobotNodesOutput detachAllObjectsFromRobotNodes();
+        DetachAllObjectsFromRobotNodesOutput detachAllObjectsFromRobotNodes(const DetachAllObjectsFromRobotNodesInput& input);
 
 
-        void updateObjectPoses(
-            ObjectPoseSeq& objectPoses,
-            IceUtil::Time now,
-            VirtualRobot::RobotPtr agent,
-            bool& agentSynchronized
-        ) const;
-        void updateObjectPose(
-            ObjectPose& objectPose,
-            IceUtil::Time now,
-            VirtualRobot::RobotPtr agent,
-            bool& agentSynchronized
-        ) const;
-
         /**
          * @brief If the object is attached to a robot node, update it according to the current robot state.
          *
@@ -117,7 +104,35 @@ namespace armarx::objpose::observer
     private:
 
         ObjectPoseSeq getLatestObjectPoses() const;
-        ObjectPoseSeq updateAndFilterObjectPoses(ObjectPoseSeq& objectPoses, IceUtil::Time now) const;
+
+        void updateObjectPoses(
+            ObjectPoseSeq& objectPoses,
+            IceUtil::Time now);
+        void updateObjectPoses(
+            ObjectPoseSeq& objectPoses,
+            IceUtil::Time now,
+            VirtualRobot::RobotPtr agent,
+            bool& agentSynchronized
+        ) const;
+        void updateObjectPose(
+            ObjectPose& objectPose,
+            IceUtil::Time now,
+            VirtualRobot::RobotPtr agent,
+            bool& agentSynchronized
+        ) const;
+
+
+        ObjectPoseSeq filterObjectPoses(const ObjectPoseSeq& objectPoses) const;
+
+
+        void storeDetachedSnapshot(
+            armem::Entity& entity,
+            const arondto::ObjectPose& data,
+            armem::Time now,
+            bool commitAttachedPose);
+
+
+        friend struct DetachVisitor;
 
 
     public:
@@ -128,12 +143,7 @@ namespace armarx::objpose::observer
         ProviderInfoMap providers;
 
 
-        std::map<std::pair<std::string, ObjectID>, ObjectAttachmentInfo> attachments;
-
-
         ObjectFinder objectFinder;
-        /// Caches results of attempts to retrieve the OOBB from ArmarXObjects.
-        simox::caching::CacheMap<ObjectID, std::optional<simox::OrientedBoxf>> oobbCache;
 
         /// Decay model.
         Decay decay;
@@ -145,7 +155,32 @@ namespace armarx::objpose::observer
         armem::server::MemoryToIceAdapter& memoryToIceAdapter;
 
         armem::CoreSegment* coreSegment = nullptr;
+
+
         long maxHistorySize = -1;
+        bool discardSnapshotsWhileAttached = true;
+
+
+        /// Caches results of attempts to retrieve the OOBB from ArmarXObjects.
+        simox::caching::CacheMap<ObjectID, std::optional<simox::OrientedBoxf>> oobbCache;
+
+        /// Class name -> dataset name.
+        simox::caching::CacheMap<std::string, std::string> classNameToDatasetCache;
+
+
+    public:
+
+        struct RemoteGui
+        {
+            armarx::RemoteGui::Client::GroupBox group;
+
+            armarx::RemoteGui::Client::IntSpinBox maxHistorySize;
+            armarx::RemoteGui::Client::CheckBox infiniteHistory;
+            armarx::RemoteGui::Client::CheckBox discardSnapshotsWhileAttached;
+
+            void setup(const Data& data);
+            void update(Data& data);
+        };
 
     };
 
diff --git a/source/RobotAPI/components/ObjectPoseObserver/detail/Visu.cpp b/source/RobotAPI/components/ObjectPoseObserver/detail/Visu.cpp
index 11f8a3ebb..3aaa36feb 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/detail/Visu.cpp
+++ b/source/RobotAPI/components/ObjectPoseObserver/detail/Visu.cpp
@@ -1,5 +1,7 @@
 #include "Visu.h"
 
+#include <SimoxUtility/math/pose.h>
+
 #include <ArmarXCore/core/time/TimeUtil.h>
 
 #include <RobotAPI/libraries/ArmarXObjects/ObjectFinder.h>
@@ -91,7 +93,7 @@ namespace armarx::objpose::observer
         const armarx::ObjectID id = objectPose.objectID;
         const std::string key = id.str();
 
-        Eigen::Matrix4f pose = inGlobalFrame ? objectPose.objectPoseGlobal : objectPose.objectPoseRobot;
+        const Eigen::Matrix4f pose = inGlobalFrame ? objectPose.objectPoseGlobal : objectPose.objectPoseRobot;
         {
             viz::Object object = viz::Object(key).pose(pose);
             if (std::optional<ObjectInfo> objectInfo = objectFinder.findObject(id))
@@ -115,9 +117,10 @@ namespace armarx::objpose::observer
 
         if (oobbs && objectPose.localOOBB)
         {
-            const simox::OrientedBoxf& oobb = *objectPose.localOOBB;
-            layer.add(viz::Box(key + " OOBB").set(oobb.transformed(pose))
-                      .color(simox::Color::lime(255, 64)));
+            const simox::OrientedBoxf oobb = inGlobalFrame
+                                             ? objectPose.oobbGlobal().value()
+                                             : objectPose.oobbRobot().value();
+            layer.add(viz::Box(key + " OOBB").set(oobb).color(simox::Color::lime(255, 64)));
         }
         if (objectFrames)
         {
diff --git a/source/RobotAPI/interface/objectpose/ObjectPoseObserverInterface.ice b/source/RobotAPI/interface/objectpose/ObjectPoseObserverInterface.ice
index 6fe63e5f9..3f047323b 100644
--- a/source/RobotAPI/interface/objectpose/ObjectPoseObserverInterface.ice
+++ b/source/RobotAPI/interface/objectpose/ObjectPoseObserverInterface.ice
@@ -89,12 +89,27 @@ module armarx
         {
             string providerName;
             armarx::data::ObjectID objectID;
+
+            /**
+             * @brief If true, the object will stay at the position before
+             * detaching until it is provided again.
+             */
+            bool commitAttachedPose = true;
         };
         struct DetachObjectFromRobotNodeOutput
         {
             /// Whether the object was attached before.
             bool wasAttached;
         };
+
+        struct DetachAllObjectsFromRobotNodesInput
+        {
+            /**
+             * @brief If true, the objects will stay at the position before
+             * detaching until they are provided again.
+             */
+            bool commitAttachedPose = true;
+        }
         struct DetachAllObjectsFromRobotNodesOutput
         {
             /// Number of objects that have been detached.
@@ -165,7 +180,7 @@ module armarx
             /// Detach an attached object from a robot node.
             DetachObjectFromRobotNodeOutput detachObjectFromRobotNode(DetachObjectFromRobotNodeInput input);
             /// Detach all objects from robot nodes.
-            DetachAllObjectsFromRobotNodesOutput detachAllObjectsFromRobotNodes();
+            DetachAllObjectsFromRobotNodesOutput detachAllObjectsFromRobotNodes(DetachAllObjectsFromRobotNodesInput input);
 
             AgentFramesSeq getAttachableFrames();
 
-- 
GitLab