diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index 506613640c..624196f3af 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -162,6 +162,7 @@ App::DocumentObject* AssemblyObject::getJointOfPartConnectingToGround(App::Docum bool AssemblyObject::isJointConnectingPartToGround(App::DocumentObject* joint, const char* propname) { + auto* propPart = dynamic_cast(joint->getPropertyByName(propname)); if (!propPart) { return false; @@ -173,14 +174,32 @@ bool AssemblyObject::isJointConnectingPartToGround(App::DocumentObject* joint, c return false; } - // now we disconnect this joint temporarily - propPart->setValue(nullptr); + // to know if a joint is connecting to ground we disable all the other joints + std::vector jointsOfPart = getJointsOfPart(part); + std::vector activatedStates; + + for (auto jointi : jointsOfPart) { + if (jointi->getFullName() == joint->getFullName()) { + continue; + } + + activatedStates.push_back(getJointActivated(jointi)); + setJointActivated(jointi, false); + } isConnected = isPartConnected(part); - propPart->setValue(part); + // restore activation states + for (auto jointi : jointsOfPart) { + if (jointi->getFullName() == joint->getFullName() || activatedStates.empty()) { + continue; + } - return !isConnected; + setJointActivated(jointi, activatedStates[0]); + activatedStates.erase(activatedStates.begin()); + } + + return isConnected; } std::vector AssemblyObject::getJoints(bool updateJCS) @@ -193,16 +212,20 @@ std::vector AssemblyObject::getJoints(bool updateJCS) } Base::PyGILStateLocker lock; - for (auto obj : jointGroup->getObjects()) { - if (!obj) { + for (auto joint : jointGroup->getObjects()) { + if (!joint) { continue; } - auto proxy = dynamic_cast(obj->getPropertyByName("Proxy")); + auto* prop = dynamic_cast(joint->getPropertyByName("Activated")); + if (prop && !prop->getValue()) { + continue; + } + + auto proxy = dynamic_cast(joint->getPropertyByName("Proxy")); if (proxy) { - Py::Object joint = proxy->getValue(); - if (joint.hasAttr("setJointConnectors")) { - joints.push_back(obj); + if (proxy->getValue().hasAttr("setJointConnectors")) { + joints.push_back(joint); } } } @@ -347,16 +370,37 @@ bool AssemblyObject::isPartConnected(App::DocumentObject* obj) } std::vector AssemblyObject::getDownstreamParts(App::DocumentObject* part, - int limit) + App::DocumentObject* joint) { - if (limit > 1000) { // Inifinite loop protection + // First we deactivate the joint + bool state = getJointActivated(joint); + setJointActivated(joint, false); + + std::vector joints = getJoints(false); + + std::set connectedParts = {part}; + traverseAndMarkConnectedParts(part, connectedParts, joints); + + std::vector downstreamParts; + for (auto parti : connectedParts) { + if (!isPartConnected(parti) && (parti != part)) { + downstreamParts.push_back(parti); + } + } + + AssemblyObject::setJointActivated(joint, state); + /*if (limit > 1000) { // Inifinite loop protection return {}; } limit++; + Base::Console().Warning("limit %d\n", limit); std::vector downstreamParts = {part}; std::string name; - App::DocumentObject* connectingJoint = getJointOfPartConnectingToGround(part, name); + App::DocumentObject* connectingJoint = + getJointOfPartConnectingToGround(part, + name); // ?????????????????????????????? if we remove + // connection to ground then it can't work for tom std::vector jointsOfPart = getJointsOfPart(part); // remove connectingJoint from jointsOfPart @@ -365,8 +409,23 @@ std::vector AssemblyObject::getDownstreamParts(App::Docume for (auto joint : jointsOfPart) { App::DocumentObject* part1 = getLinkObjFromProp(joint, "Part1"); App::DocumentObject* part2 = getLinkObjFromProp(joint, "Part2"); - App::DocumentObject* downstreamPart = - part->getFullName() == part1->getFullName() ? part2 : part1; + bool firstIsDown = part->getFullName() == part2->getFullName(); + App::DocumentObject* downstreamPart = firstIsDown ? part1 : part2; + + Base::Console().Warning("looping\n"); + // it is possible that the part is connected to ground by this joint. + // In which case we should not select those parts. To test we disconnect : + auto* propObj = dynamic_cast(joint->getPropertyByName("Part1")); + if (!propObj) { + continue; + } + propObj->setValue(nullptr); + bool isConnected = isPartConnected(downstreamPart); + propObj->setValue(part1); + if (isConnected) { + Base::Console().Warning("continue\n"); + continue; + } std::vector subDownstreamParts = getDownstreamParts(downstreamPart, limit); @@ -376,7 +435,7 @@ std::vector AssemblyObject::getDownstreamParts(App::Docume downstreamParts.push_back(downPart); } } - } + }*/ return downstreamParts; } @@ -1322,6 +1381,22 @@ void printPlacement(Base::Placement plc, const char* name) angle); } +void AssemblyObject::setJointActivated(App::DocumentObject* joint, bool val) +{ + auto* propActivated = dynamic_cast(joint->getPropertyByName("Activated")); + if (propActivated) { + propActivated->setValue(val); + } +} +bool AssemblyObject::getJointActivated(App::DocumentObject* joint) +{ + auto* propActivated = dynamic_cast(joint->getPropertyByName("Activated")); + if (propActivated) { + return propActivated->getValue(); + } + return false; +} + Base::Placement AssemblyObject::getPlacementFromProp(App::DocumentObject* obj, const char* propName) { Base::Placement plc = Base::Placement(); @@ -1494,7 +1569,6 @@ App::DocumentObject* AssemblyObject::getLinkObjFromProp(App::DocumentObject* joi { auto* propObj = dynamic_cast(joint->getPropertyByName(propLinkName)); if (!propObj) { - Base::Console().Warning("getLinkObjFromProp nullptr\n"); return nullptr; } return propObj->getValue(); diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h index 2f870ad09f..7bd7a51b78 100644 --- a/src/Mod/Assembly/App/AssemblyObject.h +++ b/src/Mod/Assembly/App/AssemblyObject.h @@ -136,7 +136,8 @@ public: bool isPartGrounded(App::DocumentObject* part); bool isPartConnected(App::DocumentObject* part); - std::vector getDownstreamParts(App::DocumentObject* part, int limit = 0); + std::vector getDownstreamParts(App::DocumentObject* part, + App::DocumentObject* joint); std::vector getUpstreamParts(App::DocumentObject* part, int limit = 0); App::DocumentObject* getUpstreamMovingPart(App::DocumentObject* part); @@ -166,6 +167,8 @@ public: // see https://forum.freecad.org/viewtopic.php?p=729577#p729577 // getters to get from properties + static void setJointActivated(App::DocumentObject* joint, bool val); + static bool getJointActivated(App::DocumentObject* joint); static double getJointDistance(App::DocumentObject* joint); static JointType getJointType(App::DocumentObject* joint); static const char* getElementFromProp(App::DocumentObject* obj, const char* propName); diff --git a/src/Mod/Assembly/AssemblyImport.py b/src/Mod/Assembly/AssemblyImport.py index be4ffb8b46..5379122a46 100644 --- a/src/Mod/Assembly/AssemblyImport.py +++ b/src/Mod/Assembly/AssemblyImport.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ def open(filename): diff --git a/src/Mod/Assembly/CommandCreateAssembly.py b/src/Mod/Assembly/CommandCreateAssembly.py index 6c6cfd702a..0f22d29a29 100644 --- a/src/Mod/Assembly/CommandCreateAssembly.py +++ b/src/Mod/Assembly/CommandCreateAssembly.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import FreeCAD as App @@ -28,6 +28,9 @@ from PySide.QtCore import QT_TRANSLATE_NOOP if App.GuiUp: import FreeCADGui as Gui +import UtilsAssembly +import Preferences + # translate = App.Qt.translate __title__ = "Assembly Command Create Assembly" @@ -46,19 +49,32 @@ class CommandCreateAssembly: "Accel": "A", "ToolTip": QT_TRANSLATE_NOOP( "Assembly_CreateAssembly", - "Create an assembly object in the current document.", + "Create an assembly object in the current document or if in the current active assembly if any. One root assembly per file max.", ), "CmdType": "ForEdit", } def IsActive(self): + if Preferences.preferences().GetBool("EnforceOneAssemblyRule", True): + activeAssembly = UtilsAssembly.activeAssembly() + + if UtilsAssembly.isThereOneRootAssembly() and not activeAssembly: + return False + return App.ActiveDocument is not None def Activated(self): App.setActiveTransaction("Create assembly") - assembly = App.ActiveDocument.addObject("Assembly::AssemblyObject", "Assembly") + + activeAssembly = UtilsAssembly.activeAssembly() + if activeAssembly: + assembly = activeAssembly.newObject("Assembly::AssemblyObject", "Assembly") + else: + assembly = App.ActiveDocument.addObject("Assembly::AssemblyObject", "Assembly") + assembly.Type = "Assembly" - Gui.ActiveDocument.setEdit(assembly) + if not activeAssembly: + 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 765de4c994..09ef98048a 100644 --- a/src/Mod/Assembly/CommandCreateJoint.py +++ b/src/Mod/Assembly/CommandCreateJoint.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import os import FreeCAD as App @@ -254,16 +254,15 @@ class CommandToggleGrounded: # If you select 2 solids (bodies for example) within an assembly. # There'll be a single sel but 2 SubElementNames. for sub in sel.SubElementNames: - - 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 + full_element_name = UtilsAssembly.getFullElementName(sel.ObjectName, sub) + obj = UtilsAssembly.getObject(full_element_name) + part_containing_obj = UtilsAssembly.getContainingPart(full_element_name, obj) + # Check if part is grounded and if so delete the joint. for joint in joint_group.Group: if ( diff --git a/src/Mod/Assembly/CommandExportASMT.py b/src/Mod/Assembly/CommandExportASMT.py index b32734f388..c175e28c53 100644 --- a/src/Mod/Assembly/CommandExportASMT.py +++ b/src/Mod/Assembly/CommandExportASMT.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import FreeCAD as App import UtilsAssembly diff --git a/src/Mod/Assembly/CommandInsertLink.py b/src/Mod/Assembly/CommandInsertLink.py index f70cd8732b..70e19b11df 100644 --- a/src/Mod/Assembly/CommandInsertLink.py +++ b/src/Mod/Assembly/CommandInsertLink.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import re import os diff --git a/src/Mod/Assembly/CommandSolveAssembly.py b/src/Mod/Assembly/CommandSolveAssembly.py index b332f742d2..fec765b682 100644 --- a/src/Mod/Assembly/CommandSolveAssembly.py +++ b/src/Mod/Assembly/CommandSolveAssembly.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import os import FreeCAD as App diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index 174d46e6d6..2c90bcd1ac 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -117,7 +117,6 @@ 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; } @@ -245,6 +244,10 @@ bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInvent if (enableMovement && getSelectedObjectsWithinAssembly()) { moveMode = findMoveMode(); + if (moveMode == MoveMode::None) { + return false; + } + SbVec3f vec; if (moveMode == MoveMode::RotationOnPlane || moveMode == MoveMode::TranslationOnAxisAndRotationOnePlane) { @@ -346,8 +349,13 @@ bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInvent } } - auto* assemblyPart = static_cast(getObject()); - assemblyPart->solve(); + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Mod/Assembly"); + bool solveOnMove = hGrp->GetBool("SolveOnMove", true); + if (solveOnMove) { + auto* assemblyPart = static_cast(getObject()); + assemblyPart->solve(); + } } return false; } @@ -542,8 +550,11 @@ ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode() // actually move A App::DocumentObject* upstreamPart = assemblyPart->getUpstreamMovingPart(docsToMove[0].first); - docsToMove.clear(); + if (!upstreamPart) { + return MoveMode::None; + } + auto* propPlacement = dynamic_cast(upstreamPart->getPropertyByName("Placement")); if (propPlacement) { @@ -570,8 +581,7 @@ ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode() jcsGlobalPlc = global_plc * jcsPlc; // Add downstream parts so that they move together - auto downstreamParts = assemblyPart->getDownstreamParts(docsToMove[0].first); - docsToMove.clear(); // current [0] is added by the recursive getDownstreamParts. + auto downstreamParts = assemblyPart->getDownstreamParts(docsToMove[0].first, joint); for (auto part : downstreamParts) { auto* propPlacement = dynamic_cast(part->getPropertyByName("Placement")); @@ -648,6 +658,18 @@ void ViewProviderAssembly::onSelectionChanged(const Gui::SelectionChanges& msg) } } +bool ViewProviderAssembly::onDelete(const std::vector& subNames) +{ + // Delete the joingroup when assembly is deleted + for (auto obj : getObject()->getOutList()) { + if (obj->getTypeId() == Assembly::JointGroup::getClassTypeId()) { + obj->getDocument()->removeObject(obj->getNameInDocument()); + } + } + + return ViewProviderPart::onDelete(subNames); +} + PyObject* ViewProviderAssembly::getPyObject() { if (!pyViewObject) { diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.h b/src/Mod/Assembly/Gui/ViewProviderAssembly.h index 151c1ff0e1..727a934d9d 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.h +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.h @@ -53,6 +53,7 @@ public: QIcon getIcon() const override; bool doubleClicked() override; + bool onDelete(const std::vector& subNames) override; /** @name enter/exit edit mode */ //@{ @@ -77,6 +78,7 @@ public: Rotation, RotationOnPlane, TranslationOnAxisAndRotationOnePlane, + None, }; MoveMode moveMode; diff --git a/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp b/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp index 31a4090a65..5d6cffe4f1 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp @@ -18,7 +18,7 @@ * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * - ***************************************************************************/ + **************************************************************************/ #include "PreCompiled.h" diff --git a/src/Mod/Assembly/Init.py b/src/Mod/Assembly/Init.py index e8af397da3..350b5238a1 100644 --- a/src/Mod/Assembly/Init.py +++ b/src/Mod/Assembly/Init.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ # Get the Parameter Group of this module ParGrp = App.ParamGet("System parameter:Modules").GetGroup("Assembly") diff --git a/src/Mod/Assembly/InitGui.py b/src/Mod/Assembly/InitGui.py index 6968a2a350..d8493837a3 100644 --- a/src/Mod/Assembly/InitGui.py +++ b/src/Mod/Assembly/InitGui.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import Assembly_rc diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index a30854a380..6a7d31e31b 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import math @@ -79,12 +79,6 @@ def solveIfAllowed(assembly, storePrev=False): assembly.solve(storePrev) -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, assembly): self.Type = "Joint" @@ -230,13 +224,14 @@ class Joint: joint.addProperty( "App::PropertyBool", - "FirstPartConnected", + "Activated", "Joint", QT_TRANSLATE_NOOP( "App::Property", - "This indicate if the first part was connected to ground at the time of joint creation.", + "This indicate if the joint is active.", ), ) + joint.Activated = True self.setJointConnectors(joint, []) @@ -275,7 +270,7 @@ class Joint: if len(current_selection) >= 1: joint.Part1 = None - joint.FirstPartConnected = assembly.isPartConnected(current_selection[0]["part"]) + self.part1Connected = assembly.isPartConnected(current_selection[0]["part"]) joint.Object1 = current_selection[0]["object"].Name joint.Part1 = current_selection[0]["part"] @@ -290,8 +285,12 @@ class Joint: joint.Element1 = "" joint.Vertex1 = "" joint.Placement1 = App.Placement() + self.partMovedByPresolved = None if len(current_selection) >= 2: + joint.Part2 = None + self.part2Connected = assembly.isPartConnected(current_selection[1]["part"]) + joint.Object2 = current_selection[1]["object"].Name joint.Part2 = current_selection[1]["part"] joint.Element2 = current_selection[1]["element_name"] @@ -299,7 +298,13 @@ class Joint: joint.Placement2 = self.findPlacement( joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True ) - self.preventOrthogonal(joint) + self.preSolve( + joint, + current_selection[0]["object"], + joint.Part1, + current_selection[1]["object"], + joint.Part2, + ) solveIfAllowed(assembly, True) else: @@ -309,6 +314,7 @@ class Joint: joint.Vertex2 = "" joint.Placement2 = App.Placement() assembly.undoSolve() + self.undoPreSolve() def updateJCSPlacements(self, joint): if not joint.Detach1: @@ -455,34 +461,29 @@ class Joint: plc.Rotation = rot * zRotation return plc - def flipPart(self, joint): - if joint.FirstPartConnected: - plc = joint.Placement2 # relative to obj - obj = UtilsAssembly.getObjectInPart(joint.Object2, joint.Part2) + def flipPlacement(self, plc): + return self.applyRotationToPlacementAlongAxis(plc, 180, App.Vector(1, 0, 0)) - # we need plc to be relative to the containing part - obj_global_plc = UtilsAssembly.getGlobalPlacement(obj, joint.Part2) - part_global_plc = UtilsAssembly.getGlobalPlacement(joint.Part2) - plc = obj_global_plc * plc - plc = part_global_plc.inverse() * plc - - jcsXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0)) - - joint.Part2.Placement = flipPlacement(joint.Part2.Placement, jcsXAxis) + def flipOnePart(self, joint): + if hasattr(self, "part2Connected") and not self.part2Connected: + jcsPlc = UtilsAssembly.getJcsPlcRelativeToPart( + joint.Placement2, joint.Object2, joint.Part2 + ) + globalJcsPlc = UtilsAssembly.getJcsGlobalPlc( + joint.Placement2, joint.Object2, joint.Part2 + ) + jcsPlc = self.flipPlacement(jcsPlc) + joint.Part2.Placement = globalJcsPlc * jcsPlc.inverse() else: - plc = joint.Placement1 # relative to obj - obj = UtilsAssembly.getObjectInPart(joint.Object1, joint.Part1) - - # we need plc to be relative to the containing part - obj_global_plc = UtilsAssembly.getGlobalPlacement(obj, joint.Part1) - part_global_plc = UtilsAssembly.getGlobalPlacement(joint.Part1) - plc = obj_global_plc * plc - plc = part_global_plc.inverse() * plc - - jcsXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0)) - - joint.Part1.Placement = flipPlacement(joint.Part1.Placement, jcsXAxis) + jcsPlc = UtilsAssembly.getJcsPlcRelativeToPart( + joint.Placement1, joint.Object1, joint.Part1 + ) + globalJcsPlc = UtilsAssembly.getJcsGlobalPlc( + joint.Placement1, joint.Object1, joint.Part1 + ) + jcsPlc = self.flipPlacement(jcsPlc) + joint.Part1.Placement = globalJcsPlc * jcsPlc.inverse() solveIfAllowed(self.getAssembly(joint)) @@ -508,18 +509,55 @@ class Joint: return App.Vector(res[0].X, res[0].Y, res[0].Z) return surface.Center - def preventOrthogonal(self, joint): - zAxis1 = joint.Placement1.Rotation.multVec(App.Vector(0, 0, 1)) - zAxis2 = joint.Placement2.Rotation.multVec(App.Vector(0, 0, 1)) - if abs(zAxis1.dot(zAxis2)) < Part.Precision.confusion(): - if joint.FirstPartConnected: - joint.Part2.Placement = self.applyRotationToPlacementAlongAxis( - joint.Part2.Placement, 30.0, App.Vector(1, 2, 0) - ) - else: - joint.Part1.Placement = self.applyRotationToPlacementAlongAxis( - joint.Part1.Placement, 30.0, App.Vector(1, 2, 0) - ) + def preSolve(self, joint, obj1, part1, obj2, part2): + # 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. + sameDir = self.areJcsSameDir(joint) + + if hasattr(self, "part2Connected") and not self.part2Connected: + self.partMovedByPresolved = joint.Part2 + self.presolveBackupPlc = joint.Part2.Placement + + globalJcsPlc1 = UtilsAssembly.getJcsGlobalPlc( + joint.Placement1, joint.Object1, joint.Part1 + ) + jcsPlc2 = UtilsAssembly.getJcsPlcRelativeToPart( + joint.Placement2, joint.Object2, joint.Part2 + ) + if not sameDir: + jcsPlc2 = self.flipPlacement(jcsPlc2) + joint.Part2.Placement = globalJcsPlc1 * jcsPlc2.inverse() + + elif hasattr(self, "part1Connected") and not self.part1Connected: + self.partMovedByPresolved = joint.Part1 + self.presolveBackupPlc = joint.Part1.Placement + + globalJcsPlc2 = UtilsAssembly.getJcsGlobalPlc( + joint.Placement2, joint.Object2, joint.Part2 + ) + jcsPlc1 = UtilsAssembly.getJcsPlcRelativeToPart( + joint.Placement1, joint.Object1, joint.Part1 + ) + if not sameDir: + jcsPlc1 = self.flipPlacement(jcsPlc1) + joint.Part1.Placement = globalJcsPlc2 * jcsPlc1.inverse() + + def undoPreSolve( + self, + ): + if self.partMovedByPresolved: + self.partMovedByPresolved.Placement = self.presolveBackupPlc + self.partMovedByPresolved = None + + def areJcsSameDir(self, joint): + globalJcsPlc1 = UtilsAssembly.getJcsGlobalPlc(joint.Placement1, joint.Object1, joint.Part1) + globalJcsPlc2 = UtilsAssembly.getJcsGlobalPlc(joint.Placement2, joint.Object2, joint.Part2) + + zAxis1 = globalJcsPlc1.Rotation.multVec(App.Vector(0, 0, 1)) + zAxis2 = globalJcsPlc2.Rotation.multVec(App.Vector(0, 0, 1)) + return zAxis1.dot(zAxis2) > 0 class ViewProviderJoint: @@ -567,7 +605,7 @@ class ViewProviderJoint: self.switch_JCS_preview = self.JCS_sep(self.transform3) self.pick = coin.SoPickStyle() - self.pick.style.setValue(coin.SoPickStyle.UNPICKABLE) + self.setPickableState(True) self.display_mode = coin.SoType.fromName("SoFCSelection").createInstance() self.display_mode.addChild(self.pick) @@ -682,20 +720,11 @@ class ViewProviderJoint: if joint.Object2: plc = joint.Placement2 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, joint.Object2, joint.Part2) 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, objName="", part=None): if visible: self.switch_JCS_preview.whichChild = coin.SO_SWITCH_ALL @@ -763,7 +792,7 @@ class ViewProviderJoint: def doubleClicked(self, vobj): assembly = vobj.Object.InList[0] if UtilsAssembly.activeAssembly() != assembly: - Gui.ActiveDocument.ActiveView.setActiveObject("part", assembly) + Gui.ActiveDocument.setEdit(assembly) panel = TaskAssemblyCreateJoint(0, vobj.Object) Gui.Control.showDialog(panel) @@ -925,16 +954,19 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.form.rotationSpinbox.valueChanged.connect(self.onRotationChanged) self.form.PushButtonReverse.clicked.connect(self.onReverseClicked) - Gui.Selection.clearSelection() - if jointObj: + Gui.Selection.clearSelection() + self.creating = False self.joint = jointObj self.jointName = jointObj.Label App.setActiveTransaction("Edit " + self.jointName + " Joint") self.updateTaskboxFromJoint() + self.visibilityBackup = self.joint.Visibility + self.joint.Visibility = True else: + self.creating = True self.jointName = self.form.jointType.currentText().replace(" ", "") App.setActiveTransaction("Create " + self.jointName + " Joint") @@ -942,6 +974,8 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.preselection_dict = None self.createJointObject() + self.visibilityBackup = False + self.handleInitialSelection() self.toggleDistanceVisibility() self.toggleOffsetVisibility() @@ -964,13 +998,10 @@ class TaskAssemblyCreateJoint(QtCore.QObject): App.Console.PrintWarning("You need to select 2 elements from 2 separate parts.") return False - # Hide JSC's when joint is created and enable selection highlighting - # self.joint.ViewObject.Visibility = False - # self.joint.ViewObject.OnTopWhenSelected = "Enabled" - self.deactivate() solveIfAllowed(self.assembly) + self.joint.Visibility = self.visibilityBackup App.closeActiveTransaction() return True @@ -978,6 +1009,8 @@ class TaskAssemblyCreateJoint(QtCore.QObject): 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 return True def deactivate(self): @@ -996,6 +1029,64 @@ class TaskAssemblyCreateJoint(QtCore.QObject): if Gui.Control.activeDialog(): Gui.Control.closeDialog() + def handleInitialSelection(self): + selection = Gui.Selection.getSelectionEx("*", 0) + if not selection: + return + for sel in selection: + # If you select 2 solids (bodies for example) within an assembly. + # There'll be a single sel but 2 SubElementNames. + + if not sel.SubElementNames: + # no subnames, so its a root assembly itself that is selected. + Gui.Selection.removeSelection(sel.Object) + continue + + for sub_name in sel.SubElementNames: + # Only objects within the assembly. + objs_names, element_name = UtilsAssembly.getObjsNamesAndElement( + sel.ObjectName, sub_name + ) + if len(self.current_selection) >= 2 or self.assembly.Name not in objs_names: + Gui.Selection.removeSelection(sel.Object, sub_name) + continue + + obj_name = sel.ObjectName + + full_obj_name = UtilsAssembly.getFullObjName(obj_name, sub_name) + 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 + ) + + if selected_object == self.assembly: + # do not accept selection of assembly itself + Gui.Selection.removeSelection(sel.Object, sub_name) + continue + + if ( + len(self.current_selection) == 1 + and selected_object == self.current_selection[0]["object"] + ): + # do not select several feature of the same object. + Gui.Selection.removeSelection(sel.Object, sub_name) + continue + + 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, + "vertex_name": element_name, + } + + self.current_selection.append(selection_dict) + + self.updateJoint() + def createJointObject(self): type_index = self.form.jointType.currentIndex() @@ -1022,7 +1113,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.joint.Rotation = self.form.rotationSpinbox.property("rawValue") def onReverseClicked(self): - self.joint.Proxy.flipPart(self.joint) + self.joint.Proxy.flipOnePart(self.joint) def toggleDistanceVisibility(self): if self.form.jointType.currentText() in JointUsingDistance: @@ -1130,7 +1221,10 @@ class TaskAssemblyCreateJoint(QtCore.QObject): def moveMouse(self, info): if len(self.current_selection) >= 2 or ( len(self.current_selection) == 1 - and self.current_selection[0]["part"] == self.preselection_dict["part"] + and ( + not self.preselection_dict + or self.current_selection[0]["part"] == self.preselection_dict["part"] + ) ): self.joint.ViewObject.Proxy.showPreviewJCS(False) return diff --git a/src/Mod/Assembly/Preferences.py b/src/Mod/Assembly/Preferences.py index f5d9b5e57d..71c60323ef 100644 --- a/src/Mod/Assembly/Preferences.py +++ b/src/Mod/Assembly/Preferences.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import FreeCAD import FreeCADGui diff --git a/src/Mod/Assembly/TestAssemblyWorkbench.py b/src/Mod/Assembly/TestAssemblyWorkbench.py index 331d645b1d..000241de0c 100644 --- a/src/Mod/Assembly/TestAssemblyWorkbench.py +++ b/src/Mod/Assembly/TestAssemblyWorkbench.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import TestApp diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index 5f4f1a614f..6e6f4a6ea0 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# /**************************************************************************** +# /************************************************************************** # * # Copyright (c) 2023 Ondsel * # * @@ -19,7 +19,7 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import FreeCAD as App import Part @@ -237,6 +237,33 @@ def getObjectInPart(objName, part): return None +# get the placement of Obj relative to its containing Part +# Example : assembly.part1.part2.partn.body1 : placement of Obj relative to part1 +def getObjPlcRelativeToPart(objName, part): + obj = getObjectInPart(objName, part) + + # we need plc to be relative to the containing part + obj_global_plc = getGlobalPlacement(obj, part) + part_global_plc = getGlobalPlacement(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(jcsPlc, objName, part): + obj_relative_plc = getObjPlcRelativeToPart(objName, part) + return obj_relative_plc * jcsPlc + + +# Return the jcs global placement +def getJcsGlobalPlc(jcsPlc, objName, part): + obj = getObjectInPart(objName, part) + + obj_global_plc = getGlobalPlacement(obj, part) + return obj_global_plc * jcsPlc + + # 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): @@ -249,6 +276,13 @@ def getGlobalPlacement(targetObj, container=None): return App.Placement() +def isThereOneRootAssembly(): + for part in App.activeDocument().RootObjects: + if part.TypeId == "Assembly::AssemblyObject": + return True + return False + + def getTargetPlacementRelativeTo( targetObj, part, container, inContainerBranch, ignorePlacement=False ):