From 323deff46a2281297a40148d5a11f434bcb72702 Mon Sep 17 00:00:00 2001 From: Paddle Date: Tue, 14 Nov 2023 18:39:09 +0100 Subject: [PATCH] Assembly: Replace Tangent+Parallel+Planar by 'Distance'. --- src/Gui/CommandDoc.cpp | 2 +- .../DlgSettingsWorkbenchesImp.cpp | 2 +- src/Gui/ViewProviderDocumentObject.h | 2 + src/Mod/AddonManager/Addon.py | 4 +- src/Mod/Assembly/App/AssemblyObject.cpp | 991 +++++++++++++++--- src/Mod/Assembly/App/AssemblyObject.h | 65 +- src/Mod/Assembly/App/AssemblyObjectPy.xml | 43 +- src/Mod/Assembly/App/AssemblyObjectPyImp.cpp | 47 +- src/Mod/Assembly/App/PreCompiled.h | 5 + src/Mod/Assembly/CMakeLists.txt | 6 + src/Mod/Assembly/CommandCreateAssembly.py | 2 +- src/Mod/Assembly/CommandCreateJoint.py | 143 +-- src/Mod/Assembly/CommandExportASMT.py | 2 +- src/Mod/Assembly/CommandInsertLink.py | 197 +++- src/Mod/Assembly/CommandSolveAssembly.py | 4 +- src/Mod/Assembly/Gui/PreCompiled.h | 4 + src/Mod/Assembly/Gui/Resources/Assembly.qrc | 5 +- ...l.svg => Assembly_CreateJointDistance.svg} | 0 .../icons/Assembly_SolveAssembly.svg | 615 +++++++++++ .../panels/TaskAssemblyCreateJoint.ui | 99 ++ .../panels/TaskAssemblyInsertLink.ui | 26 + src/Mod/Assembly/Gui/ViewProviderAssembly.cpp | 102 +- src/Mod/Assembly/Gui/ViewProviderAssembly.h | 11 + .../Assembly/Gui/ViewProviderJointGroup.cpp | 6 + src/Mod/Assembly/Gui/ViewProviderJointGroup.h | 16 + src/Mod/Assembly/InitGui.py | 4 +- src/Mod/Assembly/JointObject.py | 410 +++++++- src/Mod/Assembly/UtilsAssembly.py | 302 +++++- 28 files changed, 2716 insertions(+), 399 deletions(-) rename src/Mod/Assembly/Gui/Resources/icons/{Assembly_CreateJointParallel.svg => Assembly_CreateJointDistance.svg} (100%) create mode 100644 src/Mod/Assembly/Gui/Resources/icons/Assembly_SolveAssembly.svg diff --git a/src/Gui/CommandDoc.cpp b/src/Gui/CommandDoc.cpp index 55d4b91d64..d8bf79af53 100644 --- a/src/Gui/CommandDoc.cpp +++ b/src/Gui/CommandDoc.cpp @@ -1341,7 +1341,7 @@ void StdCmdDelete::activated(int iMsg) ViewProviderDocumentObject *vpedit = nullptr; if(editDoc) vpedit = dynamic_cast(editDoc->getInEdit()); - if(vpedit) { + if(vpedit && !vpedit->acceptDeletionsInEdit()) { for(auto &sel : Selection().getSelectionEx(editDoc->getDocument()->getName())) { if(sel.getObject() == vpedit->getObject()) { if (!sel.getSubNames().empty()) { diff --git a/src/Gui/PreferencePages/DlgSettingsWorkbenchesImp.cpp b/src/Gui/PreferencePages/DlgSettingsWorkbenchesImp.cpp index 5cd157d509..c621a26cd9 100644 --- a/src/Gui/PreferencePages/DlgSettingsWorkbenchesImp.cpp +++ b/src/Gui/PreferencePages/DlgSettingsWorkbenchesImp.cpp @@ -465,7 +465,7 @@ QStringList DlgSettingsWorkbenchesImp::getDisabledWorkbenches() ParameterGrp::handle hGrp; hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Workbenches"); - disabled_wbs = QString::fromStdString(hGrp->GetASCII("Disabled", "NoneWorkbench,TestWorkbench,AssemblyWorkbench")); + disabled_wbs = QString::fromStdString(hGrp->GetASCII("Disabled", "NoneWorkbench,TestWorkbench")); #if QT_VERSION >= QT_VERSION_CHECK(5,15,0) unfiltered_disabled_wbs_list = disabled_wbs.split(QLatin1String(","), Qt::SkipEmptyParts); #else diff --git a/src/Gui/ViewProviderDocumentObject.h b/src/Gui/ViewProviderDocumentObject.h index 14b8cd38c6..2490e6ed42 100644 --- a/src/Gui/ViewProviderDocumentObject.h +++ b/src/Gui/ViewProviderDocumentObject.h @@ -94,6 +94,8 @@ public: App::DocumentObject *getObject() const {return pcObject;} /// Asks the view provider if the given object can be deleted. bool canDelete(App::DocumentObject* obj) const override; + /// Ask the view provider if it accepts object deletions while in edit + virtual bool acceptDeletionsInEdit() { return false; } /// Get the GUI document to this ViewProvider object Gui::Document* getDocument() const; /// Get the python wrapper for that ViewProvider diff --git a/src/Mod/AddonManager/Addon.py b/src/Mod/AddonManager/Addon.py index 5289f2752b..f6aa56ea62 100644 --- a/src/Mod/AddonManager/Addon.py +++ b/src/Mod/AddonManager/Addon.py @@ -621,7 +621,7 @@ class Addon: wbName = self.get_workbench_name() # Add the wb to the list of disabled if it was not already - disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench,AssemblyWorkbench") + disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench") # print(f"start disabling {disabled_wbs}") disabled_wbs_list = disabled_wbs.split(",") if not (wbName in disabled_wbs_list): @@ -652,7 +652,7 @@ class Addon: def remove_from_disabled_wbs(self, wbName: str): pref = fci.ParamGet("User parameter:BaseApp/Preferences/Workbenches") - disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench,AssemblyWorkbench") + disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench") # print(f"start enabling : {disabled_wbs}") disabled_wbs_list = disabled_wbs.split(",") disabled_wbs = "" diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index 18d56fe947..490987e085 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -23,6 +23,10 @@ #include "PreCompiled.h" #ifndef _PreComp_ +#include +#include +#include +#include #include #include #include @@ -39,6 +43,9 @@ #include #include +#include +#include + #include #include #include @@ -51,6 +58,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include @@ -58,11 +71,11 @@ #include "AssemblyObjectPy.h" #include "JointGroup.h" -using namespace App; +namespace PartApp = Part; + using namespace Assembly; using namespace MbD; - PROPERTY_SOURCE(Assembly::AssemblyObject, App::Part) AssemblyObject::AssemblyObject() @@ -84,22 +97,22 @@ std::vector AssemblyObject::getJoints() { std::vector joints = {}; - App::Document* doc = getDocument(); - - std::vector jointGroups = - doc->getObjectsOfType(Assembly::JointGroup::getClassTypeId()); + JointGroup* jointGroup = getJointGroup(); + if (!jointGroup) { + return {}; + } Base::PyGILStateLocker lock; - if (jointGroups.size() > 0) { - for (auto* obj : static_cast(jointGroups[0])->getObjects()) { - App::PropertyPythonObject* proxy = obj - ? dynamic_cast(obj->getPropertyByName("Proxy")) - : nullptr; - if (proxy) { - Py::Object joint = proxy->getValue(); - if (joint.hasAttr("setJointConnectors")) { - joints.push_back(obj); - } + for (auto obj : jointGroup->getObjects()) { + if (!obj) { + continue; + } + + auto proxy = dynamic_cast(obj->getPropertyByName("Proxy")); + if (proxy) { + Py::Object joint = proxy->getValue(); + if (joint.hasAttr("setJointConnectors")) { + joints.push_back(obj); } } } @@ -110,29 +123,163 @@ std::vector AssemblyObject::getJoints() return joints; } -bool AssemblyObject::fixGroundedParts() +std::vector AssemblyObject::getGroundedJoints() { - App::Document* doc = getDocument(); - App::DocumentObject* jointsGroup = doc->getObject("Joints"); + std::vector joints = {}; - bool onePartFixed = false; + JointGroup* jointGroup = getJointGroup(); + if (!jointGroup) { + return {}; + } Base::PyGILStateLocker lock; - if (jointsGroup && jointsGroup->isDerivedFrom(App::DocumentObjectGroup::getClassTypeId())) { - for (auto* obj : static_cast(jointsGroup)->getObjects()) { - auto* propObj = - dynamic_cast(obj->getPropertyByName("ObjectToGround")); - if (propObj) { - App::DocumentObject* objToGround = propObj->getValue(); + for (auto obj : jointGroup->getObjects()) { + if (!obj) { + continue; + } - Base::Placement plc = getPlacementFromProp(obj, "Placement"); - std::string str = obj->getFullName(); - fixGroundedPart(objToGround, plc, str); - onePartFixed = true; - } + auto* propObj = dynamic_cast(obj->getPropertyByName("ObjectToGround")); + + if (propObj) { + joints.push_back(obj); } } - return onePartFixed; + + return joints; +} + +void AssemblyObject::removeUnconnectedJoints(std::vector& joints, + std::vector groundedObjs) +{ + std::set connectedParts; + + // Initialize connectedParts with groundedObjs + for (auto* groundedObj : groundedObjs) { + connectedParts.insert(groundedObj); + } + + // 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(), + [&connectedParts, this](App::DocumentObject* joint) { + App::DocumentObject* obj1 = getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* obj2 = getLinkObjFromProp(joint, "Part2"); + if ((connectedParts.find(obj1) == connectedParts.end()) + || (connectedParts.find(obj2) == connectedParts.end())) { + Base::Console().Warning( + "%s is unconnected to a grounded part so it is ignored.\n", + joint->getFullName()); + return true; // Remove joint if any connected object is not in connectedParts + } + return false; + }), + joints.end()); +} + +void AssemblyObject::traverseAndMarkConnectedParts(App::DocumentObject* currentObj, + std::set& connectedParts, + const std::vector& joints) +{ + // getConnectedParts returns the objs connected to the currentObj by any joint + auto connectedObjs = getConnectedParts(currentObj, joints); + for (auto* nextObj : connectedObjs) { + if (connectedParts.find(nextObj) == connectedParts.end()) { + connectedParts.insert(nextObj); + traverseAndMarkConnectedParts(nextObj, connectedParts, joints); + } + } +} + +std::vector +AssemblyObject::getConnectedParts(App::DocumentObject* part, + const std::vector& joints) +{ + std::vector connectedParts; + for (auto joint : joints) { + App::DocumentObject* obj1 = getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* obj2 = getLinkObjFromProp(joint, "Part2"); + if (obj1 == part) { + connectedParts.push_back(obj2); + } + else if (obj2 == part) { + connectedParts.push_back(obj1); + } + } + return connectedParts; +} + +bool AssemblyObject::isPartConnected(App::DocumentObject* obj) +{ + std::vector groundedObjs = fixGroundedParts(); + std::vector joints = getJoints(); + + std::set connectedParts; + + // Initialize connectedParts with groundedObjs + for (auto* groundedObj : groundedObjs) { + connectedParts.insert(groundedObj); + } + + // Perform a traversal from each grounded object + for (auto* groundedObj : groundedObjs) { + traverseAndMarkConnectedParts(groundedObj, connectedParts, joints); + } + + for (auto part : connectedParts) { + if (obj == part) { + return true; + } + } + + return false; +} + +JointGroup* AssemblyObject::getJointGroup() +{ + App::Document* doc = getDocument(); + + std::vector jointGroups = + doc->getObjectsOfType(Assembly::JointGroup::getClassTypeId()); + if (jointGroups.empty()) { + return nullptr; + } + for (auto jointGroup : jointGroups) { + if (hasObject(jointGroup)) { + return dynamic_cast(jointGroup); + } + } + return nullptr; +} + +std::vector AssemblyObject::fixGroundedParts() +{ + std::vector groundedJoints = getGroundedJoints(); + + std::vector groundedObjs; + for (auto obj : groundedJoints) { + if (!obj) { + continue; + } + + auto* propObj = dynamic_cast(obj->getPropertyByName("ObjectToGround")); + + if (propObj) { + App::DocumentObject* objToGround = propObj->getValue(); + + Base::Placement plc = getPlacementFromProp(obj, "Placement"); + std::string str = obj->getFullName(); + fixGroundedPart(objToGround, plc, str); + groundedObjs.push_back(objToGround); + } + } + return groundedObjs; } void AssemblyObject::fixGroundedPart(App::DocumentObject* obj, @@ -146,7 +293,8 @@ void AssemblyObject::fixGroundedPart(App::DocumentObject* obj, std::shared_ptr mbdPart = getMbDPart(obj); std::string markerName2 = "FixingMarker"; - auto mbdMarker2 = makeMbdMarker(markerName2, plc); + Base::Placement basePlc = Base::Placement(); + auto mbdMarker2 = makeMbdMarker(markerName2, basePlc); mbdPart->addMarker(mbdMarker2); markerName1 = "/OndselAssembly/" + mbdMarker1->name; @@ -163,36 +311,38 @@ void AssemblyObject::fixGroundedPart(App::DocumentObject* obj, void AssemblyObject::jointParts(std::vector joints) { for (auto* joint : joints) { - std::shared_ptr mbdJoint = makeMbdJoint(joint); - mbdAssembly->addJoint(mbdJoint); + if (!joint) { + continue; + } + + std::vector> mbdJoints = makeMbdJoint(joint); + for (auto& mbdJoint : mbdJoints) { + mbdAssembly->addJoint(mbdJoint); + } } } -Base::Placement AssemblyObject::getPlacementFromProp(App::DocumentObject* obj, const char* propName) +int AssemblyObject::solve(bool enableRedo) { - Base::Placement plc = Base::Placement(); - auto* propPlacement = dynamic_cast(obj->getPropertyByName(propName)); - if (propPlacement) { - plc = propPlacement->getValue(); - } - return plc; -} - -int AssemblyObject::solve() -{ - // Base::Console().Warning("solve\n"); mbdAssembly = makeMbdAssembly(); objectPartMap.clear(); - if (!fixGroundedParts()) { + std::vector groundedObjs = fixGroundedParts(); + if (groundedObjs.empty()) { // If no part fixed we can't solve. return -6; } std::vector joints = getJoints(); + removeUnconnectedJoints(joints, groundedObjs); + jointParts(joints); + if (enableRedo) { + savePlacementsForUndo(); + } + try { mbdAssembly->solve(); } @@ -206,7 +356,7 @@ int AssemblyObject::solve() // The Placement1 and Placement2 of each joint needs to be updated as the parts moved. // Note calling only recomputeJointPlacements makes a weird illegal storage access // When solving while moving part. Happens in Py::Callable(attr).apply(); - // it apparantly can't access the JointObject 'updateJCSPlacements' function. + // it apparently can't access the JointObject 'updateJCSPlacements' function. getJoints(); return 0; @@ -214,77 +364,375 @@ int AssemblyObject::solve() void AssemblyObject::exportAsASMT(std::string fileName) { - Base::Console().Warning("hello 1\n"); mbdAssembly = makeMbdAssembly(); objectPartMap.clear(); - Base::Console().Warning("hello 2\n"); fixGroundedParts(); std::vector joints = getJoints(); - Base::Console().Warning("hello 3\n"); jointParts(joints); - Base::Console().Warning("hello 4\n"); - Base::Console().Warning("%s\n", fileName.c_str()); mbdAssembly->outputFile(fileName); - Base::Console().Warning("hello 5\n"); } -std::shared_ptr AssemblyObject::makeMbdJointOfType(JointType jointType) +std::shared_ptr AssemblyObject::makeMbdJointOfType(App::DocumentObject* joint, + JointType type) { - std::shared_ptr mbdJoint; - - if (jointType == JointType::Fixed) { - mbdJoint = CREATE::With(); + if (type == JointType::Fixed) { + return CREATE::With(); } - else if (jointType == JointType::Revolute) { - mbdJoint = CREATE::With(); + else if (type == JointType::Revolute) { + return CREATE::With(); } - else if (jointType == JointType::Cylindrical) { - mbdJoint = CREATE::With(); + else if (type == JointType::Cylindrical) { + return CREATE::With(); } - else if (jointType == JointType::Slider) { - mbdJoint = CREATE::With(); + else if (type == JointType::Slider) { + return CREATE::With(); } - else if (jointType == JointType::Ball) { - mbdJoint = CREATE::With(); + else if (type == JointType::Ball) { + return CREATE::With(); } - else if (jointType == JointType::Planar) { - mbdJoint = CREATE::With(); - } - else if (jointType == JointType::Parallel) { - // TODO - mbdJoint = CREATE::With(); - } - else if (jointType == JointType::Tangent) { - // TODO - mbdJoint = CREATE::With(); + else if (type == JointType::Distance) { + return makeMbdJointDistance(joint); } - return mbdJoint; + return nullptr; } -std::shared_ptr AssemblyObject::makeMbdJoint(App::DocumentObject* joint) +std::shared_ptr AssemblyObject::makeMbdJointDistance(App::DocumentObject* joint) { - JointType jointType = JointType::Fixed; + // Depending on the type of element of the JCS, we apply the correct set of constraints. + std::string type1 = getElementTypeFromProp(joint, "Element1"); + std::string type2 = getElementTypeFromProp(joint, "Element2"); - auto* prop = joint - ? dynamic_cast(joint->getPropertyByName("JointType")) - : nullptr; - if (prop) { - jointType = static_cast(prop->getValue()); + if (type1 == "Vertex" && type2 == "Vertex") { + // Point to point distance, or ball joint if distance=0. + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = getJointDistance(joint); + return mbdJoint; + } + else if (type1 == "Edge" && type2 == "Edge") { + return makeMbdJointDistanceEdgeEdge(joint); + } + else if (type1 == "Face" && type2 == "Face") { + return makeMbdJointDistanceFaceFace(joint); + } + else if ((type1 == "Vertex" && type2 == "Face") || (type1 == "Face" && type2 == "Vertex")) { + if (type1 == "Vertex") { // Make sure face is the first. + swapJCS(joint); + } + return makeMbdJointDistanceFaceVertex(joint); + } + else if ((type1 == "Edge" && type2 == "Face") || (type1 == "Face" && type2 == "Edge")) { + if (type1 == "Edge") { // Make sure face is the first. + swapJCS(joint); + } + return makeMbdJointDistanceFaceEdge(joint); + } + else if ((type1 == "Vertex" && type2 == "Edge") || (type1 == "Edge" && type2 == "Vertex")) { + if (type1 == "Vertex") { // Make sure edge is the first. + swapJCS(joint); + } + return makeMbdJointDistanceEdgeVertex(joint); } - std::shared_ptr mbdJoint = makeMbdJointOfType(jointType); + return nullptr; +} - std::string fullMarkerName1 = handleOneSideOfJoint(joint, "Object1", "Placement1"); - std::string fullMarkerName2 = handleOneSideOfJoint(joint, "Object2", "Placement2"); +std::shared_ptr AssemblyObject::makeMbdJointDistanceEdgeEdge(App::DocumentObject* joint) +{ + const char* elt1 = getElementFromProp(joint, "Element1"); + const char* elt2 = getElementFromProp(joint, "Element2"); + auto* obj1 = getLinkedObjFromProp(joint, "Object1"); + auto* obj2 = getLinkedObjFromProp(joint, "Object2"); + + if (isEdgeType(obj1, elt1, GeomAbs_Line) || isEdgeType(obj2, elt2, GeomAbs_Line)) { + if (!isEdgeType(obj1, elt1, GeomAbs_Line)) { + swapJCS(joint); // make sure that line is first if not 2 lines. + std::swap(elt1, elt2); + std::swap(obj1, obj2); + } + + if (isEdgeType(obj2, elt2, GeomAbs_Line)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = getJointDistance(joint); + return mbdJoint; + } + else if (isEdgeType(obj2, elt2, GeomAbs_Circle)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = getJointDistance(joint) + getEdgeRadius(obj2, elt2); + return mbdJoint; + } + // TODO : other cases Ellipse, parabola, hyperbola... + } + + else if (isEdgeType(obj1, elt1, GeomAbs_Circle) || isEdgeType(obj2, elt2, GeomAbs_Circle)) { + if (!isEdgeType(obj1, elt1, GeomAbs_Circle)) { + swapJCS(joint); // make sure that circle is first if not 2 lines. + std::swap(elt1, elt2); + std::swap(obj1, obj2); + } + + if (isEdgeType(obj2, elt2, GeomAbs_Circle)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = + getJointDistance(joint) + getEdgeRadius(obj1, elt1) + getEdgeRadius(obj2, elt2); + return mbdJoint; + } + // TODO : other cases Ellipse, parabola, hyperbola... + } + + // TODO : other cases Ellipse, parabola, hyperbola... + + return nullptr; +} + +std::shared_ptr AssemblyObject::makeMbdJointDistanceFaceFace(App::DocumentObject* joint) +{ + const char* elt1 = getElementFromProp(joint, "Element1"); + const char* elt2 = getElementFromProp(joint, "Element2"); + auto* obj1 = getLinkedObjFromProp(joint, "Object1"); + auto* obj2 = getLinkedObjFromProp(joint, "Object2"); + + if (isFaceType(obj1, elt1, GeomAbs_Plane) || isFaceType(obj2, elt2, GeomAbs_Plane)) { + if (!isFaceType(obj1, elt1, GeomAbs_Plane)) { + swapJCS(joint); // make sure plane is first if its not 2 planes. + std::swap(elt1, elt2); + std::swap(obj1, obj2); + } + + if (isFaceType(obj2, elt2, GeomAbs_Plane)) { + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + else if (isFaceType(obj2, elt2, GeomAbs_Cylinder)) { + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint) + getFaceRadius(obj2, elt2); + return mbdJoint; + } + else if (isFaceType(obj2, elt2, GeomAbs_Sphere)) { + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint) + getFaceRadius(obj2, elt2); + return mbdJoint; + } + else if (isFaceType(obj2, elt2, GeomAbs_Cone)) { + // TODO + } + else if (isFaceType(obj2, elt2, GeomAbs_Torus)) { + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + } + + else if (isFaceType(obj1, elt1, GeomAbs_Cylinder) || isFaceType(obj2, elt2, GeomAbs_Cylinder)) { + if (!isFaceType(obj1, elt1, GeomAbs_Cylinder)) { + swapJCS(joint); // make sure cylinder is first if its not 2 cylinders. + std::swap(elt1, elt2); + std::swap(obj1, obj2); + } + + if (isFaceType(obj2, elt2, GeomAbs_Cylinder)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = + getJointDistance(joint) + getFaceRadius(obj1, elt1) + getFaceRadius(obj2, elt2); + return mbdJoint; + } + else if (isFaceType(obj2, elt2, GeomAbs_Sphere)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = + getJointDistance(joint) + getFaceRadius(obj1, elt1) + getFaceRadius(obj2, elt2); + return mbdJoint; + } + else if (isFaceType(obj2, elt2, GeomAbs_Cone)) { + // TODO + } + else if (isFaceType(obj2, elt2, GeomAbs_Torus)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = + getJointDistance(joint) + getFaceRadius(obj1, elt1) + getFaceRadius(obj2, elt2); + return mbdJoint; + } + } + + else if (isFaceType(obj1, elt1, GeomAbs_Cone) || isFaceType(obj2, elt2, GeomAbs_Cone)) { + if (!isFaceType(obj1, elt1, GeomAbs_Cone)) { + swapJCS(joint); // make sure cone is first if its not 2 cones. + std::swap(elt1, elt2); + std::swap(obj1, obj2); + } + + if (isFaceType(obj2, elt2, GeomAbs_Cone)) { + // TODO + } + else if (isFaceType(obj2, elt2, GeomAbs_Torus)) { + // TODO + } + else if (isFaceType(obj2, elt2, GeomAbs_Sphere)) { + // TODO + } + } + + else if (isFaceType(obj1, elt1, GeomAbs_Torus) || isFaceType(obj2, elt2, GeomAbs_Torus)) { + if (!isFaceType(obj1, elt1, GeomAbs_Torus)) { + swapJCS(joint); // make sure torus is first if its not 2 torus. + std::swap(elt1, elt2); + std::swap(obj1, obj2); + } + + if (isFaceType(obj2, elt2, GeomAbs_Torus)) { + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + else if (isFaceType(obj2, elt2, GeomAbs_Sphere)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = + getJointDistance(joint) + getFaceRadius(obj1, elt1) + getFaceRadius(obj2, elt2); + return mbdJoint; + } + } + + else if (isFaceType(obj1, elt1, GeomAbs_Sphere) || isFaceType(obj2, elt2, GeomAbs_Sphere)) { + if (!isFaceType(obj1, elt1, GeomAbs_Sphere)) { + swapJCS(joint); // make sure sphere is first if its not 2 spheres. + std::swap(elt1, elt2); + std::swap(obj1, obj2); + } + + if (isFaceType(obj2, elt2, GeomAbs_Sphere)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = + getJointDistance(joint) + getFaceRadius(obj1, elt1) + getFaceRadius(obj2, elt2); + return mbdJoint; + } + } + else { + // by default we make a planar joint. + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + + return nullptr; +} + +std::shared_ptr +AssemblyObject::makeMbdJointDistanceFaceVertex(App::DocumentObject* joint) +{ + const char* elt1 = getElementFromProp(joint, "Element1"); + auto* obj1 = getLinkedObjFromProp(joint, "Object1"); + + if (isFaceType(obj1, elt1, GeomAbs_Plane)) { + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + else if (isFaceType(obj1, elt1, GeomAbs_Cylinder)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = getJointDistance(joint) + getFaceRadius(obj1, elt1); + return mbdJoint; + } + else if (isFaceType(obj1, elt1, GeomAbs_Sphere)) { + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = getJointDistance(joint) + getFaceRadius(obj1, elt1); + return mbdJoint; + } + /*else if (isFaceType(obj1, elt1, GeomAbs_Cone)) { + // TODO + } + else if (isFaceType(obj1, elt1, GeomAbs_Thorus)) { + // TODO + }*/ + + return nullptr; +} + +std::shared_ptr +AssemblyObject::makeMbdJointDistanceEdgeVertex(App::DocumentObject* joint) +{ + const char* elt1 = getElementFromProp(joint, "Element1"); + auto* obj1 = getLinkedObjFromProp(joint, "Object1"); + + if (isEdgeType(obj1, elt1, GeomAbs_Line)) { // Point on line joint. + auto mbdJoint = CREATE::With(); + mbdJoint->distanceIJ = getJointDistance(joint); + return mbdJoint; + } + else { + // 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::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + + return nullptr; +} + +std::shared_ptr AssemblyObject::makeMbdJointDistanceFaceEdge(App::DocumentObject* joint) +{ + const char* elt2 = getElementFromProp(joint, "Element2"); + auto* obj2 = getLinkedObjFromProp(joint, "Object2"); + + if (isEdgeType(obj2, elt2, GeomAbs_Line)) { + // Make line in plane joint. + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + else { + // planar joint for other edges. + auto mbdJoint = CREATE::With(); + mbdJoint->offset = getJointDistance(joint); + return mbdJoint; + } + + return nullptr; +} + + +std::vector> +AssemblyObject::makeMbdJoint(App::DocumentObject* joint) +{ + JointType jointType = getJointType(joint); + + std::shared_ptr mbdJoint = makeMbdJointOfType(joint, jointType); + if (!mbdJoint) { + return {}; + } + + std::string fullMarkerName1 = handleOneSideOfJoint(joint, jointType, "Part1", "Placement1"); + std::string fullMarkerName2 = handleOneSideOfJoint(joint, jointType, "Part2", "Placement2"); mbdJoint->setMarkerI(fullMarkerName1); mbdJoint->setMarkerJ(fullMarkerName2); - return mbdJoint; + return {mbdJoint}; +} +std::string AssemblyObject::handleOneSideOfJoint(App::DocumentObject* joint, + JointType jointType, + const char* propLinkName, + const char* propPlcName) +{ + App::DocumentObject* obj = getLinkObjFromProp(joint, propLinkName); + + std::shared_ptr mbdPart = getMbDPart(obj); + Base::Placement objPlc = getPlacementFromProp(obj, "Placement"); + Base::Placement plc = getPlacementFromProp(joint, propPlcName); + // Now we have plc which is the JCS placement, but its relative to the doc origin, not to the + // obj. + + plc = objPlc.inverse() * plc; + + std::string markerName = joint->getFullName(); + auto mbdMarker = makeMbdMarker(markerName, plc); + mbdPart->addMarker(mbdMarker); + + return "/OndselAssembly/" + mbdPart->name + "/" + markerName; } std::shared_ptr AssemblyObject::getMbDPart(App::DocumentObject* obj) @@ -309,53 +757,6 @@ std::shared_ptr AssemblyObject::getMbDPart(App::DocumentObject* obj) return mbdPart; } -std::string AssemblyObject::handleOneSideOfJoint(App::DocumentObject* joint, - const char* propLinkName, - const char* propPlcName) -{ - auto* propObj = dynamic_cast(joint->getPropertyByName(propLinkName)); - if (!propObj) { - return nullptr; - } - App::DocumentObject* obj = propObj->getValue(); - - std::shared_ptr mbdPart = getMbDPart(obj); - Base::Placement objPlc = getPlacementFromProp(obj, "Placement"); - Base::Placement plc = getPlacementFromProp(joint, propPlcName); - // Now we have plc which is the JCS placement, but its relative to the doc origin, not to the - // obj. - - plc = objPlc.inverse() * plc; - - std::string markerName = joint->getFullName(); - auto mbdMarker = makeMbdMarker(markerName, plc); - mbdPart->addMarker(mbdMarker); - - return "/OndselAssembly/" + mbdPart->name + "/" + markerName; -} - -std::shared_ptr AssemblyObject::makeMbdMarker(std::string& name, Base::Placement& plc) -{ - auto mbdMarker = CREATE::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); - /*double q0, q1, q2, q3; - rot.getValue(q0, q1, q2, q3); - mbdMarker->setQuarternions(q0, q1, q2, q3);*/ - return mbdMarker; -} - std::shared_ptr AssemblyObject::makeMbdPart(std::string& name, Base::Placement plc, double mass) { @@ -395,6 +796,121 @@ std::shared_ptr AssemblyObject::makeMbdAssembly() return assembly; } +std::shared_ptr AssemblyObject::makeMbdMarker(std::string& name, Base::Placement& plc) +{ + auto mbdMarker = CREATE::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); + /*double q0, q1, q2, q3; + rot.getValue(q0, q1, q2, q3); + mbdMarker->setQuarternions(q0, q1, q2, q3);*/ + return mbdMarker; +} + +void AssemblyObject::swapJCS(App::DocumentObject* joint) +{ + auto propElement1 = dynamic_cast(joint->getPropertyByName("Element1")); + auto propElement2 = dynamic_cast(joint->getPropertyByName("Element2")); + if (propElement1 && propElement2) { + auto temp = std::string(propElement1->getValue()); + propElement1->setValue(propElement2->getValue()); + propElement2->setValue(temp); + } + auto propVertex1 = dynamic_cast(joint->getPropertyByName("Vertex1")); + auto propVertex2 = dynamic_cast(joint->getPropertyByName("Vertex2")); + if (propVertex1 && propVertex2) { + auto temp = std::string(propVertex1->getValue()); + propVertex1->setValue(propVertex2->getValue()); + propVertex2->setValue(temp); + } + auto propPlacement1 = + dynamic_cast(joint->getPropertyByName("Placement1")); + auto propPlacement2 = + dynamic_cast(joint->getPropertyByName("Placement2")); + if (propPlacement1 && propPlacement2) { + auto temp = propPlacement1->getValue(); + propPlacement1->setValue(propPlacement2->getValue()); + propPlacement2->setValue(temp); + } + auto propObject1 = dynamic_cast(joint->getPropertyByName("Object1")); + auto propObject2 = dynamic_cast(joint->getPropertyByName("Object2")); + if (propObject1 && propObject2) { + auto temp = propObject1->getValue(); + propObject1->setValue(propObject2->getValue()); + propObject2->setValue(temp); + } + auto propPart1 = dynamic_cast(joint->getPropertyByName("Part1")); + auto propPart2 = dynamic_cast(joint->getPropertyByName("Part2")); + if (propPart1 && propPart2) { + auto temp = propPart1->getValue(); + propPart1->setValue(propPart2->getValue()); + propPart2->setValue(temp); + } +} + +void AssemblyObject::savePlacementsForUndo() +{ + previousPositions.clear(); + + for (auto& pair : objectPartMap) { + App::DocumentObject* obj = pair.first; + if (!obj) { + continue; + } + + std::pair savePair; + savePair.first = obj; + + // Check if the object has a "Placement" property + auto* propPlc = dynamic_cast(obj->getPropertyByName("Placement")); + if (!propPlc) { + continue; + } + savePair.second = propPlc->getValue(); + + previousPositions.push_back(savePair); + } +} + +void AssemblyObject::undoSolve() +{ + for (auto& pair : previousPositions) { + App::DocumentObject* obj = pair.first; + if (!obj) { + continue; + } + + // Check if the object has a "Placement" property + auto* propPlacement = + dynamic_cast(obj->getPropertyByName("Placement")); + if (!propPlacement) { + continue; + } + + propPlacement->setValue(pair.second); + } + previousPositions.clear(); + + // update joint placements: + getJoints(); +} + +void AssemblyObject::clearUndo() +{ + previousPositions.clear(); +} + void AssemblyObject::setNewPlacements() { for (auto& pair : objectPartMap) { @@ -408,34 +924,35 @@ void AssemblyObject::setNewPlacements() // Check if the object has a "Placement" property auto* propPlacement = dynamic_cast(obj->getPropertyByName("Placement")); - if (propPlacement) { - - double x, y, z; - mbdPart->getPosition3D(x, y, z); - // Base::Console().Warning("in set placement : (%f, %f, %f)\n", x, y, z); - Base::Vector3d pos = Base::Vector3d(x, y, z); - - // TODO : replace with quaternion to simplify - auto& r0 = mbdPart->rotationMatrix->at(0); - auto& r1 = mbdPart->rotationMatrix->at(1); - auto& r2 = mbdPart->rotationMatrix->at(2); - Base::Vector3d row0 = Base::Vector3d(r0->at(0), r0->at(1), r0->at(2)); - Base::Vector3d row1 = Base::Vector3d(r1->at(0), r1->at(1), r1->at(2)); - Base::Vector3d row2 = Base::Vector3d(r2->at(0), r2->at(1), r2->at(2)); - Base::Matrix4D mat; - mat.setRow(0, row0); - mat.setRow(1, row1); - mat.setRow(2, row2); - Base::Rotation rot = Base::Rotation(mat); - - /*double q0, q1, q2, q3; - mbdPart->getQuarternions(q0, q1, q2, q3); - Base::Rotation rot = Base::Rotation(q0, q1, q2, q3);*/ - - Base::Placement newPlacement = Base::Placement(pos, rot); - - propPlacement->setValue(newPlacement); + if (!propPlacement) { + continue; } + + double x, y, z; + mbdPart->getPosition3D(x, y, z); + // Base::Console().Warning("in set placement : (%f, %f, %f)\n", x, y, z); + Base::Vector3d pos = Base::Vector3d(x, y, z); + + // TODO : replace with quaternion to simplify + auto& r0 = mbdPart->rotationMatrix->at(0); + auto& r1 = mbdPart->rotationMatrix->at(1); + auto& r2 = mbdPart->rotationMatrix->at(2); + Base::Vector3d row0 = Base::Vector3d(r0->at(0), r0->at(1), r0->at(2)); + Base::Vector3d row1 = Base::Vector3d(r1->at(0), r1->at(1), r1->at(2)); + Base::Vector3d row2 = Base::Vector3d(r2->at(0), r2->at(1), r2->at(2)); + Base::Matrix4D mat; + mat.setRow(0, row0); + mat.setRow(1, row1); + mat.setRow(2, row2); + Base::Rotation rot = Base::Rotation(mat); + + /*double q0, q1, q2, q3; + mbdPart->getQuarternions(q0, q1, q2, q3); + Base::Rotation rot = Base::Rotation(q0, q1, q2, q3);*/ + + Base::Placement newPlacement = Base::Placement(pos, rot); + + propPlacement->setValue(newPlacement); } } @@ -482,13 +999,157 @@ void AssemblyObject::setObjMasses(std::vector(obj); + const PartApp::TopoShape& TopShape = base->Shape.getShape(); + + // Check for valid face types + TopoDS_Face face = TopoDS::Face(TopShape.getSubShape(elName)); + BRepAdaptor_Surface sf(face); + // GeomAbs_Plane GeomAbs_Cylinder GeomAbs_Cone GeomAbs_Sphere GeomAbs_Thorus + if (sf.GetType() == type) { + return true; + } + + return false; +} + +bool AssemblyObject::isEdgeType(App::DocumentObject* obj, + const char* elName, + GeomAbs_CurveType type) +{ + PartApp::Feature* base = static_cast(obj); + const PartApp::TopoShape& TopShape = base->Shape.getShape(); + + // Check for valid face types + TopoDS_Edge edge = TopoDS::Edge(TopShape.getSubShape(elName)); + BRepAdaptor_Curve sf(edge); + + if (sf.GetType() == type) { + return true; + } + + return false; +} + +double AssemblyObject::getFaceRadius(App::DocumentObject* obj, const char* elt) +{ + auto base = static_cast(obj); + const PartApp::TopoShape& TopShape = base->Shape.getShape(); + + // Check for valid face types + TopoDS_Face face = TopoDS::Face(TopShape.getSubShape(elt)); + BRepAdaptor_Surface sf(face); + + if (sf.GetType() == GeomAbs_Cylinder) { + return sf.Cylinder().Radius(); + } + else if (sf.GetType() == GeomAbs_Sphere) { + return sf.Sphere().Radius(); + } + + return 0.0; +} + +double AssemblyObject::getEdgeRadius(App::DocumentObject* obj, const char* elt) +{ + auto base = static_cast(obj); + const PartApp::TopoShape& TopShape = base->Shape.getShape(); + + // Check for valid face types + TopoDS_Edge edge = TopoDS::Edge(TopShape.getSubShape(elt)); + BRepAdaptor_Curve sf(edge); + + if (sf.GetType() == GeomAbs_Circle) { + return sf.Circle().Radius(); + } + + return 0.0; +} + +// getters to get from properties +double AssemblyObject::getJointDistance(App::DocumentObject* joint) +{ + double distance = 0.0; + + auto* prop = dynamic_cast(joint->getPropertyByName("Distance")); + if (prop) { + distance = prop->getValue(); + } + + return distance; +} + +JointType AssemblyObject::getJointType(App::DocumentObject* joint) +{ + JointType jointType = JointType::Fixed; + + auto* prop = dynamic_cast(joint->getPropertyByName("JointType")); + if (prop) { + jointType = static_cast(prop->getValue()); + } + + return jointType; +} + +const char* AssemblyObject::getElementFromProp(App::DocumentObject* obj, const char* propName) +{ + auto* prop = dynamic_cast(obj->getPropertyByName(propName)); + if (!prop) { + return ""; + } + + return prop->getValue(); +} + +std::string AssemblyObject::getElementTypeFromProp(App::DocumentObject* obj, const char* propName) +{ + // The prop is going to be something like 'Edge14' or 'Face7'. We need 'Edge' or 'Face' + std::string elementType; + for (char ch : std::string(getElementFromProp(obj, propName))) { + if (std::isalpha(ch)) { + elementType += ch; + } + } + return elementType; +} + +App::DocumentObject* AssemblyObject::getLinkObjFromProp(App::DocumentObject* joint, + const char* propLinkName) +{ + auto* propObj = dynamic_cast(joint->getPropertyByName(propLinkName)); + if (!propObj) { + return nullptr; + } + return propObj->getValue(); +} + +App::DocumentObject* AssemblyObject::getLinkedObjFromProp(App::DocumentObject* joint, + const char* propLinkName) +{ + return getLinkObjFromProp(joint, propLinkName)->getLinkedObject(true); +} + +Base::Placement AssemblyObject::getPlacementFromProp(App::DocumentObject* obj, const char* propName) +{ + Base::Placement plc = Base::Placement(); + auto* propPlacement = dynamic_cast(obj->getPropertyByName(propName)); + if (propPlacement) { + plc = propPlacement->getValue(); + } + return plc; +} + /*void Part::handleChangedPropertyType(Base::XMLReader& reader, const char* TypeName, App::Property* prop) { App::Part::handleChangedPropertyType(reader, TypeName, prop); }*/ -/* Apparantly not necessary as App::Part doesn't have this. +/* Apparently not necessary as App::Part doesn't have this. // Python Assembly feature --------------------------------------------------------- namespace App diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h index 8fe6b09c84..49a4e8765d 100644 --- a/src/Mod/Assembly/App/AssemblyObject.h +++ b/src/Mod/Assembly/App/AssemblyObject.h @@ -25,6 +25,10 @@ #ifndef ASSEMBLY_AssemblyObject_H #define ASSEMBLY_AssemblyObject_H + +#include +#include + #include #include @@ -49,6 +53,8 @@ class Rotation; namespace Assembly { +class JointGroup; + // This enum has to be the same as the one in JointObject.py enum class JointType { @@ -57,9 +63,7 @@ enum class JointType Cylindrical, Slider, Ball, - Planar, - Parallel, - Tangent + Distance }; class AssemblyExport AssemblyObject: public App::Part @@ -78,35 +82,78 @@ public: return "AssemblyGui::ViewProviderAssembly"; } - int solve(); + int solve(bool enableRedo = false); + void savePlacementsForUndo(); + void undoSolve(); + void clearUndo(); void exportAsASMT(std::string fileName); std::shared_ptr makeMbdAssembly(); std::shared_ptr makeMbdPart(std::string& name, Base::Placement plc = Base::Placement(), double mass = 1.0); std::shared_ptr getMbDPart(App::DocumentObject* obj); std::shared_ptr makeMbdMarker(std::string& name, Base::Placement& plc); - std::shared_ptr makeMbdJoint(App::DocumentObject* joint); - std::shared_ptr makeMbdJointOfType(JointType jointType); + std::vector> makeMbdJoint(App::DocumentObject* joint); + std::shared_ptr makeMbdJointOfType(App::DocumentObject* joint, + JointType jointType); + std::shared_ptr makeMbdJointDistance(App::DocumentObject* joint); + std::shared_ptr makeMbdJointDistanceFaceVertex(App::DocumentObject* joint); + std::shared_ptr makeMbdJointDistanceEdgeVertex(App::DocumentObject* joint); + std::shared_ptr makeMbdJointDistanceFaceEdge(App::DocumentObject* joint); + std::shared_ptr makeMbdJointDistanceEdgeEdge(App::DocumentObject* joint); + std::shared_ptr makeMbdJointDistanceFaceFace(App::DocumentObject* joint); + std::string handleOneSideOfJoint(App::DocumentObject* joint, + JointType jointType, const char* propObjLinkName, const char* propPlcName); - void fixGroundedPart(App::DocumentObject* obj, Base::Placement& plc, std::string& jointName); - bool fixGroundedParts(); void jointParts(std::vector joints); std::vector getJoints(); - Base::Placement getPlacementFromProp(App::DocumentObject* obj, const char* propName); + std::vector getGroundedJoints(); + void fixGroundedPart(App::DocumentObject* obj, Base::Placement& plc, std::string& jointName); + std::vector fixGroundedParts(); + + void removeUnconnectedJoints(std::vector& joints, + std::vector groundedObjs); + void traverseAndMarkConnectedParts(App::DocumentObject* currentPart, + std::set& connectedParts, + const std::vector& joints); + std::vector + getConnectedParts(App::DocumentObject* part, const std::vector& joints); + + JointGroup* getJointGroup(); + + void swapJCS(App::DocumentObject* joint); + void setNewPlacements(); void recomputeJointPlacements(std::vector joints); + bool isPartConnected(App::DocumentObject* obj); + double getObjMass(App::DocumentObject* obj); void setObjMasses(std::vector> objectMasses); + bool isEdgeType(App::DocumentObject* obj, const char* elName, GeomAbs_CurveType type); + bool isFaceType(App::DocumentObject* obj, const char* elName, GeomAbs_SurfaceType type); + double getFaceRadius(App::DocumentObject* obj, const char* elName); + double getEdgeRadius(App::DocumentObject* obj, const char* elName); + + // getters to get from properties + double getJointDistance(App::DocumentObject* joint); + JointType getJointType(App::DocumentObject* joint); + const char* getElementFromProp(App::DocumentObject* obj, const char* propName); + std::string getElementTypeFromProp(App::DocumentObject* obj, const char* propName); + Base::Placement getPlacementFromProp(App::DocumentObject* obj, const char* propName); + App::DocumentObject* getLinkObjFromProp(App::DocumentObject* joint, const char* propName); + App::DocumentObject* getLinkedObjFromProp(App::DocumentObject* joint, const char* propName); + private: std::shared_ptr mbdAssembly; std::unordered_map> objectPartMap; std::vector> objMasses; + std::vector> previousPositions; + // void handleChangedPropertyType(Base::XMLReader &reader, const char *TypeName, App::Property // *prop) override; }; diff --git a/src/Mod/Assembly/App/AssemblyObjectPy.xml b/src/Mod/Assembly/App/AssemblyObjectPy.xml index 7b38bd92be..7ddee537dd 100644 --- a/src/Mod/Assembly/App/AssemblyObjectPy.xml +++ b/src/Mod/Assembly/App/AssemblyObjectPy.xml @@ -18,7 +18,13 @@ Solve the assembly and update part placements. - solve() + solve(enableRedo=False) -> int + + Args: + enableRedo: Whether the solve save the initial position of parts + to enable undoing it even without a transaction. + Defaults to `False` ie the solve cannot be undone if called + outside of a transaction. Returns: 0 in case of success, otherwise the following codes in this order of @@ -32,6 +38,41 @@ + + + + Undo the last solve of the assembly and return part placements to their initial position. + + undoSolve() + + Returns: None + + + + + + + Clear the registered undo positions. + + clearUndo() + + Returns: None + + + + + + + Check if a part is connected to the ground through joints. + + isPartConnected(obj) -> bool + + Args: document object to check. + + Returns: True if part is connected to ground + + + diff --git a/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp b/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp index bfb50e346c..abe4a4428c 100644 --- a/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp +++ b/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp @@ -46,12 +46,55 @@ int AssemblyObjectPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj* } PyObject* AssemblyObjectPy::solve(PyObject* args) +{ + PyObject* enableUndoPy; + bool enableUndo; + + if (!PyArg_ParseTuple(args, "O!", &PyBool_Type, &enableUndoPy)) { + PyErr_Clear(); + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + else { + enableUndo = false; + } + } + else { + enableUndo = Base::asBoolean(enableUndoPy); + } + + int ret = this->getAssemblyObjectPtr()->solve(enableUndo); + return Py_BuildValue("i", ret); +} + +PyObject* AssemblyObjectPy::undoSolve(PyObject* args) { if (!PyArg_ParseTuple(args, "")) { return nullptr; } - int ret = this->getAssemblyObjectPtr()->solve(); - return Py_BuildValue("i", ret); + this->getAssemblyObjectPtr()->undoSolve(); + Py_Return; +} + +PyObject* AssemblyObjectPy::clearUndo(PyObject* args) +{ + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + this->getAssemblyObjectPtr()->clearUndo(); + Py_Return; +} + +PyObject* AssemblyObjectPy::isPartConnected(PyObject* args) +{ + PyObject* pyobj; + + if (!PyArg_ParseTuple(args, "O", &pyobj)) { + return nullptr; + } + auto* obj = static_cast(pyobj)->getDocumentObjectPtr(); + bool ok = this->getAssemblyObjectPtr()->isPartConnected(obj); + return Py_BuildValue("O", (ok ? Py_True : Py_False)); } PyObject* AssemblyObjectPy::exportAsASMT(PyObject* args) diff --git a/src/Mod/Assembly/App/PreCompiled.h b/src/Mod/Assembly/App/PreCompiled.h index 4e087e3c91..f70a22f5f1 100644 --- a/src/Mod/Assembly/App/PreCompiled.h +++ b/src/Mod/Assembly/App/PreCompiled.h @@ -42,5 +42,10 @@ #include #include +#include +#include +#include +#include + #endif // _PreComp_ #endif // ASSEMBLY_PRECOMPILED_H diff --git a/src/Mod/Assembly/CMakeLists.txt b/src/Mod/Assembly/CMakeLists.txt index 713b231aeb..b6f964016c 100644 --- a/src/Mod/Assembly/CMakeLists.txt +++ b/src/Mod/Assembly/CMakeLists.txt @@ -67,3 +67,9 @@ INSTALL( DESTINATION Mod/Assembly/AssemblyTests ) +INSTALL( + FILES + ${AssemblyScripts_SRCS} + DESTINATION + Mod/Assembly/Assembly +) diff --git a/src/Mod/Assembly/CommandCreateAssembly.py b/src/Mod/Assembly/CommandCreateAssembly.py index ac8d7d3e35..6c6cfd702a 100644 --- a/src/Mod/Assembly/CommandCreateAssembly.py +++ b/src/Mod/Assembly/CommandCreateAssembly.py @@ -58,7 +58,7 @@ class CommandCreateAssembly: App.setActiveTransaction("Create assembly") assembly = App.ActiveDocument.addObject("Assembly::AssemblyObject", "Assembly") assembly.Type = "Assembly" - Gui.ActiveDocument.ActiveView.setActiveObject("part", assembly) + Gui.ActiveDocument.setEdit(assembly) assembly.newObject("Assembly::JointGroup", "Joints") App.closeActiveTransaction() diff --git a/src/Mod/Assembly/CommandCreateJoint.py b/src/Mod/Assembly/CommandCreateJoint.py index aa649448cf..765de4c994 100644 --- a/src/Mod/Assembly/CommandCreateJoint.py +++ b/src/Mod/Assembly/CommandCreateJoint.py @@ -42,6 +42,18 @@ __author__ = "Ondsel" __url__ = "https://www.freecad.org" +def isCreateJointActive(): + return UtilsAssembly.isAssemblyGrounded() and UtilsAssembly.assembly_has_at_least_n_parts(2) + + +def activateJoint(index): + if JointObject.activeTask: + JointObject.activeTask.reject() + + panel = TaskAssemblyCreateJoint(index) + Gui.Control.showDialog(panel) + + class CommandCreateJointFixed: def __init__(self): pass @@ -51,7 +63,7 @@ class CommandCreateJointFixed: return { "Pixmap": "Assembly_CreateJointFixed", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointFixed", "Create Fixed Joint"), - "Accel": "F", + "Accel": "J", "ToolTip": "

" + QT_TRANSLATE_NOOP( "Assembly_CreateJointFixed", @@ -62,11 +74,10 @@ class CommandCreateJointFixed: } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return isCreateJointActive() def Activated(self): - panel = TaskAssemblyCreateJoint(0) - Gui.Control.showDialog(panel) + activateJoint(0) class CommandCreateJointRevolute: @@ -89,11 +100,10 @@ class CommandCreateJointRevolute: } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return isCreateJointActive() def Activated(self): - panel = TaskAssemblyCreateJoint(1) - Gui.Control.showDialog(panel) + activateJoint(1) class CommandCreateJointCylindrical: @@ -118,11 +128,10 @@ class CommandCreateJointCylindrical: } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return isCreateJointActive() def Activated(self): - panel = TaskAssemblyCreateJoint(2) - Gui.Control.showDialog(panel) + activateJoint(2) class CommandCreateJointSlider: @@ -145,11 +154,10 @@ class CommandCreateJointSlider: } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return isCreateJointActive() def Activated(self): - panel = TaskAssemblyCreateJoint(3) - Gui.Control.showDialog(panel) + activateJoint(3) class CommandCreateJointBall: @@ -172,92 +180,37 @@ class CommandCreateJointBall: } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return isCreateJointActive() def Activated(self): - panel = TaskAssemblyCreateJoint(4) - Gui.Control.showDialog(panel) + activateJoint(4) -class CommandCreateJointPlanar: +class CommandCreateJointDistance: def __init__(self): pass def GetResources(self): return { - "Pixmap": "Assembly_CreateJointPlanar", - "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointPlanar", "Create Planar Joint"), - "Accel": "P", + "Pixmap": "Assembly_CreateJointDistance", + "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointDistance", "Create Distance Joint"), + "Accel": "D", "ToolTip": "

" + QT_TRANSLATE_NOOP( - "Assembly_CreateJointPlanar", - "Create a Planar Joint: Ensures two selected features are in the same plane, restricting movement to that plane.", + "Assembly_CreateJointDistance", + "Create a Distance Joint: Depending on your selection this tool will apply different constraints.", ) + "

", "CmdType": "ForEdit", } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + # return False + return isCreateJointActive() def Activated(self): - panel = TaskAssemblyCreateJoint(5) - Gui.Control.showDialog(panel) - - -class CommandCreateJointParallel: - def __init__(self): - pass - - def GetResources(self): - - return { - "Pixmap": "Assembly_CreateJointParallel", - "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointParallel", "Create Parallel Joint"), - "Accel": "L", - "ToolTip": "

" - + QT_TRANSLATE_NOOP( - "Assembly_CreateJointParallel", - "Create a Parallel Joint: Aligns two features to be parallel, constraining relative movement to parallel translations.", - ) - + "

", - "CmdType": "ForEdit", - } - - def IsActive(self): - return UtilsAssembly.activeAssembly() is not None - - def Activated(self): - panel = TaskAssemblyCreateJoint(6) - Gui.Control.showDialog(panel) - - -class CommandCreateJointTangent: - def __init__(self): - pass - - def GetResources(self): - - return { - "Pixmap": "Assembly_CreateJointTangent", - "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointTangent", "Create Tangent Joint"), - "Accel": "T", - "ToolTip": "

" - + QT_TRANSLATE_NOOP( - "Assembly_CreateJointTangent", - "Create a Tangent Joint: Forces two features to be tangent, restricting movement to smooth transitions along their contact surface.", - ) - + "

", - "CmdType": "ForEdit", - } - - def IsActive(self): - return UtilsAssembly.activeAssembly() is not None - - def Activated(self): - panel = TaskAssemblyCreateJoint(7) - Gui.Control.showDialog(panel) + activateJoint(5) class CommandToggleGrounded: @@ -269,18 +222,21 @@ class CommandToggleGrounded: return { "Pixmap": "Assembly_ToggleGrounded", "MenuText": QT_TRANSLATE_NOOP("Assembly_ToggleGrounded", "Toggle grounded"), - "Accel": "F", + "Accel": "G", "ToolTip": "

" + QT_TRANSLATE_NOOP( "Assembly_ToggleGrounded", - "Toggle the grounded state of a part. Grounding a part permanently locks its position in the assembly, preventing any movement or rotation. You need at least one grounded part per assembly.", + "Grounding a part permanently locks its position in the assembly, preventing any movement or rotation. You need at least one grounded part before starting to assemble.", ) + "

", "CmdType": "ForEdit", } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return ( + UtilsAssembly.isAssemblyCommandActive() + and UtilsAssembly.assembly_has_at_least_n_parts(1) + ) def Activated(self): assembly = UtilsAssembly.activeAssembly() @@ -301,20 +257,33 @@ class CommandToggleGrounded: full_element_name = UtilsAssembly.getFullElementName(sel.ObjectName, sub) obj = UtilsAssembly.getObject(full_element_name) + part_containing_obj = UtilsAssembly.getContainingPart(full_element_name, obj) + + # Only objects within the assembly. + objs_names, element_name = UtilsAssembly.getObjsNamesAndElement(sel.ObjectName, sub) + if assembly.Name not in objs_names: + continue # Check if part is grounded and if so delete the joint. for joint in joint_group.Group: - if hasattr(joint, "ObjectToGround") and joint.ObjectToGround == obj: + if ( + hasattr(joint, "ObjectToGround") + and joint.ObjectToGround == part_containing_obj + ): + # Remove grounded tag. + if part_containing_obj.Label.endswith(" 🔒"): + part_containing_obj.Label = part_containing_obj.Label[:-2] doc = App.ActiveDocument doc.removeObject(joint.Name) doc.recompute() return # Create groundedJoint. + + part_containing_obj.Label = part_containing_obj.Label + " 🔒" ground = joint_group.newObject("App::FeaturePython", "GroundedJoint") - JointObject.GroundedJoint(ground, obj) + JointObject.GroundedJoint(ground, part_containing_obj) JointObject.ViewProviderGroundedJoint(ground.ViewObject) - Gui.Selection.clearSelection() App.closeActiveTransaction() @@ -325,6 +294,4 @@ if App.GuiUp: Gui.addCommand("Assembly_CreateJointCylindrical", CommandCreateJointCylindrical()) Gui.addCommand("Assembly_CreateJointSlider", CommandCreateJointSlider()) Gui.addCommand("Assembly_CreateJointBall", CommandCreateJointBall()) - Gui.addCommand("Assembly_CreateJointPlanar", CommandCreateJointPlanar()) - Gui.addCommand("Assembly_CreateJointParallel", CommandCreateJointParallel()) - Gui.addCommand("Assembly_CreateJointTangent", CommandCreateJointTangent()) + Gui.addCommand("Assembly_CreateJointDistance", CommandCreateJointDistance()) diff --git a/src/Mod/Assembly/CommandExportASMT.py b/src/Mod/Assembly/CommandExportASMT.py index 438c669629..b32734f388 100644 --- a/src/Mod/Assembly/CommandExportASMT.py +++ b/src/Mod/Assembly/CommandExportASMT.py @@ -54,7 +54,7 @@ class CommandExportASMT: } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return UtilsAssembly.isAssemblyCommandActive() and UtilsAssembly.isAssemblyGrounded() def Activated(self): document = App.ActiveDocument diff --git a/src/Mod/Assembly/CommandInsertLink.py b/src/Mod/Assembly/CommandInsertLink.py index 5f7ddd3126..f70cd8732b 100644 --- a/src/Mod/Assembly/CommandInsertLink.py +++ b/src/Mod/Assembly/CommandInsertLink.py @@ -21,6 +21,7 @@ # * # ***************************************************************************/ +import re import os import FreeCAD as App @@ -31,6 +32,7 @@ if App.GuiUp: from PySide import QtCore, QtGui, QtWidgets import UtilsAssembly +import Preferences # translate = App.Qt.translate @@ -44,22 +46,30 @@ class CommandInsertLink: pass def GetResources(self): - tooltip = "

Insert a Link into the assembly. " - tooltip += "This will create dynamic links to parts/bodies/primitives/assemblies." - tooltip += "To insert external objects, make sure that the file " - tooltip += "is open in the current session

" - tooltip += "

Press shift to add several links while clicking on the view." - return { "Pixmap": "Assembly_InsertLink", "MenuText": QT_TRANSLATE_NOOP("Assembly_InsertLink", "Insert Link"), "Accel": "I", - "ToolTip": QT_TRANSLATE_NOOP("Assembly_InsertLink", tooltip), + "ToolTip": "

" + + QT_TRANSLATE_NOOP( + "Assembly_InsertLink", + "Insert a Link into the currently active assembly. This will create dynamic links to parts/bodies/primitives/assemblies. To insert external objects, make sure that the file is open in the current session", + ) + + "

  • " + + QT_TRANSLATE_NOOP("Assembly_InsertLink", "Insert by left clicking items in the list.") + + "
  • " + + QT_TRANSLATE_NOOP("Assembly_InsertLink", "Undo by right clicking items in the list.") + + "
  • " + + QT_TRANSLATE_NOOP( + "Assembly_InsertLink", + "Press shift to add several links while clicking on the view.", + ) + + "

", "CmdType": "ForEdit", } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return UtilsAssembly.isAssemblyCommandActive() def Activated(self): assembly = UtilsAssembly.activeAssembly() @@ -81,6 +91,10 @@ class TaskAssemblyInsertLink(QtCore.QObject): self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyInsertLink.ui") self.form.installEventFilter(self) + self.form.partList.installEventFilter(self) + + pref = Preferences.preferences() + self.form.CheckBox_InsertInParts.setChecked(pref.GetBool("InsertInParts", True)) # Actions self.form.openFileButton.clicked.connect(self.openFiles) @@ -89,28 +103,37 @@ class TaskAssemblyInsertLink(QtCore.QObject): self.allParts = [] self.partsDoc = [] - self.numberOfAddedParts = 0 self.translation = 0 self.partMoving = False + self.totalTranslation = App.Vector() + + self.insertionStack = [] # used to handle cancellation of insertions. self.buildPartList() App.setActiveTransaction("Insert Link") def accept(self): - App.closeActiveTransaction() self.deactivated() + + if self.partMoving: + self.endMove() + + App.closeActiveTransaction() return True def reject(self): - App.closeActiveTransaction(True) self.deactivated() + + if self.partMoving: + self.dismissPart() + + App.closeActiveTransaction(True) return True def deactivated(self): - if self.partMoving: - self.endMove() - self.doc.removeObject(self.createdLink.Name) + pref = Preferences.preferences() + pref.SetBool("InsertInParts", self.form.CheckBox_InsertInParts.isChecked()) def buildPartList(self): self.allParts.clear() @@ -136,7 +159,7 @@ class TaskAssemblyInsertLink(QtCore.QObject): self.allParts.append(obj) self.partsDoc.append(doc) - for obj in doc.findObjects("PartDesign::Body"): + for obj in doc.findObjects("Part::Feature"): # but only those at top level (not nested inside other containers) if obj.getParentGeoFeatureGroup() is None: self.allParts.append(obj) @@ -145,7 +168,7 @@ class TaskAssemblyInsertLink(QtCore.QObject): self.form.partList.clear() for part in self.allParts: newItem = QtGui.QListWidgetItem() - newItem.setText(part.Document.Name + " - " + part.Name) + newItem.setText(part.Label + " (" + part.Document.Name + ".FCStd)") newItem.setIcon(part.ViewObject.Icon) self.form.partList.addItem(newItem) @@ -193,23 +216,82 @@ class TaskAssemblyInsertLink(QtCore.QObject): # check that the current document had been saved or that it's the same document as that of the selected part if not self.doc.FileName != "" and not self.doc == selectedPart.Document: - print("The current document must be saved before inserting an external part") - return + msgBox = QtWidgets.QMessageBox() + msgBox.setIcon(QtWidgets.QMessageBox.Warning) + msgBox.setText("The current document must be saved before inserting external parts.") + msgBox.setWindowTitle("Save Document") + saveButton = msgBox.addButton("Save", QtWidgets.QMessageBox.AcceptRole) + cancelButton = msgBox.addButton("Cancel", QtWidgets.QMessageBox.RejectRole) - self.createdLink = self.assembly.newObject("App::Link", selectedPart.Name) - self.createdLink.LinkedObject = selectedPart - self.createdLink.Placement.Base = self.getTranslationVec(selectedPart) - self.createdLink.recompute() + msgBox.exec_() - self.numberOfAddedParts += 1 + if not (msgBox.clickedButton() == saveButton and Gui.ActiveDocument.saveAs()): + return + + objectWhereToInsert = self.assembly + + if self.form.CheckBox_InsertInParts.isChecked() and selectedPart.TypeId != "App::Part": + objectWhereToInsert = self.assembly.newObject("App::Part", "Part_" + selectedPart.Label) + + createdLink = objectWhereToInsert.newObject("App::Link", selectedPart.Label) + createdLink.LinkedObject = selectedPart + createdLink.recompute() + + addedObject = createdLink + if self.form.CheckBox_InsertInParts.isChecked() and selectedPart.TypeId != "App::Part": + addedObject = objectWhereToInsert + + insertionDict = {} + insertionDict["item"] = item + insertionDict["addedObject"] = addedObject + self.insertionStack.append(insertionDict) + self.increment_counter(item) + + translation = self.getTranslationVec(addedObject) + insertionDict["translation"] = translation + self.totalTranslation += translation + addedObject.Placement.Base = self.totalTranslation # highlight the link Gui.Selection.clearSelection() - Gui.Selection.addSelection(self.doc.Name, self.assembly.Name, self.createdLink.Name + ".") + Gui.Selection.addSelection(self.doc.Name, addedObject.Name, "") # Start moving the part if user brings mouse on view self.initMove() + self.form.partList.setItemSelected(item, False) + + def increment_counter(self, item): + text = item.text() + match = re.search(r"(\d+) inserted$", text) + + if match: + # Counter exists, increment it + counter = int(match.group(1)) + 1 + new_text = re.sub(r"\d+ inserted$", f"{counter} inserted", text) + else: + # Counter does not exist, add it + new_text = f"{text} : 1 inserted" + + item.setText(new_text) + + def decrement_counter(self, item): + text = item.text() + match = re.search(r"(\d+) inserted$", text) + + if match: + counter = int(match.group(1)) - 1 + if counter > 0: + # Update the counter + new_text = re.sub(r"\d+ inserted$", f"{counter} inserted", text) + elif counter == 0: + # Remove the counter part from the text + new_text = re.sub(r" : \d+ inserted$", "", text) + else: + return + + item.setText(new_text) + def initMove(self): self.callbackMove = self.view.addEventCallback("SoLocation2Event", self.moveMouse) self.callbackClick = self.view.addEventCallback("SoMouseButtonEvent", self.clickMouse) @@ -230,42 +312,83 @@ class TaskAssemblyInsertLink(QtCore.QObject): def moveMouse(self, info): newPos = self.view.getPoint(*info["Position"]) - self.createdLink.Placement.Base = newPos + self.insertionStack[-1]["addedObject"].Placement.Base = newPos def clickMouse(self, info): if info["Button"] == "BUTTON1" and info["State"] == "DOWN": + Gui.Selection.clearSelection() if info["ShiftDown"]: # Create a new link and moves this one now - currentPos = self.createdLink.Placement.Base - selectedPart = self.createdLink.LinkedObject - self.createdLink = self.assembly.newObject("App::Link", selectedPart.Name) - self.createdLink.LinkedObject = selectedPart - self.createdLink.Placement.Base = currentPos + addedObject = self.insertionStack[-1]["addedObject"] + currentPos = addedObject.Placement.Base + selectedPart = addedObject + if addedObject.TypeId == "App::Link": + selectedPart = addedObject.LinkedObject + + addedObject = self.assembly.newObject("App::Link", selectedPart.Label) + addedObject.LinkedObject = selectedPart + addedObject.Placement.Base = currentPos + + insertionDict = {} + insertionDict["translation"] = App.Vector() + insertionDict["item"] = self.insertionStack[-1]["item"] + insertionDict["addedObject"] = addedObject + self.insertionStack.append(insertionDict) + else: self.endMove() + elif info["Button"] == "BUTTON2" and info["State"] == "DOWN": + self.dismissPart() + # 3D view keyboard handler def KeyboardEvent(self, info): if info["State"] == "UP" and info["Key"] == "ESCAPE": - self.endMove() - self.doc.removeObject(self.createdLink.Name) + self.dismissPart() + + def dismissPart(self): + self.endMove() + stack_item = self.insertionStack.pop() + self.totalTranslation -= stack_item["translation"] + UtilsAssembly.removeObjAndChilds(stack_item["addedObject"]) + self.decrement_counter(stack_item["item"]) # Taskbox keyboard event handler def eventFilter(self, watched, event): if watched == self.form and event.type() == QtCore.QEvent.KeyPress: if event.key() == QtCore.Qt.Key_Escape and self.partMoving: - self.endMove() - self.doc.removeObject(self.createdLink.Name) + self.dismissPart() return True # Consume the event + + if event.type() == QtCore.QEvent.ContextMenu and watched is self.form.partList: + item = watched.itemAt(event.pos()) + + if item: + # Iterate through the insertionStack in reverse + for i in reversed(range(len(self.insertionStack))): + stack_item = self.insertionStack[i] + + if stack_item["item"] == item: + if self.partMoving: + self.endMove() + + self.totalTranslation -= stack_item["translation"] + UtilsAssembly.removeObjAndChilds(stack_item["addedObject"]) + self.decrement_counter(item) + del self.insertionStack[i] + self.form.partList.setItemSelected(item, False) + + return True + return super().eventFilter(watched, event) def getTranslationVec(self, part): bb = part.Shape.BoundBox if bb: - self.translation += (bb.XMax + bb.YMax + bb.ZMax) * 0.15 + translation = (bb.XMax + bb.YMax + bb.ZMax) * 0.15 else: - self.translation += 10 - return App.Vector(self.translation, self.translation, self.translation) + translation = 10 + return App.Vector(translation, translation, translation) if App.GuiUp: diff --git a/src/Mod/Assembly/CommandSolveAssembly.py b/src/Mod/Assembly/CommandSolveAssembly.py index 95ec98f0db..b332f742d2 100644 --- a/src/Mod/Assembly/CommandSolveAssembly.py +++ b/src/Mod/Assembly/CommandSolveAssembly.py @@ -49,7 +49,7 @@ class CommandSolveAssembly: return { "Pixmap": "Assembly_SolveAssembly", "MenuText": QT_TRANSLATE_NOOP("Assembly_SolveAssembly", "Solve Assembly"), - "Accel": "F", + "Accel": "Z", "ToolTip": "

" + QT_TRANSLATE_NOOP( "Assembly_SolveAssembly", @@ -60,7 +60,7 @@ class CommandSolveAssembly: } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return UtilsAssembly.isAssemblyCommandActive() and UtilsAssembly.isAssemblyGrounded() def Activated(self): assembly = UtilsAssembly.activeAssembly() diff --git a/src/Mod/Assembly/Gui/PreCompiled.h b/src/Mod/Assembly/Gui/PreCompiled.h index 6c1fc5583b..02bfff4f01 100644 --- a/src/Mod/Assembly/Gui/PreCompiled.h +++ b/src/Mod/Assembly/Gui/PreCompiled.h @@ -37,6 +37,10 @@ #include #include +// Qt +#ifndef __QtAll__ +#include +#endif #endif //_PreComp_ diff --git a/src/Mod/Assembly/Gui/Resources/Assembly.qrc b/src/Mod/Assembly/Gui/Resources/Assembly.qrc index 6ace2e5c34..ad258eecf3 100644 --- a/src/Mod/Assembly/Gui/Resources/Assembly.qrc +++ b/src/Mod/Assembly/Gui/Resources/Assembly.qrc @@ -1,19 +1,20 @@ - + icons/Assembly_InsertLink.svg icons/preferences-assembly.svg icons/Assembly_ToggleGrounded.svg icons/Assembly_CreateJointBall.svg icons/Assembly_CreateJointCylindrical.svg icons/Assembly_CreateJointFixed.svg - icons/Assembly_CreateJointParallel.svg icons/Assembly_CreateJointPlanar.svg icons/Assembly_CreateJointRevolute.svg icons/Assembly_CreateJointSlider.svg icons/Assembly_CreateJointTangent.svg icons/Assembly_ExportASMT.svg + icons/Assembly_SolveAssembly.svg panels/TaskAssemblyCreateJoint.ui panels/TaskAssemblyInsertLink.ui preferences/Assembly.ui + icons/Assembly_CreateJointDistance.svg diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointParallel.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointDistance.svg similarity index 100% rename from src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointParallel.svg rename to src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointDistance.svg diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_SolveAssembly.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_SolveAssembly.svg new file mode 100644 index 0000000000..d3044a83f6 --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_SolveAssembly.svg @@ -0,0 +1,615 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Path-Stock + 2015-07-04 + https://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-Stock.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui index ed194b75c5..be41367552 100644 --- a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui +++ b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui @@ -20,8 +20,107 @@ + + + + + + Distance + + + + + + + + 0 + 0 + + + + mm + + + + + + + + + + + Offset + + + + + + + + 0 + 0 + + + + mm + + + + + + + + + + + Rotation + + + + + + + + 0 + 0 + + + + deg + + + + + + + + + + 0 + 0 + + + + Reverse the direction of the joint. + + + Reverse + + + + :/icons/button_sort.svg:/icons/button_sort.svg + + + + + + Gui::QuantitySpinBox + QWidget +

Gui/QuantitySpinBox.h
+ + diff --git a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui index 76a641643a..214df9adfb 100644 --- a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui +++ b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui @@ -42,8 +42,34 @@ + + + + If checked, the selected object will be inserted inside a Part container, unless it is already a Part. + + + Insert as part + + + true + + + InsertInParts + + + Mod/Assembly + + + + + + Gui::PrefCheckBox + QCheckBox +
Gui/PrefWidgets.h
+
+
diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index 97c3ab4935..865dc51364 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-License-Identifier: LGPL-2.1-or-later /**************************************************************************** * * * Copyright (c) 2023 Ondsel * @@ -24,6 +24,7 @@ #include "PreCompiled.h" #ifndef _PreComp_ +#include #include #include #include @@ -35,11 +36,12 @@ #include #include #include -#include +#include #include #include #include #include +#include #include #include "ViewProviderAssembly.h" @@ -80,6 +82,62 @@ bool ViewProviderAssembly::doubleClicked() return true; } +bool ViewProviderAssembly::canDragObject(App::DocumentObject* obj) const +{ + Base::Console().Warning("ViewProviderAssembly::canDragObject\n"); + if (!obj || obj->getTypeId() == Assembly::JointGroup::getClassTypeId()) { + Base::Console().Warning("so should be false...\n"); + return false; + } + + // else if a solid is removed, remove associated joints if any. + bool prompted = false; + auto* assemblyPart = static_cast(getObject()); + std::vector joints = assemblyPart->getJoints(); + + // Combine the joints and groundedJoints vectors into one for simplicity. + std::vector allJoints = assemblyPart->getJoints(); + std::vector groundedJoints = assemblyPart->getGroundedJoints(); + allJoints.insert(allJoints.end(), groundedJoints.begin(), groundedJoints.end()); + + Gui::Command::openCommand(tr("Delete associated joints").toStdString().c_str()); + for (auto joint : allJoints) { + // Assume getLinkObjFromProp can return nullptr if the property doesn't exist. + App::DocumentObject* obj1 = assemblyPart->getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* obj2 = assemblyPart->getLinkObjFromProp(joint, "Part2"); + App::DocumentObject* obj3 = assemblyPart->getLinkObjFromProp(joint, "ObjectToGround"); + if (obj == obj1 || obj == obj2 || obj == obj3) { + if (!prompted) { + prompted = true; + QMessageBox msgBox; + msgBox.setText(tr("The object is associated to one or more joints.")); + msgBox.setInformativeText( + tr("Do you want to move the object and delete associated joints?")); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::No); + int ret = msgBox.exec(); + + if (ret == QMessageBox::No) { + return false; + } + } + Gui::Command::doCommand(Gui::Command::Gui, + "App.activeDocument().removeObject('%s')", + joint->getNameInDocument()); + } + } + Gui::Command::commitCommand(); + + // Remove grounded tag if any. (as it is not done in jointObject.py onDelete) + std::string label = obj->Label.getValue(); + + if (label.size() >= 4 && label.substr(label.size() - 2) == " 🔒") { + label = label.substr(0, label.size() - 2); + obj->Label.setValue(label.c_str()); + } + + return true; +} bool ViewProviderAssembly::setEdit(int ModNum) { @@ -308,35 +366,53 @@ App::DocumentObject* ViewProviderAssembly::getObjectFromSubNames(std::vectorgetObject(subNames[0].c_str()); } - else { - objName = subNames[subNames.size() - 3]; + // From here subnames is at least 3 and can be more. There are several cases to consider : + // bodyOrLink.pad.face1 -> bodyOrLink should be the moving + // entity partOrLink.bodyOrLink.pad.face1 -> partOrLink should be the + // moving entity partOrLink.box.face1 -> partOrLink should + // be the moving entity partOrLink1...ParOrLinkn.bodyOrLink.pad.face1 -> partOrLink1 + // should be the moving entity assembly1.partOrLink1...ParOrLinkn.bodyOrLink.pad.face1 -> + // partOrLink1 should be the moving entity assembly1.boxOrLink1.face1 -> boxOrLink1 should be + // the moving entity + + for (auto objName : subNames) { App::DocumentObject* obj = appDoc->getObject(objName.c_str()); if (!obj) { - return nullptr; + continue; } - if (obj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) { + + if (obj->getTypeId().isDerivedFrom(AssemblyObject::getClassTypeId())) { + continue; + } + else if (obj->getTypeId().isDerivedFrom(App::Part::getClassTypeId()) + || obj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) { return obj; } else if (obj->getTypeId().isDerivedFrom(App::Link::getClassTypeId())) { - App::Link* link = dynamic_cast(obj); App::DocumentObject* linkedObj = link->getLinkedObject(true); + if (!linkedObj) { + continue; + } - if (linkedObj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) { + if (linkedObj->getTypeId().isDerivedFrom(App::Part::getClassTypeId()) + || linkedObj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) { return obj; } } - - // then its neither a body or a link to a body. - objName = subNames[subNames.size() - 2]; - return appDoc->getObject(objName.c_str()); } + + // then its neither a part or body or a link to a part or body. So it is something like + // assembly.box.face1 + objName = subNames[subNames.size() - 2]; + return appDoc->getObject(objName.c_str()); } void ViewProviderAssembly::initMove(Base::Vector3d& mousePosition) { + Gui::Command::openCommand(tr("Move part").toStdString().c_str()); partMoving = true; // prevent selection while moving @@ -376,6 +452,8 @@ void ViewProviderAssembly::endMove() auto* assemblyPart = static_cast(getObject()); assemblyPart->setObjMasses({}); + + Gui::Command::commitCommand(); } diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.h b/src/Mod/Assembly/Gui/ViewProviderAssembly.h index dd0378710b..32b6d8f848 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.h +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.h @@ -24,6 +24,8 @@ #ifndef ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H #define ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H +#include + #include #include @@ -40,6 +42,7 @@ namespace AssemblyGui class AssemblyGuiExport ViewProviderAssembly: public Gui::ViewProviderPart, public Gui::SelectionObserver { + Q_DECLARE_TR_FUNCTIONS(AssemblyGui::ViewProviderAssembly) PROPERTY_HEADER_WITH_OVERRIDE(AssemblyGui::ViewProviderAssembly); public: @@ -57,6 +60,14 @@ public: void unsetEdit(int ModNum) override; bool isInEditMode(); + /// Ask the view provider if it accepts object deletions while in edit + bool acceptDeletionsInEdit() override + { + return true; + } + + bool canDragObject(App::DocumentObject*) const override; + App::DocumentObject* getActivePart(); /// is called when the provider is in edit and the mouse is moved diff --git a/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp b/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp index abfa1db8c0..29302adfb0 100644 --- a/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp @@ -47,3 +47,9 @@ QIcon ViewProviderJointGroup::getIcon() const { return Gui::BitmapFactory().pixmap("Assembly_CreateJointFixed.svg"); } + +// Make the joint group impossible to delete. +bool ViewProviderJointGroup::onDelete(const std::vector& subNames) +{ + return false; +} diff --git a/src/Mod/Assembly/Gui/ViewProviderJointGroup.h b/src/Mod/Assembly/Gui/ViewProviderJointGroup.h index fb965e9c2a..76ca1b6230 100644 --- a/src/Mod/Assembly/Gui/ViewProviderJointGroup.h +++ b/src/Mod/Assembly/Gui/ViewProviderJointGroup.h @@ -43,6 +43,22 @@ public: /// deliver the icon shown in the tree view. Override from ViewProvider.h QIcon getIcon() const override; + // Prevent dragging of the joints and dropping things inside the joint group. + bool canDragObjects() const override + { + return false; + }; + bool canDropObjects() const override + { + return false; + }; + bool canDragAndDropObject(App::DocumentObject*) const override + { + return false; + }; + + bool onDelete(const std::vector& subNames) override; + // protected: /// get called by the container whenever a property has been changed // void onChanged(const App::Property* prop) override; diff --git a/src/Mod/Assembly/InitGui.py b/src/Mod/Assembly/InitGui.py index c45bae3a9d..6968a2a350 100644 --- a/src/Mod/Assembly/InitGui.py +++ b/src/Mod/Assembly/InitGui.py @@ -90,9 +90,7 @@ class AssemblyWorkbench(Workbench): "Assembly_CreateJointCylindrical", "Assembly_CreateJointSlider", "Assembly_CreateJointBall", - "Assembly_CreateJointPlanar", - "Assembly_CreateJointParallel", - "Assembly_CreateJointTangent", + "Assembly_CreateJointDistance", ] self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Assembly"), cmdlist) diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index b4eaffd432..eac706658e 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -47,14 +47,40 @@ JointTypes = [ QT_TRANSLATE_NOOP("AssemblyJoint", "Cylindrical"), QT_TRANSLATE_NOOP("AssemblyJoint", "Slider"), QT_TRANSLATE_NOOP("AssemblyJoint", "Ball"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Planar"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Parallel"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Tangent"), + QT_TRANSLATE_NOOP("AssemblyJoint", "Distance"), +] + +JointUsingDistance = [ + QT_TRANSLATE_NOOP("AssemblyJoint", "Distance"), +] + +JointUsingOffset = [ + QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"), + QT_TRANSLATE_NOOP("AssemblyJoint", "Revolute"), +] + +JointUsingRotation = [ + QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"), + QT_TRANSLATE_NOOP("AssemblyJoint", "Slider"), +] + +JointUsingReverse = [ + QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"), + QT_TRANSLATE_NOOP("AssemblyJoint", "Revolute"), + QT_TRANSLATE_NOOP("AssemblyJoint", "Cylindrical"), + QT_TRANSLATE_NOOP("AssemblyJoint", "Slider"), + QT_TRANSLATE_NOOP("AssemblyJoint", "Distance"), ] +def flipPlacement(plc, localXAxis): + flipRot = App.Rotation(localXAxis, 180) + plc.Rotation = plc.Rotation.multiply(flipRot) + return plc + + class Joint: - def __init__(self, joint, type_index): + def __init__(self, joint, type_index, assembly): self.Type = "Joint" joint.Proxy = self @@ -76,6 +102,13 @@ class Joint: QT_TRANSLATE_NOOP("App::Property", "The first object of the joint"), ) + joint.addProperty( + "App::PropertyLink", + "Part1", + "Joint Connector 1", + QT_TRANSLATE_NOOP("App::Property", "The first part of the joint"), + ) + joint.addProperty( "App::PropertyString", "Element1", @@ -108,6 +141,13 @@ class Joint: QT_TRANSLATE_NOOP("App::Property", "The second object of the joint"), ) + joint.addProperty( + "App::PropertyLink", + "Part2", + "Joint Connector 2", + QT_TRANSLATE_NOOP("App::Property", "The second part of the joint"), + ) + joint.addProperty( "App::PropertyString", "Element2", @@ -132,6 +172,46 @@ class Joint: ), ) + joint.addProperty( + "App::PropertyFloat", + "Distance", + "Joint", + QT_TRANSLATE_NOOP( + "App::Property", + "This is the distance of the joint. It is used only by the distance joint.", + ), + ) + + joint.addProperty( + "App::PropertyFloat", + "Rotation", + "Joint", + QT_TRANSLATE_NOOP( + "App::Property", + "This is the rotation of the joint.", + ), + ) + + joint.addProperty( + "App::PropertyVector", + "Offset", + "Joint", + QT_TRANSLATE_NOOP( + "App::Property", + "This is the offset vector of the joint.", + ), + ) + + joint.addProperty( + "App::PropertyBool", + "FirstPartConnected", + "Joint", + QT_TRANSLATE_NOOP( + "App::Property", + "This indicate if the first part was connected to ground at the time of joint creation.", + ), + ) + self.setJointConnectors(joint, []) def __getstate__(self): @@ -141,14 +221,22 @@ class Joint: if state: self.Type = state + def getAssembly(self, joint): + return joint.InList[0] + def setJointType(self, joint, jointType): joint.JointType = jointType joint.Label = jointType.replace(" ", "") - def onChanged(self, fp, prop): + def onChanged(self, joint, prop): """Do something when a property has changed""" # App.Console.PrintMessage("Change property: " + str(prop) + "\n") - pass + + if prop == "Rotation" or prop == "Offset" or prop == "Distance": + if hasattr( + joint, "Vertex1" + ): # during loading the onchanged may be triggered before full init. + self.getAssembly(joint).solve() def execute(self, fp): """Do something when doing a recomputation, this method is mandatory""" @@ -157,32 +245,51 @@ class Joint: def setJointConnectors(self, joint, current_selection): # current selection is a vector of strings like "Assembly.Assembly1.Assembly2.Body.Pad.Edge16" including both what selection return as obj_name and obj_sub + assembly = self.getAssembly(joint) if len(current_selection) >= 1: + joint.Part1 = None + joint.FirstPartConnected = assembly.isPartConnected(current_selection[0]["part"]) + joint.Object1 = current_selection[0]["object"] + joint.Part1 = current_selection[0]["part"] joint.Element1 = current_selection[0]["element_name"] joint.Vertex1 = current_selection[0]["vertex_name"] - joint.Placement1 = self.findPlacement(joint.Object1, joint.Element1, joint.Vertex1) + joint.Placement1 = self.findPlacement( + joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1 + ) else: joint.Object1 = None + joint.Part1 = None joint.Element1 = "" joint.Vertex1 = "" - joint.Placement1 = UtilsAssembly.activeAssembly().Placement + joint.Placement1 = App.Placement() if len(current_selection) >= 2: joint.Object2 = current_selection[1]["object"] + joint.Part2 = current_selection[1]["part"] joint.Element2 = current_selection[1]["element_name"] joint.Vertex2 = current_selection[1]["vertex_name"] - joint.Placement2 = self.findPlacement(joint.Object2, joint.Element2, joint.Vertex2) + joint.Placement2 = self.findPlacement( + joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True + ) + assembly.solve(True) + else: joint.Object2 = None + joint.Part2 = None joint.Element2 = "" joint.Vertex2 = "" - joint.Placement2 = UtilsAssembly.activeAssembly().Placement + joint.Placement2 = App.Placement() + assembly.undoSolve() def updateJCSPlacements(self, joint): - joint.Placement1 = self.findPlacement(joint.Object1, joint.Element1, joint.Vertex1) - joint.Placement2 = self.findPlacement(joint.Object2, joint.Element2, joint.Vertex2) + joint.Placement1 = self.findPlacement( + joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1 + ) + joint.Placement2 = self.findPlacement( + joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True + ) """ So here we want to find a placement that corresponds to a local coordinate system that would be placed at the selected vertex. @@ -194,12 +301,19 @@ class Joint: - if elt is a cylindrical face, vtx can also be the center of the arcs of the cylindrical face. """ - def findPlacement(self, obj, elt, vtx): + def findPlacement(self, joint, obj, part, elt, vtx, isSecond=False): + assembly = self.getAssembly(joint) plc = App.Placement() - if not obj or not elt or not vtx: + if not obj: return App.Placement() + if not elt or not vtx: + # case of whole parts such as PartDesign::Body or PartDesign::CordinateSystem. + plc = UtilsAssembly.getGlobalPlacement(obj, part) + plc = assembly.Placement.inverse() * plc + return plc + elt_type, elt_index = UtilsAssembly.extract_type_and_number(elt) vtx_type, vtx_index = UtilsAssembly.extract_type_and_number(vtx) @@ -211,11 +325,15 @@ class Joint: curve = edge.Curve # First we find the translation - if vtx_type == "Edge": - # In this case the edge is a circle/arc and the wanted vertex is its center. + if vtx_type == "Edge" or joint.JointType == "Distance": + # In this case the wanted vertex is the center. if curve.TypeId == "Part::GeomCircle": center_point = curve.Location plc.Base = (center_point.x, center_point.y, center_point.z) + elif curve.TypeId == "Part::GeomLine": + edge_points = UtilsAssembly.getPointsFromVertexes(edge.Vertexes) + line_middle = (edge_points[0] + edge_points[1]) * 0.5 + plc.Base = line_middle else: vertex = obj.Shape.Vertexes[vtx_index - 1] plc.Base = (vertex.X, vertex.Y, vertex.Z) @@ -229,31 +347,113 @@ class Joint: plane_origin = App.Vector(0, 0, 0) plane = Part.Plane(plane_origin, plane_normal) plc.Rotation = App.Rotation(plane.Rotation) - elif elt_type == "Face": face = obj.Shape.Faces[elt_index - 1] + surface = face.Surface # First we find the translation - if vtx_type == "Edge": + if vtx_type == "Face" or joint.JointType == "Distance": + if surface.TypeId == "Part::GeomCylinder" or surface.TypeId == "Part::GeomCone": + centerOfG = face.CenterOfGravity - surface.Center + centerPoint = surface.Center + centerOfG + centerPoint = centerPoint + App.Vector().projectToLine(centerOfG, surface.Axis) + plc.Base = centerPoint + elif surface.TypeId == "Part::GeomTorus" or surface.TypeId == "Part::GeomSphere": + plc.Base = surface.Center + else: + plc.Base = face.CenterOfGravity + elif vtx_type == "Edge": # In this case the edge is a circle/arc and the wanted vertex is its center. - circleOrArc = face.Edges[vtx_index - 1] - curve = circleOrArc.Curve + edge = face.Edges[vtx_index - 1] + curve = edge.Curve if curve.TypeId == "Part::GeomCircle": center_point = curve.Location plc.Base = (center_point.x, center_point.y, center_point.z) + elif ( + surface.TypeId == "Part::GeomCylinder" + and curve.TypeId == "Part::GeomBSplineCurve" + ): + # handle special case of 2 cylinder intersecting. + plc.Base = self.findCylindersIntersection(obj, surface, edge, elt_index) + else: vertex = obj.Shape.Vertexes[vtx_index - 1] plc.Base = (vertex.X, vertex.Y, vertex.Z) # Then we find the Rotation - surface = face.Surface if surface.TypeId == "Part::GeomPlane": plc.Rotation = App.Rotation(surface.Rotation) + else: + plc.Rotation = surface.Rotation + + # Now plc is the placement relative to the origin determined by the object placement. + # But it does not take into account Part placements. So if the solid is in a part and + # if the part has a placement then plc is wrong. + + # change plc to be relative to the object placement. + plc = obj.Placement.inverse() * plc + + # change plc to be relative to the origin of the document. + global_plc = UtilsAssembly.getGlobalPlacement(obj, part) + plc = global_plc * plc + + # change plc to be relative to the assembly. + plc = assembly.Placement.inverse() * plc + + # We apply rotation / reverse / offset it necessary, but only to the second JCS. + if isSecond: + if joint.Offset.Length != 0.0: + plc = self.applyOffsetToPlacement(plc, joint.Offset) + if joint.Rotation != 0.0: + plc = self.applyRotationToPlacement(plc, joint.Rotation) - # Now plc is the placement in the doc. But we need the placement relative to the solid origin. return plc + def applyOffsetToPlacement(self, plc, offset): + plc.Base = plc.Base + plc.Rotation.multVec(offset) + return plc + + def applyRotationToPlacement(self, plc, angle): + rot = plc.Rotation + zRotation = App.Rotation(App.Vector(0, 0, 1), angle) + rot = rot.multiply(zRotation) + plc.Rotation = rot + return plc + + def flipPart(self, joint): + if joint.FirstPartConnected: + plc = joint.Part2.Placement.inverse() * joint.Placement2 + localXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0)) + joint.Part2.Placement = flipPlacement(joint.Part2.Placement, localXAxis) + else: + plc = joint.Part1.Placement.inverse() * joint.Placement1 + localXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0)) + joint.Part1.Placement = flipPlacement(joint.Part1.Placement, localXAxis) + self.getAssembly(joint).solve() + + def findCylindersIntersection(self, obj, surface, edge, elt_index): + for j, facej in enumerate(obj.Shape.Faces): + surfacej = facej.Surface + if (elt_index - 1) == j or surfacej.TypeId != "Part::GeomCylinder": + continue + + for edgej in facej.Edges: + if ( + edgej.Curve.TypeId == "Part::GeomBSplineCurve" + and edgej.CenterOfGravity == edge.CenterOfGravity + and edgej.Length == edge.Length + ): + # we need intersection between the 2 cylinder axis. + line1 = Part.Line(surface.Center, surface.Center + surface.Axis) + line2 = Part.Line(surfacej.Center, surfacej.Center + surfacej.Axis) + + res = line1.intersect(line2, Part.Precision.confusion()) + + if res: + return App.Vector(res[0].X, res[0].Y, res[0].Z) + return surface.Center + class ViewProviderJoint: def __init__(self, vobj): @@ -394,25 +594,34 @@ class ViewProviderJoint: r = placement.Rotation.Q soTransform.rotation.setValue(r[0], r[1], r[2], r[3]) - def updateData(self, fp, prop): + def updateData(self, joint, prop): """If a property of the handled feature has changed we have the chance to handle this here""" - # fp is the handled feature, prop is the name of the property that has changed + # joint is the handled feature, prop is the name of the property that has changed if prop == "Placement1": - plc = fp.getPropertyByName("Placement1") - if fp.getPropertyByName("Object1"): + plc = joint.getPropertyByName("Placement1") + if joint.getPropertyByName("Object1"): self.switch_JCS1.whichChild = coin.SO_SWITCH_ALL self.set_JCS_placement(self.transform1, plc) else: self.switch_JCS1.whichChild = coin.SO_SWITCH_NONE if prop == "Placement2": - plc = fp.getPropertyByName("Placement2") - if fp.getPropertyByName("Object2"): + plc = joint.getPropertyByName("Placement2") + if joint.getPropertyByName("Object2"): self.switch_JCS2.whichChild = coin.SO_SWITCH_ALL + if self.areJCSReversed(joint): + plc = flipPlacement(plc, App.Vector(1, 0, 0)) self.set_JCS_placement(self.transform2, plc) else: self.switch_JCS2.whichChild = coin.SO_SWITCH_NONE + def areJCSReversed(self, joint): + zaxis1 = joint.Placement1.Rotation.multVec(App.Vector(0, 0, 1)) + zaxis2 = joint.Placement2.Rotation.multVec(App.Vector(0, 0, 1)) + + sameDir = zaxis1.dot(zaxis2) > 0 + return not sameDir + def showPreviewJCS(self, visible, placement=None): if visible: self.switch_JCS_preview.whichChild = coin.SO_SWITCH_ALL @@ -454,12 +663,8 @@ class ViewProviderJoint: return ":/icons/Assembly_CreateJointSlider.svg" elif self.app_obj.JointType == "Ball": return ":/icons/Assembly_CreateJointBall.svg" - elif self.app_obj.JointType == "Planar": - return ":/icons/Assembly_CreateJointPlanar.svg" - elif self.app_obj.JointType == "Parallel": - return ":/icons/Assembly_CreateJointParallel.svg" - elif self.app_obj.JointType == "Tangent": - return ":/icons/Assembly_CreateJointTangent.svg" + elif self.app_obj.JointType == "Distance": + return ":/icons/Assembly_CreateJointDistance.svg" return ":/icons/Assembly_CreateJoint.svg" @@ -475,6 +680,10 @@ class ViewProviderJoint: return None def doubleClicked(self, vobj): + assembly = vobj.Object.InList[0] + if UtilsAssembly.activeAssembly() != assembly: + Gui.ActiveDocument.ActiveView.setActiveObject("part", assembly) + panel = TaskAssemblyCreateJoint(0, vobj.Object) Gui.Control.showDialog(panel) @@ -555,6 +764,15 @@ class ViewProviderGroundedJoint: # App.Console.PrintMessage("Change property: " + str(prop) + "\n") pass + def onDelete(self, feature, subelements): # subelements is a tuple of strings + # Remove grounded tag. + if hasattr(feature.Object, "ObjectToGround"): + obj = feature.Object.ObjectToGround + if obj.Label.endswith(" 🔒"): + obj.Label = obj.Label[:-2] + + return True # If False is returned the object won't be deleted + def getIcon(self): return ":/icons/Assembly_ToggleGrounded.svg" @@ -570,8 +788,8 @@ class MakeJointSelGate: objs_names, element_name = UtilsAssembly.getObjsNamesAndElement(obj.Name, sub) - if self.assembly.Name not in objs_names or element_name == "": - # Only objects within the assembly. And not whole objects, only elements. + if self.assembly.Name not in objs_names: + # Only objects within the assembly. return False if Gui.Selection.isSelected(obj, sub, Gui.Selection.ResolveMode.NoResolve): @@ -585,19 +803,28 @@ class MakeJointSelGate: full_obj_name = ".".join(objs_names) full_element_name = full_obj_name + "." + element_name selected_object = UtilsAssembly.getObject(full_element_name) + part_containing_selected_object = UtilsAssembly.getContainingPart( + full_element_name, selected_object + ) for selection_dict in self.taskbox.current_selection: - if selection_dict["object"] == selected_object: + if selection_dict["part"] == part_containing_selected_object: # Can't join a solid to itself. So the user need to select 2 different parts. return False return True +activeTask = None + + class TaskAssemblyCreateJoint(QtCore.QObject): def __init__(self, jointTypeIndex, jointObj=None): super().__init__() + global activeTask + activeTask = self + self.assembly = UtilsAssembly.activeAssembly() self.view = Gui.activeDocument().activeView() self.doc = App.ActiveDocument @@ -612,6 +839,10 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.form.jointType.addItems(JointTypes) self.form.jointType.setCurrentIndex(jointTypeIndex) self.form.jointType.currentIndexChanged.connect(self.onJointTypeChanged) + self.form.distanceSpinbox.valueChanged.connect(self.onDistanceChanged) + self.form.offsetSpinbox.valueChanged.connect(self.onOffsetChanged) + self.form.rotationSpinbox.valueChanged.connect(self.onRotationChanged) + self.form.PushButtonReverse.clicked.connect(self.onReverseClicked) Gui.Selection.clearSelection() @@ -631,6 +862,11 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.createJointObject() + self.toggleDistanceVisibility() + self.toggleOffsetVisibility() + self.toggleRotationVisibility() + self.toggleReverseVisibility() + Gui.Selection.addSelectionGate( MakeJointSelGate(self, self.assembly), Gui.Selection.ResolveMode.NoResolve ) @@ -662,6 +898,10 @@ class TaskAssemblyCreateJoint(QtCore.QObject): return True def deactivate(self): + global activeTask + activeTask = None + self.assembly.clearUndo() + self.assembly.ViewObject.EnableMovement = True Gui.Selection.removeSelectionGate() Gui.Selection.removeObserver(self) @@ -678,11 +918,57 @@ class TaskAssemblyCreateJoint(QtCore.QObject): joint_group = UtilsAssembly.getJointGroup(self.assembly) self.joint = joint_group.newObject("App::FeaturePython", self.jointName) - Joint(self.joint, type_index) + Joint(self.joint, type_index, self.assembly) ViewProviderJoint(self.joint.ViewObject) def onJointTypeChanged(self, index): self.joint.Proxy.setJointType(self.joint, self.form.jointType.currentText()) + self.toggleDistanceVisibility() + self.toggleOffsetVisibility() + self.toggleRotationVisibility() + self.toggleReverseVisibility() + + def onDistanceChanged(self, quantity): + self.joint.Distance = self.form.distanceSpinbox.property("rawValue") + + def onOffsetChanged(self, quantity): + self.joint.Offset = App.Vector(0, 0, self.form.offsetSpinbox.property("rawValue")) + + def onRotationChanged(self, quantity): + self.joint.Rotation = self.form.rotationSpinbox.property("rawValue") + + def onReverseClicked(self): + self.joint.Proxy.flipPart(self.joint) + + def toggleDistanceVisibility(self): + if self.form.jointType.currentText() in JointUsingDistance: + self.form.distanceLabel.show() + self.form.distanceSpinbox.show() + else: + self.form.distanceLabel.hide() + self.form.distanceSpinbox.hide() + + def toggleOffsetVisibility(self): + if self.form.jointType.currentText() in JointUsingOffset: + self.form.offsetLabel.show() + self.form.offsetSpinbox.show() + else: + self.form.offsetLabel.hide() + self.form.offsetSpinbox.hide() + + def toggleRotationVisibility(self): + if self.form.jointType.currentText() in JointUsingRotation: + self.form.rotationLabel.show() + self.form.rotationSpinbox.show() + else: + self.form.rotationLabel.hide() + self.form.rotationSpinbox.hide() + + def toggleReverseVisibility(self): + if self.form.jointType.currentText() in JointUsingReverse: + self.form.PushButtonReverse.show() + else: + self.form.PushButtonReverse.hide() def updateTaskboxFromJoint(self): self.current_selection = [] @@ -690,12 +976,14 @@ class TaskAssemblyCreateJoint(QtCore.QObject): selection_dict1 = { "object": self.joint.Object1, + "part": self.joint.Part1, "element_name": self.joint.Element1, "vertex_name": self.joint.Vertex1, } selection_dict2 = { "object": self.joint.Object2, + "part": self.joint.Part2, "element_name": self.joint.Element2, "vertex_name": self.joint.Vertex2, } @@ -712,9 +1000,17 @@ class TaskAssemblyCreateJoint(QtCore.QObject): elName = self.getObjSubNameFromObj(self.joint.Object2, self.joint.Element2) Gui.Selection.addSelection(self.doc.Name, self.joint.Object2.Name, elName) + self.form.distanceSpinbox.setProperty("rawValue", self.joint.Distance) + self.form.offsetSpinbox.setProperty("rawValue", self.joint.Offset.z) + self.form.rotationSpinbox.setProperty("rawValue", self.joint.Rotation) + + self.form.jointType.setCurrentIndex(JointTypes.index(self.joint.JointType)) self.updateJointList() def getObjSubNameFromObj(self, obj, elName): + if obj is None: + return elName + if obj.TypeId == "PartDesign::Body": return obj.Tip.Name + "." + elName elif obj.TypeId == "App::Link": @@ -738,14 +1034,16 @@ class TaskAssemblyCreateJoint(QtCore.QObject): simplified_names = [] for sel in self.current_selection: # TODO: ideally we probably want to hide the feature name in case of PartDesign bodies. ie body.face12 and not body.pad2.face12 - sname = sel["object"].Label + "." + sel["element_name"] + sname = sel["object"].Label + if sel["element_name"] != "": + sname = sname + "." + sel["element_name"] simplified_names.append(sname) self.form.featureList.addItems(simplified_names) def moveMouse(self, info): if len(self.current_selection) >= 2 or ( len(self.current_selection) == 1 - and self.current_selection[0]["object"] == self.preselection_dict["object"] + and self.current_selection[0]["part"] == self.preselection_dict["part"] ): self.joint.ViewObject.Proxy.showPreviewJCS(False) return @@ -767,14 +1065,22 @@ class TaskAssemblyCreateJoint(QtCore.QObject): newPos = App.Vector(cursor_info["x"], cursor_info["y"], cursor_info["z"]) self.preselection_dict["mouse_pos"] = newPos - self.preselection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex( - self.preselection_dict - ) + if self.preselection_dict["element_name"] == "": + self.preselection_dict["vertex_name"] = "" + else: + self.preselection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex( + self.preselection_dict + ) + + isSecond = len(self.current_selection) == 1 placement = self.joint.Proxy.findPlacement( + self.joint, self.preselection_dict["object"], + self.preselection_dict["part"], self.preselection_dict["element_name"], self.preselection_dict["vertex_name"], + isSecond, ) self.joint.ViewObject.Proxy.showPreviewJCS(True, placement) self.previewJCSVisible = True @@ -793,15 +1099,22 @@ class TaskAssemblyCreateJoint(QtCore.QObject): full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name) selected_object = UtilsAssembly.getObject(full_element_name) element_name = UtilsAssembly.getElementName(full_element_name) + part_containing_selected_object = UtilsAssembly.getContainingPart( + full_element_name, selected_object + ) selection_dict = { "object": selected_object, + "part": part_containing_selected_object, "element_name": element_name, "full_element_name": full_element_name, "full_obj_name": full_obj_name, "mouse_pos": App.Vector(mousePos[0], mousePos[1], mousePos[2]), } - selection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(selection_dict) + if element_name == "": + selection_dict["vertex_name"] = "" + else: + selection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(selection_dict) self.current_selection.append(selection_dict) self.updateJoint() @@ -810,11 +1123,14 @@ class TaskAssemblyCreateJoint(QtCore.QObject): full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name) selected_object = UtilsAssembly.getObject(full_element_name) element_name = UtilsAssembly.getElementName(full_element_name) + part_containing_selected_object = UtilsAssembly.getContainingPart( + full_element_name, selected_object + ) # Find and remove the corresponding dictionary from the combined list selection_dict_to_remove = None for selection_dict in self.current_selection: - if selection_dict["object"] == selected_object: + if selection_dict["part"] == part_containing_selected_object: selection_dict_to_remove = selection_dict break @@ -832,9 +1148,13 @@ class TaskAssemblyCreateJoint(QtCore.QObject): full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name) selected_object = UtilsAssembly.getObject(full_element_name) element_name = UtilsAssembly.getElementName(full_element_name) + part_containing_selected_object = UtilsAssembly.getContainingPart( + full_element_name, selected_object + ) self.preselection_dict = { "object": selected_object, + "part": part_containing_selected_object, "sub_name": sub_name, "element_name": element_name, "full_element_name": full_element_name, diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index 3feed5390b..c7e5d05edd 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -22,6 +22,7 @@ # ***************************************************************************/ import FreeCAD as App +import Part if App.GuiUp: import FreeCADGui as Gui @@ -36,17 +37,35 @@ __url__ = "https://www.freecad.org" def activeAssembly(): doc = Gui.ActiveDocument + if doc is None or doc.ActiveView is None: + return None + + active_assembly = doc.ActiveView.getActiveObject("part") + + if active_assembly is not None and active_assembly.Type == "Assembly": + return active_assembly + + return None + + +def activePart(): + doc = Gui.ActiveDocument + if doc is None or doc.ActiveView is None: return None active_part = doc.ActiveView.getActiveObject("part") - if active_part is not None and active_part.Type == "Assembly": + if active_part is not None and active_part.Type != "Assembly": return active_part return None +def isAssemblyCommandActive(): + return activeAssembly() is not None and not Gui.Control.activeDialog() + + def isDocTemporary(doc): # Guard against older versions of FreeCad which don't have the Temporary attribute try: @@ -56,19 +75,39 @@ def isDocTemporary(doc): return temp +def assembly_has_at_least_n_parts(n): + assembly = activeAssembly() + i = 0 + if not assembly: + return False + for obj in assembly.OutList: + # note : groundedJoints comes in the outlist so we filter those out. + if hasattr(obj, "Placement") and not hasattr(obj, "ObjectToGround"): + i = i + 1 + if i == n: + return True + return False + + def getObject(full_name): - # full_name is "Assembly.Assembly1.Assembly2.Assembly3.Box.Edge16" - # or "Assembly.Assembly1.Assembly2.Assembly3.Body.pad.Edge16" - # We want either Body or Box. - parts = full_name.split(".") + # full_name is "Assembly.Assembly1.LinkOrPart1.Box.Edge16" + # or "Assembly.Assembly1.LinkOrPart1.Body.pad.Edge16" + # or "Assembly.Assembly1.LinkOrPart1.Body.Local_CS.X" + # We want either Body or Box or Local_CS. + names = full_name.split(".") doc = App.ActiveDocument - if len(parts) < 3: + if len(names) < 3: App.Console.PrintError( "getObject() in UtilsAssembly.py the object name is too short, at minimum it should be something like 'Assembly.Box.edge16'. It shouldn't be shorter" ) return None - obj = doc.getObject(parts[-3]) # So either 'Body', or 'Assembly' + obj = doc.getObject(names[-2]) + + if obj and obj.TypeId == "PartDesign::CoordinateSystem": + return doc.getObject(names[-2]) + + obj = doc.getObject(names[-3]) # So either 'Body', or 'Assembly' if not obj: return None @@ -80,8 +119,116 @@ def getObject(full_name): if linked_obj.TypeId == "PartDesign::Body": return obj - else: # primitive, fastener, gear ... or link to primitive, fastener, gear... - return doc.getObject(parts[-2]) + # primitive, fastener, gear ... or link to primitive, fastener, gear... + return doc.getObject(names[-2]) + + +def getContainingPart(full_name, selected_object): + # full_name is "Assembly.Assembly1.LinkOrPart1.Box.Edge16" + # or "Assembly.Assembly1.LinkOrPart1.Body.pad.Edge16" + # We want either Body or Box. + names = full_name.split(".") + doc = App.ActiveDocument + if len(names) < 3: + App.Console.PrintError( + "getContainingPart() in UtilsAssembly.py the object name is too short, at minimum it should be something like 'Assembly.Box.edge16'. It shouldn't be shorter" + ) + return None + + for objName in names: + obj = doc.getObject(objName) + + if not obj: + continue + + if ( + obj.TypeId == "PartDesign::Body" + and selected_object.TypeId == "PartDesign::CoordinateSystem" + ): + if obj.hasObject(selected_object, True): + return obj + + # Note here we may want to specify a specific behavior for Assembly::AssemblyObject. + if obj.TypeId == "App::Part": + if obj.hasObject(selected_object, True): + return obj + + elif obj.TypeId == "App::Link": + linked_obj = obj.getLinkedObject() + if linked_obj.TypeId == "App::Part": + if linked_obj.hasObject(selected_object, True): + return obj + + # no container found so we return the object itself. + return selected_object + + +# The container is used to support cases where the same object appears at several places +# which happens when you have a link to a part. +def getGlobalPlacement(targetObj, container=None): + inContainerBranch = container is None + for part in App.activeDocument().RootObjects: + foundPlacement = getTargetPlacementRelativeTo(targetObj, part, container, inContainerBranch) + if foundPlacement is not None: + return foundPlacement + + return App.Placement() + + +def getTargetPlacementRelativeTo( + targetObj, part, container, inContainerBranch, ignorePlacement=False +): + inContainerBranch = inContainerBranch or (not ignorePlacement and part == container) + + if targetObj == part and inContainerBranch and not ignorePlacement: + return targetObj.Placement + + if part.TypeId == "App::DocumentObjectGroup": + for obj in part.OutList: + foundPlacement = getTargetPlacementRelativeTo( + targetObj, obj, container, inContainerBranch, ignorePlacement + ) + if foundPlacement is not None: + return foundPlacement + + elif part.TypeId == "App::Part" or part.TypeId == "Assembly::AssemblyObject": + for obj in part.OutList: + foundPlacement = getTargetPlacementRelativeTo( + targetObj, obj, container, inContainerBranch + ) + if foundPlacement is None: + continue + + # If we were called from a link then we need to ignore this placement as we use the link placement instead. + if not ignorePlacement: + foundPlacement = part.Placement * foundPlacement + + return foundPlacement + + elif part.TypeId == "App::Link": + linked_obj = part.getLinkedObject() + + if linked_obj.TypeId == "App::Part" or linked_obj.TypeId == "Assembly::AssemblyObject": + for obj in linked_obj.OutList: + foundPlacement = getTargetPlacementRelativeTo( + targetObj, obj, container, inContainerBranch + ) + if foundPlacement is None: + continue + + foundPlacement = part.Placement * foundPlacement + return foundPlacement + + foundPlacement = getTargetPlacementRelativeTo( + targetObj, linked_obj, container, inContainerBranch, True + ) + + if foundPlacement is not None and not ignorePlacement: + foundPlacement = part.Placement * foundPlacement + + return foundPlacement + + return None def getElementName(full_name): @@ -93,6 +240,10 @@ def getElementName(full_name): # At minimum "Assembly.Box.edge16". It shouldn't be shorter return "" + # case of PartDesign::CoordinateSystem + if parts[-1] == "X" or parts[-1] == "Y" or parts[-1] == "Z": + return "" + return parts[-1] @@ -147,14 +298,25 @@ def extract_type_and_number(element_name): def findElementClosestVertex(selection_dict): + obj = selection_dict["object"] + + mousePos = selection_dict["mouse_pos"] + + # We need mousePos to be relative to the part containing obj global placement + if selection_dict["object"] != selection_dict["part"]: + plc = App.Placement() + plc.Base = mousePos + global_plc = getGlobalPlacement(selection_dict["part"]) + plc = global_plc.inverse() * plc + mousePos = plc.Base + elt_type, elt_index = extract_type_and_number(selection_dict["element_name"]) if elt_type == "Vertex": return selection_dict["element_name"] elif elt_type == "Edge": - edge = selection_dict["object"].Shape.Edges[elt_index - 1] - + edge = obj.Shape.Edges[elt_index - 1] curve = edge.Curve if curve.TypeId == "Part::GeomCircle": # For centers, as they are not shape vertexes, we return the element name. @@ -162,17 +324,28 @@ def findElementClosestVertex(selection_dict): return selection_dict["element_name"] edge_points = getPointsFromVertexes(edge.Vertexes) - closest_vertex_index, _ = findClosestPointToMousePos( - edge_points, selection_dict["mouse_pos"] - ) - vertex_name = findVertexNameInObject( - edge.Vertexes[closest_vertex_index], selection_dict["object"] - ) + + if curve.TypeId == "Part::GeomLine": + # For lines we allow users to select the middle of lines as well. + line_middle = (edge_points[0] + edge_points[1]) * 0.5 + edge_points.append(line_middle) + + closest_vertex_index, _ = findClosestPointToMousePos(edge_points, mousePos) + + if curve.TypeId == "Part::GeomLine" and closest_vertex_index == 2: + # If line center is closest then we have no vertex name to set so we put element name + return selection_dict["element_name"] + + vertex_name = findVertexNameInObject(edge.Vertexes[closest_vertex_index], obj) return vertex_name elif elt_type == "Face": - face = selection_dict["object"].Shape.Faces[elt_index - 1] + face = obj.Shape.Faces[elt_index - 1] + surface = face.Surface + _type = surface.TypeId + if _type == "Part::GeomSphere" or _type == "Part::GeomTorus": + return selection_dict["element_name"] # Handle the circle/arc edges for their centers center_points = [] @@ -181,19 +354,46 @@ def findElementClosestVertex(selection_dict): for i, edge in enumerate(edges): curve = edge.Curve - if curve.TypeId == "Part::GeomCircle": + if curve.TypeId == "Part::GeomCircle" or curve.TypeId == "Part::GeomEllipse": center_points.append(curve.Location) center_points_edge_indexes.append(i) + elif _type == "Part::GeomCylinder" and curve.TypeId == "Part::GeomBSplineCurve": + # handle special case of 2 cylinder intersecting. + for j, facej in enumerate(obj.Shape.Faces): + surfacej = facej.Surface + if (elt_index - 1) != j and surfacej.TypeId == "Part::GeomCylinder": + for edgej in facej.Edges: + if edgej.Curve.TypeId == "Part::GeomBSplineCurve": + if ( + edgej.CenterOfGravity == edge.CenterOfGravity + and edgej.Length == edge.Length + ): + center_points.append(edgej.CenterOfGravity) + center_points_edge_indexes.append(i) + if len(center_points) > 0: closest_center_index, closest_center_distance = findClosestPointToMousePos( - center_points, selection_dict["mouse_pos"] + center_points, mousePos ) - # Hendle the face vertexes - face_points = getPointsFromVertexes(face.Vertexes) + # Handle the face vertexes + face_points = [] + + if _type != "Part::GeomCylinder" and _type != "Part::GeomCone": + face_points = getPointsFromVertexes(face.Vertexes) + + # We also allow users to select the center of gravity. + if _type == "Part::GeomCylinder" or _type == "Part::GeomCone": + centerOfG = face.CenterOfGravity - surface.Center + centerPoint = surface.Center + centerOfG + centerPoint = centerPoint + App.Vector().projectToLine(centerOfG, surface.Axis) + face_points.append(centerPoint) + else: + face_points.append(face.CenterOfGravity) + closest_vertex_index, closest_vertex_distance = findClosestPointToMousePos( - face_points, selection_dict["mouse_pos"] + face_points, mousePos ) if len(center_points) > 0: @@ -202,9 +402,14 @@ def findElementClosestVertex(selection_dict): index = center_points_edge_indexes[closest_center_index] + 1 return "Edge" + str(index) - vertex_name = findVertexNameInObject( - face.Vertexes[closest_vertex_index], selection_dict["object"] - ) + if _type == "Part::GeomCylinder" or _type == "Part::GeomCone": + return selection_dict["element_name"] + + if closest_vertex_index == len(face.Vertexes): + # If center of gravity then we have no vertex name to set so we put element name + return selection_dict["element_name"] + + vertex_name = findVertexNameInObject(face.Vertexes[closest_vertex_index], obj) return vertex_name @@ -247,8 +452,51 @@ def color_from_unsigned(c): def getJointGroup(assembly): - joint_group = assembly.getObject("Joints") + joint_group = None + + for obj in assembly.OutList: + if obj.TypeId == "Assembly::JointGroup": + joint_group = obj + break if not joint_group: joint_group = assembly.newObject("Assembly::JointGroup", "Joints") + return joint_group + + +def isAssemblyGrounded(): + assembly = activeAssembly() + if not assembly: + return False + + jointGroup = getJointGroup(assembly) + + for joint in jointGroup.Group: + if hasattr(joint, "ObjectToGround"): + return True + + return False + + +def removeObjAndChilds(obj): + removeObjsAndChilds([obj]) + + +def removeObjsAndChilds(objs): + def addsubobjs(obj, toremoveset): + if obj.TypeId == "App::Origin": # Origins are already handled + return + + toremoveset.add(obj) + if obj.TypeId != "App::Link": + for subobj in obj.OutList: + addsubobjs(subobj, toremoveset) + + toremove = set() + for obj in objs: + addsubobjs(obj, toremove) + + for obj in toremove: + if obj: + obj.Document.removeObject(obj.Name)