Files
create/src/Mod/Assembly/App/AssemblyObject.cpp
Markus Reitböck e46020ada4 Assembly: use CMake to generate precompiled headers on all platforms
"Professional CMake" book suggest the following:

"Targets should build successfully with or without compiler support for precompiled headers. It
 should be considered an optimization, not a requirement. In particular, do not explicitly include a
 precompile header (e.g. stdafx.h) in the source code, let CMake force-include an automatically
 generated precompile header on the compiler command line instead. This is more portable across
 the major compilers and is likely to be easier to maintain. It will also avoid warnings being
 generated from certain code checking tools like iwyu (include what you use)."

Therefore, removed the "#include <PreCompiled.h>" from sources, also
there is no need for the "#ifdef _PreComp_" anymore
2025-09-23 22:39:34 +02:00

2046 lines
66 KiB
C++

// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2023 Ondsel <development@ondsel.com> *
* *
* 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 <boost/core/ignore_unused.hpp>
#include <cmath>
#include <vector>
#include <unordered_map>
#include <App/Application.h>
#include <App/Datums.h>
#include <App/Document.h>
#include <App/DocumentObjectGroup.h>
#include <App/FeaturePythonPyImp.h>
#include <App/Link.h>
#include <App/PropertyPythonObject.h>
#include <Base/Console.h>
#include <Base/Placement.h>
#include <Base/Rotation.h>
#include <Base/Tools.h>
#include <Base/Interpreter.h>
#include <Mod/Part/App/TopoShape.h>
#include <Mod/Part/App/AttachExtension.h>
#include <OndselSolver/CREATE.h>
#include <OndselSolver/ASMTSimulationParameters.h>
#include <OndselSolver/ASMTAssembly.h>
#include <OndselSolver/ASMTMarker.h>
#include <OndselSolver/ASMTPart.h>
#include <OndselSolver/ASMTJoint.h>
#include <OndselSolver/ASMTAngleJoint.h>
#include <OndselSolver/ASMTFixedJoint.h>
#include <OndselSolver/ASMTGearJoint.h>
#include <OndselSolver/ASMTRevoluteJoint.h>
#include <OndselSolver/ASMTCylindricalJoint.h>
#include <OndselSolver/ASMTTranslationalJoint.h>
#include <OndselSolver/ASMTSphericalJoint.h>
#include <OndselSolver/ASMTParallelAxesJoint.h>
#include <OndselSolver/ASMTPerpendicularJoint.h>
#include <OndselSolver/ASMTPointInPlaneJoint.h>
#include <OndselSolver/ASMTPointInLineJoint.h>
#include <OndselSolver/ASMTLineInPlaneJoint.h>
#include <OndselSolver/ASMTPlanarJoint.h>
#include <OndselSolver/ASMTRevCylJoint.h>
#include <OndselSolver/ASMTCylSphJoint.h>
#include <OndselSolver/ASMTRackPinionJoint.h>
#include <OndselSolver/ASMTRotationLimit.h>
#include <OndselSolver/ASMTTranslationLimit.h>
#include <OndselSolver/ASMTRotationalMotion.h>
#include <OndselSolver/ASMTTranslationalMotion.h>
#include <OndselSolver/ASMTGeneralMotion.h>
#include <OndselSolver/ASMTScrewJoint.h>
#include <OndselSolver/ASMTSphSphJoint.h>
#include <OndselSolver/ASMTTime.h>
#include <OndselSolver/ASMTConstantGravity.h>
#include <OndselSolver/ExternalSystem.h>
#include <OndselSolver/enum.h>
#include "AssemblyLink.h"
#include "AssemblyObject.h"
#include "AssemblyObjectPy.h"
#include "AssemblyUtils.h"
#include "JointGroup.h"
#include "ViewGroup.h"
FC_LOG_LEVEL_INIT("Assembly", true, true, true)
using namespace Assembly;
using namespace MbD;
namespace PartApp = Part;
// ================================ Assembly Object ============================
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;
PyObject* AssemblyObject::getPyObject()
{
if (PythonObject.is(Py::_None())) {
// ref counter is set to 1
PythonObject = Py::Object(new AssemblyObjectPy(this), true);
}
return Py::new_reference_to(PythonObject);
}
App::DocumentObjectExecReturn* AssemblyObject::execute()
{
App::DocumentObjectExecReturn* ret = App::Part::execute();
ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Mod/Assembly");
if (hGrp->GetBool("SolveOnRecompute", true)) {
solve();
}
return ret;
}
int AssemblyObject::solve(bool enableRedo, bool updateJCS)
{
lastDoF = numberOfComponents() * 6;
ensureIdentityPlacements();
mbdAssembly = makeMbdAssembly();
objectPartMap.clear();
motions.clear();
auto groundedObjs = fixGroundedParts();
if (groundedObjs.empty()) {
// If no part fixed we can't solve.
return -6;
}
std::vector<App::DocumentObject*> joints = getJoints(updateJCS);
removeUnconnectedJoints(joints, groundedObjs);
jointParts(joints);
if (enableRedo) {
savePlacementsForUndo();
}
try {
mbdAssembly->runKINEMATIC();
}
catch (const std::exception& e) {
FC_ERR("Solve failed: " << e.what());
return -1;
}
catch (...) {
FC_ERR("Solve failed: unhandled exception");
return -1;
}
setNewPlacements();
redrawJointPlacements(joints);
signalSolverUpdate();
return 0;
}
int AssemblyObject::generateSimulation(App::DocumentObject* sim)
{
mbdAssembly = makeMbdAssembly();
objectPartMap.clear();
motions = getMotionsFromSimulation(sim);
auto groundedObjs = fixGroundedParts();
if (groundedObjs.empty()) {
// If no part fixed we can't solve.
return -6;
}
std::vector<App::DocumentObject*> joints = getJoints();
removeUnconnectedJoints(joints, groundedObjs);
jointParts(joints);
create_mbdSimulationParameters(sim);
try {
mbdAssembly->runKINEMATIC();
}
catch (...) {
Base::Console().error("Generation of simulation failed\n");
motions.clear();
return -1;
}
motions.clear();
return 0;
}
std::vector<App::DocumentObject*> AssemblyObject::getMotionsFromSimulation(App::DocumentObject* sim)
{
if (!sim) {
return {};
}
auto* prop = dynamic_cast<App::PropertyLinkList*>(sim->getPropertyByName("Group"));
if (!prop) {
return {};
}
return prop->getValue();
}
int Assembly::AssemblyObject::updateForFrame(size_t index, bool updateJCS)
{
if (!mbdAssembly) {
return -1;
}
auto nfrms = mbdAssembly->numberOfFrames();
if (index >= nfrms) {
return -1;
}
mbdAssembly->updateForFrame(index);
setNewPlacements();
auto jointDocs = getJoints(updateJCS);
redrawJointPlacements(jointDocs);
return 0;
}
size_t Assembly::AssemblyObject::numberOfFrames()
{
return mbdAssembly->numberOfFrames();
}
void AssemblyObject::preDrag(std::vector<App::DocumentObject*> dragParts)
{
bundleFixed = true;
solve();
bundleFixed = false;
draggedParts.clear();
for (auto part : dragParts) {
// make sure no duplicate
if (std::ranges::find(draggedParts, part) != draggedParts.end()) {
continue;
}
// Some objects have been bundled, we don't want to add these to dragged parts
Base::Placement plc;
for (auto& pair : objectPartMap) {
App::DocumentObject* parti = pair.first;
if (parti != part) {
continue;
}
plc = pair.second.offsetPlc;
}
if (!plc.isIdentity()) {
// If not identity, then it's a bundled object. Some bundled objects may
// have identity placement if they have the same position as the main object of
// the bundle. But they're not going to be a problem.
continue;
}
draggedParts.push_back(part);
}
mbdAssembly->runPreDrag();
}
void AssemblyObject::doDragStep()
{
try {
std::vector<std::shared_ptr<MbD::ASMTPart>> dragMbdParts;
for (auto& part : draggedParts) {
if (!part) {
continue;
}
auto mbdPart = getMbDPart(part);
dragMbdParts.push_back(mbdPart);
// Update the MBD part's position
Base::Placement plc = getPlacementFromProp(part, "Placement");
Base::Vector3d pos = plc.getPosition();
mbdPart->updateMbDFromPosition3D(
std::make_shared<FullColumn<double>>(ListD {pos.x, pos.y, pos.z}));
// Update the MBD part's rotation
Base::Rotation rot = plc.getRotation();
Base::Matrix4D mat;
rot.getValue(mat);
Base::Vector3d r0 = mat.getRow(0);
Base::Vector3d r1 = mat.getRow(1);
Base::Vector3d r2 = mat.getRow(2);
mbdPart
->updateMbDFromRotationMatrix(r0.x, r0.y, r0.z, r1.x, r1.y, r1.z, r2.x, r2.y, r2.z);
}
// Timing mbdAssembly->runDragStep()
auto dragPartsVec = std::make_shared<std::vector<std::shared_ptr<ASMTPart>>>(dragMbdParts);
mbdAssembly->runDragStep(dragPartsVec);
// Timing the validation and placement setting
if (validateNewPlacements()) {
setNewPlacements();
auto joints = getJoints(false);
for (auto* joint : joints) {
if (joint->Visibility.getValue()) {
// redraw only the moving joint as its quite slow as its python code.
redrawJointPlacement(joint);
}
}
}
}
catch (...) {
// We do nothing if a solve step fails.
}
}
Base::Placement AssemblyObject::getMbdPlacement(std::shared_ptr<ASMTPart> mbdPart)
{
if (!mbdPart) {
return Base::Placement();
}
double x, y, z;
mbdPart->getPosition3D(x, y, z);
Base::Vector3d pos = Base::Vector3d(x, y, z);
double q0, q1, q2, q3;
mbdPart->getQuarternions(q3, q0, q1, q2);
Base::Rotation rot = Base::Rotation(q0, q1, q2, q3);
return Base::Placement(pos, rot);
}
bool AssemblyObject::validateNewPlacements()
{
// First we check if a grounded object has moved. It can happen that they flip.
auto groundedParts = getGroundedParts();
for (auto* obj : groundedParts) {
auto* propPlacement =
dynamic_cast<App::PropertyPlacement*>(obj->getPropertyByName("Placement"));
if (propPlacement) {
Base::Placement oldPlc = propPlacement->getValue();
auto it = objectPartMap.find(obj);
if (it != objectPartMap.end()) {
std::shared_ptr<MbD::ASMTPart> mbdPart = it->second.part;
Base::Placement newPlacement = getMbdPlacement(mbdPart);
if (!it->second.offsetPlc.isIdentity()) {
newPlacement = newPlacement * it->second.offsetPlc;
}
if (!oldPlc.isSame(newPlacement)) {
Base::Console().warning(
"Assembly : Ignoring bad solve, a grounded object (%s) moved.\n",
obj->getFullLabel());
return false;
}
}
}
}
// TODO: We could do further tests
// For example check if the joints connectors are correctly aligned.
return true;
}
void AssemblyObject::postDrag()
{
mbdAssembly->runPostDrag(); // Do this after last drag
}
void AssemblyObject::savePlacementsForUndo()
{
previousPositions.clear();
for (auto& pair : objectPartMap) {
App::DocumentObject* obj = pair.first;
if (!obj) {
continue;
}
std::pair<App::DocumentObject*, Base::Placement> savePair;
savePair.first = obj;
// Check if the object has a "Placement" property
auto* propPlc = dynamic_cast<App::PropertyPlacement*>(obj->getPropertyByName("Placement"));
if (!propPlc) {
continue;
}
savePair.second = propPlc->getValue();
previousPositions.push_back(savePair);
}
}
void AssemblyObject::undoSolve()
{
if (previousPositions.size() == 0) {
return;
}
for (auto& pair : previousPositions) {
App::DocumentObject* obj = pair.first;
if (!obj) {
continue;
}
// Check if the object has a "Placement" property
auto* propPlacement =
dynamic_cast<App::PropertyPlacement*>(obj->getPropertyByName("Placement"));
if (!propPlacement) {
continue;
}
propPlacement->setValue(pair.second);
}
previousPositions.clear();
// update joint placements:
getJoints(/*updateJCS*/ true, /*delBadJoints*/ false);
}
void AssemblyObject::clearUndo()
{
previousPositions.clear();
}
void AssemblyObject::exportAsASMT(std::string fileName)
{
mbdAssembly = makeMbdAssembly();
objectPartMap.clear();
fixGroundedParts();
std::vector<App::DocumentObject*> joints = getJoints();
jointParts(joints);
mbdAssembly->outputFile(fileName);
}
void AssemblyObject::setNewPlacements()
{
for (auto& pair : objectPartMap) {
App::DocumentObject* obj = pair.first;
std::shared_ptr<ASMTPart> mbdPart = pair.second.part;
if (!obj || !mbdPart) {
continue;
}
// Check if the object has a "Placement" property
auto* propPlacement =
dynamic_cast<App::PropertyPlacement*>(obj->getPropertyByName("Placement"));
if (!propPlacement) {
continue;
}
Base::Placement newPlacement = getMbdPlacement(mbdPart);
if (!pair.second.offsetPlc.isIdentity()) {
newPlacement = newPlacement * pair.second.offsetPlc;
}
if (!propPlacement->getValue().isSame(newPlacement)) {
propPlacement->setValue(newPlacement);
obj->purgeTouched();
}
}
}
void AssemblyObject::redrawJointPlacements(std::vector<App::DocumentObject*> joints)
{
// Notify the joint objects that the transform of the coin object changed.
for (auto* joint : joints) {
if (!joint) {
continue;
}
redrawJointPlacement(joint);
}
}
void AssemblyObject::redrawJointPlacement(App::DocumentObject* joint)
{
if (!joint) {
return;
}
// Notify the joint object that the transform of the coin object changed.
auto* pPlc = dynamic_cast<App::PropertyPlacement*>(joint->getPropertyByName("Placement1"));
if (pPlc) {
pPlc->setValue(pPlc->getValue());
}
pPlc = dynamic_cast<App::PropertyPlacement*>(joint->getPropertyByName("Placement2"));
if (pPlc) {
pPlc->setValue(pPlc->getValue());
}
joint->purgeTouched();
}
void AssemblyObject::recomputeJointPlacements(std::vector<App::DocumentObject*> joints)
{
// The Placement1 and Placement2 of each joint needs to be updated as the parts moved.
Base::PyGILStateLocker lock;
for (auto* joint : joints) {
if (!joint) {
continue;
}
App::PropertyPythonObject* proxy = joint
? dynamic_cast<App::PropertyPythonObject*>(joint->getPropertyByName("Proxy"))
: nullptr;
if (!proxy) {
continue;
}
Py::Object jointPy = proxy->getValue();
if (!jointPy.hasAttr("updateJCSPlacements")) {
continue;
}
Py::Object attr = jointPy.getAttr("updateJCSPlacements");
if (attr.ptr() && attr.isCallable()) {
Py::Tuple args(1);
args.setItem(0, Py::asObject(joint->getPyObject()));
Py::Callable(attr).apply(args);
joint->purgeTouched();
}
}
}
std::shared_ptr<ASMTAssembly> AssemblyObject::makeMbdAssembly()
{
auto assembly = CREATE<ASMTAssembly>::With();
assembly->externalSystem->freecadAssemblyObject = this;
assembly->setName("OndselAssembly");
ParameterGrp::handle hPgr = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Mod/Assembly");
assembly->setDebug(hPgr->GetBool("LogSolverDebug", false));
return assembly;
}
App::DocumentObject* AssemblyObject::getJointOfPartConnectingToGround(
App::DocumentObject* part,
std::string& name,
const std::vector<App::DocumentObject*>& excludeJoints)
{
if (!part) {
return nullptr;
}
std::vector<App::DocumentObject*> joints = getJointsOfPart(part);
for (auto joint : joints) {
if (!joint) {
continue;
}
if (std::ranges::find(excludeJoints, joint) != excludeJoints.end()) {
continue;
}
App::DocumentObject* part1 = getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(this, joint, "Reference2");
if (!part1 || !part2) {
continue;
}
if (part == part1 && isJointConnectingPartToGround(joint, "Reference1")) {
name = "Reference1";
return joint;
}
if (part == part2 && isJointConnectingPartToGround(joint, "Reference2")) {
name = "Reference2";
return joint;
}
}
return nullptr;
}
template<typename T>
T* AssemblyObject::getGroup()
{
App::Document* doc = getDocument();
std::vector<DocumentObject*> groups = doc->getObjectsOfType(T::getClassTypeId());
if (groups.empty()) {
return nullptr;
}
for (auto group : groups) {
if (hasObject(group)) {
return freecad_cast<T*>(group);
}
}
return nullptr;
}
JointGroup* AssemblyObject::getJointGroup() const
{
return Assembly::getJointGroup(this);
}
ViewGroup* AssemblyObject::getExplodedViewGroup() const
{
App::Document* doc = getDocument();
std::vector<DocumentObject*> viewGroups = doc->getObjectsOfType(ViewGroup::getClassTypeId());
if (viewGroups.empty()) {
return nullptr;
}
for (auto viewGroup : viewGroups) {
if (hasObject(viewGroup)) {
return freecad_cast<ViewGroup*>(viewGroup);
}
}
return nullptr;
}
std::vector<App::DocumentObject*>
AssemblyObject::getJoints(bool updateJCS, bool delBadJoints, bool subJoints)
{
std::vector<App::DocumentObject*> joints = {};
JointGroup* jointGroup = getJointGroup();
if (!jointGroup) {
return {};
}
Base::PyGILStateLocker lock;
for (auto joint : jointGroup->getObjects()) {
if (!joint) {
continue;
}
auto* prop = dynamic_cast<App::PropertyBool*>(joint->getPropertyByName("Suppressed"));
if (joint->isError() || !prop || prop->getValue()) {
// Filter grounded joints and deactivated joints.
continue;
}
auto* part1 = getMovingPartFromRef(this, joint, "Reference1");
auto* part2 = getMovingPartFromRef(this, joint, "Reference2");
if (!part1 || !part2 || part1->getFullName() == part2->getFullName()) {
// Remove incomplete joints. Left-over when the user deletes a part.
// Remove incoherent joints (self-pointing joints)
if (delBadJoints) {
getDocument()->removeObject(joint->getNameInDocument());
}
continue;
}
auto proxy = dynamic_cast<App::PropertyPythonObject*>(joint->getPropertyByName("Proxy"));
if (proxy) {
if (proxy->getValue().hasAttr("setJointConnectors")) {
joints.push_back(joint);
}
}
}
// add sub assemblies joints.
if (subJoints) {
for (auto& assembly : getSubAssemblies()) {
auto subJoints = assembly->getJoints();
joints.insert(joints.end(), subJoints.begin(), subJoints.end());
}
}
// Make sure the joints are up to date.
if (updateJCS) {
recomputeJointPlacements(joints);
}
return joints;
}
std::vector<App::DocumentObject*> AssemblyObject::getGroundedJoints()
{
std::vector<App::DocumentObject*> joints = {};
JointGroup* jointGroup = getJointGroup();
if (!jointGroup) {
return {};
}
Base::PyGILStateLocker lock;
for (auto obj : jointGroup->getObjects()) {
if (!obj) {
continue;
}
auto* propObj = dynamic_cast<App::PropertyLink*>(obj->getPropertyByName("ObjectToGround"));
if (propObj) {
joints.push_back(obj);
}
}
return joints;
}
std::vector<App::DocumentObject*> AssemblyObject::getJointsOfObj(App::DocumentObject* obj)
{
if (!obj) {
return {};
}
std::vector<App::DocumentObject*> joints = getJoints(false);
std::vector<App::DocumentObject*> jointsOf;
for (auto joint : joints) {
App::DocumentObject* obj1 = getObjFromRef(joint, "Reference1");
App::DocumentObject* obj2 = getObjFromRef(joint, "Reference2");
if (obj == obj1 || obj == obj2) {
jointsOf.push_back(joint);
}
}
return jointsOf;
}
std::vector<App::DocumentObject*> AssemblyObject::getJointsOfPart(App::DocumentObject* part)
{
if (!part) {
return {};
}
std::vector<App::DocumentObject*> joints = getJoints(false);
std::vector<App::DocumentObject*> jointsOf;
for (auto joint : joints) {
App::DocumentObject* part1 = getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(this, joint, "Reference2");
if (part == part1 || part == part2) {
jointsOf.push_back(joint);
}
}
return jointsOf;
}
std::unordered_set<App::DocumentObject*> AssemblyObject::getGroundedParts()
{
std::vector<App::DocumentObject*> groundedJoints = getGroundedJoints();
std::unordered_set<App::DocumentObject*> groundedSet;
for (auto gJoint : groundedJoints) {
if (!gJoint) {
continue;
}
auto* propObj =
dynamic_cast<App::PropertyLink*>(gJoint->getPropertyByName("ObjectToGround"));
if (propObj) {
App::DocumentObject* objToGround = propObj->getValue();
if (objToGround) {
groundedSet.insert(objToGround);
}
}
}
// We also need to add all the root-level datums objects that are not attached.
std::vector<App::DocumentObject*> objs = Group.getValues();
for (auto* obj : objs) {
if (obj->isDerivedFrom<App::LocalCoordinateSystem>()
|| obj->isDerivedFrom<App::DatumElement>()) {
auto* pcAttach = obj->getExtensionByType<PartApp::AttachExtension>();
if (pcAttach) {
// If it's a Part datums, we check if it's attached. If yes then we ignore it.
std::string mode = pcAttach->MapMode.getValueAsString();
if (mode != "Deactivated") {
continue;
}
}
groundedSet.insert(obj);
}
}
// Origin is not in Group so we add it separately
groundedSet.insert(Origin.getValue());
return groundedSet;
}
std::unordered_set<App::DocumentObject*> AssemblyObject::fixGroundedParts()
{
auto groundedParts = getGroundedParts();
for (auto obj : groundedParts) {
if (!obj) {
continue;
}
Base::Placement plc = getPlacementFromProp(obj, "Placement");
std::string str = obj->getFullName();
fixGroundedPart(obj, plc, str);
}
return groundedParts;
}
void AssemblyObject::fixGroundedPart(App::DocumentObject* obj,
Base::Placement& plc,
std::string& name)
{
if (!obj) {
return;
}
std::string markerName1 = "marker-" + obj->getFullName();
auto mbdMarker1 = makeMbdMarker(markerName1, plc);
mbdAssembly->addMarker(mbdMarker1);
std::shared_ptr<ASMTPart> mbdPart = getMbDPart(obj);
std::string markerName2 = "FixingMarker";
Base::Placement basePlc = Base::Placement();
auto mbdMarker2 = makeMbdMarker(markerName2, basePlc);
mbdPart->addMarker(mbdMarker2);
markerName1 = "/OndselAssembly/" + mbdMarker1->name;
markerName2 = "/OndselAssembly/" + mbdPart->name + "/" + mbdMarker2->name;
auto mbdJoint = CREATE<ASMTFixedJoint>::With();
mbdJoint->setName(name);
mbdJoint->setMarkerI(markerName1);
mbdJoint->setMarkerJ(markerName2);
mbdAssembly->addJoint(mbdJoint);
}
bool AssemblyObject::isJointConnectingPartToGround(App::DocumentObject* joint, const char* propname)
{
if (!joint || !isJointTypeConnecting(joint)) {
return false;
}
App::DocumentObject* part = getMovingPartFromRef(this, joint, propname);
if (!part) {
return false;
}
// Check if the part is grounded.
bool isGrounded = isPartGrounded(part);
if (isGrounded) {
return false;
}
// Check if the part is disconnected even with the joint
bool isConnected = isPartConnected(part);
if (!isConnected) {
return false;
}
// to know if a joint is connecting to ground we disable all the other joints
std::vector<App::DocumentObject*> jointsOfPart = getJointsOfPart(part);
std::vector<bool> activatedStates;
for (auto jointi : jointsOfPart) {
if (jointi->getFullName() == joint->getFullName()) {
continue;
}
activatedStates.push_back(getJointActivated(jointi));
setJointActivated(jointi, false);
}
isConnected = isPartConnected(part);
// restore activation states
for (auto jointi : jointsOfPart) {
if (jointi->getFullName() == joint->getFullName() || activatedStates.empty()) {
continue;
}
setJointActivated(jointi, activatedStates[0]);
activatedStates.erase(activatedStates.begin());
}
return isConnected;
}
bool AssemblyObject::isJointTypeConnecting(App::DocumentObject* joint)
{
if (!joint) {
return false;
}
JointType jointType = getJointType(joint);
return jointType != JointType::RackPinion && jointType != JointType::Screw
&& jointType != JointType::Gears && jointType != JointType::Belt;
}
bool AssemblyObject::isObjInSetOfObjRefs(App::DocumentObject* obj, const std::vector<ObjRef>& set)
{
if (!obj) {
return false;
}
for (const auto& pair : set) {
if (pair.obj == obj) {
return true;
}
}
return false;
}
void AssemblyObject::removeUnconnectedJoints(std::vector<App::DocumentObject*>& joints,
std::unordered_set<App::DocumentObject*> groundedObjs)
{
std::vector<ObjRef> connectedParts;
// Initialize connectedParts with groundedObjs
for (auto* groundedObj : groundedObjs) {
connectedParts.push_back({groundedObj, nullptr});
}
// Perform a traversal from each grounded object
for (auto* groundedObj : groundedObjs) {
traverseAndMarkConnectedParts(groundedObj, connectedParts, joints);
}
// Filter out unconnected joints
joints.erase(std::remove_if(joints.begin(),
joints.end(),
[&](App::DocumentObject* joint) {
App::DocumentObject* obj1 =
getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* obj2 =
getMovingPartFromRef(this, joint, "Reference2");
return (!isObjInSetOfObjRefs(obj1, connectedParts)
|| !isObjInSetOfObjRefs(obj2, connectedParts));
}),
joints.end());
}
void AssemblyObject::traverseAndMarkConnectedParts(App::DocumentObject* currentObj,
std::vector<ObjRef>& connectedParts,
const std::vector<App::DocumentObject*>& joints)
{
// getConnectedParts returns the objs connected to the currentObj by any joint
auto connectedObjs = getConnectedParts(currentObj, joints);
for (auto& nextObjRef : connectedObjs) {
if (!isObjInSetOfObjRefs(nextObjRef.obj, connectedParts)) {
// Create a new ObjRef with the nextObj and a nullptr for PropertyXLinkSub*
connectedParts.push_back(nextObjRef);
traverseAndMarkConnectedParts(nextObjRef.obj, connectedParts, joints);
}
}
}
std::vector<ObjRef>
AssemblyObject::getConnectedParts(App::DocumentObject* part,
const std::vector<App::DocumentObject*>& joints)
{
if (!part) {
return {};
}
std::vector<ObjRef> connectedParts;
for (auto joint : joints) {
if (!isJointTypeConnecting(joint)) {
continue;
}
App::DocumentObject* obj1 = getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* obj2 = getMovingPartFromRef(this, joint, "Reference2");
if (!obj1 || !obj2) {
continue;
}
if (obj1 == part) {
auto* ref =
dynamic_cast<App::PropertyXLinkSub*>(joint->getPropertyByName("Reference2"));
if (!ref) {
continue;
}
connectedParts.push_back({obj2, ref});
}
else if (obj2 == part) {
auto* ref =
dynamic_cast<App::PropertyXLinkSub*>(joint->getPropertyByName("Reference1"));
if (!ref) {
continue;
}
connectedParts.push_back({obj1, ref});
}
}
return connectedParts;
}
bool AssemblyObject::isPartGrounded(App::DocumentObject* obj)
{
if (!obj) {
return false;
}
auto groundedObjs = getGroundedParts();
for (auto* groundedObj : groundedObjs) {
if (groundedObj->getFullName() == obj->getFullName()) {
return true;
}
}
return false;
}
bool AssemblyObject::isPartConnected(App::DocumentObject* obj)
{
if (!obj) {
return false;
}
auto groundedObjs = getGroundedParts();
std::vector<App::DocumentObject*> joints = getJoints(false);
std::vector<ObjRef> connectedParts;
// Initialize connectedParts with groundedObjs
for (auto* groundedObj : groundedObjs) {
connectedParts.push_back({groundedObj, nullptr});
}
// Perform a traversal from each grounded object
for (auto* groundedObj : groundedObjs) {
traverseAndMarkConnectedParts(groundedObj, connectedParts, joints);
}
for (auto& objRef : connectedParts) {
if (obj == objRef.obj) {
return true;
}
}
return false;
}
void AssemblyObject::jointParts(std::vector<App::DocumentObject*> joints)
{
for (auto* joint : joints) {
if (!joint) {
continue;
}
std::vector<std::shared_ptr<MbD::ASMTJoint>> mbdJoints = makeMbdJoint(joint);
for (auto& mbdJoint : mbdJoints) {
mbdAssembly->addJoint(mbdJoint);
}
}
}
void Assembly::AssemblyObject::create_mbdSimulationParameters(App::DocumentObject* sim)
{
auto mbdSim = mbdAssembly->simulationParameters;
if (!sim) {
return;
}
auto valueOf = [](DocumentObject* docObj, const char* propName) {
auto* prop = dynamic_cast<App::PropertyFloat*>(docObj->getPropertyByName(propName));
if (!prop) {
return 0.0;
}
return prop->getValue();
};
mbdSim->settstart(valueOf(sim, "aTimeStart"));
mbdSim->settend(valueOf(sim, "bTimeEnd"));
mbdSim->sethout(valueOf(sim, "cTimeStepOutput"));
mbdSim->sethmin(1.0e-9);
mbdSim->sethmax(1.0);
mbdSim->seterrorTol(valueOf(sim, "fGlobalErrorTolerance"));
}
std::shared_ptr<ASMTJoint> AssemblyObject::makeMbdJointOfType(App::DocumentObject* joint,
JointType type)
{
switch (type) {
case JointType::Fixed:
if (bundleFixed) {
return nullptr;
}
return CREATE<ASMTFixedJoint>::With();
case JointType::Revolute:
return CREATE<ASMTRevoluteJoint>::With();
case JointType::Cylindrical:
return CREATE<ASMTCylindricalJoint>::With();
case JointType::Slider:
return CREATE<ASMTTranslationalJoint>::With();
case JointType::Ball:
return CREATE<ASMTSphericalJoint>::With();
case JointType::Distance:
return makeMbdJointDistance(joint);
case JointType::Parallel:
return CREATE<ASMTParallelAxesJoint>::With();
case JointType::Perpendicular:
return CREATE<ASMTPerpendicularJoint>::With();
case JointType::Angle: {
double angle = fabs(Base::toRadians(getJointDistance(joint)));
if (fmod(angle, 2 * std::numbers::pi) < Precision::Confusion()) {
return CREATE<ASMTParallelAxesJoint>::With();
}
auto mbdJoint = CREATE<ASMTAngleJoint>::With();
mbdJoint->theIzJz = angle;
return mbdJoint;
}
case JointType::RackPinion: {
auto mbdJoint = CREATE<ASMTRackPinionJoint>::With();
mbdJoint->pitchRadius = getJointDistance(joint);
return mbdJoint;
}
case JointType::Screw: {
int slidingIndex = slidingPartIndex(joint);
if (slidingIndex == 0) { // invalid this joint needs a slider
return nullptr;
}
if (slidingIndex != 1) {
swapJCS(joint); // make sure that sliding is first.
}
auto mbdJoint = CREATE<ASMTScrewJoint>::With();
mbdJoint->pitch = getJointDistance(joint);
return mbdJoint;
}
case JointType::Gears: {
auto mbdJoint = CREATE<ASMTGearJoint>::With();
mbdJoint->radiusI = getJointDistance(joint);
mbdJoint->radiusJ = getJointDistance2(joint);
return mbdJoint;
}
case JointType::Belt: {
auto mbdJoint = CREATE<ASMTGearJoint>::With();
mbdJoint->radiusI = getJointDistance(joint);
mbdJoint->radiusJ = -getJointDistance2(joint);
return mbdJoint;
}
default:
return nullptr;
}
}
std::shared_ptr<ASMTJoint> AssemblyObject::makeMbdJointDistance(App::DocumentObject* joint)
{
DistanceType type = getDistanceType(joint);
std::string elt1 = getElementFromProp(joint, "Reference1");
std::string elt2 = getElementFromProp(joint, "Reference2");
auto* obj1 = getLinkedObjFromRef(joint, "Reference1");
auto* obj2 = getLinkedObjFromRef(joint, "Reference2");
switch (type) {
case DistanceType::PointPoint: {
// Point to point distance, or ball joint if distance=0.
double distance = getJointDistance(joint);
if (distance < Precision::Confusion()) {
return CREATE<ASMTSphericalJoint>::With();
}
auto mbdJoint = CREATE<ASMTSphSphJoint>::With();
mbdJoint->distanceIJ = distance;
return mbdJoint;
}
// Edge - edge cases
case DistanceType::LineLine: {
auto mbdJoint = CREATE<ASMTRevCylJoint>::With();
mbdJoint->distanceIJ = getJointDistance(joint);
return mbdJoint;
}
case DistanceType::LineCircle: {
auto mbdJoint = CREATE<ASMTRevCylJoint>::With();
mbdJoint->distanceIJ = getJointDistance(joint) + getEdgeRadius(obj2, elt2);
return mbdJoint;
}
case DistanceType::CircleCircle: {
auto mbdJoint = CREATE<ASMTRevCylJoint>::With();
mbdJoint->distanceIJ =
getJointDistance(joint) + getEdgeRadius(obj1, elt1) + getEdgeRadius(obj2, elt2);
return mbdJoint;
}
// Face - Face cases
case DistanceType::PlanePlane: {
auto mbdJoint = CREATE<ASMTPlanarJoint>::With();
mbdJoint->offset = getJointDistance(joint);
return mbdJoint;
}
case DistanceType::PlaneCylinder: {
auto mbdJoint = CREATE<ASMTLineInPlaneJoint>::With();
mbdJoint->offset = getJointDistance(joint) + getFaceRadius(obj2, elt2);
return mbdJoint;
}
case DistanceType::PlaneSphere: {
auto mbdJoint = CREATE<ASMTPointInPlaneJoint>::With();
mbdJoint->offset = getJointDistance(joint) + getFaceRadius(obj2, elt2);
return mbdJoint;
}
case DistanceType::PlaneTorus: {
auto mbdJoint = CREATE<ASMTPlanarJoint>::With();
mbdJoint->offset = getJointDistance(joint);
return mbdJoint;
}
case DistanceType::CylinderCylinder: {
auto mbdJoint = CREATE<ASMTRevCylJoint>::With();
mbdJoint->distanceIJ =
getJointDistance(joint) + getFaceRadius(obj1, elt1) + getFaceRadius(obj2, elt2);
return mbdJoint;
}
case DistanceType::CylinderSphere: {
auto mbdJoint = CREATE<ASMTCylSphJoint>::With();
mbdJoint->distanceIJ =
getJointDistance(joint) + getFaceRadius(obj1, elt1) + getFaceRadius(obj2, elt2);
return mbdJoint;
}
case DistanceType::CylinderTorus: {
auto mbdJoint = CREATE<ASMTRevCylJoint>::With();
mbdJoint->distanceIJ =
getJointDistance(joint) + getFaceRadius(obj1, elt1) + getFaceRadius(obj2, elt2);
return mbdJoint;
}
case DistanceType::TorusTorus: {
auto mbdJoint = CREATE<ASMTPlanarJoint>::With();
mbdJoint->offset = getJointDistance(joint);
return mbdJoint;
}
case DistanceType::TorusSphere: {
auto mbdJoint = CREATE<ASMTCylSphJoint>::With();
mbdJoint->distanceIJ =
getJointDistance(joint) + getFaceRadius(obj1, elt1) + getFaceRadius(obj2, elt2);
return mbdJoint;
}
case DistanceType::SphereSphere: {
auto mbdJoint = CREATE<ASMTSphSphJoint>::With();
mbdJoint->distanceIJ =
getJointDistance(joint) + getFaceRadius(obj1, elt1) + getFaceRadius(obj2, elt2);
return mbdJoint;
}
// Point - Face cases
case DistanceType::PointPlane: {
auto mbdJoint = CREATE<ASMTPointInPlaneJoint>::With();
mbdJoint->offset = getJointDistance(joint);
return mbdJoint;
}
case DistanceType::PointCylinder: {
auto mbdJoint = CREATE<ASMTCylSphJoint>::With();
mbdJoint->distanceIJ = getJointDistance(joint) + getFaceRadius(obj1, elt1);
return mbdJoint;
}
case DistanceType::PointSphere: {
auto mbdJoint = CREATE<ASMTSphSphJoint>::With();
mbdJoint->distanceIJ = getJointDistance(joint) + getFaceRadius(obj1, elt1);
return mbdJoint;
}
// Edge - Face cases
case DistanceType::LinePlane: {
auto mbdJoint = CREATE<ASMTLineInPlaneJoint>::With();
mbdJoint->offset = getJointDistance(joint);
return mbdJoint;
}
// Point - Edge cases
case DistanceType::PointLine: {
auto mbdJoint = CREATE<ASMTCylSphJoint>::With();
mbdJoint->distanceIJ = getJointDistance(joint);
return mbdJoint;
}
case DistanceType::PointCurve: {
// For other curves we do a point in plane-of-the-curve.
// Maybe it would be best tangent / distance to the conic?
// For arcs and circles we could use ASMTRevSphJoint. But is it better than
// pointInPlane?
auto mbdJoint = CREATE<ASMTPointInPlaneJoint>::With();
mbdJoint->offset = getJointDistance(joint);
return mbdJoint;
}
default: {
// by default we make a planar joint.
auto mbdJoint = CREATE<ASMTPlanarJoint>::With();
mbdJoint->offset = getJointDistance(joint);
return mbdJoint;
}
}
}
std::vector<std::shared_ptr<MbD::ASMTJoint>>
AssemblyObject::makeMbdJoint(App::DocumentObject* joint)
{
if (!joint) {
return {};
}
JointType jointType = getJointType(joint);
std::shared_ptr<ASMTJoint> mbdJoint = makeMbdJointOfType(joint, jointType);
if (!mbdJoint || !isMbDJointValid(joint)) {
return {};
}
std::string fullMarkerNameI, fullMarkerNameJ;
if (jointType == JointType::RackPinion) {
getRackPinionMarkers(joint, fullMarkerNameI, fullMarkerNameJ);
}
else {
fullMarkerNameI = handleOneSideOfJoint(joint, "Reference1", "Placement1");
fullMarkerNameJ = handleOneSideOfJoint(joint, "Reference2", "Placement2");
}
if (fullMarkerNameI == "" || fullMarkerNameJ == "") {
return {};
}
mbdJoint->setName(joint->getFullName());
mbdJoint->setMarkerI(fullMarkerNameI);
mbdJoint->setMarkerJ(fullMarkerNameJ);
// Add limits if needed. We do not add if this is a simulation or their might clash.
if (motions.empty()) {
if (jointType == JointType::Slider || jointType == JointType::Cylindrical) {
auto* pLenMin =
dynamic_cast<App::PropertyFloat*>(joint->getPropertyByName("LengthMin"));
auto* pLenMax =
dynamic_cast<App::PropertyFloat*>(joint->getPropertyByName("LengthMax"));
auto* pMinEnabled =
dynamic_cast<App::PropertyBool*>(joint->getPropertyByName("EnableLengthMin"));
auto* pMaxEnabled =
dynamic_cast<App::PropertyBool*>(joint->getPropertyByName("EnableLengthMax"));
if (pLenMin && pLenMax && pMinEnabled
&& pMaxEnabled) { // Make sure properties do exist
// Swap the values if necessary.
bool minEnabled = pMinEnabled->getValue();
bool maxEnabled = pMaxEnabled->getValue();
double minLength = pLenMin->getValue();
double maxLength = pLenMax->getValue();
if ((minLength > maxLength) && minEnabled && maxEnabled) {
pLenMin->setValue(maxLength);
pLenMax->setValue(minLength);
minLength = maxLength;
maxLength = pLenMax->getValue();
pMinEnabled->setValue(maxEnabled);
pMaxEnabled->setValue(minEnabled);
minEnabled = maxEnabled;
maxEnabled = pMaxEnabled->getValue();
}
if (minEnabled) {
auto limit = ASMTTranslationLimit::With();
limit->setName(joint->getFullName() + "-LimitLenMin");
limit->setMarkerI(fullMarkerNameI);
limit->setMarkerJ(fullMarkerNameJ);
limit->settype("=>");
limit->setlimit(std::to_string(minLength));
limit->settol("1.0e-9");
mbdAssembly->addLimit(limit);
}
if (maxEnabled) {
auto limit2 = ASMTTranslationLimit::With();
limit2->setName(joint->getFullName() + "-LimitLenMax");
limit2->setMarkerI(fullMarkerNameI);
limit2->setMarkerJ(fullMarkerNameJ);
limit2->settype("=<");
limit2->setlimit(std::to_string(maxLength));
limit2->settol("1.0e-9");
mbdAssembly->addLimit(limit2);
}
}
}
if (jointType == JointType::Revolute || jointType == JointType::Cylindrical) {
auto* pRotMin = dynamic_cast<App::PropertyFloat*>(joint->getPropertyByName("AngleMin"));
auto* pRotMax = dynamic_cast<App::PropertyFloat*>(joint->getPropertyByName("AngleMax"));
auto* pMinEnabled =
dynamic_cast<App::PropertyBool*>(joint->getPropertyByName("EnableAngleMin"));
auto* pMaxEnabled =
dynamic_cast<App::PropertyBool*>(joint->getPropertyByName("EnableAngleMax"));
if (pRotMin && pRotMax && pMinEnabled
&& pMaxEnabled) { // Make sure properties do exist
// Swap the values if necessary.
bool minEnabled = pMinEnabled->getValue();
bool maxEnabled = pMaxEnabled->getValue();
double minAngle = pRotMin->getValue();
double maxAngle = pRotMax->getValue();
if ((minAngle > maxAngle) && minEnabled && maxEnabled) {
pRotMin->setValue(maxAngle);
pRotMax->setValue(minAngle);
minAngle = maxAngle;
maxAngle = pRotMax->getValue();
pMinEnabled->setValue(maxEnabled);
pMaxEnabled->setValue(minEnabled);
minEnabled = maxEnabled;
maxEnabled = pMaxEnabled->getValue();
}
if (minEnabled) {
auto limit = ASMTRotationLimit::With();
limit->setName(joint->getFullName() + "-LimitRotMin");
limit->setMarkerI(fullMarkerNameI);
limit->setMarkerJ(fullMarkerNameJ);
limit->settype("=>");
limit->setlimit(std::to_string(minAngle) + "*pi/180.0");
limit->settol("1.0e-9");
mbdAssembly->addLimit(limit);
}
if (maxEnabled) {
auto limit2 = ASMTRotationLimit::With();
limit2->setName(joint->getFullName() + "-LimitRotMax");
limit2->setMarkerI(fullMarkerNameI);
limit2->setMarkerJ(fullMarkerNameJ);
limit2->settype("=<");
limit2->setlimit(std::to_string(maxAngle) + "*pi/180.0");
limit2->settol("1.0e-9");
mbdAssembly->addLimit(limit2);
}
}
}
}
std::vector<App::DocumentObject*> done;
// Add motions if needed
for (auto* motion : motions) {
if (std::ranges::find(done, motion) != done.end()) {
continue; // don't process twice (can happen in case of cylindrical)
}
auto* pJoint = dynamic_cast<App::PropertyXLinkSub*>(motion->getPropertyByName("Joint"));
if (!pJoint) {
continue;
}
App::DocumentObject* motionJoint = pJoint->getValue();
if (joint != motionJoint) {
continue;
}
auto* pType =
dynamic_cast<App::PropertyEnumeration*>(motion->getPropertyByName("MotionType"));
auto* pFormula = dynamic_cast<App::PropertyString*>(motion->getPropertyByName("Formula"));
if (!pType || !pFormula) {
continue;
}
std::string formula = pFormula->getValue();
if (formula == "") {
continue;
}
std::string motionType = pType->getValueAsString();
// check if there is a second motion as cylindrical can have both,
// in which case the solver needs a general motion.
for (auto* motion2 : motions) {
pJoint = dynamic_cast<App::PropertyXLinkSub*>(motion2->getPropertyByName("Joint"));
if (!pJoint) {
continue;
}
motionJoint = pJoint->getValue();
if (joint != motionJoint || motion2 == motion) {
continue;
}
auto* pType2 =
dynamic_cast<App::PropertyEnumeration*>(motion2->getPropertyByName("MotionType"));
auto* pFormula2 =
dynamic_cast<App::PropertyString*>(motion2->getPropertyByName("Formula"));
if (!pType2 || !pFormula2) {
continue;
}
std::string formula2 = pFormula2->getValue();
if (formula2 == "") {
continue;
}
std::string motionType2 = pType2->getValueAsString();
if (motionType2 == motionType) {
continue; // only if both motions are different. ie one angular and one linear.
}
auto ASMTmotion = CREATE<ASMTGeneralMotion>::With();
ASMTmotion->setName(joint->getFullName() + "-ScrewMotion");
ASMTmotion->setMarkerI(fullMarkerNameI);
ASMTmotion->setMarkerJ(fullMarkerNameJ);
ASMTmotion->rIJI->atiput(2, motionType == "Angular" ? formula2 : formula);
ASMTmotion->angIJJ->atiput(2, motionType == "Angular" ? formula : formula2);
mbdAssembly->addMotion(ASMTmotion);
done.push_back(motion2);
}
if (motionType == "Angular") {
auto ASMTmotion = CREATE<ASMTRotationalMotion>::With();
ASMTmotion->setName(joint->getFullName() + "-AngularMotion");
ASMTmotion->setMarkerI(fullMarkerNameI);
ASMTmotion->setMarkerJ(fullMarkerNameJ);
ASMTmotion->setRotationZ(formula);
mbdAssembly->addMotion(ASMTmotion);
}
else if (motionType == "Linear") {
auto ASMTmotion = CREATE<ASMTTranslationalMotion>::With();
ASMTmotion->setName(joint->getFullName() + "-LinearMotion");
ASMTmotion->setMarkerI(fullMarkerNameI);
ASMTmotion->setMarkerJ(fullMarkerNameJ);
ASMTmotion->setTranslationZ(formula);
mbdAssembly->addMotion(ASMTmotion);
}
}
return {mbdJoint};
}
std::string AssemblyObject::handleOneSideOfJoint(App::DocumentObject* joint,
const char* propRefName,
const char* propPlcName)
{
App::DocumentObject* part = getMovingPartFromRef(this, joint, propRefName);
App::DocumentObject* obj = getObjFromRef(joint, propRefName);
if (!part || !obj) {
Base::Console().warning("The property %s of Joint %s is bad.",
propRefName,
joint->getFullName());
return "";
}
MbDPartData data = getMbDData(part);
std::shared_ptr<ASMTPart> mbdPart = data.part;
Base::Placement plc = getPlacementFromProp(joint, propPlcName);
// Now we have plc which is the JCS placement, but its relative to the Object, not to the
// containing Part.
if (obj->getNameInDocument() != part->getNameInDocument()) {
auto* ref = dynamic_cast<App::PropertyXLinkSub*>(joint->getPropertyByName(propRefName));
if (!ref) {
return "";
}
Base::Placement obj_global_plc = getGlobalPlacement(obj, ref);
plc = obj_global_plc * plc;
Base::Placement part_global_plc = getGlobalPlacement(part, ref);
plc = part_global_plc.inverse() * plc;
}
// check if we need to add an offset in case of bundled parts.
if (!data.offsetPlc.isIdentity()) {
plc = data.offsetPlc * plc;
}
std::string markerName = joint->getFullName();
auto mbdMarker = makeMbdMarker(markerName, plc);
mbdPart->addMarker(mbdMarker);
return "/OndselAssembly/" + mbdPart->name + "/" + markerName;
}
void AssemblyObject::getRackPinionMarkers(App::DocumentObject* joint,
std::string& markerNameI,
std::string& markerNameJ)
{
// ASMT rack pinion joint must get the rack as I and pinion as J.
// - rack marker has to have Z axis parallel to pinion Z axis.
// - rack marker has to have X axis parallel to the sliding axis.
// The user will have selected the sliding marker so we need to transform it.
// And we need to detect which marker is the rack.
int slidingIndex = slidingPartIndex(joint);
if (slidingIndex == 0) {
return;
}
if (slidingIndex != 1) {
swapJCS(joint); // make sure that rack is first.
}
App::DocumentObject* part1 = getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* obj1 = getObjFromRef(joint, "Reference1");
Base::Placement plc1 = getPlacementFromProp(joint, "Placement1");
App::DocumentObject* obj2 = getObjFromRef(joint, "Reference2");
Base::Placement plc2 = getPlacementFromProp(joint, "Placement2");
if (!part1 || !obj1) {
Base::Console().warning("Reference1 of Joint %s is bad.", joint->getFullName());
return;
}
// For the pinion nothing special needed :
markerNameJ = handleOneSideOfJoint(joint, "Reference2", "Placement2");
// For the rack we need to change the placement :
// make the pinion plc relative to the rack placement.
auto* ref1 = dynamic_cast<App::PropertyXLinkSub*>(joint->getPropertyByName("Reference1"));
auto* ref2 = dynamic_cast<App::PropertyXLinkSub*>(joint->getPropertyByName("Reference2"));
if (!ref1 || !ref2) {
return;
}
Base::Placement pinion_global_plc = getGlobalPlacement(obj2, ref2);
plc2 = pinion_global_plc * plc2;
Base::Placement rack_global_plc = getGlobalPlacement(obj1, ref1);
plc2 = rack_global_plc.inverse() * plc2;
// The rot of the rack placement should be the same as the pinion, but with X axis along the
// slider axis.
Base::Rotation rot = plc2.getRotation();
// the yaw of rot has to be the same as plc1
Base::Vector3d currentZAxis = rot.multVec(Base::Vector3d(0, 0, 1));
Base::Vector3d currentXAxis = rot.multVec(Base::Vector3d(1, 0, 0));
Base::Vector3d targetXAxis = plc1.getRotation().multVec(Base::Vector3d(0, 0, 1));
// Calculate the angle between the current X axis and the target X axis
double yawAdjustment = currentXAxis.GetAngle(targetXAxis);
// Determine the direction of the yaw adjustment using cross product
Base::Vector3d crossProd = currentXAxis.Cross(targetXAxis);
if (currentZAxis * crossProd < 0) { // If cross product is in opposite direction to Z axis
yawAdjustment = -yawAdjustment;
}
// Create a yaw rotation around the Z axis
Base::Rotation yawRotation(currentZAxis, yawAdjustment);
// Combine the initial rotation with the yaw adjustment
Base::Rotation adjustedRotation = rot * yawRotation;
plc1.setRotation(adjustedRotation);
// Then end of processing similar to handleOneSideOfJoint :
MbDPartData data1 = getMbDData(part1);
std::shared_ptr<ASMTPart> mbdPart = data1.part;
if (obj1->getNameInDocument() != part1->getNameInDocument()) {
plc1 = rack_global_plc * plc1;
Base::Placement part_global_plc = getGlobalPlacement(part1, ref1);
plc1 = part_global_plc.inverse() * plc1;
}
// check if we need to add an offset in case of bundled parts.
if (!data1.offsetPlc.isIdentity()) {
plc1 = data1.offsetPlc * plc1;
}
std::string markerName = joint->getFullName();
auto mbdMarker = makeMbdMarker(markerName, plc1);
mbdPart->addMarker(mbdMarker);
markerNameI = "/OndselAssembly/" + mbdPart->name + "/" + markerName;
}
int AssemblyObject::slidingPartIndex(App::DocumentObject* joint)
{
App::DocumentObject* part1 = getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* obj1 = getObjFromRef(joint, "Reference1");
boost::ignore_unused(obj1);
Base::Placement plc1 = getPlacementFromProp(joint, "Placement1");
App::DocumentObject* part2 = getMovingPartFromRef(this, joint, "Reference2");
App::DocumentObject* obj2 = getObjFromRef(joint, "Reference2");
boost::ignore_unused(obj2);
Base::Placement plc2 = getPlacementFromProp(joint, "Placement2");
int slidingFound = 0;
for (auto* jt : getJoints(false, false)) {
if (getJointType(jt) == JointType::Slider) {
App::DocumentObject* jpart1 = getMovingPartFromRef(this, jt, "Reference1");
App::DocumentObject* jpart2 = getMovingPartFromRef(this, jt, "Reference2");
int found = 0;
Base::Placement plcjt, plci;
if (jpart1 == part1 || jpart1 == part2) {
found = (jpart1 == part1) ? 1 : 2;
plci = (jpart1 == part1) ? plc1 : plc2;
plcjt = getPlacementFromProp(jt, "Placement1");
}
else if (jpart2 == part1 || jpart2 == part2) {
found = (jpart2 == part1) ? 1 : 2;
plci = (jpart2 == part1) ? plc1 : plc2;
plcjt = getPlacementFromProp(jt, "Placement2");
}
if (found != 0) {
// check the placements plcjt and (jcs1 or jcs2 depending on found value) Z axis are
// colinear ie if their pitch and roll are the same.
double y1, p1, r1, y2, p2, r2;
plcjt.getRotation().getYawPitchRoll(y1, p1, r1);
plci.getRotation().getYawPitchRoll(y2, p2, r2);
if (fabs(p1 - p2) < Precision::Confusion()
&& fabs(r1 - r2) < Precision::Confusion()) {
slidingFound = found;
}
}
}
}
return slidingFound;
}
bool AssemblyObject::isMbDJointValid(App::DocumentObject* joint)
{
// When dragging a part, we are bundling fixed parts together.
// This may lead to a conflicting joint that is self referencing a MbD part.
// The solver crash when fed such a bad joint. So we make sure it does not happen.
App::DocumentObject* part1 = getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(this, joint, "Reference2");
if (!part1 || !part2) {
return false;
}
// If this joint is self-referential it must be ignored.
if (getMbDPart(part1) == getMbDPart(part2)) {
Base::Console().warning(
"Assembly: Ignoring joint (%s) because its parts are connected by a fixed "
"joint bundle. This joint is a conflicting or redundant constraint.\n",
joint->getFullLabel());
return false;
}
return true;
}
AssemblyObject::MbDPartData AssemblyObject::getMbDData(App::DocumentObject* part)
{
auto it = objectPartMap.find(part);
if (it != objectPartMap.end()) {
// part has been associated with an ASMTPart before
return it->second;
}
// part has not been associated with an ASMTPart before
std::string str = part->getFullName();
Base::Placement plc = getPlacementFromProp(part, "Placement");
std::shared_ptr<ASMTPart> mbdPart = makeMbdPart(str, plc);
mbdAssembly->addPart(mbdPart);
MbDPartData data = {mbdPart, Base::Placement()};
objectPartMap[part] = data; // Store the association
// Associate other objects connected with fixed joints
if (bundleFixed) {
auto addConnectedFixedParts = [&](App::DocumentObject* currentPart, auto& self) -> void {
std::vector<App::DocumentObject*> joints = getJointsOfPart(currentPart);
for (auto* joint : joints) {
JointType jointType = getJointType(joint);
if (jointType == JointType::Fixed) {
App::DocumentObject* part1 = getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(this, joint, "Reference2");
App::DocumentObject* partToAdd = currentPart == part1 ? part2 : part1;
if (objectPartMap.find(partToAdd) != objectPartMap.end()) {
// already added
continue;
}
Base::Placement plci = getPlacementFromProp(partToAdd, "Placement");
MbDPartData partData = {mbdPart, plc.inverse() * plci};
objectPartMap[partToAdd] = partData; // Store the association
// Recursively call for partToAdd
self(partToAdd, self);
}
}
};
addConnectedFixedParts(part, addConnectedFixedParts);
}
return data;
}
std::shared_ptr<ASMTPart> AssemblyObject::getMbDPart(App::DocumentObject* part)
{
if (!part) {
return nullptr;
}
return getMbDData(part).part;
}
std::shared_ptr<ASMTPart>
AssemblyObject::makeMbdPart(std::string& name, Base::Placement plc, double mass)
{
auto mbdPart = CREATE<ASMTPart>::With();
mbdPart->setName(name);
auto massMarker = CREATE<ASMTPrincipalMassMarker>::With();
massMarker->setMass(mass);
massMarker->setDensity(1.0);
massMarker->setMomentOfInertias(1.0, 1.0, 1.0);
mbdPart->setPrincipalMassMarker(massMarker);
Base::Vector3d pos = plc.getPosition();
mbdPart->setPosition3D(pos.x, pos.y, pos.z);
// TODO : replace with quaternion to simplify
Base::Rotation rot = plc.getRotation();
Base::Matrix4D mat;
rot.getValue(mat);
Base::Vector3d r0 = mat.getRow(0);
Base::Vector3d r1 = mat.getRow(1);
Base::Vector3d r2 = mat.getRow(2);
mbdPart->setRotationMatrix(r0.x, r0.y, r0.z, r1.x, r1.y, r1.z, r2.x, r2.y, r2.z);
return mbdPart;
}
std::shared_ptr<ASMTMarker> AssemblyObject::makeMbdMarker(std::string& name, Base::Placement& plc)
{
auto mbdMarker = CREATE<ASMTMarker>::With();
mbdMarker->setName(name);
Base::Vector3d pos = plc.getPosition();
mbdMarker->setPosition3D(pos.x, pos.y, pos.z);
// TODO : replace with quaternion to simplify
Base::Rotation rot = plc.getRotation();
Base::Matrix4D mat;
rot.getValue(mat);
Base::Vector3d r0 = mat.getRow(0);
Base::Vector3d r1 = mat.getRow(1);
Base::Vector3d r2 = mat.getRow(2);
mbdMarker->setRotationMatrix(r0.x, r0.y, r0.z, r1.x, r1.y, r1.z, r2.x, r2.y, r2.z);
return mbdMarker;
}
std::vector<ObjRef> AssemblyObject::getDownstreamParts(App::DocumentObject* part,
App::DocumentObject* joint)
{
if (!part) {
return {};
}
// First we deactivate the joint
bool state = false;
if (joint) {
state = getJointActivated(joint);
setJointActivated(joint, false);
}
std::vector<App::DocumentObject*> joints = getJoints(false);
std::vector<ObjRef> connectedParts = {{part, nullptr}};
traverseAndMarkConnectedParts(part, connectedParts, joints);
std::vector<ObjRef> downstreamParts;
for (auto& parti : connectedParts) {
if (!isPartConnected(parti.obj) && (parti.obj != part)) {
downstreamParts.push_back(parti);
}
}
if (joint) {
setJointActivated(joint, state);
}
return downstreamParts;
}
App::DocumentObject*
AssemblyObject::getUpstreamMovingPart(App::DocumentObject* part,
App::DocumentObject*& joint,
std::string& name,
std::vector<App::DocumentObject*> excludeJoints)
{
if (!part || isPartGrounded(part)) {
return nullptr;
}
excludeJoints.push_back(joint);
joint = getJointOfPartConnectingToGround(part, name, excludeJoints);
JointType jointType = getJointType(joint);
if (jointType != JointType::Fixed) {
return part;
}
part = getMovingPartFromRef(this, joint, name == "Reference1" ? "Reference2" : "Reference1");
return getUpstreamMovingPart(part, joint, name);
}
double AssemblyObject::getObjMass(App::DocumentObject* obj)
{
if (!obj) {
return 0.0;
}
for (auto& pair : objMasses) {
if (pair.first == obj) {
return pair.second;
}
}
return 1.0;
}
void AssemblyObject::setObjMasses(std::vector<std::pair<App::DocumentObject*, double>> objectMasses)
{
objMasses = objectMasses;
}
std::vector<AssemblyLink*> AssemblyObject::getSubAssemblies()
{
std::vector<AssemblyLink*> subAssemblies = {};
App::Document* doc = getDocument();
std::vector<DocumentObject*> assemblies =
doc->getObjectsOfType(Assembly::AssemblyLink::getClassTypeId());
for (auto assembly : assemblies) {
if (hasObject(assembly)) {
subAssemblies.push_back(freecad_cast<AssemblyLink*>(assembly));
}
}
return subAssemblies;
}
void AssemblyObject::ensureIdentityPlacements()
{
std::vector<App::DocumentObject*> group = Group.getValues();
for (auto* obj : group) {
// When used in assembly, link groups must have identity placements.
if (obj->isLinkGroup()) {
auto* link = dynamic_cast<App::Link*>(obj);
auto* pPlc = dynamic_cast<App::PropertyPlacement*>(obj->getPropertyByName("Placement"));
if (!pPlc || !link) {
continue;
}
Base::Placement plc = pPlc->getValue();
if (plc.isIdentity()) {
continue;
}
pPlc->setValue(Base::Placement());
obj->purgeTouched();
// To keep the LinkElement positions, we apply plc to their placements
std::vector<App::DocumentObject*> elts = link->ElementList.getValues();
for (auto* elt : elts) {
pPlc = dynamic_cast<App::PropertyPlacement*>(elt->getPropertyByName("Placement"));
pPlc->setValue(plc * pPlc->getValue());
elt->purgeTouched();
}
}
}
}
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;
}