diff --git a/src/Mod/Assembly/App/AssemblyObject.pyi b/src/Mod/Assembly/App/AssemblyObject.pyi index d72db28d34..016d749bf4 100644 --- a/src/Mod/Assembly/App/AssemblyObject.pyi +++ b/src/Mod/Assembly/App/AssemblyObject.pyi @@ -148,5 +148,28 @@ class AssemblyObject(Part): Args: fileName: The name of the file where the ASMT will be exported.""" ... + + @constmethod + def getDownstreamParts( + self, start_part: "App.DocumentObject", joint_to_ignore: "App.DocumentObject", / + ) -> list["App.DocumentObject"]: + """ + Finds all parts connected to a start_part that are not connected to ground + when a specific joint is ignored. + + getDownstreamParts(start_part, joint_to_ignore) -> list + + This is used to find the entire rigid group of unconstrained components that + should be moved together during a pre-solve operation or a drag. + + Args: + start_part: The App.DocumentObject to begin the search from. + joint_to_ignore: The App.DocumentObject (a joint) to temporarily + suppress during the connectivity check. + + Returns: + A list of App.DocumentObject instances representing the downstream parts. + """ + ... Joints: Final[list] """A list of all joints this assembly has.""" diff --git a/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp b/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp index 4e44a9e8ea..adf27ed798 100644 --- a/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp +++ b/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp @@ -198,3 +198,36 @@ Py::List AssemblyObjectPy::getJoints() const return ret; } + +PyObject* AssemblyObjectPy::getDownstreamParts(PyObject* args) const +{ + PyObject* pyPart; + PyObject* pyJoint; + + // Parse the two arguments: a part object and a joint object + if (!PyArg_ParseTuple(args, + "O!O!", + &(App::DocumentObjectPy::Type), + &pyPart, + &(App::DocumentObjectPy::Type), + &pyJoint)) { + return nullptr; + } + + auto* part = static_cast(pyPart)->getDocumentObjectPtr(); + auto* joint = static_cast(pyJoint)->getDocumentObjectPtr(); + + // Call the C++ method + std::vector downstreamParts = + this->getAssemblyObjectPtr()->getDownstreamParts(part, joint); + + // Convert the result into a Python list of DocumentObjects + Py::List ret; + for (const auto& objRef : downstreamParts) { + if (objRef.obj) { + ret.append(Py::Object(objRef.obj->getPyObject(), true)); + } + } + + return Py::new_reference_to(ret); +} diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index e12144f924..7d14f08d2e 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -832,46 +832,27 @@ class Joint: return plc def flipOnePart(self, joint): - assembly = self.getAssembly(joint) - - part2 = UtilsAssembly.getMovingPart(assembly, joint.Reference2) - if part2 is not None: - part2ConnectedByJoint = assembly.isJointConnectingPartToGround(joint, "Reference2") - part2Grounded = assembly.isPartGrounded(part2) - if part2ConnectedByJoint and not part2Grounded: - jcsPlc = UtilsAssembly.getJcsPlcRelativeToPart( - assembly, joint.Placement2, joint.Reference2 - ) - globalJcsPlc = UtilsAssembly.getJcsGlobalPlc(joint.Placement2, joint.Reference2) - jcsPlc = UtilsAssembly.flipPlacement(jcsPlc) - part2.Placement = globalJcsPlc * jcsPlc.inverse() - solveIfAllowed(self.getAssembly(joint)) - return - - part1 = UtilsAssembly.getMovingPart(assembly, joint.Reference1) - if part1 is not None: - part1Grounded = assembly.isPartGrounded(part1) - if not part1Grounded: - jcsPlc = UtilsAssembly.getJcsPlcRelativeToPart( - assembly, joint.Placement1, joint.Reference1 - ) - globalJcsPlc = UtilsAssembly.getJcsGlobalPlc(joint.Placement1, joint.Reference1) - jcsPlc = UtilsAssembly.flipPlacement(jcsPlc) - part1.Placement = globalJcsPlc * jcsPlc.inverse() - return + self.matchJCS(joint, False, True) def preSolve(self, joint, savePlc=True): # The goal of this is to put the part in the correct position to avoid wrong placement by the solve. # we actually don't want to match perfectly the JCS, it is best to match them # in the current closest direction, ie either matched or flipped. + self.matchJCS(joint, savePlc) - sameDir = self.areJcsSameDir(joint) + def matchJCS(self, joint, savePlc=True, reverse=False): assembly = self.getAssembly(joint) + sameDir = self.areJcsSameDir(joint) + if reverse: + sameDir = not sameDir part1 = UtilsAssembly.getMovingPart(assembly, joint.Reference1) part2 = UtilsAssembly.getMovingPart(assembly, joint.Reference2) + if not part1 or not part2: + return False + isAssembly = assembly.Type == "Assembly" if isAssembly: joint.Suppressed = True @@ -882,47 +863,54 @@ class Joint: part1Connected = True part2Connected = False - if not part1Connected: - if savePlc: - self.partMovedByPresolved = part1 - self.presolveBackupPlc = part1.Placement + moving_part = None + moving_part_ref = None + fixed_part_ref = None + moving_placement = None + fixed_placement = None - globalJcsPlc2 = UtilsAssembly.getJcsGlobalPlc(joint.Placement2, joint.Reference2) - jcsPlc1 = UtilsAssembly.getJcsPlcRelativeToPart( - assembly, joint.Placement1, joint.Reference1 - ) - if not sameDir: - jcsPlc1 = UtilsAssembly.flipPlacement(jcsPlc1) + if not part1Connected and part1: + moving_part = part1 + moving_part_ref = joint.Reference1 + fixed_part_ref = joint.Reference2 + moving_placement = joint.Placement1 + fixed_placement = joint.Placement2 + elif not part2Connected and part2: + moving_part = part2 + moving_part_ref = joint.Reference2 + fixed_part_ref = joint.Reference1 + moving_placement = joint.Placement2 + fixed_placement = joint.Placement1 + else: + # Both parts are constrained, or something is invalid. Nothing to pre-solve. + return False - # For link groups and sub-assemblies we have to take into account - # the parent placement (ie the linkgroup plc) as the linkgroup is not the moving part - # But instead of doing as follow, we rather enforce identity placement for linkgroups. - # parentPlc = UtilsAssembly.getParentPlacementIfNeeded(part2) - # part2.Placement = globalJcsPlc1 * jcsPlc2.inverse() * parentPlc.inverse() + parts_to_move = [moving_part] + if isAssembly: + parts_to_move = parts_to_move + assembly.getDownstreamParts(moving_part, joint) - part1.Placement = globalJcsPlc2 * jcsPlc1.inverse() - return True + if savePlc: + self.partsMovedByPresolved = {p: p.Placement for p in parts_to_move} - elif not part2Connected: - if savePlc: - self.partMovedByPresolved = part2 - self.presolveBackupPlc = part2.Placement + moving_part_global_jcs = UtilsAssembly.getJcsGlobalPlc(moving_placement, moving_part_ref) + fixed_part_global_jcs = UtilsAssembly.getJcsGlobalPlc(fixed_placement, fixed_part_ref) - globalJcsPlc1 = UtilsAssembly.getJcsGlobalPlc(joint.Placement1, joint.Reference1) - jcsPlc2 = UtilsAssembly.getJcsPlcRelativeToPart( - assembly, joint.Placement2, joint.Reference2 - ) - if not sameDir: - jcsPlc2 = UtilsAssembly.flipPlacement(jcsPlc2) + if not sameDir: + moving_part_global_jcs = UtilsAssembly.flipPlacement(moving_part_global_jcs) - part2.Placement = globalJcsPlc1 * jcsPlc2.inverse() - return True - return False + transform_plc = fixed_part_global_jcs * moving_part_global_jcs.inverse() + + for part in parts_to_move: + part.Placement = transform_plc * part.Placement + + return True def undoPreSolve(self, joint): - if hasattr(self, "partMovedByPresolved") and self.partMovedByPresolved: - self.partMovedByPresolved.Placement = self.presolveBackupPlc - self.partMovedByPresolved = None + if hasattr(self, "partsMovedByPresolved") and self.partsMovedByPresolved: + for part, plc in self.partsMovedByPresolved.items(): + if part and hasattr(part, "Placement"): + part.Placement = plc + self.partsMovedByPresolved = {} joint.Placement1 = joint.Placement1 # Make sure plc1 is redrawn