Assembly: Change Joint References to remove cyclic dependency. (#25513)

* Assembly: Change Joint References to remove cyclic dependency.

* Update JointObject.py

* Update TestCore.py

* Update JointObject.py

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

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

* Update AssemblyUtils.cpp

* Update UtilsAssembly.py

* Update JointObject.py

* small fix for link groups

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
PaddleStroke
2026-01-26 09:44:59 +01:00
committed by GitHub
parent 11d22e326c
commit b4025abab6
12 changed files with 363 additions and 312 deletions

View File

@@ -532,8 +532,6 @@ void AssemblyLink::synchronizeJoints()
assemblyLinkJoints = getJoints();
AssemblyObject::recomputeJointPlacements(assemblyLinkJoints);
for (auto* joint : assemblyLinkJoints) {
joint->purgeTouched();
}
@@ -546,87 +544,55 @@ void AssemblyLink::handleJointReference(
const char* refName
)
{
AssemblyObject* assembly = getLinkedAssembly();
auto prop1 = dynamic_cast<App::PropertyXLinkSubHidden*>(joint->getPropertyByName(refName));
auto prop2 = dynamic_cast<App::PropertyXLinkSubHidden*>(lJoint->getPropertyByName(refName));
auto prop1 = dynamic_cast<App::PropertyXLinkSub*>(joint->getPropertyByName(refName));
auto prop2 = dynamic_cast<App::PropertyXLinkSub*>(lJoint->getPropertyByName(refName));
if (!prop1 || !prop2) {
return;
}
App::DocumentObject* obj1 = nullptr;
App::DocumentObject* obj2 = prop2->getValue();
std::vector<std::string> subs1 = prop1->getSubValues();
std::vector<std::string> subs2 = prop2->getSubValues();
if (subs1.empty()) {
// 1. Get the external component prop1 is [ExternalPart, "Sub"]
App::DocumentObject* externalComponent = prop1->getValue();
if (!externalComponent) {
return;
}
// Example :
// Obj1 = docA-Asm1 Subs1 = ["part1.body.pad.face0", "part1.body.pad.vertex1"]
// Obj1 = docA-Part Subs1 = ["Asm1.part1.body.pad.face0", "Asm1.part1.body.pad.vertex1"] // some
// user may put the assembly inside a part... should become : Obj2 = docB-Asm2 Subs2 =
// ["Asm1Link.part1.linkTobody.pad.face0", "Asm1Link.part1.linkTobody.pad.vertex1"] Obj2 =
// docB-Part Sub2 = ["Asm2.Asm1Link.part1.linkTobody.pad.face0",
// "Asm2.Asm1Link.part1.linkTobody.pad.vertex1"]
std::string asmLink = getNameInDocument();
for (auto& sub : subs1) {
// First let's remove 'Asm1' name and everything before if any.
sub = removeUpToName(sub, assembly->getNameInDocument());
// Then we add the assembly link name.
sub = asmLink + "." + sub;
// Then the question is, is there more to prepend? Because the parent assembly may have some
// parents So we check assemblyLink parents and prepend necessary parents.
bool first = true;
std::vector<App::DocumentObject*> inList = getInList();
int limit = 0;
while (!inList.empty() && limit < 20) {
++limit;
bool found = false;
for (auto* obj : inList) {
if (obj->isDerivedFrom<App::Part>()) {
found = true;
if (first) {
first = false;
}
else {
std::string obj1Name = obj1->getNameInDocument();
sub = obj1Name + "." + sub;
}
obj1 = obj;
break;
}
}
if (found) {
inList = obj1->getInList();
}
else {
inList = {};
}
}
// Lastly we need to replace the object name by its link name.
auto* obj = getObjFromRef(prop1);
auto* link = objLinkMap[obj];
if (!obj || !link) {
return;
}
std::string objName = obj->getNameInDocument();
std::string linkName = link->getNameInDocument();
sub = replaceLastOccurrence(sub, objName, linkName);
// 2. Map to local link
auto it = objLinkMap.find(externalComponent);
if (it == objLinkMap.end()) {
Base::Console().warning(
"AssemblyLink: Could not map external component %s to a local link for joint %s\n",
externalComponent->getNameInDocument(),
joint->getNameInDocument()
);
return;
}
// Now obj1 and the subs1 are what should be in obj2 and subs2 if the joint did not changed
if (obj1 != obj2) {
prop2->setValue(obj1);
App::DocumentObject* localLink = it->second;
// 3. Set the new reference
// The local joint now points to the local link [LocalLink, "Sub"]
if (prop2->getValue() != localLink) {
prop2->setValue(localLink);
}
// 4. Sync sub-elements
// The sub-elements (e.g. "Body.Face1") are relative to the component.
// Since the LocalLink points to the ExternalPart, the relative path is identical.
std::vector<std::string> subs1 = prop1->getSubValues();
std::vector<std::string> subs2 = prop2->getSubValues();
bool changed = false;
for (size_t i = 0; i < subs1.size(); ++i) {
if (i >= subs2.size() || subs1[i] != subs2[i]) {
changed = true;
break;
if (subs1.size() != subs2.size()) {
changed = true;
}
else {
for (size_t i = 0; i < subs1.size(); ++i) {
if (subs1[i] != subs2[i]) {
changed = true;
break;
}
}
}
if (changed) {
prop2->setSubValues(std::move(subs1));
}

View File

@@ -132,7 +132,7 @@ App::DocumentObjectExecReturn* AssemblyObject::execute()
"User parameter:BaseApp/Preferences/Mod/Assembly"
);
if (hGrp->GetBool("SolveOnRecompute", true)) {
solve();
solve(false, false); // No need to update jcs since recompute updated them.
}
return ret;
}
@@ -488,6 +488,7 @@ bool AssemblyObject::validateNewPlacements()
void AssemblyObject::postDrag()
{
mbdAssembly->runPostDrag(); // Do this after last drag
purgeTouched();
}
void AssemblyObject::savePlacementsForUndo()
@@ -607,48 +608,27 @@ void AssemblyObject::redrawJointPlacement(App::DocumentObject* joint)
return;
}
// Notify the joint object that the transform of the coin object changed.
auto* pPlc = dynamic_cast<App::PropertyPlacement*>(joint->getPropertyByName("Placement1"));
if (pPlc) {
pPlc->setValue(pPlc->getValue());
}
pPlc = dynamic_cast<App::PropertyPlacement*>(joint->getPropertyByName("Placement2"));
if (pPlc) {
pPlc->setValue(pPlc->getValue());
}
joint->purgeTouched();
}
void AssemblyObject::recomputeJointPlacements(std::vector<App::DocumentObject*> joints)
{
// The Placement1 and Placement2 of each joint needs to be updated as the parts moved.
Base::PyGILStateLocker lock;
for (auto* joint : joints) {
if (!joint) {
continue;
}
App::PropertyPythonObject* proxy = joint
? dynamic_cast<App::PropertyPythonObject*>(joint->getPropertyByName("Proxy"))
: nullptr;
App::PropertyPythonObject* proxy = joint
? dynamic_cast<App::PropertyPythonObject*>(joint->getPropertyByName("Proxy"))
: nullptr;
if (!proxy) {
continue;
}
if (!proxy) {
return;
}
Py::Object jointPy = proxy->getValue();
Py::Object jointPy = proxy->getValue();
if (!jointPy.hasAttr("updateJCSPlacements")) {
continue;
}
if (!jointPy.hasAttr("redrawJointPlacements")) {
return;
}
Py::Object attr = jointPy.getAttr("updateJCSPlacements");
if (attr.ptr() && attr.isCallable()) {
Py::Tuple args(1);
args.setItem(0, Py::asObject(joint->getPyObject()));
Py::Callable(attr).apply(args);
joint->purgeTouched();
}
Py::Object attr = jointPy.getAttr("redrawJointPlacements");
if (attr.ptr() && attr.isCallable()) {
Py::Tuple args(1);
args.setItem(0, Py::asObject(joint->getPyObject()));
Py::Callable(attr).apply(args);
}
}
@@ -687,8 +667,8 @@ App::DocumentObject* AssemblyObject::getJointOfPartConnectingToGround(
continue;
}
App::DocumentObject* part1 = getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(this, joint, "Reference2");
App::DocumentObject* part1 = getMovingPartFromRef(joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(joint, "Reference2");
if (!part1 || !part2) {
continue;
}
@@ -764,8 +744,8 @@ std::vector<App::DocumentObject*> AssemblyObject::getJoints(bool updateJCS, bool
continue;
}
auto* part1 = getMovingPartFromRef(this, joint, "Reference1");
auto* part2 = getMovingPartFromRef(this, joint, "Reference2");
auto* part1 = getMovingPartFromRef(joint, "Reference1");
auto* part2 = getMovingPartFromRef(joint, "Reference2");
if (!part1 || !part2 || part1->getFullName() == part2->getFullName()) {
// Remove incomplete joints. Left-over when the user deletes a part.
// Remove incoherent joints (self-pointing joints)
@@ -791,11 +771,6 @@ std::vector<App::DocumentObject*> AssemblyObject::getJoints(bool updateJCS, bool
}
}
// Make sure the joints are up to date.
if (updateJCS) {
recomputeJointPlacements(joints);
}
return joints;
}
@@ -834,8 +809,8 @@ std::vector<App::DocumentObject*> AssemblyObject::getJointsOfObj(App::DocumentOb
std::vector<App::DocumentObject*> jointsOf;
for (auto joint : joints) {
App::DocumentObject* obj1 = getObjFromRef(joint, "Reference1");
App::DocumentObject* obj2 = getObjFromRef(joint, "Reference2");
App::DocumentObject* obj1 = getObjFromJointRef(joint, "Reference1");
App::DocumentObject* obj2 = getObjFromJointRef(joint, "Reference2");
if (obj == obj1 || obj == obj2) {
jointsOf.push_back(joint);
}
@@ -854,8 +829,8 @@ std::vector<App::DocumentObject*> AssemblyObject::getJointsOfPart(App::DocumentO
std::vector<App::DocumentObject*> jointsOf;
for (auto joint : joints) {
App::DocumentObject* part1 = getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(this, joint, "Reference2");
App::DocumentObject* part1 = getMovingPartFromRef(joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(joint, "Reference2");
if (part == part1 || part == part2) {
jointsOf.push_back(joint);
}
@@ -961,7 +936,7 @@ bool AssemblyObject::isJointConnectingPartToGround(App::DocumentObject* joint, c
return false;
}
App::DocumentObject* part = getMovingPartFromRef(this, joint, propname);
App::DocumentObject* part = getMovingPartFromRef(joint, propname);
if (!part) {
return false;
}
@@ -1055,8 +1030,8 @@ void AssemblyObject::removeUnconnectedJoints(
joints.begin(),
joints.end(),
[&](App::DocumentObject* joint) {
App::DocumentObject* obj1 = getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* obj2 = getMovingPartFromRef(this, joint, "Reference2");
App::DocumentObject* obj1 = getMovingPartFromRef(joint, "Reference1");
App::DocumentObject* obj2 = getMovingPartFromRef(joint, "Reference2");
return (
!isObjInSetOfObjRefs(obj1, connectedParts)
|| !isObjInSetOfObjRefs(obj2, connectedParts)
@@ -1100,8 +1075,8 @@ std::vector<ObjRef> AssemblyObject::getConnectedParts(
continue;
}
App::DocumentObject* obj1 = getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* obj2 = getMovingPartFromRef(this, joint, "Reference2");
App::DocumentObject* obj1 = getMovingPartFromRef(joint, "Reference1");
App::DocumentObject* obj2 = getMovingPartFromRef(joint, "Reference2");
if (!obj1 || !obj2) {
continue;
@@ -1674,12 +1649,12 @@ std::string AssemblyObject::handleOneSideOfJoint(
const char* propPlcName
)
{
App::DocumentObject* part = getMovingPartFromRef(this, joint, propRefName);
App::DocumentObject* obj = getObjFromRef(joint, propRefName);
App::DocumentObject* part = getMovingPartFromRef(joint, propRefName);
App::DocumentObject* obj = getObjFromJointRef(joint, propRefName);
if (!part || !obj) {
Base::Console()
.warning("The property %s of Joint %s is bad.", propRefName, joint->getFullName());
.warning("The property %s of Joint %s is bad.\n", propRefName, joint->getFullName());
return "";
}
@@ -1735,15 +1710,15 @@ void AssemblyObject::getRackPinionMarkers(
swapJCS(joint); // make sure that rack is first.
}
App::DocumentObject* part1 = getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* obj1 = getObjFromRef(joint, "Reference1");
App::DocumentObject* part1 = getMovingPartFromRef(joint, "Reference1");
App::DocumentObject* obj1 = getObjFromJointRef(joint, "Reference1");
Base::Placement plc1 = getPlacementFromProp(joint, "Placement1");
App::DocumentObject* obj2 = getObjFromRef(joint, "Reference2");
App::DocumentObject* obj2 = getObjFromJointRef(joint, "Reference2");
Base::Placement plc2 = getPlacementFromProp(joint, "Placement2");
if (!part1 || !obj1) {
Base::Console().warning("Reference1 of Joint %s is bad.", joint->getFullName());
Base::Console().warning("Reference1 of Joint %s is bad.\n", joint->getFullName());
return;
}
@@ -1809,21 +1784,21 @@ void AssemblyObject::getRackPinionMarkers(
int AssemblyObject::slidingPartIndex(App::DocumentObject* joint)
{
App::DocumentObject* part1 = getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* obj1 = getObjFromRef(joint, "Reference1");
App::DocumentObject* part1 = getMovingPartFromRef(joint, "Reference1");
App::DocumentObject* obj1 = getObjFromJointRef(joint, "Reference1");
boost::ignore_unused(obj1);
Base::Placement plc1 = getPlacementFromProp(joint, "Placement1");
App::DocumentObject* part2 = getMovingPartFromRef(this, joint, "Reference2");
App::DocumentObject* obj2 = getObjFromRef(joint, "Reference2");
App::DocumentObject* part2 = getMovingPartFromRef(joint, "Reference2");
App::DocumentObject* obj2 = getObjFromJointRef(joint, "Reference2");
boost::ignore_unused(obj2);
Base::Placement plc2 = getPlacementFromProp(joint, "Placement2");
int slidingFound = 0;
for (auto* jt : getJoints(false, false)) {
if (getJointType(jt) == JointType::Slider) {
App::DocumentObject* jpart1 = getMovingPartFromRef(this, jt, "Reference1");
App::DocumentObject* jpart2 = getMovingPartFromRef(this, jt, "Reference2");
App::DocumentObject* jpart1 = getMovingPartFromRef(jt, "Reference1");
App::DocumentObject* jpart2 = getMovingPartFromRef(jt, "Reference2");
int found = 0;
Base::Placement plcjt, plci;
if (jpart1 == part1 || jpart1 == part2) {
@@ -1857,8 +1832,8 @@ bool AssemblyObject::isMbDJointValid(App::DocumentObject* joint)
// When dragging a part, we are bundling fixed parts together.
// This may lead to a conflicting joint that is self referencing a MbD part.
// The solver crash when fed such a bad joint. So we make sure it does not happen.
App::DocumentObject* part1 = getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(this, joint, "Reference2");
App::DocumentObject* part1 = getMovingPartFromRef(joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(joint, "Reference2");
if (!part1 || !part2) {
return false;
}
@@ -1898,8 +1873,8 @@ AssemblyObject::MbDPartData AssemblyObject::getMbDData(App::DocumentObject* part
for (auto* joint : joints) {
JointType jointType = getJointType(joint);
if (jointType == JointType::Fixed) {
App::DocumentObject* part1 = getMovingPartFromRef(this, joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(this, joint, "Reference2");
App::DocumentObject* part1 = getMovingPartFromRef(joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(joint, "Reference2");
App::DocumentObject* partToAdd = currentPart == part1 ? part2 : part1;
if (objectPartMap.find(partToAdd) != objectPartMap.end()) {
@@ -2030,7 +2005,7 @@ App::DocumentObject* AssemblyObject::getUpstreamMovingPart(
return part;
}
part = getMovingPartFromRef(this, joint, name == "Reference1" ? "Reference2" : "Reference1");
part = getMovingPartFromRef(joint, name == "Reference1" ? "Reference2" : "Reference1");
return getUpstreamMovingPart(part, joint, name);
}

View File

@@ -108,7 +108,6 @@ public:
Base::Placement getMbdPlacement(std::shared_ptr<MbD::ASMTPart> mbdPart);
bool validateNewPlacements();
void setNewPlacements();
static void recomputeJointPlacements(std::vector<App::DocumentObject*> joints);
static void redrawJointPlacements(std::vector<App::DocumentObject*> joints);
static void redrawJointPlacement(App::DocumentObject* joint);

View File

@@ -520,14 +520,19 @@ App::DocumentObject* getObjFromProp(const App::DocumentObject* joint, const char
return propObj->getValue();
}
App::DocumentObject* getObjFromRef(const App::DocumentObject* obj, const std::string& sub)
App::DocumentObject* getObjFromRef(App::DocumentObject* comp, const std::string& sub)
{
if (!obj) {
if (!comp) {
return nullptr;
}
const auto* doc = obj->getDocument();
const auto names = Base::Tools::splitSubName(sub);
const auto* doc = comp->getDocument();
auto names = Base::Tools::splitSubName(sub);
names.insert(names.begin(), comp->getNameInDocument());
if (names.size() <= 2) {
return comp;
}
// Lambda function to check if the typeId is a BodySubObject
const auto isBodySubObject = [](App::DocumentObject* obj) -> bool {
@@ -618,7 +623,7 @@ App::DocumentObject* getObjFromRef(const App::PropertyXLinkSub* prop)
return nullptr;
}
const App::DocumentObject* obj = prop->getValue();
App::DocumentObject* obj = prop->getValue();
if (!obj) {
return nullptr;
}
@@ -631,7 +636,7 @@ App::DocumentObject* getObjFromRef(const App::PropertyXLinkSub* prop)
return getObjFromRef(obj, subs[0]);
}
App::DocumentObject* getObjFromRef(const App::DocumentObject* joint, const char* pName)
App::DocumentObject* getObjFromJointRef(const App::DocumentObject* joint, const char* pName)
{
if (!joint) {
return nullptr;
@@ -647,13 +652,13 @@ App::DocumentObject* getLinkedObjFromRef(const App::DocumentObject* joint, const
return nullptr;
}
if (const auto* obj = getObjFromRef(joint, pObj)) {
if (const auto* obj = getObjFromJointRef(joint, pObj)) {
return obj->getLinkedObject(true);
}
return nullptr;
}
App::DocumentObject* getMovingPartFromRef(
App::DocumentObject* getMovingPartFromSel(
const AssemblyObject* assemblyObject,
App::DocumentObject* obj,
const std::string& sub
@@ -710,39 +715,23 @@ App::DocumentObject* getMovingPartFromRef(
return nullptr;
}
App::DocumentObject* getMovingPartFromRef(
const AssemblyObject* assemblyObject,
App::PropertyXLinkSub* prop
)
App::DocumentObject* getMovingPartFromRef(App::PropertyXLinkSub* prop)
{
if (!prop) {
return nullptr;
}
App::DocumentObject* obj = prop->getValue();
if (!obj) {
return nullptr;
}
const std::vector<std::string> subs = prop->getSubValues();
if (subs.empty()) {
return nullptr;
}
return getMovingPartFromRef(assemblyObject, obj, subs[0]);
return prop->getValue();
}
App::DocumentObject* getMovingPartFromRef(
const AssemblyObject* assemblyObject,
App::DocumentObject* joint,
const char* pName
)
App::DocumentObject* getMovingPartFromRef(App::DocumentObject* joint, const char* pName)
{
if (!joint) {
return nullptr;
}
auto* prop = joint->getPropertyByName<App::PropertyXLinkSub>(pName);
return getMovingPartFromRef(assemblyObject, prop);
return getMovingPartFromRef(prop);
}
void syncPlacements(App::DocumentObject* src, App::DocumentObject* to)

View File

@@ -165,27 +165,24 @@ AssemblyExport App::DocumentObject* getObjFromProp(
const App::DocumentObject* joint,
const char* propName
);
AssemblyExport App::DocumentObject* getObjFromRef(const App::DocumentObject* obj, const std::string& sub);
AssemblyExport App::DocumentObject* getObjFromRef(App::DocumentObject* obj, const std::string& sub);
AssemblyExport App::DocumentObject* getObjFromRef(const App::PropertyXLinkSub* prop);
AssemblyExport App::DocumentObject* getObjFromRef(const App::DocumentObject* joint, const char* propName);
AssemblyExport App::DocumentObject* getObjFromJointRef(
const App::DocumentObject* joint,
const char* propName
);
AssemblyExport App::DocumentObject* getLinkedObjFromRef(
const App::DocumentObject* joint,
const char* propName
);
AssemblyExport App::DocumentObject* getMovingPartFromRef(
// Get the moving part from a selection, which has the full path.
AssemblyExport App::DocumentObject* getMovingPartFromSel(
const AssemblyObject* assemblyObject,
App::DocumentObject* obj,
const std::string& sub
);
AssemblyExport App::DocumentObject* getMovingPartFromRef(
const AssemblyObject* assemblyObject,
const App::PropertyXLinkSub* prop
);
AssemblyExport App::DocumentObject* getMovingPartFromRef(
const AssemblyObject* assemblyObject,
App::DocumentObject* joint,
const char* pName
);
AssemblyExport App::DocumentObject* getMovingPartFromRef(const App::PropertyXLinkSub* prop);
AssemblyExport App::DocumentObject* getMovingPartFromRef(App::DocumentObject* joint, const char* pName);
AssemblyExport std::vector<std::string> getSubAsList(const App::PropertyXLinkSub* prop);
AssemblyExport std::vector<std::string> getSubAsList(
const App::DocumentObject* joint,

View File

@@ -206,8 +206,8 @@ class TestCore(unittest.TestCase):
JointObject.Joint(joint, 0)
refs = [
[self.assembly, [box2.Name + ".Face6", box2.Name + ".Vertex7"]],
[self.assembly, [box.Name + ".Face6", box.Name + ".Vertex7"]],
[box2, ["Face6", "Vertex7"]],
[box, ["Face6", "Vertex7"]],
]
joint.Proxy.setJointConnectors(joint, refs)

View File

@@ -416,6 +416,8 @@ def createGroundedJoint(obj):
)
Gui.doCommand(commands)
Gui.doCommandGui("JointObject.ViewProviderGroundedJoint(ground.ViewObject)")
Gui.doCommand("UtilsAssembly.activeAssembly().Document.recompute()")
return Gui.doCommandEval("ground")
@@ -471,8 +473,11 @@ class CommandToggleGrounded:
Gui.doCommand(commands)
continue
ref = [sel.Object, [sub, sub]]
moving_part = UtilsAssembly.getMovingPart(assembly, ref)
moving_part, new_sub = UtilsAssembly.getComponentReference(
assembly, sel.Object, sub
)
if not moving_part:
continue
# Only objects within the assembly.
if moving_part is None:

View File

@@ -562,7 +562,8 @@ class ExplodedViewSelGate:
self.viewObj = viewObj
def allow(self, doc, obj, sub):
if (obj.Name == self.assembly.Name and sub) or self.assembly.hasObject(obj, True):
comp, new_sub = UtilsAssembly.getComponentReference(self.assembly, obj, sub)
if comp:
# Objects within the assembly.
return True
@@ -662,6 +663,7 @@ class TaskAssemblyCreateView(QtCore.QObject):
def reject(self):
self.deactivate()
App.closeActiveTransaction(True)
App.activeDocument().recompute()
return True
def deactivate(self):
@@ -709,9 +711,14 @@ class TaskAssemblyCreateView(QtCore.QObject):
continue
for sub_name in sel.SubElementNames:
ref = [sel.Object, [sub_name]]
moving_part, new_sub = UtilsAssembly.getComponentReference(
self.assembly, sel.Object, sub_name
)
if not moving_part:
continue
ref = [moving_part, [new_sub]]
obj = UtilsAssembly.getObject(ref)
moving_part = UtilsAssembly.getMovingPart(self.assembly, ref)
element_name = UtilsAssembly.getElementName(sub_name)
# Only objects within the assembly, not the assembly and not elements.
@@ -733,6 +740,7 @@ class TaskAssemblyCreateView(QtCore.QObject):
ref[1][0] = UtilsAssembly.truncateSubAtFirst(ref[1][0], obj.Name)
if not obj in self.selectedObjs and hasattr(obj, "Placement"):
ref = [sel.Object, [sub_name]]
self.selectedRefs.append(ref)
self.selectedObjs.append(obj)
self.selectedObjsInitPlc.append(App.Placement(obj.Placement))
@@ -992,9 +1000,12 @@ class TaskAssemblyCreateView(QtCore.QObject):
return
else:
ref = [App.getDocument(doc_name).getObject(obj_name), [sub_name]]
rootObj = App.getDocument(doc_name).getObject(obj_name)
moving_part, new_sub = UtilsAssembly.getComponentReference(
self.assembly, rootObj, sub_name
)
ref = [moving_part, [new_sub]]
obj = UtilsAssembly.getObject(ref)
moving_part = UtilsAssembly.getMovingPart(self.assembly, ref)
if obj is None or moving_part is None:
return

View File

@@ -64,9 +64,8 @@ class CommandSolveAssembly:
if not assembly:
return
Gui.addModule("UtilsAssembly")
App.setActiveTransaction("Solve assembly")
Gui.doCommand("UtilsAssembly.activeAssembly().solve()")
assembly.recompute(True)
App.closeActiveTransaction()

View File

@@ -199,10 +199,10 @@ bool ViewProviderAssembly::canDragObjectToTarget(App::DocumentObject* obj, App::
for (auto joint : allJoints) {
// getLinkObjFromProp returns nullptr if the property doesn't exist.
App::DocumentObject* part1 = getMovingPartFromRef(assemblyPart, joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(assemblyPart, joint, "Reference2");
App::DocumentObject* obj1 = getObjFromRef(joint, "Reference1");
App::DocumentObject* obj2 = getObjFromRef(joint, "Reference2");
App::DocumentObject* part1 = getMovingPartFromRef(joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(joint, "Reference2");
App::DocumentObject* obj1 = getObjFromJointRef(joint, "Reference1");
App::DocumentObject* obj2 = getObjFromJointRef(joint, "Reference2");
App::DocumentObject* obj3 = getObjFromProp(joint, "ObjectToGround");
if (obj == obj1 || obj == obj2 || obj == part1 || obj == part2 || obj == obj3) {
if (!prompted) {
@@ -782,7 +782,7 @@ bool ViewProviderAssembly::getSelectedObjectsWithinAssembly(bool addPreselection
App::DocumentObject* selRoot = Gui::Selection().getPreselection().Object.getObject();
std::string sub = Gui::Selection().getPreselection().pSubName;
App::DocumentObject* obj = getMovingPartFromRef(assemblyPart, selRoot, sub);
App::DocumentObject* obj = getMovingPartFromSel(assemblyPart, selRoot, sub);
if (canDragObjectIn3d(obj)) {
bool alreadyIn = false;
@@ -850,7 +850,7 @@ void ViewProviderAssembly::collectMovableObjects(
return;
}
App::DocumentObject* part = getMovingPartFromRef(assemblyPart, selRoot, subNamePrefix);
App::DocumentObject* part = getMovingPartFromSel(assemblyPart, selRoot, subNamePrefix);
if (onlySolids && assemblyPart->isPartConnected(part)) {
return; // No dragger for connected parts.
@@ -955,7 +955,7 @@ ViewProviderAssembly::DragMode ViewProviderAssembly::findDragMode()
if (!ref) {
return DragMode::Translation;
}
auto* obj = getObjFromRef(movingJoint, pName.c_str());
auto* obj = getObjFromJointRef(movingJoint, pName.c_str());
Base::Placement global_plc = App::GeoFeature::getGlobalPlacement(obj, ref);
jcsGlobalPlc = global_plc * jcsPlc;
@@ -1022,6 +1022,7 @@ void ViewProviderAssembly::tryInitMove(const SbVec2s& cursorPos, Gui::View3DInve
else if (visible) {
joint->Visibility.setValue(false);
}
joint->purgeTouched();
}
SbVec3f vec;
@@ -1100,6 +1101,7 @@ void ViewProviderAssembly::endMove()
bool visible = pair.first->Visibility.getValue();
if (visible != pair.second) {
pair.first->Visibility.setValue(pair.second);
pair.first->purgeTouched();
}
}
@@ -1468,10 +1470,8 @@ void ViewProviderAssembly::isolateJointReferences(App::DocumentObject* joint, Is
return;
}
AssemblyObject* assembly = getObject<AssemblyObject>();
App::DocumentObject* part1 = getMovingPartFromRef(assembly, joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(assembly, joint, "Reference2");
App::DocumentObject* part1 = getMovingPartFromRef(joint, "Reference1");
App::DocumentObject* part2 = getMovingPartFromRef(joint, "Reference2");
if (!part1 || !part2) {
return;
}

View File

@@ -201,16 +201,11 @@ class Joint:
self.migrationScript4(joint)
self.migrationScript5(joint)
self.migrationScript6(joint)
self.migrationScript7(joint)
# First Joint Connector
if not hasattr(joint, "Reference1"):
joint.addProperty(
"App::PropertyXLinkSubHidden",
"Reference1",
"Joint Connector 1",
QT_TRANSLATE_NOOP("App::Property", "The first reference of the joint"),
locked=True,
)
self.addReference1Property(joint)
if not hasattr(joint, "Placement1"):
joint.addProperty(
@@ -250,13 +245,7 @@ class Joint:
# Second Joint Connector
if not hasattr(joint, "Reference2"):
joint.addProperty(
"App::PropertyXLinkSubHidden",
"Reference2",
"Joint Connector 2",
QT_TRANSLATE_NOOP("App::Property", "The second reference of the joint"),
locked=True,
)
self.addReference2Property(joint)
if not hasattr(joint, "Placement2"):
joint.addProperty(
@@ -373,6 +362,24 @@ class Joint:
joint.setPropertyStatus("LengthMin", "AllowNegativeValues")
joint.setPropertyStatus("LengthMax", "AllowNegativeValues")
def addReference1Property(self, joint):
joint.addProperty(
"App::PropertyXLinkSub",
"Reference1",
"Joint Connector 1",
QT_TRANSLATE_NOOP("App::Property", "The first reference of the joint"),
locked=True,
)
def addReference2Property(self, joint):
joint.addProperty(
"App::PropertyXLinkSub",
"Reference2",
"Joint Connector 2",
QT_TRANSLATE_NOOP("App::Property", "The second reference of the joint"),
locked=True,
)
def addAngleProperty(self, joint):
joint.addProperty(
"App::PropertyAngle",
@@ -681,6 +688,54 @@ class Joint:
self.addAngleMaxProperty(joint)
joint.AngleMax = old_value
def migrationScript7(self, joint):
"""
Migrates PropertyXLinkSubHidden (Assembly-rooted) to PropertyXLinkSub (Component-rooted).
"""
def migrate_prop(prop_name, add_method, assembly):
if not hasattr(joint, prop_name):
return
# Only migrate if it's the old type
if joint.getTypeIdOfProperty(prop_name) != "App::PropertyXLinkSubHidden":
return
old_ref = getattr(joint, prop_name)
joint.setPropertyStatus(prop_name, "-LockDynamic")
joint.removeProperty(prop_name)
add_method(joint)
if old_ref:
root = old_ref[0]
old_subs = old_ref[1]
if old_subs:
comp, first_new_sub = UtilsAssembly.getComponentReference(
assembly, root, old_subs[0]
)
if comp:
new_subs = [first_new_sub]
# We can deduce the prefix length from the difference
# between old and new of the first item.
prefix_len = len(old_subs[0]) - len(first_new_sub)
for i in range(1, len(old_subs)):
sub = old_subs[i]
if len(sub) >= prefix_len:
new_subs.append(sub[prefix_len:])
else:
new_subs.append(sub)
setattr(joint, prop_name, [comp, new_subs])
assembly = self.getAssembly(joint)
migrate_prop("Reference1", self.addReference1Property, assembly)
migrate_prop("Reference2", self.addReference2Property, assembly)
def dumps(self):
return None
@@ -772,6 +827,8 @@ class Joint:
):
raise Exception(errStr + "Reference2")
self.updateJCSPlacements(joint)
def setJointConnectors(self, joint, refs):
# 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)
@@ -812,6 +869,14 @@ class Joint:
if not joint.Detach2:
joint.Placement2 = self.findPlacement(joint, joint.Reference2, 1)
self.redrawJointPlacements(joint)
def redrawJointPlacements(self, joint):
if joint.ViewObject:
proxy = joint.ViewObject.Proxy
if proxy:
proxy.redrawJointPlacements(joint)
"""
So here we want to find a placement that corresponds to a local coordinate system that would be placed at the selected vertex.
- obj is usually a App::Link to a PartDesign::Body, or primitive, fasteners. But can also be directly the object.1
@@ -850,8 +915,8 @@ class Joint:
if reverse:
sameDir = not sameDir
part1 = UtilsAssembly.getMovingPart(assembly, joint.Reference1)
part2 = UtilsAssembly.getMovingPart(assembly, joint.Reference2)
part1 = UtilsAssembly.getMovingPart(joint.Reference1)
part2 = UtilsAssembly.getMovingPart(joint.Reference2)
if not part1 or not part2:
return False
@@ -915,7 +980,8 @@ class Joint:
part.Placement = plc
self.partsMovedByPresolved = {}
joint.Placement1 = joint.Placement1 # Make sure plc1 is redrawn
if joint.ViewObject:
joint.ViewObject.Proxy.redrawJointPlacements(joint)
def preventParallel(self, joint):
# Angle and perpendicular joints in the solver cannot handle the situation where both JCS are Parallel
@@ -925,8 +991,8 @@ class Joint:
assembly = self.getAssembly(joint)
part1 = UtilsAssembly.getMovingPart(assembly, joint.Reference1)
part2 = UtilsAssembly.getMovingPart(assembly, joint.Reference2)
part1 = UtilsAssembly.getMovingPart(joint.Reference1)
part2 = UtilsAssembly.getMovingPart(joint.Reference2)
isAssembly = assembly.Type == "Assembly"
if isAssembly:
@@ -999,24 +1065,25 @@ class ViewProviderJoint:
def updateData(self, joint, prop):
"""If a property of the handled feature has changed we have the chance to handle this here"""
# joint is the handled feature, prop is the name of the property that has changed
if prop == "Placement1":
if hasattr(joint, "Reference1") and joint.Reference1:
plc = joint.Placement1
self.switch_JCS1.whichChild = coin.SO_SWITCH_ALL
if prop == "Placement1" and hasattr(joint, "Reference1"):
self.redrawJointPlacement(self.switch_JCS1, joint.Placement1, joint.Reference1)
self.switch_JCS1.set_marker_placement(plc, joint.Reference1)
else:
self.switch_JCS1.whichChild = coin.SO_SWITCH_NONE
if prop == "Placement2" and hasattr(joint, "Reference2"):
self.redrawJointPlacement(self.switch_JCS2, joint.Placement2, joint.Reference2)
if prop == "Placement2":
if hasattr(joint, "Reference2") and joint.Reference2:
plc = joint.Placement2
self.switch_JCS2.whichChild = coin.SO_SWITCH_ALL
def redrawJointPlacements(self, joint):
if not hasattr(joint, "Reference1") or not hasattr(joint, "Reference2"):
return
self.switch_JCS2.set_marker_placement(plc, joint.Reference2)
else:
self.switch_JCS2.whichChild = coin.SO_SWITCH_NONE
self.redrawJointPlacement(self.switch_JCS1, joint.Placement1, joint.Reference1)
self.redrawJointPlacement(self.switch_JCS2, joint.Placement2, joint.Reference2)
def redrawJointPlacement(self, jcs, plc, ref):
if ref:
jcs.whichChild = coin.SO_SWITCH_ALL
jcs.set_marker_placement(plc, ref)
else:
jcs.whichChild = coin.SO_SWITCH_NONE
def showPreviewJCS(self, visible, placement=None, ref=None):
if visible:
@@ -1091,7 +1158,7 @@ class ViewProviderJoint:
assembly = self.app_obj.Proxy.getAssembly(self.app_obj)
# Assuming Reference1 corresponds to the first part link
if hasattr(self.app_obj, "Reference1"):
part = UtilsAssembly.getMovingPart(assembly, self.app_obj.Reference1)
part = UtilsAssembly.getMovingPart(self.app_obj.Reference1)
if part is not None and not assembly.isPartConnected(part):
overlays[Gui.IconPosition.BottomLeft] = "Part_Detached"
@@ -1531,7 +1598,6 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self.deactivate()
solveIfAllowed(self.assembly)
if self.activeType == "Assembly":
self.joint.Visibility = self.visibilityBackup
else:
@@ -1540,14 +1606,15 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
cmds = UtilsAssembly.generatePropertySettings(self.joint)
Gui.doCommand(cmds)
self.assembly.recompute(True)
App.closeActiveTransaction()
return True
def reject(self):
self.deactivate()
App.closeActiveTransaction(True)
if not self.creating: # update visibility only if we are editing the joint
self.joint.Visibility = self.visibilityBackup
self.assembly.recompute(True)
return True
def autoClosedOnTransactionChange(self):
@@ -1596,8 +1663,15 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
for sub_name in sel.SubElementNames:
# We add sub_name twice because the joints references have element name + vertex name
# and in the case of initial selection, both are the same.
ref = [sel.Object, [sub_name, sub_name]]
moving_part = self.getMovingPart(ref)
moving_part, new_sub = UtilsAssembly.getComponentReference(
self.assembly, sel.Object, sub_name
)
if not moving_part:
break
# Construct the reference using the Component as the root
ref = [moving_part, [new_sub, new_sub]]
# Only objects within the assembly.
if moving_part is None:
@@ -1628,9 +1702,12 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
joint_group = UtilsAssembly.getJointGroup(self.assembly)
self.joint = joint_group.newObject("App::FeaturePython", "Joint")
self.joint.Label = self.jointName
joint_group.purgeTouched()
self.assembly.purgeTouched()
Joint(self.joint, type_index)
ViewProviderJoint(self.joint.ViewObject)
self.joint.purgeTouched()
def onJointTypeChanged(self, index):
self.jType = JointTypes[self.jForm.jointType.currentIndex()]
@@ -1836,14 +1913,15 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
ref1 = self.joint.Reference1
ref2 = self.joint.Reference2
self.refs.append(ref1)
self.refs.append(ref2)
if UtilsAssembly.isRefValid(ref1, 2):
self.refs.append(ref1)
sub1 = UtilsAssembly.addTipNameToSub(ref1)
Gui.Selection.addSelection(ref1[0].Document.Name, ref1[0].Name, sub1)
sub1 = UtilsAssembly.addTipNameToSub(ref1)
sub2 = UtilsAssembly.addTipNameToSub(ref2)
Gui.Selection.addSelection(ref1[0].Document.Name, ref1[0].Name, sub1)
Gui.Selection.addSelection(ref2[0].Document.Name, ref2[0].Name, sub2)
if UtilsAssembly.isRefValid(ref2, 2):
self.refs.append(ref2)
sub2 = UtilsAssembly.addTipNameToSub(ref2)
Gui.Selection.addSelection(ref2[0].Document.Name, ref2[0].Name, sub2)
self.jForm.angleSpinbox.setProperty("rawValue", self.joint.Angle.Value)
self.jForm.distanceSpinbox.setProperty("rawValue", self.joint.Distance.Value)
@@ -2025,7 +2103,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self._removeSelectedItems(selected_indexes)
def getMovingPart(self, ref):
return UtilsAssembly.getMovingPart(self.assembly, ref)
return UtilsAssembly.getMovingPart(ref)
# selectionObserver stuff
def addSelection(self, doc_name, obj_name, sub_name, mousePos):
@@ -2038,7 +2116,15 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
sub_name = UtilsAssembly.fixBodyExtraFeatureInSub(doc_name, sub_name)
ref = [rootObj, [sub_name]]
comp, new_sub = UtilsAssembly.getComponentReference(self.assembly, rootObj, sub_name)
if not comp:
# Selection was not valid (not inside assembly or logic failed)
Gui.Selection.removeSelection(doc_name, obj_name, sub_name)
return
# Construct the reference using the Component as the root
ref = [comp, [new_sub]]
moving_part = self.getMovingPart(ref)
# Check if the addition is acceptable (we are not doing this in selection gate to let user move objects)
@@ -2085,12 +2171,16 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
sub_name = UtilsAssembly.fixBodyExtraFeatureInSub(doc_name, sub_name)
comp, new_sub = UtilsAssembly.getComponentReference(self.assembly, rootObj, sub_name)
if not comp:
return
for reference in self.refs[:]:
ref_obj = reference[0]
ref_element_name = reference[1][0] if len(reference[1]) > 0 else ""
# match both object and processed element name for precise identification
if ref_obj == rootObj and ref_element_name == sub_name:
if ref_obj == comp and ref_element_name == new_sub:
self.refs.remove(reference)
break
else:
@@ -2103,7 +2193,13 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self.presel_ref = None
return
self.presel_ref = [App.getDocument(doc_name).getObject(obj_name), [sub_name]]
rootObj = App.getDocument(doc_name).getObject(obj_name)
comp, new_sub = UtilsAssembly.getComponentReference(self.assembly, rootObj, sub_name)
if not comp:
return
self.presel_ref = [comp, [new_sub]]
def clearSelection(self, doc_name):
self.refs.clear()

View File

@@ -128,7 +128,7 @@ def isLinkGroup(obj):
def getObject(ref):
if len(ref) != 2:
if len(ref) != 2 or ref[0] is None:
return None
subs = ref[1]
if len(subs) < 1:
@@ -139,7 +139,10 @@ def getObject(ref):
# or "LinkOrAssembly1.LinkOrPart1.LinkOrBody.pad.Edge16"
# or "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBody.Local_CS.X"
# We want either LinkOrBody or LinkOrBox or Local_CS.
# Note since the ref now holds the moving part, sub_name can now be just the element name.
# In this case the obj we need is the reference obj
names = sub_name.split(".")
names.insert(0, ref[0].Name)
if len(names) < 2:
return None
@@ -347,24 +350,6 @@ def getRootPath(obj, part):
return None, ""
# get the placement of Obj relative to its moving Part
# Example : assembly.part1.part2.partn.body1 : placement of Obj relative to part1
def getObjPlcRelativeToPart(assembly, ref):
# we need plc to be relative to the moving part
moving_part = getMovingPart(assembly, ref)
obj_global_plc = getGlobalPlacement(ref)
part_global_plc = getGlobalPlacement(ref, moving_part)
return part_global_plc.inverse() * obj_global_plc
# Example : assembly.part1.part2.partn.body1 : jcsPlc is relative to body1
# This function returns jcsPlc relative to part1
def getJcsPlcRelativeToPart(assembly, jcsPlc, ref):
obj_relative_plc = getObjPlcRelativeToPart(assembly, ref)
return obj_relative_plc * jcsPlc
# Return the jcs global placement
def getJcsGlobalPlc(jcsPlc, ref):
obj_global_plc = getGlobalPlacement(ref)
@@ -382,6 +367,15 @@ def getGlobalPlacement(ref, targetObj=None):
rootObj = ref[0]
subName = ref[1][0]
# ref[0] is no longer the root object. Now it's the moving part.
# So to get the correct global placement in case user transformed the assembly,
# or if he has nested the assembly, we need to adjust it.
# Example : Part / Assembly / Cylinder / Face1
# ref is [(Cylinder, 'Face1')]
parents = rootObj.Parents # [(<Part object>, 'Assembly.Cylinder.')]
if parents is not None and len(parents) == 1 and len(parents[0]) == 2:
rootObj = parents[0][0]
subName = parents[0][1] + subName
return rootObj.getPlacementOf(subName, targetObj)
@@ -398,7 +392,7 @@ def getElementName(full_name):
# We want either Edge16.
parts = full_name.split(".")
if len(parts) < 2:
if len(parts) < 1:
# At minimum "Box.edge16". It shouldn't be shorter
return ""
@@ -1214,59 +1208,79 @@ def getJointXYAngle(joint):
return math.atan2(x_axis.y, x_axis.x)
def getMovingPart(assembly, ref):
# ref can be :
# [assembly, ['box.edge1', 'box.vertex2']]
# [Part, ['Assembly.box.edge1', 'Assembly.box.vertex2']]
# [assembly, ['Body.Pad.edge1', 'Body.Pad.vertex2']]
if assembly is None or ref is None or len(ref) != 2:
def getMovingPart(ref):
if ref is None or len(ref) != 2:
return None
obj = ref[0]
subs = ref[1]
if subs is None or len(subs) < 1:
return None
return obj
sub = ref[1][0] # All subs should have the same object paths.
names = [obj.Name] + sub.split(".")
try:
index = names.index(assembly.Name)
# Get the sublist starting after the after the assembly (in case of Part1/Assembly/...)
names = names[index + 1 :]
except ValueError:
return None
def getComponentReference(assembly, root_obj, sub_string):
"""
Takes a full selection path (root + subnames) and normalizes it
to be rooted at the Assembly Component (Moving Part).
Returns: (ComponentObject, RelativeSubString) or (None, "")
"""
if not assembly or not root_obj:
return None, ""
doc = assembly.Document
if len(names) < 2:
App.Console.PrintError(
f"getMovingPart() in UtilsAssembly.py the object name {names} is too short. It should be at least similar to ['Box','edge16'], not shorter.\n"
)
return None
# 1. Reconstruct full path
# e.g. ['Part', 'Assembly', 'Cylinder', 'Face1']
names = [root_obj.Name] + sub_string.split(".")
for objName in names:
obj = doc.getObject(objName)
# 2. Find Assembly in path
try:
asm_idx = names.index(assembly.Name)
except ValueError:
return None, ""
# 3. Identify Component (first valid object after Assembly)
candidates = names[asm_idx + 1 :]
if not candidates:
return None, ""
component = None
comp_idx = -1
for i, obj_name in enumerate(candidates):
obj = doc.getObject(obj_name)
if not obj:
continue
if obj.TypeId == "App::DocumentObjectGroup":
continue # we ignore groups.
# We ignore dynamic sub-assemblies.
if obj.isDerivedFrom("Assembly::AssemblyLink") and obj.Rigid == False:
# Skips (Groups / Flexible Links / LinkGroups / non-geofeature objects)
if obj.isDerivedFrom("App::DocumentObjectGroup"):
continue
# If it is a LinkGroup then we skip it
if obj.isDerivedFrom("Assembly::AssemblyLink"):
if hasattr(obj, "Rigid") and not obj.Rigid:
continue
if isLinkGroup(obj):
continue
return obj
if isLink(obj):
linkedObj = obj.getLinkedObject()
if linkedObj and not linkedObj.isDerivedFrom("App::GeoFeature"):
continue
elif not obj.isDerivedFrom("App::GeoFeature"):
continue
return None
component = obj
comp_idx = asm_idx + 1 + i
break
if not component:
return None, ""
# 4. Construct new sub-string
# Everything after the component in the original names list
relative_parts = names[comp_idx + 1 :]
new_sub = ".".join(relative_parts)
return component, new_sub
def truncateSubAtFirst(sub, target):