From d4dc5c01d853fe1509dde7a84bbec1a060b92c36 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sat, 10 Jan 2026 01:15:10 +0100 Subject: [PATCH] Part: Introduce PreviewUpdateScheduler This commit introduces PreviewUpdateScheduler class that is responsible to schedule the true recompute of the preview. View Providers (or other components) can use this service to ask for the preview recompute to happend at a time that is convinent for a program and that won't impact performance. The provided implementation uses Queued Connections in Qt to calculate preview essentially on next run of the event loop. It allows business logic in FreeCAD (like property propagation) to execute fully and then recompute preview once. This greately reduces number of recompute calls for previews. --- src/App/DocumentObserver.cpp | 3 + src/App/DocumentObserver.h | 23 +++++-- src/Mod/Part/App/PreviewExtension.h | 28 ++++++++ src/Mod/Part/Gui/AppPartGui.cpp | 7 ++ src/Mod/Part/Gui/CMakeLists.txt | 2 + src/Mod/Part/Gui/PreviewUpdateScheduler.cpp | 64 +++++++++++++++++++ src/Mod/Part/Gui/PreviewUpdateScheduler.h | 71 +++++++++++++++++++++ src/Mod/PartDesign/Gui/ViewProvider.cpp | 9 ++- 8 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 src/Mod/Part/Gui/PreviewUpdateScheduler.cpp create mode 100644 src/Mod/Part/Gui/PreviewUpdateScheduler.h 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); }