Assembly: Solver message taskbox. UI setup, App not implemented yet. (#23420)

* Assembly: Solver message taskbox. UI setup, App not implemented yet.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update TaskAssemblyMessages.cpp

* Update ViewProviderAssembly.cpp

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
PaddleStroke
2025-09-01 23:29:06 +02:00
committed by GitHub
parent 24a6d59ca8
commit e40e01b039
9 changed files with 408 additions and 2 deletions

View File

@@ -541,7 +541,7 @@ AssemblyObject* AssemblyLink::getParentAssembly() const
return nullptr;
}
bool AssemblyLink::isRigid()
bool AssemblyLink::isRigid() const
{
auto* prop = dynamic_cast<App::PropertyBool*>(getPropertyByName("Rigid"));
if (!prop) {
@@ -559,3 +559,18 @@ std::vector<App::DocumentObject*> AssemblyLink::getJoints()
}
return jointGroup->getJoints();
}
bool AssemblyLink::allowDuplicateLabel() const
{
return true;
}
int AssemblyLink::numberOfComponents() const
{
return isRigid() ? 1 : getLinkedAssembly()->numberOfComponents();
}
bool AssemblyLink::isEmpty() const
{
return numberOfComponents() == 0;
}

View File

@@ -66,7 +66,7 @@ public:
// This function returns the linked object, either an AssemblyObject or an AssemblyLink
App::DocumentObject* getLinkedObject2(bool recurse = true) const;
bool isRigid();
bool isRigid() const;
/**
* Update all of the components and joints from the Assembly
@@ -82,6 +82,11 @@ public:
JointGroup* ensureJointGroup();
std::vector<App::DocumentObject*> getJoints();
bool allowDuplicateLabel() const override;
bool isEmpty() const;
int numberOfComponents() const;
App::PropertyXLink LinkedObject;
App::PropertyBool Rigid;

View File

@@ -102,8 +102,17 @@ PROPERTY_SOURCE(Assembly::AssemblyObject, App::Part)
AssemblyObject::AssemblyObject()
: mbdAssembly(std::make_shared<ASMTAssembly>())
, bundleFixed(false)
, lastDoF(0)
, lastHasConflict(false)
, lastHasRedundancies(false)
, lastHasPartialRedundancies(false)
, lastHasMalformedConstraints(false)
, lastSolverStatus(0)
{
mbdAssembly->externalSystem->freecadAssemblyObject = this;
lastDoF = numberOfComponents() * 6;
signalSolverUpdate();
}
AssemblyObject::~AssemblyObject() = default;
@@ -131,6 +140,8 @@ App::DocumentObjectExecReturn* AssemblyObject::execute()
int AssemblyObject::solve(bool enableRedo, bool updateJCS)
{
lastDoF = numberOfComponents() * 6;
ensureIdentityPlacements();
mbdAssembly = makeMbdAssembly();
@@ -169,6 +180,8 @@ int AssemblyObject::solve(bool enableRedo, bool updateJCS)
redrawJointPlacements(joints);
signalSolverUpdate();
return 0;
}
@@ -1972,3 +1985,52 @@ void AssemblyObject::ensureIdentityPlacements()
}
}
}
int AssemblyObject::numberOfComponents() const
{
int count = 0;
const std::vector<App::DocumentObject*> objects = Group.getValues();
for (auto* obj : objects) {
if (!obj) {
continue;
}
if (obj->isLinkGroup()) {
auto* link = static_cast<const App::Link*>(obj);
count += link->ElementCount.getValue();
continue;
}
if (obj->isDerivedFrom(Assembly::AssemblyLink::getClassTypeId())) {
auto* subAssembly = static_cast<const AssemblyLink*>(obj);
count += subAssembly->numberOfComponents();
continue;
}
// Resolve standard App::Links to their target object
if (obj->isDerivedFrom(App::Link::getClassTypeId())) {
obj = static_cast<const App::Link*>(obj)->getLinkedObject();
if (!obj) {
continue;
}
}
if (!obj->isDerivedFrom(App::GeoFeature::getClassTypeId())) {
continue;
}
if (obj->isDerivedFrom(App::LocalCoordinateSystem::getClassTypeId())) {
continue;
}
count++;
}
return count;
}
bool AssemblyObject::isEmpty() const
{
return numberOfComponents() == 0;
}

View File

@@ -25,6 +25,7 @@
#ifndef ASSEMBLY_AssemblyObject_H
#define ASSEMBLY_AssemblyObject_H
#include <boost/signals2.hpp>
#include <Mod/Assembly/AssemblyGlobal.h>
@@ -193,6 +194,51 @@ public:
bool isMbDJointValid(App::DocumentObject* joint);
bool isEmpty() const;
int numberOfComponents() const;
inline int getLastDoF() const
{
return lastDoF;
}
inline bool getLastHasConflicts() const
{
return lastHasConflict;
}
inline bool getLastHasRedundancies() const
{
return lastHasRedundancies;
}
inline bool getLastHasPartialRedundancies() const
{
return lastHasPartialRedundancies;
}
inline bool getLastHasMalformedConstraints() const
{
return lastHasMalformedConstraints;
}
inline int getLastSolverStatus() const
{
return lastSolverStatus;
}
inline const std::vector<int>& getLastConflicting() const
{
return lastConflicting;
}
inline const std::vector<int>& getLastRedundant() const
{
return lastRedundant;
}
inline const std::vector<int>& getLastPartiallyRedundant() const
{
return lastPartiallyRedundant;
}
inline const std::vector<int>& getLastMalformedConstraints() const
{
return lastMalformedConstraints;
}
boost::signals2::signal<void()> signalSolverUpdate;
private:
std::shared_ptr<MbD::ASMTAssembly> mbdAssembly;
@@ -204,6 +250,18 @@ private:
std::vector<std::pair<App::DocumentObject*, Base::Placement>> previousPositions;
bool bundleFixed;
int lastDoF;
bool lastHasConflict;
bool lastHasRedundancies;
bool lastHasPartialRedundancies;
bool lastHasMalformedConstraints;
int lastSolverStatus;
std::vector<int> lastConflicting;
std::vector<int> lastRedundant;
std::vector<int> lastPartiallyRedundant;
std::vector<int> lastMalformedConstraints;
};
} // namespace Assembly

View File

@@ -45,6 +45,8 @@ SET(AssemblyGui_SRCS_Module
AppAssemblyGuiPy.cpp
PreCompiled.cpp
PreCompiled.h
TaskAssemblyMessages.cpp
TaskAssemblyMessages.h
ViewProviderAssembly.cpp
ViewProviderAssembly.h
ViewProviderAssemblyLink.cpp

View File

@@ -0,0 +1,93 @@
// SPDX - License - Identifier: LGPL - 2.1 - or -later
/****************************************************************************
* *
* Copyright (c) 2025 Pierre-Louis Boyer *
* *
* 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 "PreCompiled.h"
#ifndef _PreComp_
#endif
#include <Gui/Application.h>
#include <Gui/BitmapFactory.h>
#include <Gui/Command.h>
// #include <Mod/Assembly/App/AssemblyObject.h>
#include "TaskAssemblyMessages.h"
#include "ViewProviderAssembly.h"
using namespace AssemblyGui;
using namespace Gui::TaskView;
namespace sp = std::placeholders;
TaskAssemblyMessages::TaskAssemblyMessages(ViewProviderAssembly* vp)
: TaskSolverMessages(Gui::BitmapFactory().pixmap("Geoassembly"), tr("Solver messages"))
, vp(vp)
{
// NOLINTBEGIN
connectionSetUp = vp->signalSetUp.connect(
std::bind(&TaskAssemblyMessages::slotSetUp, this, sp::_1, sp::_2, sp::_3, sp::_4));
// NOLINTEND
}
TaskAssemblyMessages::~TaskAssemblyMessages()
{
connectionSetUp.disconnect();
}
void TaskAssemblyMessages::updateToolTip(const QString& link)
{
if (link == QStringLiteral("#conflicting")) {
setLinkTooltip(tr("Click to select these conflicting joints."));
}
else if (link == QStringLiteral("#redundant")) {
setLinkTooltip(tr("Click to select these redundant joints."));
}
else if (link == QStringLiteral("#dofs")) {
setLinkTooltip(tr("The assembly has unconstrained components giving rise to those "
"Degrees Of Freedom. Click to select these unconstrained components."));
}
else if (link == QStringLiteral("#malformed")) {
setLinkTooltip(tr("Click to select these malformed joints."));
}
}
void TaskAssemblyMessages::onLabelStatusLinkClicked(const QString& str)
{
// The commands are not implemented yet since App is not reporting yet the solver's status
/* if (str == QStringLiteral("#conflicting")) {
Gui::Application::Instance->commandManager().runCommandByName(
"Assembly_SelectConflictingConstraints");
}
else if (str == QStringLiteral("#redundant")) {
Gui::Application::Instance->commandManager().runCommandByName(
"Assembly_SelectRedundantConstraints");
}
else if (str == QStringLiteral("#dofs")) {
Gui::Application::Instance->commandManager().runCommandByName(
"Assembly_SelectComponentsWithDoFs");
}
else if (str == QStringLiteral("#malformed")) {
Gui::Application::Instance->commandManager().runCommandByName(
"Assembly_SelectMalformedConstraints");
}*/
}
#include "moc_TaskAssemblyMessages.cpp"

