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.
This commit is contained in:
Kacper Donat
2026-01-10 01:15:10 +01:00
committed by Chris Hennes
parent 699c539446
commit d4dc5c01d8
8 changed files with 200 additions and 7 deletions

View File

@@ -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();

View File

@@ -31,6 +31,7 @@
#include <fastsignals/signal.h>
#include <memory>
#include <set>
#include <string>
#include <FCGlobal.h>
@@ -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<Private> d;
@@ -614,6 +618,15 @@ private:
} // namespace App
template<>
struct std::hash<App::DocumentObjectWeakPtrT>
{
std::size_t operator()(const App::DocumentObjectWeakPtrT& ptr) const noexcept
{
return std::hash<App::DocumentObject*>{}(*ptr);
}
};
ENABLE_BITMASK_OPERATORS(App::SubObjectT::NormalizeOption)
#endif // APP_DOCUMENTOBSERVER_H

View File

@@ -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<typename ExtensionT>
class PreviewExtensionPythonT: public ExtensionT
{

View File

@@ -26,17 +26,22 @@
#include <Base/Console.h>
#include <Base/Interpreter.h>
#include <Base/PyObjectBase.h>
#include <Base/ServiceProvider.h>
#include <Gui/Application.h>
#include <Gui/BitmapFactory.h>
#include <Gui/Dialogs/DlgPreferencesImp.h>
#include <Gui/WidgetFactory.h>
#include <Gui/Language/Translator.h>
#include <Mod/Part/App/PreviewExtension.h>
#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<PartGui::WorkbenchManipulator>();
Gui::WorkbenchManipulator::installManipulator(manip);
Base::registerServiceImplementation<Part::PreviewUpdateScheduler>(new PartGui::QtPreviewUpdateScheduler);
// instantiating the commands
CreatePartCommands();
CreateSimplePartCommands();

View File

@@ -142,6 +142,8 @@ SET(PartGui_SRCS
PatternParametersWidget.ui
Resources/Part.qrc
PreCompiled.h
PreviewUpdateScheduler.cpp
PreviewUpdateScheduler.h
PropertyEnumAttacherItem.cpp
PropertyEnumAttacherItem.h
SoFCShapeObject.cpp

View File

@@ -0,0 +1,64 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2026 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#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<Part::PreviewExtension>(true)) {
previewExtension->updatePreview();
}
}
}
#include "moc_PreviewUpdateScheduler.cpp"

View File

@@ -0,0 +1,71 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2026 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef FREECAD_PREVIEWUPDATESCHEDULER_H
#define FREECAD_PREVIEWUPDATESCHEDULER_H
#include "App/DocumentObserver.h"
#include <QObject>
#include <QSet>
#include <Mod/Part/App/PreviewExtension.h>
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<App::DocumentObjectWeakPtrT> toBeUpdated;
bool scheduled = false;
};
} // namespace PartGui
#endif // FREECAD_PREVIEWUPDATESCHEDULER_H

View File

@@ -215,10 +215,15 @@ void ViewProvider::updateData(const App::Property* prop)
}
else if (auto* previewExtension = getObject()->getExtensionByType<Part::PreviewExtension>(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<Part::PreviewUpdateScheduler>()) {
scheduler->schedulePreviewRecompute(getObject());
}
}
}
inherited::updateData(prop);
}