diff --git a/src/Mod/Assembly/App/AssemblyLink.cpp b/src/Mod/Assembly/App/AssemblyLink.cpp index 64f1f5da08..f12169669c 100644 --- a/src/Mod/Assembly/App/AssemblyLink.cpp +++ b/src/Mod/Assembly/App/AssemblyLink.cpp @@ -541,7 +541,7 @@ AssemblyObject* AssemblyLink::getParentAssembly() const return nullptr; } -bool AssemblyLink::isRigid() +bool AssemblyLink::isRigid() const { auto* prop = dynamic_cast(getPropertyByName("Rigid")); if (!prop) { @@ -559,3 +559,18 @@ std::vector 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; +} diff --git a/src/Mod/Assembly/App/AssemblyLink.h b/src/Mod/Assembly/App/AssemblyLink.h index 370a78acb4..942f67d93c 100644 --- a/src/Mod/Assembly/App/AssemblyLink.h +++ b/src/Mod/Assembly/App/AssemblyLink.h @@ -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 getJoints(); + bool allowDuplicateLabel() const override; + + bool isEmpty() const; + int numberOfComponents() const; + App::PropertyXLink LinkedObject; App::PropertyBool Rigid; diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index dc49cf7eb2..e83db1f1e0 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -102,8 +102,17 @@ PROPERTY_SOURCE(Assembly::AssemblyObject, App::Part) AssemblyObject::AssemblyObject() : mbdAssembly(std::make_shared()) , 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 objects = Group.getValues(); + + for (auto* obj : objects) { + if (!obj) { + continue; + } + + if (obj->isLinkGroup()) { + auto* link = static_cast(obj); + count += link->ElementCount.getValue(); + continue; + } + + if (obj->isDerivedFrom(Assembly::AssemblyLink::getClassTypeId())) { + auto* subAssembly = static_cast(obj); + count += subAssembly->numberOfComponents(); + continue; + } + + // Resolve standard App::Links to their target object + if (obj->isDerivedFrom(App::Link::getClassTypeId())) { + obj = static_cast(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; +} diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h index 59689fd708..2d2959c9dd 100644 --- a/src/Mod/Assembly/App/AssemblyObject.h +++ b/src/Mod/Assembly/App/AssemblyObject.h @@ -25,6 +25,7 @@ #ifndef ASSEMBLY_AssemblyObject_H #define ASSEMBLY_AssemblyObject_H +#include #include @@ -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& getLastConflicting() const + { + return lastConflicting; + } + inline const std::vector& getLastRedundant() const + { + return lastRedundant; + } + inline const std::vector& getLastPartiallyRedundant() const + { + return lastPartiallyRedundant; + } + inline const std::vector& getLastMalformedConstraints() const + { + return lastMalformedConstraints; + } + boost::signals2::signal signalSolverUpdate; + private: std::shared_ptr mbdAssembly; @@ -204,6 +250,18 @@ private: std::vector> previousPositions; bool bundleFixed; + + int lastDoF; + bool lastHasConflict; + bool lastHasRedundancies; + bool lastHasPartialRedundancies; + bool lastHasMalformedConstraints; + int lastSolverStatus; + + std::vector lastConflicting; + std::vector lastRedundant; + std::vector lastPartiallyRedundant; + std::vector lastMalformedConstraints; }; } // namespace Assembly diff --git a/src/Mod/Assembly/Gui/CMakeLists.txt b/src/Mod/Assembly/Gui/CMakeLists.txt index 534ec90174..c69ae788fa 100644 --- a/src/Mod/Assembly/Gui/CMakeLists.txt +++ b/src/Mod/Assembly/Gui/CMakeLists.txt @@ -45,6 +45,8 @@ SET(AssemblyGui_SRCS_Module AppAssemblyGuiPy.cpp PreCompiled.cpp PreCompiled.h + TaskAssemblyMessages.cpp + TaskAssemblyMessages.h ViewProviderAssembly.cpp ViewProviderAssembly.h ViewProviderAssemblyLink.cpp diff --git a/src/Mod/Assembly/Gui/TaskAssemblyMessages.cpp b/src/Mod/Assembly/Gui/TaskAssemblyMessages.cpp new file mode 100644 index 0000000000..a85ed0e0e0 --- /dev/null +++ b/src/Mod/Assembly/Gui/TaskAssemblyMessages.cpp @@ -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 * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" +#ifndef _PreComp_ +#endif + +#include +#include +#include +// #include + +#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" diff --git a/src/Mod/Assembly/Gui/TaskAssemblyMessages.h b/src/Mod/Assembly/Gui/TaskAssemblyMessages.h new file mode 100644 index 0000000000..2ff1fc3ecf --- /dev/null +++ b/src/Mod/Assembly/Gui/TaskAssemblyMessages.h @@ -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 * + * . * + * * + ***************************************************************************/ + +#ifndef GUI_TASKVIEW_TaskAssemblyMessages_H +#define GUI_TASKVIEW_TaskAssemblyMessages_H + +#include + + +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 diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index c76c713927..65cd6fce79 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -69,6 +69,8 @@ #include #include +#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(); + 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& mo return center; } + +inline QString intListHelper(const std::vector& 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(); + + 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()); + } +} diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.h b/src/Mod/Assembly/Gui/ViewProviderAssembly.h index 6cb10070e5..27db6ea3c3 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.h +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.h @@ -25,6 +25,7 @@ #define ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H #include +#include #include @@ -44,6 +45,7 @@ class View3DInventorViewer; namespace AssemblyGui { +class TaskAssemblyMessages; struct MovingObject { @@ -200,6 +202,8 @@ public: static Base::Vector3d getCenterOfBoundingBox(const std::vector& 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