View File

@@ -0,0 +1,54 @@
// SPDX - License - Identifier: LGPL - 2.1 - or -later
/****************************************************************************
* *
* Copyright (c) 2025 Pierre-Louis Boyer *
* *
* 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 GUI_TASKVIEW_TaskAssemblyMessages_H
#define GUI_TASKVIEW_TaskAssemblyMessages_H
#include <Gui/TaskView/TaskSolverMessages.h>
namespace AssemblyGui
{
class ViewProviderAssembly;
class TaskAssemblyMessages: public Gui::TaskSolverMessages
{
Q_OBJECT
public:
explicit TaskAssemblyMessages(ViewProviderAssembly* vp);
~TaskAssemblyMessages() override;
private:
void onLabelStatusLinkClicked(const QString&) override;
void updateToolTip(const QString& link);
protected:
ViewProviderAssembly* vp;
};
} // namespace AssemblyGui
#endif // GUI_TASKVIEW_TaskAssemblyMessages_H

View File

@@ -69,6 +69,8 @@
#include <Mod/Assembly/App/BomGroup.h>
#include <Mod/PartDesign/App/Body.h>
#include "TaskAssemblyMessages.h"
#include "ViewProviderAssembly.h"
#include "ViewProviderAssemblyPy.h"
@@ -277,6 +279,17 @@ bool ViewProviderAssembly::setEdit(int mode)
attachSelection();
Gui::TaskView::TaskView* taskView = Gui::Control().taskPanel();
if (taskView) {
// Waiting for the solver to support reporting information.
// taskSolver = new TaskAssemblyMessages(this);
// taskView->addContextualPanel(taskSolver);
}
auto* assembly = getObject<AssemblyObject>();
connectSolverUpdate = assembly->signalSolverUpdate.connect(
boost::bind(&ViewProviderAssembly::UpdateSolverInformation, this));
return true;
}
return ViewProviderPart::setEdit(mode);
@@ -304,6 +317,15 @@ void ViewProviderAssembly::unsetEdit(int mode)
"Gui.getDocument(appDoc).ActiveView.setActiveObject('%s', None)",
this->getObject()->getDocument()->getName(),
PARTKEY);
Gui::TaskView::TaskView* taskView = Gui::Control().taskPanel();
if (taskView) {
// Waiting for the solver to support reporting information.
// taskView->removeContextualPanel(taskSolver);
}
connectSolverUpdate.disconnect();
return;
}
ViewProviderPart::unsetEdit(mode);
@@ -1271,3 +1293,87 @@ ViewProviderAssembly::getCenterOfBoundingBox(const std::vector<MovingObject>& mo
return center;
}
inline QString intListHelper(const std::vector<int>& ints)
{
QString results;
if (ints.size() < 8) { // The 8 is a bit heuristic... more than that and we shift formats
for (const auto i : ints) {
if (results.isEmpty()) {
results.append(QStringLiteral("%1").arg(i));
}
else {
results.append(QStringLiteral(", %1").arg(i));
}
}
}
else {
const int numToShow = 3;
int more = ints.size() - numToShow;
for (int i = 0; i < numToShow; ++i) {
results.append(QStringLiteral("%1, ").arg(ints[i]));
}
results.append(ViewProviderAssembly::tr("ViewProviderAssembly", "and %1 more").arg(more));
}
return results;
}
void ViewProviderAssembly::UpdateSolverInformation()
{
// Updates Solver Information with the Last solver execution at AssemblyObject level
auto* assembly = getObject<AssemblyObject>();
int dofs = assembly->getLastDoF();
bool hasConflicts = assembly->getLastHasConflicts();
bool hasRedundancies = assembly->getLastHasRedundancies();
bool hasPartiallyRedundant = assembly->getLastHasPartialRedundancies();
bool hasMalformed = assembly->getLastHasMalformedConstraints();
if (assembly->isEmpty()) {
signalSetUp(QStringLiteral("empty"), tr("Empty Assembly"), QString(), QString());
}
else if (dofs < 0 || hasConflicts) { // over-constrained
signalSetUp(QStringLiteral("conflicting_constraints"),
tr("Over-constrained:") + QLatin1String(" "),
QStringLiteral("#conflicting"),
QStringLiteral("(%1)").arg(intListHelper(assembly->getLastConflicting())));
}
else if (hasMalformed) { // malformed joints
signalSetUp(
QStringLiteral("malformed_constraints"),
tr("Malformed joints:") + QLatin1String(" "),
QStringLiteral("#malformed"),
QStringLiteral("(%1)").arg(intListHelper(assembly->getLastMalformedConstraints())));
}
else if (hasRedundancies) {
signalSetUp(QStringLiteral("redundant_constraints"),
tr("Redundant joints:") + QLatin1String(" "),
QStringLiteral("#redundant"),
QStringLiteral("(%1)").arg(intListHelper(assembly->getLastRedundant())));
}
else if (hasPartiallyRedundant) {
signalSetUp(
QStringLiteral("partially_redundant_constraints"),
tr("Partially redundant:") + QLatin1String(" "),
QStringLiteral("#partiallyredundant"),
QStringLiteral("(%1)").arg(intListHelper(assembly->getLastPartiallyRedundant())));
}
else if (assembly->getLastSolverStatus() != 0) {
signalSetUp(QStringLiteral("solver_failed"),
tr("Solver failed to converge"),
QStringLiteral(""),
QStringLiteral(""));
}
else if (dofs > 0) {
signalSetUp(QStringLiteral("under_constrained"),
tr("Under-constrained:") + QLatin1String(" "),
QStringLiteral("#dofs"),
tr("%n Degrees of Freedom", "", dofs));
}
else {
signalSetUp(QStringLiteral("fully_constrained"),
tr("Fully constrained"),
QString(),
QString());
}
}

View File

@@ -25,6 +25,7 @@
#define ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H
#include <QCoreApplication>
#include <boost/signals2.hpp>
#include <Mod/Assembly/AssemblyGlobal.h>
@@ -44,6 +45,7 @@ class View3DInventorViewer;
namespace AssemblyGui
{
class TaskAssemblyMessages;
struct MovingObject
{
@@ -200,6 +202,8 @@ public:
static Base::Vector3d getCenterOfBoundingBox(const std::vector<MovingObject>& movingObjs);
void UpdateSolverInformation();
DragMode dragMode;
bool canStartDragging;
bool partMoving;
@@ -229,6 +233,10 @@ public:
SoFieldSensor* translationSensor = nullptr;
SoFieldSensor* rotationSensor = nullptr;
boost::signals2::signal<
void(const QString& state, const QString& msg, const QString& url, const QString& linkText)>
signalSetUp;
private:
bool tryMouseMove(const SbVec2s& cursorPos, Gui::View3DInventorViewer* viewer);
void tryInitMove(const SbVec2s& cursorPos, Gui::View3DInventorViewer* viewer);
@@ -237,6 +245,9 @@ private:
const std::string& subNamePrefix,
App::DocumentObject* currentObject,
bool onlySolids);
TaskAssemblyMessages* taskSolver;
boost::signals2::connection connectSolverUpdate;
};
} // namespace AssemblyGui