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.",
)
+ "
"
- + QT_TRANSLATE_NOOP(
- "Assembly_CreateJointParallel",
- "Create a Parallel Joint: Aligns two features to be parallel, constraining relative movement to parallel translations.",
- )
- + "
"
- + QT_TRANSLATE_NOOP(
- "Assembly_CreateJointTangent",
- "Create a Tangent Joint: Forces two features to be tangent, restricting movement to smooth transitions along their contact surface.",
- )
- + "
"
+ 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.svgicons/preferences-assembly.svgicons/Assembly_ToggleGrounded.svgicons/Assembly_CreateJointBall.svgicons/Assembly_CreateJointCylindrical.svgicons/Assembly_CreateJointFixed.svg
- icons/Assembly_CreateJointParallel.svgicons/Assembly_CreateJointPlanar.svgicons/Assembly_CreateJointRevolute.svgicons/Assembly_CreateJointSlider.svgicons/Assembly_CreateJointTangent.svgicons/Assembly_ExportASMT.svg
+ icons/Assembly_SolveAssembly.svgpanels/TaskAssemblyCreateJoint.uipanels/TaskAssemblyInsertLink.uipreferences/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 @@
+
+
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)