diff --git a/src/App/DocumentObserver.cpp b/src/App/DocumentObserver.cpp index a2204eed7c..d012326a3e 100644 --- a/src/App/DocumentObserver.cpp +++ b/src/App/DocumentObserver.cpp @@ -817,6 +817,9 @@ App::DocumentObject* DocumentObjectWeakPtrT::_get() const noexcept return d->get(); } +DocumentObjectWeakPtrT::DocumentObjectWeakPtrT(DocumentObjectWeakPtrT&&) = default; +DocumentObjectWeakPtrT& DocumentObjectWeakPtrT::operator=(DocumentObjectWeakPtrT&&) = default; + void DocumentObjectWeakPtrT::reset() { d->reset(); diff --git a/src/App/DocumentObserver.h b/src/App/DocumentObserver.h index 3c4ea5c591..bffaf12425 100644 --- a/src/App/DocumentObserver.h +++ b/src/App/DocumentObserver.h @@ -31,6 +31,7 @@ #include #include #include +#include #include @@ -372,6 +373,14 @@ public: explicit DocumentObjectWeakPtrT(App::DocumentObject*); ~DocumentObjectWeakPtrT(); + // disable copy + DocumentObjectWeakPtrT(const DocumentObjectWeakPtrT &) = delete; + DocumentObjectWeakPtrT &operator=(const DocumentObjectWeakPtrT &) = delete; + + // default move + DocumentObjectWeakPtrT(DocumentObjectWeakPtrT &&); + DocumentObjectWeakPtrT &operator=(DocumentObjectWeakPtrT &&); + /*! * \brief reset * Releases the reference to the managed object. After the call *this manages no object. @@ -417,11 +426,6 @@ public: private: App::DocumentObject* _get() const noexcept; -public: - // disable - DocumentObjectWeakPtrT(const DocumentObjectWeakPtrT&) = delete; - DocumentObjectWeakPtrT& operator=(const DocumentObjectWeakPtrT&) = delete; - private: class Private; std::unique_ptr d; @@ -614,6 +618,15 @@ private: } // namespace App +template<> +struct std::hash +{ + std::size_t operator()(const App::DocumentObjectWeakPtrT& ptr) const noexcept + { + return std::hash{}(*ptr); + } +}; + ENABLE_BITMASK_OPERATORS(App::SubObjectT::NormalizeOption) #endif // APP_DOCUMENTOBSERVER_H diff --git a/src/Mod/Part/App/PreviewExtension.h b/src/Mod/Part/App/PreviewExtension.h index 98cba5581a..7b8926af0f 100644 --- a/src/Mod/Part/App/PreviewExtension.h +++ b/src/Mod/Part/App/PreviewExtension.h @@ -68,6 +68,34 @@ private: bool _isPreviewFresh {false}; }; +/** + * Service interface for update scheduler implementation. + * + * The scheduler manages the timing of preview recomputations. It is designed to debounce + * multiple requests—such as those occurring during batch property updates—ensuring that + * expensive preview computations are only performed when the system is idle or at a + * more convenient time, rather than for every intermediate step. + */ +class PartExport PreviewUpdateScheduler +{ +public: + PreviewUpdateScheduler() = default; + virtual ~PreviewUpdateScheduler() = default; + + FC_DISABLE_COPY_MOVE(PreviewUpdateScheduler); + + /** + * Schedules a preview recompute for the given object. + * + * Instead of triggering an immediate update, this method registers the object + * with the scheduler. If multiple updates are requested in rapid succession, + * the scheduler should collapse them into a single recomputation to improve performance. + * + * @param object The preview extension of the object that requires an update. + */ + virtual void schedulePreviewRecompute(App::DocumentObject* object) = 0; +}; + template class PreviewExtensionPythonT: public ExtensionT { diff --git a/src/Mod/Part/Gui/AppPartGui.cpp b/src/Mod/Part/Gui/AppPartGui.cpp index 94a88dc000..bce0f62912 100644 --- a/src/Mod/Part/Gui/AppPartGui.cpp +++ b/src/Mod/Part/Gui/AppPartGui.cpp @@ -26,17 +26,22 @@ #include #include #include +#include + #include #include #include #include #include +#include + #include "AttacherTexts.h" #include "PropertyEnumAttacherItem.h" #include "DlgSettings3DViewPartImp.h" #include "DlgSettingsGeneral.h" #include "DlgSettingsObjectColor.h" +#include "PreviewUpdateScheduler.h" #include "SoBrepEdgeSet.h" #include "SoBrepFaceSet.h" #include "SoBrepPointSet.h" @@ -227,6 +232,8 @@ PyMOD_INIT_FUNC(PartGui) auto manip = std::make_shared(); Gui::WorkbenchManipulator::installManipulator(manip); + Base::registerServiceImplementation(new PartGui::QtPreviewUpdateScheduler); + // instantiating the commands CreatePartCommands(); CreateSimplePartCommands(); diff --git a/src/Mod/Part/Gui/CMakeLists.txt b/src/Mod/Part/Gui/CMakeLists.txt index 9afea60d5e..5e59c36c18 100644 --- a/src/Mod/Part/Gui/CMakeLists.txt +++ b/src/Mod/Part/Gui/CMakeLists.txt @@ -142,6 +142,8 @@ SET(PartGui_SRCS PatternParametersWidget.ui Resources/Part.qrc PreCompiled.h + PreviewUpdateScheduler.cpp + PreviewUpdateScheduler.h PropertyEnumAttacherItem.cpp PropertyEnumAttacherItem.h SoFCShapeObject.cpp diff --git a/src/Mod/Part/Gui/PreviewUpdateScheduler.cpp b/src/Mod/Part/Gui/PreviewUpdateScheduler.cpp new file mode 100644 index 0000000000..cc8e985d6b --- /dev/null +++ b/src/Mod/Part/Gui/PreviewUpdateScheduler.cpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2026 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreviewUpdateScheduler.h" + +using namespace PartGui; + +QtPreviewUpdateScheduler::QtPreviewUpdateScheduler(QObject* parent) + : QObject(parent) +{} + +inline void QtPreviewUpdateScheduler::schedulePreviewRecompute(App::DocumentObject* object) +{ + if (!object) { + return; + } + + toBeUpdated.emplace(object); + + // if method call was already scheduled there is no need to queue another one + if (scheduled) { + return; + } + + QMetaObject::invokeMethod(this, &QtPreviewUpdateScheduler::flush, Qt::QueuedConnection); +} + +void QtPreviewUpdateScheduler::flush() +{ + scheduled = false; + + // use std::exchange to prevent race conditions on updates that could occur during a flush + for (auto objects = std::exchange(this->toBeUpdated, {}); auto& object : objects) { + if (object.expired()) { + continue; + } + + if (auto* previewExtension = object->getExtensionByType(true)) { + previewExtension->updatePreview(); + } + } +} + +#include "moc_PreviewUpdateScheduler.cpp" diff --git a/src/Mod/Part/Gui/PreviewUpdateScheduler.h b/src/Mod/Part/Gui/PreviewUpdateScheduler.h new file mode 100644 index 0000000000..f229ec0f89 --- /dev/null +++ b/src/Mod/Part/Gui/PreviewUpdateScheduler.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2026 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#ifndef FREECAD_PREVIEWUPDATESCHEDULER_H +#define FREECAD_PREVIEWUPDATESCHEDULER_H + +#include "App/DocumentObserver.h" + + +#include +#include + +#include + +namespace PartGui +{ + +/** + * Qt-based implementation of the PreviewUpdateScheduler. + * + * This implementation uses the Qt Event Loop to debounce recompute requests. + * Requests are queued and a flush is triggered via a queued connection, + * ensuring that the actual update happens once the control returns to the + * event loop after all pending property changes are processed. + */ +class QtPreviewUpdateScheduler final: public QObject, public Part::PreviewUpdateScheduler +{ + Q_OBJECT + +public: + explicit QtPreviewUpdateScheduler(QObject* parent = nullptr); + + /** + * Schedules a preview recompute using the Qt Event Loop. + * + * Adds the object to a unique set to avoid duplicate work and schedules + * a call to flush() using Qt::QueuedConnection if one isn't already pending. + */ + void schedulePreviewRecompute(App::DocumentObject* object) override; + +private Q_SLOTS: + void flush(); + +private: + std::unordered_set toBeUpdated; + bool scheduled = false; +}; + +} // namespace PartGui + +#endif // FREECAD_PREVIEWUPDATESCHEDULER_H diff --git a/src/Mod/PartDesign/Gui/ViewProvider.cpp b/src/Mod/PartDesign/Gui/ViewProvider.cpp index ede66664a7..40ffcdcd50 100644 --- a/src/Mod/PartDesign/Gui/ViewProvider.cpp +++ b/src/Mod/PartDesign/Gui/ViewProvider.cpp @@ -215,10 +215,15 @@ void ViewProvider::updateData(const App::Property* prop) } else if (auto* previewExtension = getObject()->getExtensionByType(true)) { if (!previewExtension->isPreviewFresh() && isEditing()) { - previewExtension->updatePreview(); + // Properties can be updated in batches, where some properties trigger other updates. + // We don't need to compute the preview for intermediate steps. Instead of updating + // the preview immediately (and potentially doing it multiple times in a row), we + // schedule the update to happen at a more convenient time. + if (auto* scheduler = Base::provideService()) { + scheduler->schedulePreviewRecompute(getObject()); + } } } - inherited::updateData(prop); }