diff --git a/cMake/FreeCAD_Helpers/SetupLibOndselSolver.cmake b/cMake/FreeCAD_Helpers/SetupLibOndselSolver.cmake new file mode 100644 index 0000000000..9b2dda28c8 --- /dev/null +++ b/cMake/FreeCAD_Helpers/SetupLibOndselSolver.cmake @@ -0,0 +1,4 @@ +macro(SetupOndselSolverCpp) + # -------------------------------- OndselSolver -------------------------------- + find_package(OndselSolver REQUIRED) +endmacro(SetupOndselSolverCpp) diff --git a/src/3rdParty/OndselSolver b/src/3rdParty/OndselSolver index 3952f45945..fe99ad2593 160000 --- a/src/3rdParty/OndselSolver +++ b/src/3rdParty/OndselSolver @@ -1 +1 @@ -Subproject commit 3952f459457e339a0987ad2b271f3823b67323e7 +Subproject commit fe99ad259391b8fd9390f919926aa3c8b6cde787 diff --git a/src/Base/Vector3D.cpp b/src/Base/Vector3D.cpp index 88a8a37397..4d6b049634 100644 --- a/src/Base/Vector3D.cpp +++ b/src/Base/Vector3D.cpp @@ -464,6 +464,22 @@ float_type Vector3::GetAngle(const Vector3& rcVect) const return float_type(acos(dot)); } +template +float_type Vector3::GetAngleOriented(const Vector3& rcVect, const Vector3& norm) const +{ + float_type angle = GetAngle(rcVect); + + Vector3 crossProduct = Cross(rcVect); + + // Use dot product to determine the sign + float_type dot = crossProduct.Dot(norm); + if (dot < 0) { + angle = 2 * traits_type::pi() - angle; + } + + return angle; +} + template void Vector3::TransformToCoordinateSystem(const Vector3& rclBase, const Vector3& rclDirX, diff --git a/src/Base/Vector3D.h b/src/Base/Vector3D.h index de9b2d19c7..926c87aaf2 100644 --- a/src/Base/Vector3D.h +++ b/src/Base/Vector3D.h @@ -194,6 +194,9 @@ public: bool IsNull() const; /// Get angle between both vectors. The returned value lies in the interval [0,pi]. float_type GetAngle(const Vector3& rcVect) const; + /// Get oriented angle between both vectors using a normal. The returned value lies in the + /// interval [0,2*pi]. + float_type GetAngleOriented(const Vector3& rcVect, const Vector3& norm) const; /** Transforms this point to the coordinate system defined by origin \a rclBase, * vector \a vector rclDirX and vector \a vector rclDirY. * \note \a rclDirX must be perpendicular to \a rclDirY, i.e. \a rclDirX * \a rclDirY = 0.. 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/View3DInventorViewer.cpp b/src/Gui/View3DInventorViewer.cpp index f713e476b1..110ddf64a7 100644 --- a/src/Gui/View3DInventorViewer.cpp +++ b/src/Gui/View3DInventorViewer.cpp @@ -2647,6 +2647,108 @@ SbVec2f View3DInventorViewer::getNormalizedPosition(const SbVec2s& pnt) const return {pX, pY}; } +SbVec3f View3DInventorViewer::getPointOnXYPlaneOfPlacement(const SbVec2s& pnt, Base::Placement& plc) const +{ + SbVec2f pnt2d = getNormalizedPosition(pnt); + SoCamera* pCam = this->getSoRenderManager()->getCamera(); + + if (!pCam) { + // return invalid point + return {}; + } + + SbViewVolume vol = pCam->getViewVolume(); + SbLine line; + vol.projectPointToLine(pnt2d, line); + + // Calculate the plane using plc + Base::Rotation rot = plc.getRotation(); + Base::Vector3d normalVector = rot.multVec(Base::Vector3d(0, 0, 1)); + SbVec3f planeNormal(normalVector.x, normalVector.y, normalVector.z); + + // Get the position and convert Base::Vector3d to SbVec3f + Base::Vector3d pos = plc.getPosition(); + SbVec3f planePosition(pos.x, pos.y, pos.z); + SbPlane xyPlane(planeNormal, planePosition); + + SbVec3f pt; + if (xyPlane.intersect(line, pt)) { + return pt; // Intersection point on the XY plane + } + else { + // No intersection found + return {}; + } + + return pt; +} + +SbVec3f projectPointOntoPlane(const SbVec3f& point, const SbPlane& plane) { + SbVec3f planeNormal = plane.getNormal(); + float d = plane.getDistanceFromOrigin(); + float distance = planeNormal.dot(point) + d; + return point - planeNormal * distance; +} + +// Project a line onto a plane +SbLine projectLineOntoPlane(const SbVec3f& p1, const SbVec3f& p2, const SbPlane& plane) { + SbVec3f projectedPoint1 = projectPointOntoPlane(p1, plane); + SbVec3f projectedPoint2 = projectPointOntoPlane(p2, plane); + return SbLine(projectedPoint1, projectedPoint2); +} + +SbVec3f intersection(const SbVec3f& p11, const SbVec3f& p12, const SbVec3f& p21, const SbVec3f& p22) +{ + SbVec3f da = p12 - p11; + SbVec3f db = p22 - p21; + SbVec3f dc = p21 - p11; + + double s = (dc.cross(db)).dot(da.cross(db)) / da.cross(db).sqrLength(); + return p11 + da * s; +} + +SbVec3f View3DInventorViewer::getPointOnLine(const SbVec2s& pnt, const SbVec3f& axisCenter, const SbVec3f& axis) const +{ + SbVec2f pnt2d = getNormalizedPosition(pnt); + SoCamera* pCam = this->getSoRenderManager()->getCamera(); + + if (!pCam) { + // return invalid point + return {}; + } + + // First we get pnt projection on the focal plane + SbViewVolume vol = pCam->getViewVolume(); + + float nearDist = pCam->nearDistance.getValue(); + float farDist = pCam->farDistance.getValue(); + float focalDist = pCam->focalDistance.getValue(); + + if (focalDist < nearDist || focalDist > farDist) { + focalDist = 0.5F * (nearDist + farDist); // NOLINT + } + + SbLine line; + SbVec3f pt, ptOnFocalPlaneAndOnLine, ptOnFocalPlane; + SbPlane focalPlane = vol.getPlane(focalDist); + vol.projectPointToLine(pnt2d, line); + focalPlane.intersect(line, ptOnFocalPlane); + + SbLine projectedLine = projectLineOntoPlane(axisCenter, axisCenter + axis, focalPlane); + ptOnFocalPlaneAndOnLine = projectedLine.getClosestPoint(ptOnFocalPlane); + + // now we need the intersection point between + // - the line passing by ptOnFocalPlaneAndOnLine normal to focalPlane + // - The line (axisCenter, axisCenter + axis) + + // Line normal to focal plane through ptOnFocalPlane + SbLine normalLine(ptOnFocalPlane, ptOnFocalPlane + focalPlane.getNormal()); + SbLine axisLine(axisCenter, axisCenter + axis); + pt = intersection(ptOnFocalPlane, ptOnFocalPlane + focalPlane.getNormal(), axisCenter, axisCenter + axis); + + return pt; +} + SbVec3f View3DInventorViewer::getPointOnFocalPlane(const SbVec2s& pnt) const { SbVec2f pnt2d = getNormalizedPosition(pnt); @@ -3975,4 +4077,4 @@ void View3DInventorViewer::dragLeaveEvent(QDragLeaveEvent* ev) inherited::dragLeaveEvent(ev); } -#include "moc_View3DInventorViewer.cpp" +#include "moc_View3DInventorViewer.cpp" // NOLINT diff --git a/src/Gui/View3DInventorViewer.h b/src/Gui/View3DInventorViewer.h index 5efa45de4f..221ad4ba95 100644 --- a/src/Gui/View3DInventorViewer.h +++ b/src/Gui/View3DInventorViewer.h @@ -318,6 +318,12 @@ public: /** Returns the 3d point on the focal plane to the given 2d point. */ SbVec3f getPointOnFocalPlane(const SbVec2s&) const; + /** Returns the 3d point on a line to the given 2d point. */ + SbVec3f getPointOnLine(const SbVec2s&, const SbVec3f& axisCenter, const SbVec3f& axis) const; + + /** Returns the 3d point on the XY plane of a placement to the given 2d point. */ + SbVec3f getPointOnXYPlaneOfPlacement(const SbVec2s&, Base::Placement&) const; + /** Returns the 2d coordinates on the viewport to the given 3d point. */ SbVec2s getPointOnViewport(const SbVec3f&) const; 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/AppAssembly.cpp b/src/Mod/Assembly/App/AppAssembly.cpp new file mode 100644 index 0000000000..c73899dd84 --- /dev/null +++ b/src/Mod/Assembly/App/AppAssembly.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#include +#include +#include + +#include "AssemblyObject.h" +#include "JointGroup.h" + + +namespace Assembly +{ +extern PyObject* initModule(); +} + +/* Python entry */ +PyMOD_INIT_FUNC(AssemblyApp) +{ + // load dependent module + try { + Base::Interpreter().runString("import Part"); + } + catch (const Base::Exception& e) { + PyErr_SetString(PyExc_ImportError, e.what()); + PyMOD_Return(nullptr); + } + + PyObject* mod = Assembly::initModule(); + Base::Console().Log("Loading Assembly module... done\n"); + + + // NOTE: To finish the initialization of our own type objects we must + // call PyType_Ready, otherwise we run into a segmentation fault, later on. + // This function is responsible for adding inherited slots from a type's base class. + + Assembly::AssemblyObject ::init(); + Assembly::JointGroup ::init(); + + PyMOD_Return(mod); +} diff --git a/src/Mod/Assembly/App/AppAssemblyPy.cpp b/src/Mod/Assembly/App/AppAssemblyPy.cpp new file mode 100644 index 0000000000..f62cf30c8f --- /dev/null +++ b/src/Mod/Assembly/App/AppAssemblyPy.cpp @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#include +#include + + +namespace Assembly +{ +class Module: public Py::ExtensionModule +{ +public: + Module() + : Py::ExtensionModule("AssemblyApp") + { + initialize("This module is the Assembly module."); // register with Python + } +}; + +PyObject* initModule() +{ + return Base::Interpreter().addModule(new Module); +} + +} // namespace Assembly diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp new file mode 100644 index 0000000000..359d00ef4c --- /dev/null +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -0,0 +1,1707 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" +#ifndef _PreComp_ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AssemblyObject.h" +#include "AssemblyObjectPy.h" +#include "JointGroup.h" + +namespace PartApp = Part; + +using namespace Assembly; +using namespace MbD; + +// ================================ Assembly Object ============================ + +PROPERTY_SOURCE(Assembly::AssemblyObject, App::Part) + +AssemblyObject::AssemblyObject() + : mbdAssembly(std::make_shared()) +{} + +AssemblyObject::~AssemblyObject() = default; + +PyObject* AssemblyObject::getPyObject() +{ + if (PythonObject.is(Py::_None())) { + // ref counter is set to 1 + PythonObject = Py::Object(new AssemblyObjectPy(this), true); + } + return Py::new_reference_to(PythonObject); +} + + +int AssemblyObject::solve(bool enableRedo) +{ + mbdAssembly = makeMbdAssembly(); + objectPartMap.clear(); + + 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(); + } + catch (...) { + Base::Console().Error("Solve failed\n"); + return -1; + } + + setNewPlacements(); + + redrawJointPlacements(joints); + + return 0; +} + +void AssemblyObject::preDrag(std::vector dragParts) +{ + solve(); + + dragMbdParts.clear(); + for (auto part : dragParts) { + dragMbdParts.push_back(getMbDPart(part)); + } + + mbdAssembly->runPreDrag(); +} + +void AssemblyObject::doDragStep() +{ + for (auto& mbdPart : dragMbdParts) { + App::DocumentObject* part = nullptr; + for (auto& pair : objectPartMap) { + if (pair.second == mbdPart) { + part = pair.first; + break; + } + } + if (!part) { + continue; + } + + Base::Placement plc = getPlacementFromProp(part, "Placement"); + Base::Vector3d pos = plc.getPosition(); + mbdPart->setPosition3D(pos.x, pos.y, pos.z); + + 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); + mbdPart->setRotationMatrix(r0.x, r0.y, r0.z, r1.x, r1.y, r1.z, r2.x, r2.y, r2.z); + } + + auto dragPartsVec = std::make_shared>>(dragMbdParts); + mbdAssembly->runDragStep(dragPartsVec); + setNewPlacements(); + redrawJointPlacements(getJoints()); +} + +void AssemblyObject::postDrag() +{ + mbdAssembly->runPostDrag(); // Do this after last drag +} + +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() +{ + if (previousPositions.size() == 0) { + return; + } + + 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::exportAsASMT(std::string fileName) +{ + mbdAssembly = makeMbdAssembly(); + objectPartMap.clear(); + fixGroundedParts(); + + std::vector joints = getJoints(); + + jointParts(joints); + + mbdAssembly->outputFile(fileName); +} + +void AssemblyObject::setNewPlacements() +{ + for (auto& pair : objectPartMap) { + App::DocumentObject* obj = pair.first; + std::shared_ptr mbdPart = pair.second; + + if (!obj || !mbdPart) { + continue; + } + + // Check if the object has a "Placement" property + auto* propPlacement = + dynamic_cast(obj->getPropertyByName("Placement")); + 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); + } +} + +void AssemblyObject::redrawJointPlacements(std::vector joints) +{ + // Notify the joint objects that the transform of the coin object changed. + for (auto* joint : joints) { + auto* propPlacement = + dynamic_cast(joint->getPropertyByName("Placement1")); + if (propPlacement) { + propPlacement->setValue(propPlacement->getValue()); + } + propPlacement = + dynamic_cast(joint->getPropertyByName("Placement2")); + if (propPlacement) { + propPlacement->setValue(propPlacement->getValue()); + } + } +} + +void AssemblyObject::recomputeJointPlacements(std::vector joints) +{ + // The Placement1 and Placement2 of each joint needs to be updated as the parts moved. + for (auto* joint : joints) { + App::PropertyPythonObject* proxy = joint + ? dynamic_cast(joint->getPropertyByName("Proxy")) + : nullptr; + + if (!proxy) { + continue; + } + + Py::Object jointPy = proxy->getValue(); + + if (!jointPy.hasAttr("updateJCSPlacements")) { + continue; + } + + 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); + } + } +} + +std::shared_ptr AssemblyObject::makeMbdAssembly() +{ + auto assembly = CREATE::With(); + assembly->setName("OndselAssembly"); + + return assembly; +} + +App::DocumentObject* AssemblyObject::getJointOfPartConnectingToGround(App::DocumentObject* part, + std::string& name) +{ + std::vector joints = getJointsOfPart(part); + + for (auto joint : joints) { + if (!joint) { + continue; + } + App::DocumentObject* part1 = getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* part2 = getLinkObjFromProp(joint, "Part2"); + if (!part1 || !part2) { + continue; + } + + if (part == part1 && isJointConnectingPartToGround(joint, "Part1")) { + name = "Part1"; + return joint; + } + if (part == part2 && isJointConnectingPartToGround(joint, "Part2")) { + name = "Part2"; + return joint; + } + } + + return nullptr; +} + +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::getJoints(bool updateJCS) +{ + std::vector joints = {}; + + JointGroup* jointGroup = getJointGroup(); + if (!jointGroup) { + return {}; + } + + Base::PyGILStateLocker lock; + for (auto joint : jointGroup->getObjects()) { + if (!joint) { + continue; + } + + auto* prop = dynamic_cast(joint->getPropertyByName("Activated")); + if (prop && !prop->getValue()) { + continue; + } + + auto proxy = dynamic_cast(joint->getPropertyByName("Proxy")); + if (proxy) { + if (proxy->getValue().hasAttr("setJointConnectors")) { + joints.push_back(joint); + } + } + } + + // Make sure the joints are up to date. + if (updateJCS) { + recomputeJointPlacements(joints); + } + + return joints; +} + +std::vector AssemblyObject::getGroundedJoints() +{ + std::vector joints = {}; + + JointGroup* jointGroup = getJointGroup(); + if (!jointGroup) { + return {}; + } + + Base::PyGILStateLocker lock; + for (auto obj : jointGroup->getObjects()) { + if (!obj) { + continue; + } + + auto* propObj = dynamic_cast(obj->getPropertyByName("ObjectToGround")); + + if (propObj) { + joints.push_back(obj); + } + } + + return joints; +} + +std::vector AssemblyObject::getJointsOfObj(App::DocumentObject* obj) +{ + std::vector joints = getJoints(false); + std::vector jointsOf; + + for (auto joint : joints) { + App::DocumentObject* obj1 = getObjFromNameProp(joint, "object1", "Part1"); + App::DocumentObject* obj2 = getObjFromNameProp(joint, "Object2", "Part2"); + if (obj == obj1 || obj == obj2) { + jointsOf.push_back(obj); + } + } + + return jointsOf; +} + +std::vector AssemblyObject::getJointsOfPart(App::DocumentObject* part) +{ + std::vector joints = getJoints(false); + std::vector jointsOf; + + for (auto joint : joints) { + App::DocumentObject* part1 = getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* part2 = getLinkObjFromProp(joint, "Part2"); + if (part == part1 || part == part2) { + jointsOf.push_back(joint); + } + } + + return jointsOf; +} + +std::vector AssemblyObject::getGroundedParts() +{ + std::vector groundedJoints = getGroundedJoints(); + + std::vector groundedObjs; + for (auto gJoint : groundedJoints) { + if (!gJoint) { + continue; + } + + auto* propObj = + dynamic_cast(gJoint->getPropertyByName("ObjectToGround")); + + if (propObj) { + App::DocumentObject* objToGround = propObj->getValue(); + groundedObjs.push_back(objToGround); + } + } + return groundedObjs; +} + +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, + Base::Placement& plc, + std::string& name) +{ + std::string markerName1 = "marker-" + obj->getFullName(); + auto mbdMarker1 = makeMbdMarker(markerName1, plc); + mbdAssembly->addMarker(mbdMarker1); + + std::shared_ptr mbdPart = getMbDPart(obj); + + std::string markerName2 = "FixingMarker"; + Base::Placement basePlc = Base::Placement(); + auto mbdMarker2 = makeMbdMarker(markerName2, basePlc); + mbdPart->addMarker(mbdMarker2); + + markerName1 = "/OndselAssembly/" + mbdMarker1->name; + markerName2 = "/OndselAssembly/" + mbdPart->name + "/" + mbdMarker2->name; + + auto mbdJoint = CREATE::With(); + mbdJoint->setName(name); + mbdJoint->setMarkerI(markerName1); + mbdJoint->setMarkerJ(markerName2); + + mbdAssembly->addJoint(mbdJoint); +} + +bool AssemblyObject::isJointConnectingPartToGround(App::DocumentObject* joint, const char* propname) +{ + + auto* propPart = dynamic_cast(joint->getPropertyByName(propname)); + if (!propPart) { + return false; + } + App::DocumentObject* part = propPart->getValue(); + // Check if the part is disconnected even with the joint + bool isConnected = isPartConnected(part); + if (!isConnected) { + return false; + } + + // 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); + + // restore activation states + for (auto jointi : jointsOfPart) { + if (jointi->getFullName() == joint->getFullName() || activatedStates.empty()) { + continue; + } + + setJointActivated(jointi, activatedStates[0]); + activatedStates.erase(activatedStates.begin()); + } + + return isConnected; +} + +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::isPartGrounded(App::DocumentObject* obj) +{ + std::vector groundedObjs = fixGroundedParts(); + + for (auto* groundedObj : groundedObjs) { + if (groundedObj->getFullName() == obj->getFullName()) { + return true; + } + } + + return false; +} + +bool AssemblyObject::isPartConnected(App::DocumentObject* obj) +{ + std::vector groundedObjs = getGroundedParts(); + std::vector joints = getJoints(false); + + 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; +} + +void AssemblyObject::jointParts(std::vector joints) +{ + for (auto* joint : joints) { + if (!joint) { + continue; + } + + std::vector> mbdJoints = makeMbdJoint(joint); + for (auto& mbdJoint : mbdJoints) { + mbdAssembly->addJoint(mbdJoint); + } + } +} + +std::shared_ptr AssemblyObject::makeMbdJointOfType(App::DocumentObject* joint, + JointType type) +{ + if (type == JointType::Fixed) { + return CREATE::With(); + } + else if (type == JointType::Revolute) { + return CREATE::With(); + } + else if (type == JointType::Cylindrical) { + return CREATE::With(); + } + else if (type == JointType::Slider) { + return CREATE::With(); + } + else if (type == JointType::Ball) { + return CREATE::With(); + } + else if (type == JointType::Distance) { + return makeMbdJointDistance(joint); + } + + return nullptr; +} + +std::shared_ptr AssemblyObject::makeMbdJointDistance(App::DocumentObject* joint) +{ + // 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"); + + 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); + } + + return nullptr; +} + +std::shared_ptr AssemblyObject::makeMbdJointDistanceEdgeEdge(App::DocumentObject* joint) +{ + const char* elt1 = getElementFromProp(joint, "Element1"); + const char* elt2 = getElementFromProp(joint, "Element2"); + auto* obj1 = getLinkedObjFromNameProp(joint, "Object1", "Part1"); + auto* obj2 = getLinkedObjFromNameProp(joint, "Object2", "Part2"); + + 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 = getLinkedObjFromNameProp(joint, "Object1", "Part1"); + auto* obj2 = getLinkedObjFromNameProp(joint, "Object2", "Part2"); + + 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 = getLinkedObjFromNameProp(joint, "Object1", "Part1"); + + 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 = getLinkedObjFromNameProp(joint, "Object1", "Part1"); + + 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 = getLinkedObjFromNameProp(joint, "Object2", "Part2"); + + 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, "Object1", "Part1", "Placement1"); + std::string fullMarkerName2 = handleOneSideOfJoint(joint, "Object2", "Part2", "Placement2"); + + mbdJoint->setName(joint->getFullName()); + mbdJoint->setMarkerI(fullMarkerName1); + mbdJoint->setMarkerJ(fullMarkerName2); + + return {mbdJoint}; +} + +std::string AssemblyObject::handleOneSideOfJoint(App::DocumentObject* joint, + const char* propObjName, + const char* propPartName, + const char* propPlcName) +{ + App::DocumentObject* part = getLinkObjFromProp(joint, propPartName); + App::DocumentObject* obj = getObjFromNameProp(joint, propObjName, propPartName); + + std::shared_ptr mbdPart = getMbDPart(part); + Base::Placement partPlc = getPlacementFromProp(part, "Placement"); + 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 Object, not to the + // containing Part. + + if (obj->getNameInDocument() != part->getNameInDocument()) { + // Make plc relative to the containing part + // plc = objPlc * plc; // this would not work for nested parts. + + Base::Placement obj_global_plc = getGlobalPlacement(obj, part); + plc = obj_global_plc * plc; + + Base::Placement part_global_plc = getGlobalPlacement(part); + plc = part_global_plc.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) +{ + std::shared_ptr mbdPart; + + Base::Placement plc = getPlacementFromProp(obj, "Placement"); + + auto it = objectPartMap.find(obj); + if (it != objectPartMap.end()) { + // obj has been associated with an ASMTPart before + mbdPart = it->second; + } + else { + // obj has not been associated with an ASMTPart before + std::string str = obj->getFullName(); + mbdPart = makeMbdPart(str, plc); + mbdAssembly->addPart(mbdPart); + objectPartMap[obj] = mbdPart; // Store the association + } + + return mbdPart; +} + +std::shared_ptr +AssemblyObject::makeMbdPart(std::string& name, Base::Placement plc, double mass) +{ + auto mbdPart = CREATE::With(); + mbdPart->setName(name); + + auto massMarker = CREATE::With(); + massMarker->setMass(mass); + massMarker->setDensity(1.0); + massMarker->setMomentOfInertias(1.0, 1.0, 1.0); + mbdPart->setPrincipalMassMarker(massMarker); + + Base::Vector3d pos = plc.getPosition(); + mbdPart->setPosition3D(pos.x, pos.y, pos.z); + // Base::Console().Warning("MbD Part placement : (%f, %f, %f)\n", 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); + mbdPart->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); + mbdPart->setQuarternions(q0, q1, q2, q3);*/ + + return mbdPart; +} + +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::vector AssemblyObject::getDownstreamParts(App::DocumentObject* part, + App::DocumentObject* joint) +{ + // 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); // ?????????????????????????????? if we remove + // connection to ground then it can't work for tom + std::vector jointsOfPart = getJointsOfPart(part); + + // remove connectingJoint from jointsOfPart + auto it = std::remove(jointsOfPart.begin(), jointsOfPart.end(), connectingJoint); + jointsOfPart.erase(it, jointsOfPart.end()); + for (auto joint : jointsOfPart) { + App::DocumentObject* part1 = getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* part2 = getLinkObjFromProp(joint, "Part2"); + 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); + for (auto downPart : subDownstreamParts) { + if (std::find(downstreamParts.begin(), downstreamParts.end(), downPart) + == downstreamParts.end()) { + downstreamParts.push_back(downPart); + } + } + }*/ + return downstreamParts; +} + +std::vector AssemblyObject::getUpstreamParts(App::DocumentObject* part, + int limit) +{ + if (limit > 1000) { // Inifinite loop protection + return {}; + } + limit++; + + if (isPartGrounded(part)) { + return {part}; + } + + std::string name; + App::DocumentObject* connectingJoint = getJointOfPartConnectingToGround(part, name); + App::DocumentObject* upPart = + getLinkObjFromProp(connectingJoint, name == "Part1" ? "Part2" : "Part1"); + + std::vector upstreamParts = getUpstreamParts(upPart, limit); + upstreamParts.push_back(part); + return upstreamParts; +} + +App::DocumentObject* AssemblyObject::getUpstreamMovingPart(App::DocumentObject* part) +{ + if (isPartGrounded(part)) { + return nullptr; + } + + std::string name; + App::DocumentObject* connectingJoint = getJointOfPartConnectingToGround(part, name); + JointType jointType = getJointType(connectingJoint); + if (jointType != JointType::Fixed) { + return part; + } + + App::DocumentObject* upPart = + getLinkObjFromProp(connectingJoint, name == "Part1" ? "Part2" : "Part1"); + + return getUpstreamMovingPart(upPart); +} + +double AssemblyObject::getObjMass(App::DocumentObject* obj) +{ + for (auto& pair : objMasses) { + if (pair.first == obj) { + return pair.second; + } + } + return 1.0; +} + +void AssemblyObject::setObjMasses(std::vector> objectMasses) +{ + objMasses = objectMasses; +} + +// ======================================= Utils ====================================== + +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 = std::string(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); + } +} + +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; +} + +bool AssemblyObject::isFaceType(App::DocumentObject* obj, + const char* elName, + GeomAbs_SurfaceType type) +{ + auto base = static_cast(obj); + 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; +} + +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; +} + +void printPlacement(Base::Placement plc, const char* name) +{ + Base::Vector3d pos = plc.getPosition(); + Base::Vector3d axis; + double angle; + Base::Rotation rot = plc.getRotation(); + rot.getRawValue(axis, angle); + Base::Console().Warning( + "placement %s : position (%.1f, %.1f, %.1f) - axis (%.1f, %.1f, %.1f) angle %.1f\n", + name, + pos.x, + pos.y, + pos.z, + axis.x, + axis.y, + axis.z, + 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(); + auto* propPlacement = dynamic_cast(obj->getPropertyByName(propName)); + if (propPlacement) { + plc = propPlacement->getValue(); + } + return plc; +} + +bool AssemblyObject::getTargetPlacementRelativeTo(Base::Placement& foundPlc, + App::DocumentObject* targetObj, + App::DocumentObject* part, + App::DocumentObject* container, + bool inContainerBranch, + bool ignorePlacement) +{ + inContainerBranch = inContainerBranch || (!ignorePlacement && part == container); + + if (targetObj == part && inContainerBranch && !ignorePlacement) { + foundPlc = getPlacementFromProp(targetObj, "Placement"); + return true; + } + + if (part->isDerivedFrom(App::DocumentObjectGroup::getClassTypeId())) { + for (auto& obj : part->getOutList()) { + bool found = getTargetPlacementRelativeTo(foundPlc, + targetObj, + obj, + container, + inContainerBranch, + ignorePlacement); + if (found) { + return true; + } + } + } + else if (part->isDerivedFrom(Assembly::AssemblyObject::getClassTypeId()) + || part->isDerivedFrom(App::Part::getClassTypeId()) + || part->isDerivedFrom(PartDesign::Body::getClassTypeId())) { + for (auto& obj : part->getOutList()) { + bool found = getTargetPlacementRelativeTo(foundPlc, + targetObj, + obj, + container, + inContainerBranch); + if (!found) { + continue; + } + + if (!ignorePlacement) { + foundPlc = getPlacementFromProp(part, "Placement") * foundPlc; + } + + return true; + } + } + else if (auto link = dynamic_cast(part)) { + auto linked_obj = link->getLinkedObject(); + + if (dynamic_cast(linked_obj) || dynamic_cast(linked_obj)) { + for (auto& obj : linked_obj->getOutList()) { + bool found = getTargetPlacementRelativeTo(foundPlc, + targetObj, + obj, + container, + inContainerBranch); + if (!found) { + continue; + } + + foundPlc = getPlacementFromProp(link, "Placement") * foundPlc; + return true; + } + } + + bool found = getTargetPlacementRelativeTo(foundPlc, + targetObj, + linked_obj, + container, + inContainerBranch, + true); + + if (found) { + if (!ignorePlacement) { + foundPlc = getPlacementFromProp(link, "Placement") * foundPlc; + } + + return true; + } + } + + return false; +} + +Base::Placement AssemblyObject::getGlobalPlacement(App::DocumentObject* targetObj, + App::DocumentObject* container) +{ + bool inContainerBranch = (container == nullptr); + auto rootObjects = App::GetApplication().getActiveDocument()->getRootObjects(); + for (auto& part : rootObjects) { + Base::Placement foundPlc; + bool found = + getTargetPlacementRelativeTo(foundPlc, targetObj, part, container, inContainerBranch); + if (found) { + return foundPlc; + } + } + + return Base::Placement(); +} + +Base::Placement AssemblyObject::getGlobalPlacement(App::DocumentObject* joint, + const char* targetObj, + const char* container) +{ + App::DocumentObject* obj = getObjFromNameProp(joint, targetObj, container); + App::DocumentObject* part = getLinkObjFromProp(joint, container); + return getGlobalPlacement(obj, part); +} + +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::getObjFromNameProp(App::DocumentObject* joint, + const char* pObjName, + const char* pPart) +{ + auto* propObjName = dynamic_cast(joint->getPropertyByName(pObjName)); + if (!propObjName) { + return nullptr; + } + std::string objName = std::string(propObjName->getValue()); + + App::DocumentObject* containingPart = getLinkObjFromProp(joint, pPart); + if (!containingPart) { + return nullptr; + } + + if (objName == containingPart->getNameInDocument()) { + return containingPart; + } + + /*if (containingPart->getTypeId().isDerivedFrom(App::Link::getClassTypeId())) { + App::Link* link = dynamic_cast(containingPart); + + containingPart = link->getLinkedObject(); + if (!containingPart) { + return nullptr; + } + }*/ + + for (auto obj : containingPart->getOutListRecursive()) { + if (objName == obj->getNameInDocument()) { + return obj; + } + } + + return nullptr; +} + +App::DocumentObject* AssemblyObject::getLinkedObjFromNameProp(App::DocumentObject* joint, + const char* pObjName, + const char* pPart) +{ + auto* obj = getObjFromNameProp(joint, pObjName, pPart); + if (obj) { + return obj->getLinkedObject(true); + } + return nullptr; +} + + +/*void Part::handleChangedPropertyType(Base::XMLReader& reader, const char* TypeName, App::Property* +prop) +{ + App::Part::handleChangedPropertyType(reader, TypeName, prop); +}*/ + +/* Apparently not necessary as App::Part doesn't have this. +// Python Assembly feature --------------------------------------------------------- + +namespace App +{ + /// @cond DOXERR + PROPERTY_SOURCE_TEMPLATE(Assembly::AssemblyObjectPython, Assembly::AssemblyObject) + template<> + const char* Assembly::AssemblyObjectPython::getViewProviderName() const + { + return "AssemblyGui::ViewProviderAssembly"; + } + template<> + PyObject* Assembly::AssemblyObjectPython::getPyObject() + { + if (PythonObject.is(Py::_None())) { + // ref counter is set to 1 + PythonObject = Py::Object(new FeaturePythonPyT(this), true); + } + return Py::new_reference_to(PythonObject); + } + /// @endcond + + // explicit template instantiation + template class AssemblyExport FeaturePythonT; +}// namespace App*/ diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h new file mode 100644 index 0000000000..0bf11a3fe6 --- /dev/null +++ b/src/Mod/Assembly/App/AssemblyObject.h @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + + +#ifndef ASSEMBLY_AssemblyObject_H +#define ASSEMBLY_AssemblyObject_H + + +#include +#include + +#include + +#include +#include +#include + +namespace MbD +{ +class ASMTPart; +class ASMTAssembly; +class ASMTJoint; +class ASMTMarker; +class ASMTPart; +} // namespace MbD + +namespace Base +{ +class Placement; +class Rotation; +} // namespace Base + + +namespace Assembly +{ + +class JointGroup; + +// This enum has to be the same as the one in JointObject.py +enum class JointType +{ + Fixed, + Revolute, + Cylindrical, + Slider, + Ball, + Distance +}; + +class AssemblyExport AssemblyObject: public App::Part +{ + PROPERTY_HEADER_WITH_OVERRIDE(Assembly::AssemblyObject); + +public: + AssemblyObject(); + ~AssemblyObject() override; + + PyObject* getPyObject() override; + + /// returns the type name of the ViewProvider + const char* getViewProviderName() const override + { + return "AssemblyGui::ViewProviderAssembly"; + } + + /* Solve the assembly. It will update first the joints, solve, update placements of the parts + and redraw the joints Args : enableRedo : This store initial positions to enable undo while + being in an active transaction (joint creation).*/ + int solve(bool enableRedo = false); + void preDrag(std::vector dragParts); + void doDragStep(); + void postDrag(); + void savePlacementsForUndo(); + void undoSolve(); + void clearUndo(); + + void exportAsASMT(std::string fileName); + + void setNewPlacements(); + void recomputeJointPlacements(std::vector joints); + void redrawJointPlacements(std::vector joints); + + + // Ondsel Solver interface + 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::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, + const char* propObjLinkName, + const char* propPartName, + const char* propPlcName); + + void jointParts(std::vector joints); + JointGroup* getJointGroup(); + std::vector getJoints(bool updateJCS = true); + std::vector getGroundedJoints(); + std::vector getJointsOfObj(App::DocumentObject* obj); + std::vector getJointsOfPart(App::DocumentObject* part); + App::DocumentObject* getJointOfPartConnectingToGround(App::DocumentObject* part, + std::string& name); + std::vector getGroundedParts(); + std::vector fixGroundedParts(); + void fixGroundedPart(App::DocumentObject* obj, Base::Placement& plc, std::string& jointName); + + bool isJointConnectingPartToGround(App::DocumentObject* joint, const char* partPropName); + + 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); + bool isPartGrounded(App::DocumentObject* part); + bool isPartConnected(App::DocumentObject* part); + + std::vector getDownstreamParts(App::DocumentObject* part, + App::DocumentObject* joint); + std::vector getUpstreamParts(App::DocumentObject* part, int limit = 0); + App::DocumentObject* getUpstreamMovingPart(App::DocumentObject* part); + + double getObjMass(App::DocumentObject* obj); + void setObjMasses(std::vector> objectMasses); + + +private: + std::shared_ptr mbdAssembly; + + std::unordered_map> objectPartMap; + std::vector> objMasses; + std::vector> dragMbdParts; + + std::vector> previousPositions; + + // void handleChangedPropertyType(Base::XMLReader &reader, const char *TypeName, App::Property + // *prop) override; + +public: + // ---------------- Utils ------------------- + // Can't put the functions by themselves in AssemblyUtils.cpp : + // see https://forum.freecad.org/viewtopic.php?p=729577#p729577 + + void swapJCS(App::DocumentObject* joint); + + 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 + 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); + static std::string getElementTypeFromProp(App::DocumentObject* obj, const char* propName); + static App::DocumentObject* getLinkObjFromProp(App::DocumentObject* joint, + const char* propName); + static App::DocumentObject* + getObjFromNameProp(App::DocumentObject* joint, const char* pObjName, const char* pPart); + static App::DocumentObject* + getLinkedObjFromNameProp(App::DocumentObject* joint, const char* pObjName, const char* pPart); + static Base::Placement getPlacementFromProp(App::DocumentObject* obj, const char* propName); + static bool getTargetPlacementRelativeTo(Base::Placement& foundPlc, + App::DocumentObject* targetObj, + App::DocumentObject* part, + App::DocumentObject* container, + bool inContainerBranch, + bool ignorePlacement = false); + static Base::Placement getGlobalPlacement(App::DocumentObject* targetObj, + App::DocumentObject* container = nullptr); + static Base::Placement getGlobalPlacement(App::DocumentObject* joint, + const char* targetObj, + const char* container = ""); +}; + +// using AssemblyObjectPython = App::FeaturePythonT; + +} // namespace Assembly + + +#endif // ASSEMBLY_AssemblyObject_H diff --git a/src/Mod/Assembly/App/AssemblyObjectPy.xml b/src/Mod/Assembly/App/AssemblyObjectPy.xml new file mode 100644 index 0000000000..7ddee537dd --- /dev/null +++ b/src/Mod/Assembly/App/AssemblyObjectPy.xml @@ -0,0 +1,91 @@ + + + + + + This class handles document objects in Assembly + + + + + Solve the assembly and update part placements. + + 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 + priority: + -6 if no parts are fixed. + -4 if over-constrained, + -3 if conflicting constraints, + -5 if malformed constraints + -1 if solver error, + -2 if redundant constraints. + + + + + + + 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 + + + + + + + Export the assembly in a text format called ASMT. + + exportAsASMT(fileName:str) + + Args: + fileName: The name of the file where the ASMT will be exported. + + + + + + + diff --git a/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp b/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp new file mode 100644 index 0000000000..abe4a4428c --- /dev/null +++ b/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp @@ -0,0 +1,118 @@ +/*************************************************************************** + * Copyright (c) 2014 Jürgen Riegel * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +// inclusion of the generated files (generated out of AssemblyObject.xml) +#include "AssemblyObjectPy.h" +#include "AssemblyObjectPy.cpp" + +using namespace Assembly; + +// returns a string which represents the object e.g. when printed in python +std::string AssemblyObjectPy::representation() const +{ + return {""}; +} + +PyObject* AssemblyObjectPy::getCustomAttributes(const char* /*attr*/) const +{ + return nullptr; +} + +int AssemblyObjectPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/) +{ + return 0; +} + +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; + } + 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) +{ + char* utf8Name; + if (!PyArg_ParseTuple(args, "et", "utf-8", &utf8Name)) { + return nullptr; + } + + std::string fileName = utf8Name; + PyMem_Free(utf8Name); + + if (fileName.empty()) { + PyErr_SetString(PyExc_ValueError, "Passed string is empty"); + return nullptr; + } + + this->getAssemblyObjectPtr()->exportAsASMT(fileName); + + Py_Return; +} diff --git a/src/Mod/Assembly/App/AssemblyUtils.cpp b/src/Mod/Assembly/App/AssemblyUtils.cpp new file mode 100644 index 0000000000..4c495628ac --- /dev/null +++ b/src/Mod/Assembly/App/AssemblyUtils.cpp @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" +#ifndef _PreComp_ +#endif + +#include +#include +#include +#include +// #include +#include +// #include +#include +#include +#include + +#include + +#include "AssemblyUtils.h" + +// ======================================= Utils ====================================== +/* +namespace Assembly +{ + +Base::Placement 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; +} + +/* // Currently unused +Base::Placement* getTargetPlacementRelativeTo( + App::DocumentObject* targetObj, App::DocumentObject* part, App::DocumentObject* container, + bool inContainerBranch, bool ignorePlacement = false) +{ + inContainerBranch = inContainerBranch || (!ignorePlacement && part == container); + + Base::Console().Warning("sub --------------\n"); + if (targetObj == part && inContainerBranch && !ignorePlacement) { + Base::Console().Warning("found0\n"); + return &getPlacementFromProp(targetObj, "Placement"); + } + + if (auto group = dynamic_cast(part)) { + for (auto& obj : group->getOutList()) { + auto foundPlacement = getTargetPlacementRelativeTo( + targetObj, obj, container, inContainerBranch, ignorePlacement + ); + if (foundPlacement != nullptr) { + return foundPlacement; + } + } + } + else if (auto assembly = dynamic_cast(part)) { + Base::Console().Warning("h3\n"); + for (auto& obj : assembly->getOutList()) { + auto foundPlacement = getTargetPlacementRelativeTo( + targetObj, obj, container, inContainerBranch + ); + if (foundPlacement == nullptr) { + continue; + } + + if (!ignorePlacement) { + *foundPlacement = getPlacementFromProp(part, "Placement") * *foundPlacement; + } + + Base::Console().Warning("found\n"); + return foundPlacement; + } + } + else if (auto link = dynamic_cast(part)) { + Base::Console().Warning("h4\n"); + auto linked_obj = link->getLinkedObject(); + + if (dynamic_cast(linked_obj) || dynamic_cast(linked_obj)) { + for (auto& obj : linked_obj->getOutList()) { + auto foundPlacement = getTargetPlacementRelativeTo( + targetObj, obj, container, inContainerBranch + ); + if (foundPlacement == nullptr) { + continue; + } + + *foundPlacement = getPlacementFromProp(link, "Placement") * *foundPlacement; + return foundPlacement; + } + } + + auto foundPlacement = getTargetPlacementRelativeTo( + targetObj, linked_obj, container, inContainerBranch, true + ); + + if (foundPlacement != nullptr && !ignorePlacement) { + *foundPlacement = getPlacementFromProp(link, "Placement") * *foundPlacement; + } + + Base::Console().Warning("found2\n"); + return foundPlacement; + } + + return nullptr; +} + +Base::Placement getGlobalPlacement(App::DocumentObject* targetObj, App::DocumentObject* container = +nullptr) { bool inContainerBranch = container == nullptr; auto rootObjects = +App::GetApplication().getActiveDocument()->getRootObjects(); for (auto& part : rootObjects) { auto +foundPlacement = getTargetPlacementRelativeTo(targetObj, part, container, inContainerBranch); if +(foundPlacement != nullptr) { Base::Placement plc(foundPlacement->toMatrix()); return plc; + } + } + + return Base::Placement(); +} +*/ +/* +double getJointDistance(App::DocumentObject* joint) +{ + double distance = 0.0; + + auto* prop = dynamic_cast(joint->getPropertyByName("Distance")); + if (prop) { + distance = prop->getValue(); + } + + return distance; +} + +JointType 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* getElementFromProp(App::DocumentObject* obj, const char* propName) +{ + auto* prop = dynamic_cast(obj->getPropertyByName(propName)); + if (!prop) { + return ""; + } + + return prop->getValue(); +} + +std::string 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* getLinkObjFromProp(App::DocumentObject* joint, + const char* propLinkName) +{ + auto* propObj = dynamic_cast(joint->getPropertyByName(propLinkName)); + if (!propObj) { + return nullptr; + } + return propObj->getValue(); +} + +App::DocumentObject* getObjFromNameProp(App::DocumentObject* joint, + const char* pObjName, + const char* pPart) +{ + auto* propObjName = dynamic_cast(joint->getPropertyByName(pObjName)); + if (!propObjName) { + return nullptr; + } + std::string objName = std::string(propObjName->getValue()); + + App::DocumentObject* containingPart = getLinkObjFromProp(joint, pPart); + if (!containingPart) { + return nullptr; + } + + if (objName == containingPart->getNameInDocument()) { + return containingPart; + } + + if (containingPart->getTypeId().isDerivedFrom(App::Link::getClassTypeId())) { + App::Link* link = dynamic_cast(containingPart); + + containingPart = link->getLinkedObject(); + if (!containingPart) { + return nullptr; + } + } + + for (auto obj : containingPart->getOutList()) { + if (objName == obj->getNameInDocument()) { + return obj; + } + } + + return nullptr; +} + +App::DocumentObject* getLinkedObjFromNameProp(App::DocumentObject* joint, + const char* pObjName, + const char* pPart) + { + auto* obj = getObjFromNameProp(joint, pObjName, pPart); + if (obj) { + return obj->getLinkedObject(true); + } + return nullptr; + } + +} // namespace Assembly +*/ diff --git a/src/Mod/Assembly/App/AssemblyUtils.h b/src/Mod/Assembly/App/AssemblyUtils.h new file mode 100644 index 0000000000..c5851fe30a --- /dev/null +++ b/src/Mod/Assembly/App/AssemblyUtils.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + + +#ifndef ASSEMBLY_AssemblyUtils_H +#define ASSEMBLY_AssemblyUtils_H + + +#include + +#include +#include + +namespace App +{ +class DocumentObject; +} // namespace App + +namespace Base +{ +class Placement; +} // namespace Base + +namespace Assembly +{ +/* +// This enum has to be the same as the one in JointObject.py +enum class JointType +{ + Fixed, + Revolute, + Cylindrical, + Slider, + Ball, + Distance +}; + +// 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); +App::DocumentObject* getLinkObjFromProp(App::DocumentObject* joint, const char* propName); +App::DocumentObject* getObjFromNameProp(App::DocumentObject* joint, const char* pObjName, const +char* pPart); App::DocumentObject* getLinkedObjFromNameProp(App::DocumentObject* joint, const char* +pObjName, const char* pPart); Base::Placement getPlacementFromProp(App::DocumentObject* obj, const +char* propName);*/ + +} // namespace Assembly + + +#endif // ASSEMBLY_AssemblyUtils_H diff --git a/src/Mod/Assembly/App/CMakeLists.txt b/src/Mod/Assembly/App/CMakeLists.txt index e69de29bb2..724f22e1f8 100644 --- a/src/Mod/Assembly/App/CMakeLists.txt +++ b/src/Mod/Assembly/App/CMakeLists.txt @@ -0,0 +1,60 @@ + +include_directories( + ${CMAKE_BINARY_DIR} + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/src/3rdParty/OndselSolver + ${CMAKE_BINARY_DIR}/src + ${CMAKE_CURRENT_BINARY_DIR} + ${OCC_INCLUDE_DIR} + ${PYTHON_INCLUDE_DIRS} +) +link_directories(${OCC_LIBRARY_DIR}) + +set(Assembly_LIBS + Part + PartDesign + FreeCADApp + OndselSolver +) + +generate_from_xml(AssemblyObjectPy) +generate_from_xml(JointGroupPy) + +SET(Python_SRCS + AssemblyObjectPy.xml + AssemblyObjectPyImp.cpp + JointGroupPy.xml + JointGroupPyImp.cpp +) +SOURCE_GROUP("Python" FILES ${Python_SRCS}) + +SET(Module_SRCS + AppAssembly.cpp + AppAssemblyPy.cpp + PreCompiled.cpp + PreCompiled.h +) +SOURCE_GROUP("Module" FILES ${Module_SRCS}) + +SET(Assembly_SRCS + AssemblyObject.cpp + AssemblyObject.h + JointGroup.cpp + JointGroup.h + ${Module_SRCS} + ${Python_SRCS} +) + +add_library(Assembly SHARED ${Assembly_SRCS}) +target_link_libraries(Assembly ${Assembly_LIBS}) + +if(FREECAD_USE_PCH) + add_definitions(-D_PreComp_) + GET_MSVC_PRECOMPILED_SOURCE("PreCompiled.cpp" PCH_SRCS ${Assembly_SRCS}) + ADD_MSVC_PRECOMPILED_HEADER(Assembly PreCompiled.h PreCompiled.cpp PCH_SRCS) +endif(FREECAD_USE_PCH) + +SET_BIN_DIR(Assembly AssemblyApp /Mod/Assembly) +SET_PYTHON_PREFIX_SUFFIX(Assembly) + +INSTALL(TARGETS Assembly DESTINATION ${CMAKE_INSTALL_LIBDIR}) diff --git a/src/Mod/Assembly/App/JointGroup.cpp b/src/Mod/Assembly/App/JointGroup.cpp new file mode 100644 index 0000000000..faccfd4de7 --- /dev/null +++ b/src/Mod/Assembly/App/JointGroup.cpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" +#ifndef _PreComp_ +#endif + +#include +#include +#include +#include +#include +#include + +#include "JointGroup.h" +#include "JointGroupPy.h" + +using namespace Assembly; + + +PROPERTY_SOURCE(Assembly::JointGroup, App::DocumentObjectGroup) + +JointGroup::JointGroup() +{} + +JointGroup::~JointGroup() = default; + +PyObject* JointGroup::getPyObject() +{ + if (PythonObject.is(Py::_None())) { + // ref counter is set to 1 + PythonObject = Py::Object(new JointGroupPy(this), true); + } + return Py::new_reference_to(PythonObject); +} diff --git a/src/Mod/Assembly/App/JointGroup.h b/src/Mod/Assembly/App/JointGroup.h new file mode 100644 index 0000000000..17c328d2ba --- /dev/null +++ b/src/Mod/Assembly/App/JointGroup.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + + +#ifndef ASSEMBLY_JointGroup_H +#define ASSEMBLY_JointGroup_H + +#include + +#include +#include + + +namespace Assembly +{ + +class AssemblyExport JointGroup: public App::DocumentObjectGroup +{ + PROPERTY_HEADER_WITH_OVERRIDE(Assembly::JointGroup); + +public: + JointGroup(); + ~JointGroup() override; + + PyObject* getPyObject() override; + + /// returns the type name of the ViewProvider + const char* getViewProviderName() const override + { + return "AssemblyGui::ViewProviderJointGroup"; + } +}; + + +} // namespace Assembly + + +#endif // ASSEMBLY_JointGroup_H diff --git a/src/Mod/Assembly/App/JointGroupPy.xml b/src/Mod/Assembly/App/JointGroupPy.xml new file mode 100644 index 0000000000..d3b8555461 --- /dev/null +++ b/src/Mod/Assembly/App/JointGroupPy.xml @@ -0,0 +1,19 @@ + + + + + + This class is a group subclass for joints. + + + + + diff --git a/src/Mod/Assembly/App/JointGroupPyImp.cpp b/src/Mod/Assembly/App/JointGroupPyImp.cpp new file mode 100644 index 0000000000..a403ca4d9a --- /dev/null +++ b/src/Mod/Assembly/App/JointGroupPyImp.cpp @@ -0,0 +1,46 @@ +/*************************************************************************** + * Copyright (c) 2014 Jürgen Riegel * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +// inclusion of the generated files (generated out of JointGroup.xml) +#include "JointGroupPy.h" +#include "JointGroupPy.cpp" + +using namespace Assembly; + +// returns a string which represents the object e.g. when printed in python +std::string JointGroupPy::representation() const +{ + return {""}; +} + +PyObject* JointGroupPy::getCustomAttributes(const char* /*attr*/) const +{ + return nullptr; +} + +int JointGroupPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/) +{ + return 0; +} diff --git a/src/Mod/Assembly/App/PreCompiled.cpp b/src/Mod/Assembly/App/PreCompiled.cpp new file mode 100644 index 0000000000..ed7cfc6869 --- /dev/null +++ b/src/Mod/Assembly/App/PreCompiled.cpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" diff --git a/src/Mod/Assembly/App/PreCompiled.h b/src/Mod/Assembly/App/PreCompiled.h new file mode 100644 index 0000000000..f70a22f5f1 --- /dev/null +++ b/src/Mod/Assembly/App/PreCompiled.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#ifndef ASSEMBLY_PRECOMPILED_H +#define ASSEMBLY_PRECOMPILED_H + +#include + +#ifdef _MSC_VER +#pragma warning(disable : 5208) +#endif + +#ifdef _PreComp_ + +// standard +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#endif // _PreComp_ +#endif // ASSEMBLY_PRECOMPILED_H diff --git a/src/Mod/Assembly/Assembly/__init__.py b/src/Mod/Assembly/Assembly/__init__.py index e69de29bb2..efdb3c261e 100644 --- a/src/Mod/Assembly/Assembly/__init__.py +++ b/src/Mod/Assembly/Assembly/__init__.py @@ -0,0 +1 @@ +import AssemblyApp 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/AssemblyTests/TestCore.py b/src/Mod/Assembly/AssemblyTests/TestCore.py index e2e3796733..44bab3adfc 100644 --- a/src/Mod/Assembly/AssemblyTests/TestCore.py +++ b/src/Mod/Assembly/AssemblyTests/TestCore.py @@ -21,10 +21,18 @@ # * # ***************************************************************************/ -import FreeCAD +import FreeCAD as App import Part import unittest +import UtilsAssembly +import JointObject + + +def _msg(text, end="\n"): + """Write messages to the console including the line ending.""" + App.Console.PrintMessage(text + end) + class TestCore(unittest.TestCase): @classmethod @@ -49,26 +57,167 @@ class TestCore(unittest.TestCase): """ pass - # Close geometry document without saving - # FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name) - # Setup and tear down methods called before and after each unit test def setUp(self): """setUp()... This method is called prior to each `test()` method. Add code and objects here that are needed for multiple `test()` methods. """ - self.doc = FreeCAD.ActiveDocument - self.con = FreeCAD.Console + doc_name = self.__class__.__name__ + if App.ActiveDocument: + if App.ActiveDocument.Name != doc_name: + App.newDocument(doc_name) + else: + App.newDocument(doc_name) + App.setActiveDocument(doc_name) + self.doc = App.ActiveDocument + + self.assembly = App.ActiveDocument.addObject("Assembly::AssemblyObject", "Assembly") + if self.assembly: + self.jointgroup = self.assembly.newObject("Assembly::JointGroup", "Joints") + + _msg(" Temporary document '{}'".format(self.doc.Name)) def tearDown(self): """tearDown()... This method is called after each test() method. Add cleanup instructions here. Such cleanup instructions will likely undo those in the setUp() method. """ - pass + App.closeDocument(self.doc.Name) - def test00(self): - pass + def test_create_assembly(self): + """Create an assembly.""" + operation = "Create Assembly Object" + _msg(" Test '{}'".format(operation)) + self.assertTrue(self.assembly, "'{}' failed".format(operation)) - self.assertTrue(True) + def test_create_jointGroup(self): + """Create a joint group in an assembly.""" + operation = "Create JointGroup Object" + _msg(" Test '{}'".format(operation)) + self.assertTrue(self.jointgroup, "'{}' failed".format(operation)) + + def test_create_joint(self): + """Create a joint in an assembly.""" + operation = "Create Joint Object" + _msg(" Test '{}'".format(operation)) + + joint = self.jointgroup.newObject("App::FeaturePython", "testJoint") + self.assertTrue(joint, "'{}' failed (FeaturePython creation failed)".format(operation)) + JointObject.Joint(joint, 0) + + self.assertTrue(hasattr(joint, "JointType"), "'{}' failed".format(operation)) + + def test_create_grounded_joint(self): + """Create a grounded joint in an assembly.""" + operation = "Create Grounded Joint Object" + _msg(" Test '{}'".format(operation)) + + groundedjoint = self.jointgroup.newObject("App::FeaturePython", "testJoint") + self.assertTrue( + groundedjoint, "'{}' failed (FeaturePython creation failed)".format(operation) + ) + + box = self.assembly.newObject("Part::Box", "Box") + + JointObject.GroundedJoint(groundedjoint, box) + + self.assertTrue( + hasattr(groundedjoint, "ObjectToGround"), + "'{}' failed: No attribute 'ObjectToGround'".format(operation), + ) + self.assertTrue( + groundedjoint.ObjectToGround == box, + "'{}' failed: ObjectToGround not set correctly.".format(operation), + ) + + def test_find_placement(self): + """Test find placement of joint.""" + operation = "Find placement" + _msg(" Test '{}'".format(operation)) + + joint = self.jointgroup.newObject("App::FeaturePython", "testJoint") + JointObject.Joint(joint, 0) + + L = 2 + W = 3 + H = 7 + box = self.assembly.newObject("Part::Box", "Box") + box.Length = L + box.Width = W + box.Height = H + box.Placement = App.Placement(App.Vector(10, 20, 30), App.Rotation(15, 25, 35)) + + # Step 0 : box with placement. No element selected + plc = joint.Proxy.findPlacement(joint, box.Name, box, "", "") + targetPlc = App.Placement(App.Vector(), App.Rotation()) + self.assertTrue(plc.isSame(targetPlc, 1e-6), "'{}' failed - Step 0".format(operation)) + + # Step 1 : box with placement. Face + Vertex + plc = joint.Proxy.findPlacement(joint, box.Name, box, "Face6", "Vertex7") + targetPlc = App.Placement(App.Vector(L, W, H), App.Rotation()) + self.assertTrue(plc.isSame(targetPlc, 1e-6), "'{}' failed - Step 1".format(operation)) + + # Step 2 : box with placement. Edge + Vertex + plc = joint.Proxy.findPlacement(joint, box.Name, box, "Edge8", "Vertex8") + targetPlc = App.Placement(App.Vector(L, W, 0), App.Rotation(0, 0, -90)) + self.assertTrue(plc.isSame(targetPlc, 1e-6), "'{}' failed - Step 2".format(operation)) + + # Step 3 : box with placement. Vertex + plc = joint.Proxy.findPlacement(joint, box.Name, box, "Vertex3", "Vertex3") + targetPlc = App.Placement(App.Vector(0, W, H), App.Rotation()) + _msg(" plc '{}'".format(plc)) + _msg(" targetPlc '{}'".format(targetPlc)) + self.assertTrue(plc.isSame(targetPlc, 1e-6), "'{}' failed - Step 3".format(operation)) + + # Step 4 : box with placement. Face + plc = joint.Proxy.findPlacement(joint, box.Name, box, "Face2", "Face2") + targetPlc = App.Placement(App.Vector(L, W / 2, H / 2), App.Rotation(0, -90, 180)) + _msg(" plc '{}'".format(plc)) + _msg(" targetPlc '{}'".format(targetPlc)) + self.assertTrue(plc.isSame(targetPlc, 1e-6), "'{}' failed - Step 4".format(operation)) + + def test_solve_assembly(self): + """Test solving an assembly.""" + operation = "Solve assembly" + _msg(" Test '{}'".format(operation)) + + box = self.assembly.newObject("Part::Box", "Box") + box.Length = 10 + box.Width = 10 + box.Height = 10 + box.Placement = App.Placement(App.Vector(10, 20, 30), App.Rotation(15, 25, 35)) + + box2 = self.assembly.newObject("Part::Box", "Box") + box2.Length = 10 + box2.Width = 10 + box2.Height = 10 + box2.Placement = App.Placement(App.Vector(40, 50, 60), App.Rotation(45, 55, 65)) + + ground = self.jointgroup.newObject("App::FeaturePython", "GroundedJoint") + JointObject.GroundedJoint(ground, box2) + + joint = self.jointgroup.newObject("App::FeaturePython", "testJoint") + JointObject.Joint(joint, 0) + + current_selection = [] + current_selection.append( + { + "object": box2, + "part": box2, + "element_name": "Face6", + "vertex_name": "Vertex7", + } + ) + current_selection.append( + { + "object": box, + "part": box, + "element_name": "Face6", + "vertex_name": "Vertex7", + } + ) + + joint.Proxy.setJointConnectors(joint, current_selection) + + self.assertTrue(box.Placement.isSame(box2.Placement, 1e-6), "'{}'".format(operation)) diff --git a/src/Mod/Assembly/CMakeLists.txt b/src/Mod/Assembly/CMakeLists.txt index a6a9262fd0..b6f964016c 100644 --- a/src/Mod/Assembly/CMakeLists.txt +++ b/src/Mod/Assembly/CMakeLists.txt @@ -8,7 +8,9 @@ set(Assembly_Scripts Init.py CommandCreateAssembly.py CommandInsertLink.py + CommandSolveAssembly.py CommandCreateJoint.py + CommandExportASMT.py TestAssemblyWorkbench.py JointObject.py Preferences.py @@ -65,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 980199d43a..4a6e9e34b2 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,20 +49,33 @@ 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 in the current active assembly (if any). Limit of one root assembly per file.", ), "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("App::Part", "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.ActiveView.setActiveObject("part", assembly) - assembly.newObject("App::DocumentObjectGroup", "Joints") + 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 cdac1611cb..e5d0509d18 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 @@ -31,6 +31,7 @@ if App.GuiUp: from PySide import QtCore, QtGui, QtWidgets import JointObject +from JointObject import TaskAssemblyCreateJoint import UtilsAssembly import Assembly_rc @@ -41,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 @@ -49,26 +62,34 @@ class CommandCreateJointFixed: return { "Pixmap": "Assembly_CreateJointFixed", - "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointFixed", "Create Fixed Joint"), - "Accel": "F", - "ToolTip": QT_TRANSLATE_NOOP( + "MenuText": QT_TRANSLATE_NOOP( "Assembly_CreateJointFixed", - "

Create a Fixed Joint: Permanently locks two parts together, preventing any movement or rotation.

", + "Create a Fixed Joint", ), + "Accel": "J", + "ToolTip": "

" + + QT_TRANSLATE_NOOP( + "Assembly_CreateJointFixed", + "1 - If an assembly is active : Create a joint permanently locking two parts together, preventing any movement or rotation.", + ) + + "

" + + "

" + + QT_TRANSLATE_NOOP( + "Assembly_CreateJointFixed", + "2 - If a part is active : Position sub parts by matching selected coordinate systems. The second part selected will move.", + ) + + "

", "CmdType": "ForEdit", } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + if UtilsAssembly.activePart: + return UtilsAssembly.assembly_has_at_least_n_parts(2) + + return UtilsAssembly.isAssemblyGrounded() and UtilsAssembly.assembly_has_at_least_n_parts(2) def Activated(self): - assembly = UtilsAssembly.activeAssembly() - if not assembly: - return - view = Gui.activeDocument().activeView() - - self.panel = TaskAssemblyCreateJoint(assembly, view, 0) - Gui.Control.showDialog(self.panel) + activateJoint(0) class CommandCreateJointRevolute: @@ -81,24 +102,20 @@ class CommandCreateJointRevolute: "Pixmap": "Assembly_CreateJointRevolute", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointRevolute", "Create Revolute Joint"), "Accel": "R", - "ToolTip": QT_TRANSLATE_NOOP( + "ToolTip": "

" + + QT_TRANSLATE_NOOP( "Assembly_CreateJointRevolute", - "

Create a Revolute Joint: Allows rotation around a single axis between selected parts.

", - ), + "Create a Revolute Joint: Allows rotation around a single axis between selected parts.", + ) + + "

", "CmdType": "ForEdit", } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return isCreateJointActive() def Activated(self): - assembly = UtilsAssembly.activeAssembly() - if not assembly: - return - view = Gui.activeDocument().activeView() - - self.panel = TaskAssemblyCreateJoint(assembly, view, 1) - Gui.Control.showDialog(self.panel) + activateJoint(1) class CommandCreateJointCylindrical: @@ -113,24 +130,20 @@ class CommandCreateJointCylindrical: "Assembly_CreateJointCylindrical", "Create Cylindrical Joint" ), "Accel": "C", - "ToolTip": QT_TRANSLATE_NOOP( + "ToolTip": "

" + + QT_TRANSLATE_NOOP( "Assembly_CreateJointCylindrical", - "

Create a Cylindrical Joint: Enables rotation along one axis while permitting movement along the same axis between assembled parts.

", - ), + "Create a Cylindrical Joint: Enables rotation along one axis while permitting movement along the same axis between assembled parts.", + ) + + "

", "CmdType": "ForEdit", } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return isCreateJointActive() def Activated(self): - assembly = UtilsAssembly.activeAssembly() - if not assembly: - return - view = Gui.activeDocument().activeView() - - self.panel = TaskAssemblyCreateJoint(assembly, view, 2) - Gui.Control.showDialog(self.panel) + activateJoint(2) class CommandCreateJointSlider: @@ -143,24 +156,20 @@ class CommandCreateJointSlider: "Pixmap": "Assembly_CreateJointSlider", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointSlider", "Create Slider Joint"), "Accel": "S", - "ToolTip": QT_TRANSLATE_NOOP( + "ToolTip": "

" + + QT_TRANSLATE_NOOP( "Assembly_CreateJointSlider", - "

Create a Slider Joint: Allows linear movement along a single axis but restricts rotation between selected parts.

", - ), + "Create a Slider Joint: Allows linear movement along a single axis but restricts rotation between selected parts.", + ) + + "

", "CmdType": "ForEdit", } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return isCreateJointActive() def Activated(self): - assembly = UtilsAssembly.activeAssembly() - if not assembly: - return - view = Gui.activeDocument().activeView() - - self.panel = TaskAssemblyCreateJoint(assembly, view, 3) - Gui.Control.showDialog(self.panel) + activateJoint(3) class CommandCreateJointBall: @@ -173,333 +182,141 @@ class CommandCreateJointBall: "Pixmap": "Assembly_CreateJointBall", "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointBall", "Create Ball Joint"), "Accel": "B", - "ToolTip": QT_TRANSLATE_NOOP( + "ToolTip": "

" + + QT_TRANSLATE_NOOP( "Assembly_CreateJointBall", - "

Create a Ball Joint: Connects parts at a point, allowing unrestricted movement as long as the connection points remain in contact.

", - ), + "Create a Ball Joint: Connects parts at a point, allowing unrestricted movement as long as the connection points remain in contact.", + ) + + "

", "CmdType": "ForEdit", } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + return isCreateJointActive() def Activated(self): - assembly = UtilsAssembly.activeAssembly() - if not assembly: - return - view = Gui.activeDocument().activeView() - - self.panel = TaskAssemblyCreateJoint(assembly, view, 4) - Gui.Control.showDialog(self.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", - "ToolTip": QT_TRANSLATE_NOOP( - "Assembly_CreateJointPlanar", - "

Create a Planar Joint: Ensures two selected features are in the same plane, restricting movement to that plane.

", - ), + "Pixmap": "Assembly_CreateJointDistance", + "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointDistance", "Create Distance Joint"), + "Accel": "D", + "ToolTip": "

" + + QT_TRANSLATE_NOOP( + "Assembly_CreateJointDistance", + "Create a Distance Joint: Fix the distance between the selected objects.", + ) + + "

", "CmdType": "ForEdit", } def IsActive(self): - return UtilsAssembly.activeAssembly() is not None + # return False + return isCreateJointActive() def Activated(self): - assembly = UtilsAssembly.activeAssembly() - if not assembly: - return - view = Gui.activeDocument().activeView() - - self.panel = TaskAssemblyCreateJoint(assembly, view, 5) - Gui.Control.showDialog(self.panel) + activateJoint(5) -class CommandCreateJointParallel: +def createGroundedJoint(obj): + assembly = UtilsAssembly.activeAssembly() + if not assembly: + return + + joint_group = UtilsAssembly.getJointGroup(assembly) + + obj.Label = obj.Label + " 🔒" + ground = joint_group.newObject("App::FeaturePython", "GroundedJoint") + JointObject.GroundedJoint(ground, obj) + JointObject.ViewProviderGroundedJoint(ground.ViewObject) + return ground + + +class CommandToggleGrounded: def __init__(self): pass def GetResources(self): return { - "Pixmap": "Assembly_CreateJointParallel", - "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointParallel", "Create Parallel Joint"), - "Accel": "L", - "ToolTip": QT_TRANSLATE_NOOP( - "Assembly_CreateJointParallel", - "

Create a Parallel Joint: Aligns two features to be parallel, constraining relative movement to parallel translations.

", - ), + "Pixmap": "Assembly_ToggleGrounded", + "MenuText": QT_TRANSLATE_NOOP("Assembly_ToggleGrounded", "Toggle grounded"), + "Accel": "G", + "ToolTip": "

" + + QT_TRANSLATE_NOOP( + "Assembly_ToggleGrounded", + "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 - - def Activated(self): - assembly = UtilsAssembly.activeAssembly() - if not assembly: - return - view = Gui.activeDocument().activeView() - - self.panel = TaskAssemblyCreateJoint(assembly, view, 6) - Gui.Control.showDialog(self.panel) - - -class CommandCreateJointTangent: - def __init__(self): - pass - - def GetResources(self): - - return { - "Pixmap": "Assembly_CreateJointTangent", - "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointTangent", "Create Tangent Joint"), - "Accel": "T", - "ToolTip": QT_TRANSLATE_NOOP( - "Assembly_CreateJointTangent", - "

Create a Tangent Joint: Forces two features to be tangent, restricting movement to smooth transitions along their contact surface.

", - ), - "CmdType": "ForEdit", - } - - def IsActive(self): - return UtilsAssembly.activeAssembly() is not None - - def Activated(self): - assembly = UtilsAssembly.activeAssembly() - if not assembly: - return - view = Gui.activeDocument().activeView() - - self.panel = TaskAssemblyCreateJoint(assembly, view, 7) - Gui.Control.showDialog(self.panel) - - -class MakeJointSelGate: - def __init__(self, taskbox, assembly): - self.taskbox = taskbox - self.assembly = assembly - - def allow(self, doc, obj, sub): - if not sub: - return False - - 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. - return False - - if Gui.Selection.isSelected(obj, sub, Gui.Selection.ResolveMode.NoResolve): - # If it's to deselect then it's ok - return True - - if len(self.taskbox.current_selection) >= 2: - # No more than 2 elements can be selected for basic joints. - return False - - full_obj_name = ".".join(objs_names) - for selection_dict in self.taskbox.current_selection: - if selection_dict["full_obj_name"] == full_obj_name: - # Can't join a solid to itself. So the user need to select 2 different parts. - return False - - return True - - -class TaskAssemblyCreateJoint(QtCore.QObject): - def __init__(self, assembly, view, jointTypeIndex): - super().__init__() - - self.assembly = assembly - self.view = view - self.doc = App.ActiveDocument - - self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateJoint.ui") - - self.form.jointType.addItems(JointObject.JointTypes) - self.form.jointType.setCurrentIndex(jointTypeIndex) - - Gui.Selection.clearSelection() - Gui.Selection.addSelectionGate( - MakeJointSelGate(self, self.assembly), Gui.Selection.ResolveMode.NoResolve + return ( + UtilsAssembly.isAssemblyCommandActive() + and UtilsAssembly.assembly_has_at_least_n_parts(1) ) - Gui.Selection.addObserver(self, Gui.Selection.ResolveMode.NoResolve) - Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.GreedySelection) - self.current_selection = [] - self.preselection_dict = None - self.callbackMove = self.view.addEventCallback("SoLocation2Event", self.moveMouse) - self.callbackKey = self.view.addEventCallback("SoKeyboardEvent", self.KeyboardEvent) + def Activated(self): + assembly = UtilsAssembly.activeAssembly() + if not assembly: + return - App.setActiveTransaction("Create joint") - self.createJointObject() + joint_group = UtilsAssembly.getJointGroup(assembly) - def accept(self): - if len(self.current_selection) != 2: - App.Console.PrintWarning("You need to select 2 elements from 2 separate parts.") - return False - self.deactivate() + selection = Gui.Selection.getSelectionEx("*", 0) + if not selection: + return + + App.setActiveTransaction("Toggle grounded") + for sel in selection: + # 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: + # 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. + ungrounded = False + for joint in joint_group.Group: + 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() + ungrounded = True + break + if ungrounded: + continue + + # Create groundedJoint. + createGroundedJoint(part_containing_obj) App.closeActiveTransaction() - return True - - def reject(self): - self.deactivate() - App.closeActiveTransaction(True) - return True - - def deactivate(self): - Gui.Selection.removeSelectionGate() - Gui.Selection.removeObserver(self) - Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.NormalSelection) - Gui.Selection.clearSelection() - self.view.removeEventCallback("SoLocation2Event", self.callbackMove) - self.view.removeEventCallback("SoKeyboardEvent", self.callbackKey) - if Gui.Control.activeDialog(): - Gui.Control.closeDialog() - - def createJointObject(self): - type_index = self.form.jointType.currentIndex() - - joint_group = self.assembly.getObject("Joints") - - if not joint_group: - joint_group = self.assembly.newObject("App::DocumentObjectGroup", "Joints") - - self.joint = joint_group.newObject("App::FeaturePython", "Joint") - JointObject.Joint(self.joint, type_index) - JointObject.ViewProviderJoint(self.joint.ViewObject, self.joint) - - def updateJoint(self): - # First we build the listwidget - self.form.featureList.clear() - 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["full_element_name"].split(self.assembly.Name + ".", 1)[-1] - simplified_names.append(sname) - self.form.featureList.addItems(simplified_names) - - # Then we pass the new list to the join object - self.joint.Proxy.setJointConnectors(self.current_selection) - - def moveMouse(self, info): - if len(self.current_selection) >= 2 or ( - len(self.current_selection) == 1 - and self.current_selection[0]["full_element_name"] - == self.preselection_dict["full_element_name"] - ): - self.joint.ViewObject.Proxy.showPreviewJCS(False) - return - - cursor_pos = self.view.getCursorPos() - cursor_info = self.view.getObjectInfo(cursor_pos) - # cursor_info example {'x': 41.515, 'y': 7.449, 'z': 16.861, 'ParentObject': , 'SubName': 'Body002.Pad.Face5', 'Document': 'part3', 'Object': 'Pad', 'Component': 'Face5'} - - if ( - not cursor_info - or not self.preselection_dict - or cursor_info["SubName"] != self.preselection_dict["sub_name"] - ): - self.joint.ViewObject.Proxy.showPreviewJCS(False) - return - - # newPos = self.view.getPoint(*info["Position"]) # This is not what we want, it's not pos on the object but on the focal plane - - 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 - ) - - placement = self.joint.Proxy.findPlacement( - self.preselection_dict["object"], - self.preselection_dict["element_name"], - self.preselection_dict["vertex_name"], - ) - self.joint.ViewObject.Proxy.showPreviewJCS(True, placement) - self.previewJCSVisible = True - - # 3D view keyboard handler - def KeyboardEvent(self, info): - if info["State"] == "UP" and info["Key"] == "ESCAPE": - self.reject() - - if info["State"] == "UP" and info["Key"] == "RETURN": - self.accept() - - # selectionObserver stuff - def addSelection(self, doc_name, obj_name, sub_name, mousePos): - 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) - - selection_dict = { - "object": 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) - - self.current_selection.append(selection_dict) - self.updateJoint() - - def removeSelection(self, doc_name, obj_name, sub_name, mousePos=None): - full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name) - - # 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["full_element_name"] == full_element_name: - selection_dict_to_remove = selection_dict - break - - if selection_dict_to_remove is not None: - self.current_selection.remove(selection_dict_to_remove) - - self.updateJoint() - - def setPreselection(self, doc_name, obj_name, sub_name): - if not sub_name: - self.preselection_dict = None - return - - 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) - - self.preselection_dict = { - "object": selected_object, - "sub_name": sub_name, - "element_name": element_name, - "full_element_name": full_element_name, - "full_obj_name": full_obj_name, - } - - def clearSelection(self, doc_name): - self.current_selection.clear() - self.updateJoint() if App.GuiUp: + Gui.addCommand("Assembly_ToggleGrounded", CommandToggleGrounded()) Gui.addCommand("Assembly_CreateJointFixed", CommandCreateJointFixed()) Gui.addCommand("Assembly_CreateJointRevolute", CommandCreateJointRevolute()) 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 new file mode 100644 index 0000000000..c175e28c53 --- /dev/null +++ b/src/Mod/Assembly/CommandExportASMT.py @@ -0,0 +1,82 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# /************************************************************************** +# * +# Copyright (c) 2023 Ondsel * +# * +# This file is part of FreeCAD. * +# * +# FreeCAD is free software: you can redistribute it and/or modify it * +# under the terms of the GNU Lesser General Public License as * +# published by the Free Software Foundation, either version 2.1 of the * +# License, or (at your option) any later version. * +# * +# FreeCAD is distributed in the hope that it will be useful, but * +# WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# Lesser General Public License for more details. * +# * +# You should have received a copy of the GNU Lesser General Public * +# License along with FreeCAD. If not, see * +# . * +# * +# **************************************************************************/ + +import FreeCAD as App +import UtilsAssembly + +from PySide.QtCore import QT_TRANSLATE_NOOP +from PySide.QtWidgets import QFileDialog + +if App.GuiUp: + import FreeCADGui as Gui + +# translate = App.Qt.translate + +__title__ = "Assembly Command Create Assembly" +__author__ = "Ondsel" +__url__ = "https://www.freecad.org" + + +class CommandExportASMT: + def __init__(self): + pass + + def GetResources(self): + return { + "Pixmap": "Assembly_ExportASMT", + "MenuText": QT_TRANSLATE_NOOP("Assembly_ExportASMT", "Export ASMT File"), + "Accel": "E", + "ToolTip": QT_TRANSLATE_NOOP( + "Assembly_ExportASMT", + "Export currently active assembly as a ASMT file.", + ), + "CmdType": "ForEdit", + } + + def IsActive(self): + return UtilsAssembly.isAssemblyCommandActive() and UtilsAssembly.isAssemblyGrounded() + + def Activated(self): + document = App.ActiveDocument + if not document: + return + + assembly = UtilsAssembly.activeAssembly() + if not assembly: + return + + # Prompt the user for a file location and name + defaultFileName = document.Name + ".asmt" + filePath, _ = QFileDialog.getSaveFileName( + None, + "Save ASMT File", + defaultFileName, + "ASMT Files (*.asmt);;All Files (*)", + ) + + if filePath: + assembly.exportAsASMT(filePath) + + +if App.GuiUp: + Gui.addCommand("Assembly_ExportASMT", CommandExportASMT()) diff --git a/src/Mod/Assembly/CommandInsertLink.py b/src/Mod/Assembly/CommandInsertLink.py index 0c5dd143b9..aeeb7776ff 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,8 +19,9 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ +import re import os import FreeCAD as App @@ -31,6 +32,8 @@ if App.GuiUp: from PySide import QtCore, QtGui, QtWidgets import UtilsAssembly +import Preferences +import CommandCreateJoint # translate = App.Qt.translate @@ -44,22 +47,32 @@ 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", "Remove 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 +94,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,27 +106,38 @@ class TaskAssemblyInsertLink(QtCore.QObject): self.allParts = [] self.partsDoc = [] - self.numberOfAddedParts = 0 self.translation = 0 self.partMoving = False + self.totalTranslation = App.Vector() + self.groundedObj = None + + 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() + pref = Preferences.preferences() + pref.SetBool("InsertInParts", self.form.CheckBox_InsertInParts.isChecked()) def buildPartList(self): self.allParts.clear() @@ -135,7 +163,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) @@ -144,7 +172,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) @@ -192,23 +220,125 @@ 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) + + if len(self.insertionStack) == 1 and not UtilsAssembly.isAssemblyGrounded(): + self.handleFirstInsertion() + + def handleFirstInsertion(self): + pref = Preferences.preferences() + fixPart = False + fixPartPref = pref.GetInt("GroundFirstPart", 0) + if fixPartPref == 0: # unset + msgBox = QtWidgets.QMessageBox() + msgBox.setWindowTitle("Ground Part?") + msgBox.setText( + "Do you want to ground the first inserted part automatically?\nYou need at least one grounded part in your assembly." + ) + msgBox.setIcon(QtWidgets.QMessageBox.Question) + + yesButton = msgBox.addButton("Yes", QtWidgets.QMessageBox.YesRole) + noButton = msgBox.addButton("No", QtWidgets.QMessageBox.NoRole) + yesAlwaysButton = msgBox.addButton("Always", QtWidgets.QMessageBox.YesRole) + noAlwaysButton = msgBox.addButton("Never", QtWidgets.QMessageBox.NoRole) + + msgBox.exec_() + + clickedButton = msgBox.clickedButton() + if clickedButton == yesButton: + fixPart = True + elif clickedButton == yesAlwaysButton: + fixPart = True + pref.SetInt("GroundFirstPart", 1) + elif clickedButton == noAlwaysButton: + pref.SetInt("GroundFirstPart", 2) + + elif fixPartPref == 1: # Yes always + fixPart = True + + if fixPart: + # Create groundedJoint. + if len(self.insertionStack) != 1: + return + + self.groundedObj = self.insertionStack[0]["addedObject"] + self.groundedJoint = CommandCreateJoint.createGroundedJoint(self.groundedObj) + self.endMove() + + 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) @@ -229,42 +359,87 @@ 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"] + obj = stack_item["addedObject"] + if self.groundedObj == obj: + self.groundedJoint.Document.removeObject(self.groundedJoint.Name) + UtilsAssembly.removeObjAndChilds(obj) + + 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 new file mode 100644 index 0000000000..fec765b682 --- /dev/null +++ b/src/Mod/Assembly/CommandSolveAssembly.py @@ -0,0 +1,76 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# /************************************************************************** +# * +# Copyright (c) 2023 Ondsel * +# * +# This file is part of FreeCAD. * +# * +# FreeCAD is free software: you can redistribute it and/or modify it * +# under the terms of the GNU Lesser General Public License as * +# published by the Free Software Foundation, either version 2.1 of the * +# License, or (at your option) any later version. * +# * +# FreeCAD is distributed in the hope that it will be useful, but * +# WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# Lesser General Public License for more details. * +# * +# You should have received a copy of the GNU Lesser General Public * +# License along with FreeCAD. If not, see * +# . * +# * +# **************************************************************************/ + +import os +import FreeCAD as App + +from PySide.QtCore import QT_TRANSLATE_NOOP + +if App.GuiUp: + import FreeCADGui as Gui + from PySide import QtCore, QtGui, QtWidgets + +import UtilsAssembly +import Assembly_rc + +# translate = App.Qt.translate + +__title__ = "Assembly Command to Solve Assembly" +__author__ = "Ondsel" +__url__ = "https://www.freecad.org" + + +class CommandSolveAssembly: + def __init__(self): + pass + + def GetResources(self): + + return { + "Pixmap": "Assembly_SolveAssembly", + "MenuText": QT_TRANSLATE_NOOP("Assembly_SolveAssembly", "Solve Assembly"), + "Accel": "Z", + "ToolTip": "

" + + QT_TRANSLATE_NOOP( + "Assembly_SolveAssembly", + "Solve the currently active assembly.", + ) + + "

", + "CmdType": "ForEdit", + } + + def IsActive(self): + return UtilsAssembly.isAssemblyCommandActive() and UtilsAssembly.isAssemblyGrounded() + + def Activated(self): + assembly = UtilsAssembly.activeAssembly() + if not assembly: + return + + App.setActiveTransaction("Solve assembly") + assembly.solve() + App.closeActiveTransaction() + + +if App.GuiUp: + Gui.addCommand("Assembly_SolveAssembly", CommandSolveAssembly()) diff --git a/src/Mod/Assembly/Gui/AppAssemblyGui.cpp b/src/Mod/Assembly/Gui/AppAssemblyGui.cpp new file mode 100644 index 0000000000..c0a2e8439b --- /dev/null +++ b/src/Mod/Assembly/Gui/AppAssemblyGui.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#include +#include + +#include "ViewProviderAssembly.h" +#include "ViewProviderJointGroup.h" + + +namespace AssemblyGui +{ +extern PyObject* initModule(); +} + +/* Python entry */ +PyMOD_INIT_FUNC(AssemblyGui) +{ + PyObject* mod = AssemblyGui::initModule(); + Base::Console().Log("Loading AssemblyGui module... done\n"); + + + // NOTE: To finish the initialization of our own type objects we must + // call PyType_Ready, otherwise we run into a segmentation fault, later on. + // This function is responsible for adding inherited slots from a type's base class. + + AssemblyGui::ViewProviderAssembly ::init(); + AssemblyGui::ViewProviderJointGroup::init(); + + PyMOD_Return(mod); +} diff --git a/src/Mod/Assembly/Gui/AppAssemblyGuiPy.cpp b/src/Mod/Assembly/Gui/AppAssemblyGuiPy.cpp new file mode 100644 index 0000000000..dd6f0fdf22 --- /dev/null +++ b/src/Mod/Assembly/Gui/AppAssemblyGuiPy.cpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#include + + +namespace AssemblyGui +{ +class Module: public Py::ExtensionModule +{ +public: + Module() + : Py::ExtensionModule("AssemblyGui") + { + initialize("This module is the Assembly module."); // register with Python + } +}; + +PyObject* initModule() +{ + return Base::Interpreter().addModule(new Module); +} + +} // namespace AssemblyGui diff --git a/src/Mod/Assembly/Gui/CMakeLists.txt b/src/Mod/Assembly/Gui/CMakeLists.txt index 565597d318..6dcc4acb25 100644 --- a/src/Mod/Assembly/Gui/CMakeLists.txt +++ b/src/Mod/Assembly/Gui/CMakeLists.txt @@ -1,9 +1,13 @@ include_directories( ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR} + ${OCC_INCLUDE_DIR} ) set(AssemblyGui_LIBS + Assembly + PartDesign + PartGui FreeCADGui ) @@ -17,8 +21,23 @@ qt_add_resources(AssemblyResource_SRCS Resources/Assembly.qrc ${Assembly_TR_QRC} SOURCE_GROUP("Resources" FILES ${AssemblyResource_SRCS}) +generate_from_xml(ViewProviderAssemblyPy) + +SET(Python_SRCS + ViewProviderAssemblyPy.xml + ViewProviderAssemblyPyImp.cpp +) +SOURCE_GROUP("Python" FILES ${Python_SRCS}) SET(AssemblyGui_SRCS_Module + AppAssemblyGui.cpp + AppAssemblyGuiPy.cpp + PreCompiled.cpp + PreCompiled.h + ViewProviderAssembly.cpp + ViewProviderAssembly.h + ViewProviderJointGroup.cpp + ViewProviderJointGroup.h ${Assembly_QRC_SRCS} ) @@ -29,8 +48,14 @@ SET(AssemblyGui_SRCS ${AssemblyResource_SRCS} ${AssemblyGui_UIC_HDRS} ${AssemblyGui_SRCS_Module} + ${Python_SRCS} ) +if(FREECAD_USE_PCH) + add_definitions(-D_PreComp_) + GET_MSVC_PRECOMPILED_SOURCE("PreCompiled.cpp" PCH_SRCS ${AssemblyGui_SRCS}) + ADD_MSVC_PRECOMPILED_HEADER(PathGui PreCompiled.h PreCompiled.cpp PCH_SRCS) +endif(FREECAD_USE_PCH) SET(AssemblyGuiIcon_SVG Resources/icons/AssemblyWorkbench.svg diff --git a/src/Mod/Assembly/Gui/PreCompiled.cpp b/src/Mod/Assembly/Gui/PreCompiled.cpp new file mode 100644 index 0000000000..ed7cfc6869 --- /dev/null +++ b/src/Mod/Assembly/Gui/PreCompiled.cpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" diff --git a/src/Mod/Assembly/Gui/PreCompiled.h b/src/Mod/Assembly/Gui/PreCompiled.h new file mode 100644 index 0000000000..79ef18d8f4 --- /dev/null +++ b/src/Mod/Assembly/Gui/PreCompiled.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#ifndef POINTSGUI_PRECOMPILED_H +#define POINTSGUI_PRECOMPILED_H + +#include + +#ifdef _PreComp_ + +// STL +#include +#include +#include + +#include +#include +#include + +// Qt +#ifndef __QtAll__ +#include +#endif + +#include + +// all of Inventor +#ifndef __InventorAll__ +#include +#endif + +#endif //_PreComp_ + +#endif // POINTSGUI_PRECOMPILED_H diff --git a/src/Mod/Assembly/Gui/Resources/Assembly.qrc b/src/Mod/Assembly/Gui/Resources/Assembly.qrc index 8ec25f10ba..ad258eecf3 100644 --- a/src/Mod/Assembly/Gui/Resources/Assembly.qrc +++ b/src/Mod/Assembly/Gui/Resources/Assembly.qrc @@ -1,17 +1,20 @@ - + icons/Assembly_InsertLink.svg icons/preferences-assembly.svg + icons/Assembly_ToggleGrounded.svg icons/Assembly_CreateJointBall.svg icons/Assembly_CreateJointCylindrical.svg icons/Assembly_CreateJointFixed.svg - icons/Assembly_CreateJointParallel.svg icons/Assembly_CreateJointPlanar.svg icons/Assembly_CreateJointRevolute.svg icons/Assembly_CreateJointSlider.svg icons/Assembly_CreateJointTangent.svg + icons/Assembly_ExportASMT.svg + icons/Assembly_SolveAssembly.svg panels/TaskAssemblyCreateJoint.ui panels/TaskAssemblyInsertLink.ui preferences/Assembly.ui + icons/Assembly_CreateJointDistance.svg diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointParallel.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointDistance.svg similarity index 100% rename from src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointParallel.svg rename to src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointDistance.svg diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointFixed.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointFixed.svg index 84f889e6cf..33611918f7 100644 --- a/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointFixed.svg +++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_CreateJointFixed.svg @@ -7,7 +7,7 @@ id="svg2821" sodipodi:version="0.32" inkscape:version="1.3 (0e150ed6c4, 2023-07-21)" - sodipodi:docname="Assembly_CreateJointFixed.svg" + sodipodi:docname="Assembly_CreateJointFixedNew.svg" inkscape:output_extension="org.inkscape.output.svg.inkscape" version="1.1" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" @@ -175,6 +175,17 @@ x2="85" y2="35" spreadMethod="reflect" /> + + + + + inkscape:deskcolor="#d1d1d1" + showguides="true"> + + + @@ -261,46 +294,51 @@ transform="translate(3.6192085e-6,-0.89630564)"> + sodipodi:nodetypes="ccczccc" /> + cy="31.700123" + ry="7.9999995" + rx="24" /> + + sodipodi:nodetypes="scczcccccs" /> + sodipodi:nodetypes="scczcccccs" /> - - - - - - - - - - - + diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_ExportASMT.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_ExportASMT.svg new file mode 100644 index 0000000000..50b1905bd0 --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_ExportASMT.svg @@ -0,0 +1,944 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + 2005-10-15 + + + Andreas Nilsson + + + + + edit + copy + + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_SolveAssembly.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_SolveAssembly.svg new file mode 100644 index 0000000000..d3044a83f6 --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_SolveAssembly.svg @@ -0,0 +1,615 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Path-Stock + 2015-07-04 + https://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-Stock.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_ToggleGrounded.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_ToggleGrounded.svg new file mode 100644 index 0000000000..07c83f3a85 --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_ToggleGrounded.svg @@ -0,0 +1,441 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [wmayer] + + + Part_Cylinder + 2011-10-10 + http://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Part/Gui/Resources/icons/Part_Cylinder.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui index ed194b75c5..be41367552 100644 --- a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui +++ b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui @@ -20,8 +20,107 @@ + + + + + + Distance + + + + + + + + 0 + 0 + + + + mm + + + + + + + + + + + Offset + + + + + + + + 0 + 0 + + + + mm + + + + + + + + + + + Rotation + + + + + + + + 0 + 0 + + + + deg + + + + + + + + + + 0 + 0 + + + + Reverse the direction of the joint. + + + Reverse + + + + :/icons/button_sort.svg:/icons/button_sort.svg + + + + + + Gui::QuantitySpinBox + QWidget +
Gui/QuantitySpinBox.h
+
+
diff --git a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui index 76a641643a..214df9adfb 100644 --- a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui +++ b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyInsertLink.ui @@ -42,8 +42,34 @@ + + + + If checked, the selected object will be inserted inside a Part container, unless it is already a Part. + + + Insert as part + + + true + + + InsertInParts + + + Mod/Assembly + + + + + + Gui::PrefCheckBox + QCheckBox +
Gui/PrefWidgets.h
+
+
diff --git a/src/Mod/Assembly/Gui/Resources/preferences/Assembly.ui b/src/Mod/Assembly/Gui/Resources/preferences/Assembly.ui index 83a06b97c5..642cbe1da0 100644 --- a/src/Mod/Assembly/Gui/Resources/preferences/Assembly.ui +++ b/src/Mod/Assembly/Gui/Resources/preferences/Assembly.ui @@ -13,8 +13,57 @@ General - - + + + + + Allow to leave edit mode when pressing Esc button + + + Esc leave edit mode + + + true + + + LeaveEditWithEscape + + + Mod/Assembly + + + + + + + Ground first part: + + + + + + + When you insert the first part in the assembly, you can choose to ground the part automatically. + + + 0 + + + + + + + Qt::Horizontal + + + + 20 + 20 + + + + + Qt::Vertical @@ -29,6 +78,13 @@ + + + Gui::PrefCheckBox + QCheckBox +
Gui/PrefWidgets.h
+
+
diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp new file mode 100644 index 0000000000..b2ee2e1c2b --- /dev/null +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -0,0 +1,739 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#ifndef _PreComp_ +#include +#include +#include +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ViewProviderAssembly.h" +#include "ViewProviderAssemblyPy.h" + + +using namespace Assembly; +using namespace AssemblyGui; + +void printPlacement(Base::Placement plc, const char* name) +{ + Base::Vector3d pos = plc.getPosition(); + Base::Vector3d axis; + double angle; + Base::Rotation rot = plc.getRotation(); + rot.getRawValue(axis, angle); + Base::Console().Warning( + "placement %s : position (%.1f, %.1f, %.1f) - axis (%.1f, %.1f, %.1f) angle %.1f\n", + name, + pos.x, + pos.y, + pos.z, + axis.x, + axis.y, + axis.z, + angle); +} + +PROPERTY_SOURCE(AssemblyGui::ViewProviderAssembly, Gui::ViewProviderPart) + +ViewProviderAssembly::ViewProviderAssembly() + : SelectionObserver(true) + , dragMode(DragMode::None) + , canStartDragging(false) + , partMoving(false) + , enableMovement(true) + , jointVisibilityBackup(false) + , docsToMove({}) +{} + +ViewProviderAssembly::~ViewProviderAssembly() = default; + +QIcon ViewProviderAssembly::getIcon() const +{ + return Gui::BitmapFactory().pixmap("Geoassembly.svg"); +} + +bool ViewProviderAssembly::doubleClicked() +{ + if (isInEditMode()) { + // Part is already 'Active' so we exit edit mode. + // Gui::Command::doCommand(Gui::Command::Gui, "Gui.activeDocument().resetEdit()"); + Gui::Application::Instance->activeDocument()->resetEdit(); + } + else { + // assure the Assembly workbench + if (App::GetApplication() + .GetUserParameter() + .GetGroup("BaseApp") + ->GetGroup("Preferences") + ->GetGroup("Mod/Assembly") + ->GetBool("SwitchToWB", true)) { + Gui::Command::assureWorkbench("AssemblyWorkbench"); + } + + // Part is not 'Active' so we enter edit mode to make it so. + Gui::Application::Instance->activeDocument()->setEdit(this); + } + + return true; +} + +bool ViewProviderAssembly::canDragObject(App::DocumentObject* obj) const +{ + // The user should not be able to drag the joint group out of the assembly + if (!obj || obj->getTypeId() == Assembly::JointGroup::getClassTypeId()) { + 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) { + // getLinkObjFromProp returns nullptr if the property doesn't exist. + App::DocumentObject* obj1 = AssemblyObject::getObjFromNameProp(joint, "Object1", "Part1"); + App::DocumentObject* obj2 = AssemblyObject::getObjFromNameProp(joint, "Object2", "Part2"); + App::DocumentObject* part1 = AssemblyObject::getLinkObjFromProp(joint, "Part1"); + App::DocumentObject* part2 = AssemblyObject::getLinkObjFromProp(joint, "Part2"); + App::DocumentObject* obj3 = AssemblyObject::getLinkObjFromProp(joint, "ObjectToGround"); + if (obj == obj1 || obj == obj2 || obj == part1 || obj == part2 || 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) +{ + // Set the part as 'Activated' ie bold in the tree. + Gui::Command::doCommand(Gui::Command::Gui, + "Gui.ActiveDocument.ActiveView.setActiveObject('%s', " + "App.getDocument('%s').getObject('%s'))", + PARTKEY, + this->getObject()->getDocument()->getName(), + this->getObject()->getNameInDocument()); + + return true; +} + +void ViewProviderAssembly::unsetEdit(int ModNum) +{ + Q_UNUSED(ModNum); + canStartDragging = false; + partMoving = false; + docsToMove = {}; + + // Check if the view is still active before trying to deactivate the assembly. + auto doc = getDocument(); + if (!doc) { + return; + } + auto activeView = doc->getActiveView(); + if (!activeView) { + return; + } + + // Set the part as not 'Activated' ie not bold in the tree. + Gui::Command::doCommand(Gui::Command::Gui, + "appDoc = App.getDocument('%s')\n" + "Gui.getDocument(appDoc).ActiveView.setActiveObject('%s', None)", + this->getObject()->getDocument()->getName(), + PARTKEY); +} + +bool ViewProviderAssembly::isInEditMode() const +{ + App::DocumentObject* activePart = getActivePart(); + if (!activePart) { + return false; + } + + return activePart == this->getObject(); +} + +App::DocumentObject* ViewProviderAssembly::getActivePart() const +{ + auto activeDoc = Gui::Application::Instance->activeDocument(); + if (!activeDoc) { + activeDoc = getDocument(); + } + auto activeView = activeDoc->getActiveView(); + if (!activeView) { + return nullptr; + } + + return activeView->getActiveObject(PARTKEY); +} + +bool ViewProviderAssembly::keyPressed(bool pressed, int key) +{ + if (key == SoKeyboardEvent::ESCAPE) { + if (isInEditMode()) { + + ParameterGrp::handle hPgr = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Mod/Assembly"); + + return !hPgr->GetBool("LeaveEditWithEscape", true); + } + } + + return false; // handle all other key events +} + +bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInventorViewer* viewer) +{ + // Initialize or end the dragging of parts + if (canStartDragging) { + canStartDragging = false; + + if (enableMovement && getSelectedObjectsWithinAssembly()) { + dragMode = findDragMode(); + + if (dragMode == DragMode::None) { + return false; + } + + SbVec3f vec; + if (dragMode == DragMode::RotationOnPlane + || dragMode == DragMode::TranslationOnAxisAndRotationOnePlane) { + vec = viewer->getPointOnXYPlaneOfPlacement(cursorPos, jcsGlobalPlc); + initialPositionRot = Base::Vector3d(vec[0], vec[1], vec[2]); + } + + if (dragMode == DragMode::TranslationOnAxis + || dragMode == DragMode::TranslationOnAxisAndRotationOnePlane) { + Base::Vector3d zAxis = + jcsGlobalPlc.getRotation().multVec(Base::Vector3d(0., 0., 1.)); + Base::Vector3d pos = jcsGlobalPlc.getPosition(); + SbVec3f axisCenter(pos.x, pos.y, pos.z); + SbVec3f axis(zAxis.x, zAxis.y, zAxis.z); + vec = viewer->getPointOnLine(cursorPos, axisCenter, axis); + initialPosition = Base::Vector3d(vec[0], vec[1], vec[2]); + } + else if (dragMode != DragMode::RotationOnPlane) { + vec = viewer->getPointOnFocalPlane(cursorPos); + initialPosition = Base::Vector3d(vec[0], vec[1], vec[2]); + prevPosition = initialPosition; + } + + initMove(); + } + } + + // Do the dragging of parts + if (partMoving) { + Base::Vector3d newPos, newPosRot; + if (dragMode == DragMode::RotationOnPlane + || dragMode == DragMode::TranslationOnAxisAndRotationOnePlane) { + SbVec3f vec = viewer->getPointOnXYPlaneOfPlacement(cursorPos, jcsGlobalPlc); + newPosRot = Base::Vector3d(vec[0], vec[1], vec[2]); + } + + if (dragMode == DragMode::TranslationOnAxis + || dragMode == DragMode::TranslationOnAxisAndRotationOnePlane) { + Base::Vector3d zAxis = jcsGlobalPlc.getRotation().multVec(Base::Vector3d(0., 0., 1.)); + Base::Vector3d pos = jcsGlobalPlc.getPosition(); + SbVec3f axisCenter(pos.x, pos.y, pos.z); + SbVec3f axis(zAxis.x, zAxis.y, zAxis.z); + SbVec3f vec = viewer->getPointOnLine(cursorPos, axisCenter, axis); + newPos = Base::Vector3d(vec[0], vec[1], vec[2]); + } + else if (dragMode != DragMode::RotationOnPlane) { + SbVec3f vec = viewer->getPointOnFocalPlane(cursorPos); + newPos = Base::Vector3d(vec[0], vec[1], vec[2]); + } + + + for (auto& pair : docsToMove) { + App::DocumentObject* obj = pair.first; + auto* propPlacement = + dynamic_cast(obj->getPropertyByName("Placement")); + if (propPlacement) { + Base::Placement plc = pair.second; + // Base::Console().Warning("newPos %f %f %f\n", newPos.x, newPos.y, newPos.z); + + if (dragMode == DragMode::RotationOnPlane) { + Base::Vector3d center = jcsGlobalPlc.getPosition(); + Base::Vector3d norm = + jcsGlobalPlc.getRotation().multVec(Base::Vector3d(0., 0., -1.)); + double angle = + (newPosRot - center).GetAngleOriented(initialPositionRot - center, norm); + // Base::Console().Warning("angle %f\n", angle); + Base::Rotation zRotation = Base::Rotation(Base::Vector3d(0., 0., 1.), angle); + Base::Placement rotatedGlovalJcsPlc = + jcsGlobalPlc * Base::Placement(Base::Vector3d(), zRotation); + Base::Placement jcsPlcRelativeToPart = plc.inverse() * jcsGlobalPlc; + plc = rotatedGlovalJcsPlc * jcsPlcRelativeToPart.inverse(); + } + else if (dragMode == DragMode::TranslationOnAxis) { + Base::Vector3d pos = plc.getPosition() + (newPos - initialPosition); + plc.setPosition(pos); + } + else if (dragMode == DragMode::TranslationOnAxisAndRotationOnePlane) { + Base::Vector3d pos = plc.getPosition() + (newPos - initialPosition); + plc.setPosition(pos); + + Base::Placement newJcsGlobalPlc = jcsGlobalPlc; + newJcsGlobalPlc.setPosition(jcsGlobalPlc.getPosition() + + (newPos - initialPosition)); + + Base::Vector3d center = newJcsGlobalPlc.getPosition(); + Base::Vector3d norm = + newJcsGlobalPlc.getRotation().multVec(Base::Vector3d(0., 0., -1.)); + + Base::Vector3d projInitialPositionRot = + initialPositionRot.ProjectToPlane(newJcsGlobalPlc.getPosition(), norm); + double angle = + (newPosRot - center).GetAngleOriented(initialPositionRot - center, norm); + // Base::Console().Warning("angle %f\n", angle); + Base::Rotation zRotation = Base::Rotation(Base::Vector3d(0., 0., 1.), angle); + Base::Placement rotatedGlovalJcsPlc = + newJcsGlobalPlc * Base::Placement(Base::Vector3d(), zRotation); + Base::Placement jcsPlcRelativeToPart = plc.inverse() * newJcsGlobalPlc; + plc = rotatedGlovalJcsPlc * jcsPlcRelativeToPart.inverse(); + } + else { // DragMode::Translation + Base::Vector3d delta = newPos - prevPosition; + prevPosition = newPos; + + Base::Vector3d pos = propPlacement->getValue().getPosition() + delta; + // Base::Vector3d pos = newPos + (plc.getPosition() - initialPosition); + plc.setPosition(pos); + } + propPlacement->setValue(plc); + } + } + + 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(); + // assemblyPart->doDragStep(); + } + } + return false; +} + +bool ViewProviderAssembly::mouseButtonPressed(int Button, + bool pressed, + const SbVec2s& cursorPos, + const Gui::View3DInventorViewer* viewer) +{ + // Left Mouse button **************************************************** + if (Button == 1) { + if (pressed) { + canStartDragging = true; + } + else { // Button 1 released + // release event is not received when user click on a part for selection. + // So we use SelectionObserver to know if something got selected. + + canStartDragging = false; + if (partMoving) { + endMove(); + return true; + } + } + } + + return false; +} + +bool ViewProviderAssembly::getSelectedObjectsWithinAssembly() +{ + // check the current selection, and check if any of the selected objects are within this + // App::Part + // If any, put them into the vector docsToMove and return true. + // Get the document + Gui::Document* doc = Gui::Application::Instance->activeDocument(); + + if (!doc) { + return false; + } + + // Get the assembly object for this ViewProvider + AssemblyObject* assemblyPart = static_cast(getObject()); + + if (!assemblyPart) { + return false; + } + + for (auto& selObj : Gui::Selection().getSelectionEx("", + App::DocumentObject::getClassTypeId(), + Gui::ResolveMode::NoResolve)) { + // getSubNames() returns ["Body001.Pad.Face14", "Body002.Pad.Face7"] + // if you have several objects within the same assembly selected. + + std::vector objsSubNames = selObj.getSubNames(); + for (auto& subNamesStr : objsSubNames) { + std::vector subNames = parseSubNames(subNamesStr); + + App::DocumentObject* obj = getObjectFromSubNames(subNames); + if (!obj) { + continue; + } + + // Check if the selected object is a child of the assembly + if (assemblyPart->hasObject(obj, true)) { + auto* propPlacement = + dynamic_cast(obj->getPropertyByName("Placement")); + if (propPlacement) { + docsToMove.emplace_back(obj, propPlacement->getValue()); + } + } + } + } + + // This function is called before the selection is updated. So if a user click and drag a part + // it is not selected at that point. So we need to get the preselection too. + if (Gui::Selection().hasPreselection()) { + + // Base::Console().Warning("Gui::Selection().getPreselection().pSubName %s\n", + // Gui::Selection().getPreselection().pSubName); + + std::string subNamesStr = Gui::Selection().getPreselection().pSubName; + std::vector subNames = parseSubNames(subNamesStr); + + App::DocumentObject* preselectedObj = getObjectFromSubNames(subNames); + if (preselectedObj && assemblyPart->hasObject(preselectedObj, true)) { + bool alreadyIn = false; + for (auto& pair : docsToMove) { + App::DocumentObject* obj = pair.first; + if (obj == preselectedObj) { + alreadyIn = true; + break; + } + } + + if (!alreadyIn) { + auto* propPlacement = dynamic_cast( + preselectedObj->getPropertyByName("Placement")); + if (propPlacement) { + docsToMove.emplace_back(preselectedObj, propPlacement->getValue()); + } + } + } + } + + return !docsToMove.empty(); +} + +std::vector ViewProviderAssembly::parseSubNames(std::string& subNamesStr) +{ + std::vector subNames; + std::string subName; + std::istringstream subNameStream(subNamesStr); + while (std::getline(subNameStream, subName, '.')) { + subNames.push_back(subName); + } + return subNames; +} + +App::DocumentObject* ViewProviderAssembly::getObjectFromSubNames(std::vector& subNames) +{ + App::Document* appDoc = App::GetApplication().getActiveDocument(); + + std::string objName; + if (subNames.size() < 2) { + return nullptr; + } + else if (subNames.size() == 2) { + // If two subnames then it can't be a body and the object we want is the first one + // For example we want box in "box.face1" + return appDoc->getObject(subNames[0].c_str()); + } + + // 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) { + continue; + } + + 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(App::Part::getClassTypeId()) + || linkedObj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) { + return obj; + } + } + } + + // 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()); +} + +ViewProviderAssembly::DragMode ViewProviderAssembly::findDragMode() +{ + if (docsToMove.size() == 1) { + auto* assemblyPart = static_cast(getObject()); + std::string partPropName; + movingJoint = + assemblyPart->getJointOfPartConnectingToGround(docsToMove[0].first, partPropName); + + if (!movingJoint) { + return DragMode::Translation; + } + + JointType jointType = AssemblyObject::getJointType(movingJoint); + if (jointType == JointType::Fixed) { + // If fixed joint we need to find the upstream joint to find move mode. + // For example : Gnd -(revolute)- A -(fixed)- B : if user try to move B, then we should + // actually move A + App::DocumentObject* upstreamPart = + assemblyPart->getUpstreamMovingPart(docsToMove[0].first); + docsToMove.clear(); + if (!upstreamPart) { + return DragMode::None; + } + + auto* propPlacement = + dynamic_cast(upstreamPart->getPropertyByName("Placement")); + if (propPlacement) { + docsToMove.emplace_back(upstreamPart, propPlacement->getValue()); + } + + movingJoint = + assemblyPart->getJointOfPartConnectingToGround(docsToMove[0].first, partPropName); + if (!movingJoint) { + return DragMode::Translation; + } + jointType = AssemblyObject::getJointType(movingJoint); + } + + const char* plcPropName = (partPropName == "Part1") ? "Placement1" : "Placement2"; + const char* objPropName = (partPropName == "Part1") ? "Object1" : "Object2"; + + // jcsPlc is relative to the Object + jcsPlc = AssemblyObject::getPlacementFromProp(movingJoint, plcPropName); + + // Make jcsGlobalPlc relative to the origin of the doc + Base::Placement global_plc = + AssemblyObject::getGlobalPlacement(movingJoint, objPropName, partPropName.c_str()); + jcsGlobalPlc = global_plc * jcsPlc; + + // Add downstream parts so that they move together + auto downstreamParts = assemblyPart->getDownstreamParts(docsToMove[0].first, movingJoint); + for (auto part : downstreamParts) { + auto* propPlacement = + dynamic_cast(part->getPropertyByName("Placement")); + if (propPlacement) { + docsToMove.emplace_back(part, propPlacement->getValue()); + } + } + + jointVisibilityBackup = movingJoint->Visibility.getValue(); + if (!jointVisibilityBackup) { + movingJoint->Visibility.setValue(true); + } + + if (jointType == JointType::Revolute) { + return DragMode::RotationOnPlane; + } + else if (jointType == JointType::Slider) { + return DragMode::TranslationOnAxis; + } + else if (jointType == JointType::Cylindrical) { + return DragMode::TranslationOnAxisAndRotationOnePlane; + } + else if (jointType == JointType::Ball) { + // return DragMode::Ball; + } + else if (jointType == JointType::Distance) { + // depends on the type of distance. For example plane-plane: + // return DragMode::TranslationOnPlane; + } + } + return DragMode::Translation; +} + +void ViewProviderAssembly::initMove() +{ + Gui::Command::openCommand(tr("Move part").toStdString().c_str()); + partMoving = true; + + // prevent selection while moving + auto* view = dynamic_cast( + Gui::Application::Instance->editDocument()->getActiveView()); + if (view) { + Gui::View3DInventorViewer* viewerNotConst; + viewerNotConst = static_cast(view)->getViewer(); + viewerNotConst->setSelectionEnabled(false); + } + + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Mod/Assembly"); + bool solveOnMove = hGrp->GetBool("SolveOnMove", true); + if (solveOnMove) { + objectMasses.clear(); + for (auto& pair : docsToMove) { + objectMasses.push_back({pair.first, 10.0}); + } + + auto* assemblyPart = static_cast(getObject()); + assemblyPart->setObjMasses(objectMasses); + /*std::vector dragParts; + for (auto& pair : docsToMove) { + dragParts.push_back(pair.first); + } + assemblyPart->preDrag(dragParts);*/ + } +} + +void ViewProviderAssembly::endMove() +{ + docsToMove = {}; + partMoving = false; + canStartDragging = false; + + if (movingJoint && !jointVisibilityBackup) { + movingJoint->Visibility.setValue(false); + } + + movingJoint = nullptr; + + // enable selection after the move + auto* view = dynamic_cast( + Gui::Application::Instance->editDocument()->getActiveView()); + if (view) { + Gui::View3DInventorViewer* viewerNotConst; + viewerNotConst = static_cast(view)->getViewer(); + viewerNotConst->setSelectionEnabled(true); + } + + 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->postDrag(); + assemblyPart->setObjMasses({}); + } + + Gui::Command::commitCommand(); +} + +void ViewProviderAssembly::onSelectionChanged(const Gui::SelectionChanges& msg) +{ + if (msg.Type == Gui::SelectionChanges::AddSelection + || msg.Type == Gui::SelectionChanges::ClrSelection + || msg.Type == Gui::SelectionChanges::RmvSelection) { + canStartDragging = false; + } +} + +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) { + pyViewObject = new ViewProviderAssemblyPy(this); + } + pyViewObject->IncRef(); + return pyViewObject; +} diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.h b/src/Mod/Assembly/Gui/ViewProviderAssembly.h new file mode 100644 index 0000000000..5cc7bd684f --- /dev/null +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.h @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#ifndef ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H +#define ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H + +#include + +#include + +#include +#include + +namespace Gui +{ +class View3DInventorViewer; +} + +namespace AssemblyGui +{ + +class AssemblyGuiExport ViewProviderAssembly: public Gui::ViewProviderPart, + public Gui::SelectionObserver +{ + Q_DECLARE_TR_FUNCTIONS(AssemblyGui::ViewProviderAssembly) + PROPERTY_HEADER_WITH_OVERRIDE(AssemblyGui::ViewProviderAssembly); + + enum class DragMode + { + Translation, + TranslationOnAxis, + TranslationOnPlane, + Rotation, + RotationOnPlane, + TranslationOnAxisAndRotationOnePlane, + Ball, + None, + }; + +public: + ViewProviderAssembly(); + ~ViewProviderAssembly() override; + + /// deliver the icon shown in the tree view. Override from ViewProvider.h + QIcon getIcon() const override; + + bool doubleClicked() override; + bool onDelete(const std::vector& subNames) override; + + /** @name enter/exit edit mode */ + //@{ + bool setEdit(int ModNum) override; + void unsetEdit(int ModNum) override; + bool isInEditMode() const; + + /// 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() const; + + + /// is called when the Provider is in edit and a key event ocours. Only ESC ends edit. + bool keyPressed(bool pressed, int key) override; + /// is called when the provider is in edit and the mouse is moved + bool mouseMove(const SbVec2s& pos, Gui::View3DInventorViewer* viewer) override; + /// is called when the Provider is in edit and the mouse is clicked + bool mouseButtonPressed(int Button, + bool pressed, + const SbVec2s& cursorPos, + const Gui::View3DInventorViewer* viewer) override; + + /// Finds what drag mode should be used based on the user selection. + DragMode findDragMode(); + void initMove(); + void endMove(); + virtual void setEnableMovement(bool enable = true) + { + enableMovement = enable; + } + virtual bool getEnableMovement() const + { + return enableMovement; + } + + bool getSelectedObjectsWithinAssembly(); + App::DocumentObject* getObjectFromSubNames(std::vector& subNames); + std::vector parseSubNames(std::string& subNamesStr); + + /// Get the python wrapper for that ViewProvider + PyObject* getPyObject() override; + + // protected: + /// get called by the container whenever a property has been changed + // void onChanged(const App::Property* prop) override; + + void onSelectionChanged(const Gui::SelectionChanges& msg) override; + + DragMode dragMode; + bool canStartDragging; + bool partMoving; + bool enableMovement; + bool jointVisibilityBackup; + int numberOfSel; + Base::Vector3d prevPosition; + Base::Vector3d initialPosition; + Base::Vector3d initialPositionRot; + Base::Placement jcsPlc; + Base::Placement jcsGlobalPlc; + + App::DocumentObject* movingJoint; + + std::vector> objectMasses; + std::vector> docsToMove; +}; + +} // namespace AssemblyGui + +#endif // ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H diff --git a/src/Mod/Assembly/Gui/ViewProviderAssemblyPy.xml b/src/Mod/Assembly/Gui/ViewProviderAssemblyPy.xml new file mode 100644 index 0000000000..503bf213ab --- /dev/null +++ b/src/Mod/Assembly/Gui/ViewProviderAssemblyPy.xml @@ -0,0 +1,23 @@ + + + + + + This is the ViewProviderAssembly class + + + + Enable moving the parts by clicking and dragging. + + + + + diff --git a/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp b/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp new file mode 100644 index 0000000000..5d6cffe4f1 --- /dev/null +++ b/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp @@ -0,0 +1,59 @@ +/*************************************************************************** + * Copyright (c) 2008 Werner Mayer * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + **************************************************************************/ + +#include "PreCompiled.h" + +// inclusion of the generated files (generated out of ViewProviderAssemblyPy.xml) +#include "ViewProviderAssemblyPy.h" +#include "ViewProviderAssemblyPy.cpp" + + +using namespace Gui; + +// returns a string which represents the object e.g. when printed in python +std::string ViewProviderAssemblyPy::representation() const +{ + std::stringstream str; + str << ""; + + return str.str(); +} + +Py::Boolean ViewProviderAssemblyPy::getEnableMovement() const +{ + return {getViewProviderAssemblyPtr()->getEnableMovement()}; +} + +void ViewProviderAssemblyPy::setEnableMovement(Py::Boolean arg) +{ + getViewProviderAssemblyPtr()->setEnableMovement(arg); +} + +PyObject* ViewProviderAssemblyPy::getCustomAttributes(const char* /*attr*/) const +{ + return nullptr; +} + +int ViewProviderAssemblyPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/) +{ + return 0; +} diff --git a/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp b/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp new file mode 100644 index 0000000000..29302adfb0 --- /dev/null +++ b/src/Mod/Assembly/Gui/ViewProviderJointGroup.cpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#ifndef _PreComp_ +#endif + +#include +#include +#include +#include + +#include "ViewProviderJointGroup.h" + + +using namespace AssemblyGui; + +PROPERTY_SOURCE(AssemblyGui::ViewProviderJointGroup, Gui::ViewProviderDocumentObjectGroup) + +ViewProviderJointGroup::ViewProviderJointGroup() +{} + +ViewProviderJointGroup::~ViewProviderJointGroup() = default; + +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 new file mode 100644 index 0000000000..76ca1b6230 --- /dev/null +++ b/src/Mod/Assembly/Gui/ViewProviderJointGroup.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2023 Ondsel * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#ifndef ASSEMBLYGUI_VIEWPROVIDER_ViewProviderJointGroup_H +#define ASSEMBLYGUI_VIEWPROVIDER_ViewProviderJointGroup_H + +#include + +#include + + +namespace AssemblyGui +{ + +class AssemblyGuiExport ViewProviderJointGroup: public Gui::ViewProviderDocumentObjectGroup +{ + PROPERTY_HEADER_WITH_OVERRIDE(AssemblyGui::ViewProviderJointGroup); + +public: + ViewProviderJointGroup(); + ~ViewProviderJointGroup() override; + + /// 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; +}; + +} // namespace AssemblyGui + +#endif // ASSEMBLYGUI_VIEWPROVIDER_ViewProviderJointGroup_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 7df9fb15d4..723b877aa3 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 @@ -49,7 +49,6 @@ class AssemblyWorkbench(Workbench): "Assembly workbench" def __init__(self): - print("Loading Assembly workbench...") self.__class__.Icon = ( FreeCAD.getResourceDir() + "Mod/Assembly/Resources/icons/AssemblyWorkbench.svg" ) @@ -57,7 +56,6 @@ class AssemblyWorkbench(Workbench): self.__class__.ToolTip = "Assembly workbench" def Initialize(self): - print("Initializing Assembly workbench...") global AssemblyCommandGroup translate = FreeCAD.Qt.translate @@ -65,7 +63,7 @@ class AssemblyWorkbench(Workbench): # load the builtin modules from PySide import QtCore, QtGui from PySide.QtCore import QT_TRANSLATE_NOOP - import CommandCreateAssembly, CommandInsertLink, CommandCreateJoint + import CommandCreateAssembly, CommandInsertLink, CommandCreateJoint, CommandSolveAssembly, CommandExportASMT from Preferences import PreferencesPage # from Preferences import preferences @@ -76,28 +74,35 @@ class AssemblyWorkbench(Workbench): FreeCADGui.addPreferencePage(PreferencesPage, QT_TRANSLATE_NOOP("QObject", "Assembly")) # build commands list - cmdlist = ["Assembly_CreateAssembly", "Assembly_InsertLink"] + cmdList = [ + "Assembly_CreateAssembly", + "Assembly_InsertLink", + "Assembly_SolveAssembly", + ] + + cmdListMenuOnly = [ + "Assembly_ExportASMT", + ] + cmdListJoints = [ + "Assembly_ToggleGrounded", + "Separator", "Assembly_CreateJointFixed", "Assembly_CreateJointRevolute", "Assembly_CreateJointCylindrical", "Assembly_CreateJointSlider", "Assembly_CreateJointBall", - "Assembly_CreateJointPlanar", - "Assembly_CreateJointParallel", - "Assembly_CreateJointTangent", + "Assembly_CreateJointDistance", ] - self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Assembly"), cmdlist) + self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Assembly"), cmdList) self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Assembly Joints"), cmdListJoints) self.appendMenu( [QT_TRANSLATE_NOOP("Workbench", "&Assembly")], - cmdlist + ["Separator"] + cmdListJoints, + cmdList + cmdListMenuOnly + ["Separator"] + cmdListJoints, ) - print("Assembly workbench loaded") - def Activated(self): # update the translation engine FreeCADGui.updateLocale() diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index 92aebfb10c..92c998f632 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,13 +19,14 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import math import FreeCAD as App import Part +from PySide import QtCore from PySide.QtCore import QT_TRANSLATE_NOOP if App.GuiUp: @@ -39,23 +40,74 @@ __url__ = "https://www.freecad.org" from pivy import coin import UtilsAssembly +import Preferences + +translate = App.Qt.translate + +TranslatedJointTypes = [ + translate("Assembly", "Fixed"), + translate("Assembly", "Revolute"), + translate("Assembly", "Cylindrical"), + translate("Assembly", "Slider"), + translate("Assembly", "Ball"), + translate("Assembly", "Distance"), +] JointTypes = [ - QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"), - QT_TRANSLATE_NOOP("AssemblyJoint", "Revolute"), - 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"), + "Fixed", + "Revolute", + "Cylindrical", + "Slider", + "Ball", + "Distance", +] + +JointUsingDistance = [ + "Distance", +] + +JointUsingOffset = [ + "Fixed", + "Revolute", +] + +JointUsingRotation = [ + "Fixed", + "Slider", +] + +JointUsingReverse = [ + "Fixed", + "Revolute", + "Cylindrical", + "Slider", + "Distance", ] +def solveIfAllowed(assembly, storePrev=False): + if assembly.Type == "Assembly" and Preferences.preferences().GetBool( + "SolveInJointCreation", True + ): + assembly.solve(storePrev) + + +# The joint object consists of 2 JCS (joint coordinate systems) and a Joint Type. +# A JCS is a placement that is computed (unless it is detached) from : +# - An Object name: this is the name of the solid. It can be any Part::Feature solid. +# Or a PartDesign Body. Or a App::Link to those. We use the name and not directly the DocumentObject +# because the object can be external. +# - A Part DocumentObject : This is the lowest level containing part. It can be either the Object itself if it +# stands alone. Or a App::Part. Or a App::Link to a App::Part. +# For example : +# Assembly.Assembly1.Part1.Part2.Box : Object is Box, part is 'Part1' +# Assembly.Assembly1.LinkToPart1.Part2.Box : Object is Box, part is 'LinkToPart1' +# - An element name: This can be either a face, an edge, a vertex or empty. Empty means that the Object placement will be used +# - A vertex name: For faces and edges, we need to specify which vertex of said face/edge to use +# From these a placement is computed. It is relative to the Object. class Joint: def __init__(self, joint, type_index): joint.Proxy = self - self.joint = joint joint.addProperty( "App::PropertyEnumeration", @@ -68,12 +120,19 @@ class Joint: # First Joint Connector joint.addProperty( - "App::PropertyLink", + "App::PropertyString", # Not PropertyLink because they don't support external objects "Object1", "Joint Connector 1", 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", @@ -94,18 +153,35 @@ class Joint: "Joint Connector 1", QT_TRANSLATE_NOOP( "App::Property", - "This is the local coordinate system within the object1 that will be used to joint.", + "This is the local coordinate system within object1 that will be used for the joint.", + ), + ) + + joint.addProperty( + "App::PropertyBool", + "Detach1", + "Joint Connector 1", + QT_TRANSLATE_NOOP( + "App::Property", + "This prevents Placement1 from recomputing, enabling custom positioning of the placement.", ), ) # Second Joint Connector joint.addProperty( - "App::PropertyLink", + "App::PropertyString", "Object2", "Joint Connector 2", 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", @@ -126,50 +202,179 @@ class Joint: "Joint Connector 2", QT_TRANSLATE_NOOP( "App::Property", - "This is the local coordinate system within the object2 that will be used to joint.", + "This is the local coordinate system within object2 that will be used for the joint.", ), ) - self.setJointConnectors([]) + joint.addProperty( + "App::PropertyBool", + "Detach2", + "Joint Connector 2", + QT_TRANSLATE_NOOP( + "App::Property", + "This prevents Placement2 from recomputing, enabling custom positioning of the placement.", + ), + ) - def onChanged(self, fp, prop): + 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", + "Activated", + "Joint", + QT_TRANSLATE_NOOP( + "App::Property", + "This indicates if the joint is active.", + ), + ) + joint.Activated = True + + self.setJointConnectors(joint, []) + + def dumps(self): + return None + + def loads(self, state): + return None + + def getAssembly(self, joint): + return joint.InList[0] + + def setJointType(self, joint, jointType): + joint.JointType = jointType + joint.Label = jointType.replace(" ", "") + + 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": + # during loading the onchanged may be triggered before full init. + if hasattr(joint, "Vertex1"): # so we check Vertex1 + self.updateJCSPlacements(joint) + obj1 = UtilsAssembly.getObjectInPart(joint.Object1, joint.Part1) + obj2 = UtilsAssembly.getObjectInPart(joint.Object2, joint.Part2) + presolved = self.preSolve( + joint, + obj1, + joint.Part1, + obj2, + joint.Part2, + False, + ) + + isAssembly = self.getAssembly(joint).Type == "Assembly" + if isAssembly and not presolved: + solveIfAllowed(self.getAssembly(joint)) + else: + self.updateJCSPlacements(joint) def execute(self, fp): """Do something when doing a recomputation, this method is mandatory""" # App.Console.PrintMessage("Recompute Python Box feature\n") pass - def setJointConnectors(self, current_selection): + 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) + isAssembly = assembly.Type == "Assembly" if len(current_selection) >= 1: - self.joint.Object1 = current_selection[0]["object"] - self.joint.Element1 = current_selection[0]["element_name"] - self.joint.Vertex1 = current_selection[0]["vertex_name"] - self.joint.Placement1 = self.findPlacement( - self.joint.Object1, self.joint.Element1, self.joint.Vertex1 + joint.Part1 = None + if isAssembly: + self.part1Connected = assembly.isPartConnected(current_selection[0]["part"]) + else: + self.part1Connected = True + + joint.Object1 = current_selection[0]["object"].Name + 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, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1 ) else: - self.joint.Object1 = None - self.joint.Element1 = "" - self.joint.Vertex1 = "" - self.joint.Placement1 = UtilsAssembly.activeAssembly().Placement + joint.Object1 = "" + joint.Part1 = None + joint.Element1 = "" + joint.Vertex1 = "" + joint.Placement1 = App.Placement() + self.partMovedByPresolved = None if len(current_selection) >= 2: - self.joint.Object2 = current_selection[1]["object"] - self.joint.Element2 = current_selection[1]["element_name"] - self.joint.Vertex2 = current_selection[1]["vertex_name"] - self.joint.Placement2 = self.findPlacement( - self.joint.Object2, self.joint.Element2, self.joint.Vertex2 + joint.Part2 = None + if isAssembly: + self.part2Connected = assembly.isPartConnected(current_selection[1]["part"]) + else: + self.part2Connected = False + + joint.Object2 = current_selection[1]["object"].Name + 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, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True ) + self.preSolve( + joint, + current_selection[0]["object"], + joint.Part1, + current_selection[1]["object"], + joint.Part2, + ) + if isAssembly: + solveIfAllowed(assembly, True) + else: + self.updateJCSPlacements(joint) + else: - self.joint.Object2 = None - self.joint.Element2 = "" - self.joint.Vertex2 = "" - self.joint.Placement2 = UtilsAssembly.activeAssembly().Placement + joint.Object2 = "" + joint.Part2 = None + joint.Element2 = "" + joint.Vertex2 = "" + joint.Placement2 = App.Placement() + if isAssembly: + assembly.undoSolve() + self.undoPreSolve() + + def updateJCSPlacements(self, joint): + if not joint.Detach1: + joint.Placement1 = self.findPlacement( + joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1 + ) + + if not joint.Detach2: + 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. @@ -181,11 +386,25 @@ 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): - plc = App.Placement(obj.Placement) + def findPlacement(self, joint, objName, part, elt, vtx, isSecond=False): + if not objName or not part: + return App.Placement() + + obj = UtilsAssembly.getObjectInPart(objName, part) + plc = App.Placement() + + if not obj: + return App.Placement() + + if not elt or not vtx: + # case of whole parts such as PartDesign::Body or PartDesign::CordinateSystem/Point/Line/Plane. + return App.Placement() + elt_type, elt_index = UtilsAssembly.extract_type_and_number(elt) vtx_type, vtx_index = UtilsAssembly.extract_type_and_number(vtx) + isLine = False + if elt_type == "Vertex": vertex = obj.Shape.Vertexes[elt_index - 1] plc.Base = (vertex.X, vertex.Y, vertex.Z) @@ -194,11 +413,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) @@ -208,38 +431,207 @@ class Joint: plc.Rotation = App.Rotation(curve.Rotation) if curve.TypeId == "Part::GeomLine": + isLine = True plane_normal = curve.Direction 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 + + # post-process of plc for some special cases + if elt_type == "Vertex": + plc.Rotation = App.Rotation() + elif isLine: + plane_normal = plc.Rotation.multVec(App.Vector(0, 0, 1)) + plane_origin = App.Vector(0, 0, 0) + plane = Part.Plane(plane_origin, plane_normal) + plc.Rotation = App.Rotation(plane.Rotation) + + # 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. + # assembly = self.getAssembly(joint) + # 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) return plc + def applyOffsetToPlacement(self, plc, offset): + plc.Base = plc.Base + plc.Rotation.multVec(offset) + return plc + + def applyRotationToPlacement(self, plc, angle): + return self.applyRotationToPlacementAlongAxis(plc, angle, App.Vector(0, 0, 1)) + + def applyRotationToPlacementAlongAxis(self, plc, angle, axis): + rot = plc.Rotation + zRotation = App.Rotation(axis, angle) + plc.Rotation = rot * zRotation + return plc + + def flipPlacement(self, plc): + return self.applyRotationToPlacementAlongAxis(plc, 180, App.Vector(1, 0, 0)) + + 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: + 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)) + + 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 + + def preSolve(self, joint, obj1, part1, obj2, part2, 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. + sameDir = self.areJcsSameDir(joint) + + if hasattr(self, "part2Connected") and not self.part2Connected: + if savePlc: + 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() + return True + + elif hasattr(self, "part1Connected") and not self.part1Connected: + if savePlc: + 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() + return True + return False + + 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: - def __init__(self, obj, app_obj): + def __init__(self, vobj): """Set this object to the proxy object of the actual view provider""" + + vobj.Proxy = self + + def attach(self, vobj): + """Setup the scene sub-graph of the view provider, this method is mandatory""" self.axis_thickness = 3 view_params = App.ParamGet("User parameter:BaseApp/Preferences/View") @@ -256,13 +648,13 @@ class ViewProviderJoint: camera = Gui.ActiveDocument.ActiveView.getCameraNode() self.cameraSensor = coin.SoFieldSensor(self.camera_callback, camera) - self.cameraSensor.attach(camera.height) + if isinstance(camera, coin.SoPerspectiveCamera): + self.cameraSensor.attach(camera.focalDistance) + elif isinstance(camera, coin.SoOrthographicCamera): + self.cameraSensor.attach(camera.height) - self.app_obj = app_obj - obj.Proxy = self + self.app_obj = vobj.Object - def attach(self, obj): - """Setup the scene sub-graph of the view provider, this method is mandatory""" self.transform1 = coin.SoTransform() self.transform2 = coin.SoTransform() self.transform3 = coin.SoTransform() @@ -275,27 +667,27 @@ class ViewProviderJoint: self.draw_style.style = coin.SoDrawStyle.LINES self.draw_style.lineWidth = self.axis_thickness - self.switch_JCS1 = self.JCS_sep(obj, self.transform1) - self.switch_JCS2 = self.JCS_sep(obj, self.transform2) - self.switch_JCS_preview = self.JCS_sep(obj, self.transform3) + self.switch_JCS1 = self.JCS_sep(self.transform1) + self.switch_JCS2 = self.JCS_sep(self.transform2) + self.switch_JCS_preview = self.JCS_sep(self.transform3) - self.display_mode = coin.SoGroup() + self.pick = coin.SoPickStyle() + self.setPickableState(True) + + self.display_mode = coin.SoType.fromName("SoFCSelection").createInstance() + self.display_mode.addChild(self.pick) self.display_mode.addChild(self.switch_JCS1) self.display_mode.addChild(self.switch_JCS2) self.display_mode.addChild(self.switch_JCS_preview) - obj.addDisplayMode(self.display_mode, "Wireframe") + vobj.addDisplayMode(self.display_mode, "Wireframe") def camera_callback(self, *args): scaleF = self.get_JCS_size() self.axisScale.scaleFactor.setValue(scaleF, scaleF, scaleF) - def JCS_sep(self, obj, soTransform): - pick = coin.SoPickStyle() - pick.style.setValue(coin.SoPickStyle.UNPICKABLE) - + def JCS_sep(self, soTransform): JCS = coin.SoAnnotation() JCS.addChild(soTransform) - JCS.addChild(pick) base_plane_sep = self.plane_sep(0.4, 15) X_axis_sep = self.line_sep([0.5, 0, 0], [1, 0, 0], self.x_axis_so_color) @@ -345,11 +737,11 @@ class ViewProviderJoint: draw_style.style = coin.SoDrawStyle.FILLED material = coin.SoMaterial() - material.diffuseColor.setValue([1, 1, 1]) - material.ambientColor.setValue([1, 1, 1]) - material.specularColor.setValue([1, 1, 1]) - material.emissiveColor.setValue([1, 1, 1]) - material.transparency.setValue(0.7) + material.diffuseColor.setValue([0.5, 0.5, 0.5]) + material.ambientColor.setValue([0.5, 0.5, 0.5]) + material.specularColor.setValue([0.5, 0.5, 0.5]) + material.emissiveColor.setValue([0.5, 0.5, 0.5]) + material.transparency.setValue(0.3) face_sep = coin.SoAnnotation() face_sep.addChild(self.axisScale) @@ -362,44 +754,67 @@ class ViewProviderJoint: def get_JCS_size(self): camera = Gui.ActiveDocument.ActiveView.getCameraNode() - if not camera: + + # Check if the camera is a perspective camera + if isinstance(camera, coin.SoPerspectiveCamera): + return camera.focalDistance.getValue() / 20 + elif isinstance(camera, coin.SoOrthographicCamera): + return camera.height.getValue() / 20 + else: + # Default value if camera type is unknown return 10 return camera.height.getValue() / 20 - def set_JCS_placement(self, soTransform, placement): + def set_JCS_placement(self, soTransform, placement, objName, part): + # change plc to be relative to the origin of the document. + obj = UtilsAssembly.getObjectInPart(objName, part) + global_plc = UtilsAssembly.getGlobalPlacement(obj, part) + placement = global_plc * placement + t = placement.Base soTransform.translation.setValue(t.x, t.y, t.z) 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"): + if joint.Object1: + plc = joint.Placement1 self.switch_JCS1.whichChild = coin.SO_SWITCH_ALL - self.set_JCS_placement(self.transform1, plc) + + if joint.Part1: + self.set_JCS_placement(self.transform1, plc, joint.Object1, joint.Part1) else: self.switch_JCS1.whichChild = coin.SO_SWITCH_NONE if prop == "Placement2": - plc = fp.getPropertyByName("Placement2") - if fp.getPropertyByName("Object2"): + if joint.Object2: + plc = joint.Placement2 self.switch_JCS2.whichChild = coin.SO_SWITCH_ALL - self.set_JCS_placement(self.transform2, plc) + + if joint.Part2: + self.set_JCS_placement(self.transform2, plc, joint.Object2, joint.Part2) else: self.switch_JCS2.whichChild = coin.SO_SWITCH_NONE - def showPreviewJCS(self, visible, placement=None): + def showPreviewJCS(self, visible, placement=None, objName="", part=None): if visible: self.switch_JCS_preview.whichChild = coin.SO_SWITCH_ALL - self.set_JCS_placement(self.transform3, placement) + self.set_JCS_placement(self.transform3, placement, objName, part) else: self.switch_JCS_preview.whichChild = coin.SO_SWITCH_NONE + def setPickableState(self, state: bool): + """Set JCS selectable or unselectable in 3D view""" + if not state: + self.pick.style.setValue(coin.SoPickStyle.UNPICKABLE) + else: + self.pick.style.setValue(coin.SoPickStyle.SHAPE_ON_TOP) + def getDisplayModes(self, obj): """Return a list of display modes.""" modes = [] @@ -424,22 +839,18 @@ class ViewProviderJoint: self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2]) def getIcon(self): - if self.app_obj.getPropertyByName("JointType") == "Fixed": + if self.app_obj.JointType == "Fixed": return ":/icons/Assembly_CreateJointFixed.svg" - elif self.app_obj.getPropertyByName("JointType") == "Revolute": + elif self.app_obj.JointType == "Revolute": return ":/icons/Assembly_CreateJointRevolute.svg" - elif self.app_obj.getPropertyByName("JointType") == "Cylindrical": + elif self.app_obj.JointType == "Cylindrical": return ":/icons/Assembly_CreateJointCylindrical.svg" - elif self.app_obj.getPropertyByName("JointType") == "Slider": + elif self.app_obj.JointType == "Slider": return ":/icons/Assembly_CreateJointSlider.svg" - elif self.app_obj.getPropertyByName("JointType") == "Ball": + elif self.app_obj.JointType == "Ball": return ":/icons/Assembly_CreateJointBall.svg" - elif self.app_obj.getPropertyByName("JointType") == "Planar": - return ":/icons/Assembly_CreateJointPlanar.svg" - elif self.app_obj.getPropertyByName("JointType") == "Parallel": - return ":/icons/Assembly_CreateJointParallel.svg" - elif self.app_obj.getPropertyByName("JointType") == "Tangent": - return ":/icons/Assembly_CreateJointTangent.svg" + elif self.app_obj.JointType == "Distance": + return ":/icons/Assembly_CreateJointDistance.svg" return ":/icons/Assembly_CreateJoint.svg" @@ -453,3 +864,632 @@ class ViewProviderJoint: """When restoring the serialized object from document we have the chance to set some internals here.\ Since no data were serialized nothing needs to be done here.""" return None + + def doubleClicked(self, vobj): + assembly = vobj.Object.InList[0] + if UtilsAssembly.activeAssembly() != assembly: + Gui.ActiveDocument.setEdit(assembly) + + panel = TaskAssemblyCreateJoint(0, vobj.Object) + Gui.Control.showDialog(panel) + + +################ Grounded Joint object ################# + + +class GroundedJoint: + def __init__(self, joint, obj_to_ground): + joint.Proxy = self + self.joint = joint + + joint.addProperty( + "App::PropertyLink", + "ObjectToGround", + "Ground", + QT_TRANSLATE_NOOP("App::Property", "The object to ground"), + ) + + joint.ObjectToGround = obj_to_ground + + joint.addProperty( + "App::PropertyPlacement", + "Placement", + "Ground", + QT_TRANSLATE_NOOP( + "App::Property", + "This is where the part is grounded.", + ), + ) + + joint.Placement = obj_to_ground.Placement + + def dumps(self): + return None + + def loads(self, state): + return None + + def onChanged(self, fp, prop): + """Do something when a property has changed""" + # App.Console.PrintMessage("Change property: " + str(prop) + "\n") + pass + + def execute(self, fp): + """Do something when doing a recomputation, this method is mandatory""" + # App.Console.PrintMessage("Recompute Python Box feature\n") + pass + + +class ViewProviderGroundedJoint: + def __init__(self, obj): + """Set this object to the proxy object of the actual view provider""" + obj.Proxy = self + + def attach(self, obj): + """Setup the scene sub-graph of the view provider, this method is mandatory""" + pass + + def updateData(self, fp, 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 + pass + + def getDisplayModes(self, obj): + """Return a list of display modes.""" + modes = ["Wireframe"] + return modes + + def getDefaultDisplayMode(self): + """Return the name of the default display mode. It must be defined in getDisplayModes.""" + return "Wireframe" + + def onChanged(self, vp, prop): + """Here we can do something when a single property got changed""" + # 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 is not None and 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" + + +class MakeJointSelGate: + def __init__(self, taskbox, assembly): + self.taskbox = taskbox + self.assembly = assembly + + def allow(self, doc, obj, sub): + if not sub: + return False + + objs_names, element_name = UtilsAssembly.getObjsNamesAndElement(obj.Name, sub) + + 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): + # If it's to deselect then it's ok + return True + + if len(self.taskbox.current_selection) >= 2: + # No more than 2 elements can be selected for basic joints. + return False + + 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, self.assembly + ) + + for selection_dict in self.taskbox.current_selection: + 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() + if not self.assembly: + self.assembly = UtilsAssembly.activePart() + self.activeType = "Part" + else: + self.activeType = "Assembly" + + self.view = Gui.activeDocument().activeView() + self.doc = App.ActiveDocument + + if not self.assembly or not self.view or not self.doc: + return + + if self.activeType == "Assembly": + self.assembly.ViewObject.EnableMovement = False + + self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateJoint.ui") + + if self.activeType == "Part": + self.form.setWindowTitle("Match parts") + self.form.jointType.hide() + self.form.jointType.addItems(TranslatedJointTypes) + 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) + + 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(" ", "") + if self.activeType == "Part": + App.setActiveTransaction("Transform") + else: + App.setActiveTransaction("Create " + self.jointName + " Joint") + + self.current_selection = [] + self.preselection_dict = None + + self.createJointObject() + self.visibilityBackup = False + self.handleInitialSelection() + + self.toggleDistanceVisibility() + self.toggleOffsetVisibility() + self.toggleRotationVisibility() + self.toggleReverseVisibility() + + self.setJointsPickableState(False) + + Gui.Selection.addSelectionGate( + MakeJointSelGate(self, self.assembly), Gui.Selection.ResolveMode.NoResolve + ) + Gui.Selection.addObserver(self, Gui.Selection.ResolveMode.NoResolve) + Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.GreedySelection) + + self.callbackMove = self.view.addEventCallback("SoLocation2Event", self.moveMouse) + self.callbackKey = self.view.addEventCallback("SoKeyboardEvent", self.KeyboardEvent) + + def accept(self): + if len(self.current_selection) != 2: + App.Console.PrintWarning("You need to select 2 elements from 2 separate parts.") + return False + + self.deactivate() + + solveIfAllowed(self.assembly) + if self.activeType == "Assembly": + self.joint.Visibility = self.visibilityBackup + else: + self.joint.Document.removeObject(self.joint.Name) + + 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 + return True + + def deactivate(self): + global activeTask + activeTask = None + + if self.activeType == "Assembly": + self.assembly.clearUndo() + self.assembly.ViewObject.EnableMovement = True + + Gui.Selection.removeSelectionGate() + Gui.Selection.removeObserver(self) + Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.NormalSelection) + Gui.Selection.clearSelection() + self.view.removeEventCallback("SoLocation2Event", self.callbackMove) + self.view.removeEventCallback("SoKeyboardEvent", self.callbackKey) + self.setJointsPickableState(True) + 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 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 = self.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. + self.current_selection.clear() + Gui.Selection.clearSelection() + return + + 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) + + # do not accept initial selection if we don't have 2 selected features + if len(self.current_selection) != 2: + self.current_selection.clear() + Gui.Selection.clearSelection() + else: + self.updateJoint() + + def createJointObject(self): + type_index = self.form.jointType.currentIndex() + + if self.activeType == "Part": + self.joint = self.assembly.newObject("App::FeaturePython", "Temporary joint") + else: + joint_group = UtilsAssembly.getJointGroup(self.assembly) + self.joint = joint_group.newObject("App::FeaturePython", self.jointName) + + Joint(self.joint, type_index) + ViewProviderJoint(self.joint.ViewObject) + + def onJointTypeChanged(self, index): + + self.joint.Proxy.setJointType(self.joint, JointTypes[self.form.jointType.currentIndex()]) + 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.flipOnePart(self.joint) + + def toggleDistanceVisibility(self): + if JointTypes[self.form.jointType.currentIndex()] in JointUsingDistance: + self.form.distanceLabel.show() + self.form.distanceSpinbox.show() + else: + self.form.distanceLabel.hide() + self.form.distanceSpinbox.hide() + + def toggleOffsetVisibility(self): + if JointTypes[self.form.jointType.currentIndex()] in JointUsingOffset: + self.form.offsetLabel.show() + self.form.offsetSpinbox.show() + else: + self.form.offsetLabel.hide() + self.form.offsetSpinbox.hide() + + def toggleRotationVisibility(self): + if JointTypes[self.form.jointType.currentIndex()] in JointUsingRotation: + self.form.rotationLabel.show() + self.form.rotationSpinbox.show() + else: + self.form.rotationLabel.hide() + self.form.rotationSpinbox.hide() + + def toggleReverseVisibility(self): + if JointTypes[self.form.jointType.currentIndex()] in JointUsingReverse: + self.form.PushButtonReverse.show() + else: + self.form.PushButtonReverse.hide() + + def updateTaskboxFromJoint(self): + self.current_selection = [] + self.preselection_dict = None + + obj1 = UtilsAssembly.getObjectInPart(self.joint.Object1, self.joint.Part1) + obj2 = UtilsAssembly.getObjectInPart(self.joint.Object2, self.joint.Part2) + + selection_dict1 = { + "object": obj1, + "part": self.joint.Part1, + "element_name": self.joint.Element1, + "vertex_name": self.joint.Vertex1, + } + + selection_dict2 = { + "object": obj2, + "part": self.joint.Part2, + "element_name": self.joint.Element2, + "vertex_name": self.joint.Vertex2, + } + + self.current_selection.append(selection_dict1) + self.current_selection.append(selection_dict2) + + # Add the elements to the selection. Note we cannot do : + # Gui.Selection.addSelection(self.doc.Name, obj1.Name, elName) + # Because obj1 can be external in which case addSelection will fail. And + # Gui.Selection.addSelection(obj1.Document.Name, obj1.Name, elName) + # will not select in the assembly doc. + elName = self.getSubnameForSelection(obj1, self.joint.Part1, self.joint.Element1) + Gui.Selection.addSelection(self.doc.Name, self.joint.Part1.Name, elName) + + elName = self.getSubnameForSelection(obj2, self.joint.Part2, self.joint.Element2) + Gui.Selection.addSelection(self.doc.Name, self.joint.Part2.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 getSubnameForSelection(self, obj, part, elName): + # We need the subname starting from the part. + # Example for : Assembly.Part1.LinkToPart2.Part3.Body.Tip.Face1 + # part is Part1 and obj is Body + # we should get : LinkToPart2.Part3.Body.Tip.Face1 + + if obj is None or part is None: + return elName + + if obj.TypeId == "PartDesign::Body": + elName = obj.Tip.Name + "." + elName + elif obj.TypeId == "App::Link": + linked_obj = obj.getLinkedObject() + if linked_obj.TypeId == "PartDesign::Body": + elName = linked_obj.Tip.Name + "." + elName + + if obj != part and obj in part.OutListRecursive: + bSub = "" + currentObj = part + + limit = 0 + while limit < 1000: + limit = limit + 1 + + if currentObj != part: + if bSub != "": + bSub = bSub + "." + bSub = bSub + currentObj.Name + + if currentObj == obj: + break + + if currentObj.TypeId == "App::Link": + currentObj = currentObj.getLinkedObject() + + for obji in currentObj.OutList: + if obji == obj or obj in obji.OutListRecursive: + currentObj = obji + break + + elName = bSub + "." + elName + return elName + + def updateJoint(self): + # First we build the listwidget + self.updateJointList() + + # Then we pass the new list to the join object + self.joint.Proxy.setJointConnectors(self.joint, self.current_selection) + + def updateJointList(self): + self.form.featureList.clear() + simplified_names = [] + for sel in self.current_selection: + 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 ( + not self.preselection_dict + or self.current_selection[0]["part"] == self.preselection_dict["part"] + ) + ): + self.joint.ViewObject.Proxy.showPreviewJCS(False) + return + + cursor_pos = self.view.getCursorPos() + cursor_info = self.view.getObjectInfo(cursor_pos) + # cursor_info example {'x': 41.515, 'y': 7.449, 'z': 16.861, 'ParentObject': , 'SubName': 'Body002.Pad.Face5', 'Document': 'part3', 'Object': 'Pad', 'Component': 'Face5'} + + if ( + not cursor_info + or not self.preselection_dict + # or cursor_info["SubName"] != self.preselection_dict["sub_name"] + # Removed because they are not equal when hovering a line endpoints. + # But we don't actually need to test because if there's no preselection then not cursor is None + ): + self.joint.ViewObject.Proxy.showPreviewJCS(False) + return + + # newPos = self.view.getPoint(*info["Position"]) # This is not what we want, it's not pos on the object but on the focal plane + + newPos = App.Vector(cursor_info["x"], cursor_info["y"], cursor_info["z"]) + self.preselection_dict["mouse_pos"] = newPos + + 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 + objName = self.preselection_dict["object"].Name + part = self.preselection_dict["part"] + placement = self.joint.Proxy.findPlacement( + self.joint, + objName, + part, + self.preselection_dict["element_name"], + self.preselection_dict["vertex_name"], + isSecond, + ) + self.joint.ViewObject.Proxy.showPreviewJCS(True, placement, objName, part) + self.previewJCSVisible = True + + # 3D view keyboard handler + def KeyboardEvent(self, info): + if info["State"] == "UP" and info["Key"] == "ESCAPE": + self.reject() + + if info["State"] == "UP" and info["Key"] == "RETURN": + self.accept() + + def getContainingPart(self, full_element_name, obj): + return UtilsAssembly.getContainingPart(full_element_name, obj, self.assembly) + + # selectionObserver stuff + def addSelection(self, doc_name, obj_name, sub_name, mousePos): + 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 = self.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]), + } + if element_name == "": + selection_dict["vertex_name"] = "" + else: + selection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(selection_dict) + + self.current_selection.append(selection_dict) + self.updateJoint() + + # We hide the preview JCS if we just added to the selection + self.joint.ViewObject.Proxy.showPreviewJCS(False) + + def removeSelection(self, doc_name, obj_name, sub_name, mousePos=None): + 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 = self.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["part"] == part_containing_selected_object: + selection_dict_to_remove = selection_dict + break + + if selection_dict_to_remove is not None: + self.current_selection.remove(selection_dict_to_remove) + + self.updateJoint() + + def setPreselection(self, doc_name, obj_name, sub_name): + if not sub_name: + self.preselection_dict = None + return + + 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 = self.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, + "full_obj_name": full_obj_name, + } + + def clearSelection(self, doc_name): + self.current_selection.clear() + self.updateJoint() + + def setJointsPickableState(self, state: bool): + """Make all joints in assembly selectable (True) or unselectable (False) in 3D view""" + if self.activeType == "Assembly": + jointGroup = UtilsAssembly.getJointGroup(self.assembly) + for joint in jointGroup.Group: + if hasattr(joint, "JointType"): + joint.ViewObject.Proxy.setPickableState(state) + else: + for obj in self.assembly.OutList: + if obj.TypeId == "App::FeaturePython" and hasattr(obj, "JointType"): + obj.ViewObject.Proxy.setPickableState(state) diff --git a/src/Mod/Assembly/Preferences.py b/src/Mod/Assembly/Preferences.py index f5d9b5e57d..8f96362a4d 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,11 +19,13 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import FreeCAD import FreeCADGui +translate = FreeCAD.Qt.translate + def preferences(): return FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly") @@ -34,7 +36,15 @@ class PreferencesPage: self.form = FreeCADGui.PySideUic.loadUi(":preferences/Assembly.ui") def saveSettings(self): - pass + pref = preferences() + pref.SetBool("LeaveEditWithEscape", self.form.checkBoxEnableEscape.isChecked()) + pref.SetInt("GroundFirstPart", self.form.groundFirstPart.currentIndex()) def loadSettings(self): - pass + pref = preferences() + self.form.checkBoxEnableEscape.setChecked(pref.GetBool("LeaveEditWithEscape", True)) + self.form.groundFirstPart.clear() + self.form.groundFirstPart.addItem(translate("Assembly", "Ask")) + self.form.groundFirstPart.addItem(translate("Assembly", "Always")) + self.form.groundFirstPart.addItem(translate("Assembly", "Never")) + self.form.groundFirstPart.setCurrentIndex(pref.GetInt("GroundFirstPart", 0)) diff --git a/src/Mod/Assembly/TestAssemblyWorkbench.py b/src/Mod/Assembly/TestAssemblyWorkbench.py index 331d645b1d..a9d404fd54 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,13 +19,12 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import TestApp from AssemblyTests.TestCore import TestCore -# dummy usage to get flake8 and lgtm quiet -False if TestCore.__name__ else True -False if TestApp.__name__ else True +# Use the modules so that code checkers don't complain (flake8) +True if TestCore else False diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index c8bc90359e..9a8a39570a 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,13 +19,18 @@ # License along with FreeCAD. If not, see * # . * # * -# ***************************************************************************/ +# **************************************************************************/ import FreeCAD as App +import Part if App.GuiUp: import FreeCADGui as Gui +import PySide.QtCore as QtCore +import PySide.QtGui as QtGui + + # translate = App.Qt.translate __title__ = "Assembly utilitary functions" @@ -36,17 +41,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,32 +79,299 @@ def isDocTemporary(doc): return temp +def assembly_has_at_least_n_parts(n): + assembly = activeAssembly() + i = 0 + if not assembly: + assembly = activePart() + 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.LinkOrAssembly1.LinkOrPart1.LinkOrBox.Edge16" + # or "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBody.pad.Edge16" + # or "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBody.Local_CS.X" + # We want either LinkOrBody or LinkOrBox 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' + prevObj = None - if not obj: - return None + for i, objName in enumerate(names): + if i == 0: + prevObj = doc.getObject(objName) + if prevObj.TypeId == "App::Link": + prevObj = prevObj.getLinkedObject() + continue - if obj.TypeId == "PartDesign::Body": - return obj - elif obj.TypeId == "App::Link": - linked_obj = obj.getLinkedObject() - if linked_obj.TypeId == "PartDesign::Body": + obj = None + if prevObj.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}: + for obji in prevObj.OutList: + if obji.Name == objName: + obj = obji + break + + if obj is None: + return None + + # the last is the element name. So if we are at the last but one name, then it must be the selected + if i == len(names) - 2: return obj - else: # primitive, fastener, gear ... or link to primitive, fastener, gear... - return doc.getObject(parts[-2]) + if obj.TypeId == "App::Link": + linked_obj = obj.getLinkedObject() + if linked_obj.TypeId == "PartDesign::Body": + if i + 1 < len(names): + obj2 = None + for obji in linked_obj.OutList: + if obji.Name == names[i + 1]: + obj2 = obji + break + if obj2 and isBodySubObject(obj2.TypeId): + return obj2 + return obj + elif linked_obj.isDerivedFrom("Part::Feature"): + return obj + else: + prevObj = linked_obj + continue + + elif obj.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}: + prevObj = obj + continue + + elif obj.TypeId == "PartDesign::Body": + if i + 1 < len(names): + obj2 = None + for obji in obj.OutList: + if obji.Name == names[i + 1]: + obj2 = obji + break + if obj2 and isBodySubObject(obj2.TypeId): + return obj2 + return obj + + elif obj.isDerivedFrom("Part::Feature"): + # primitive, fastener, gear ... + return obj + + return None + + +def isBodySubObject(typeId): + return ( + typeId == "Sketcher::SketchObject" + or typeId == "PartDesign::Point" + or typeId == "PartDesign::Line" + or typeId == "PartDesign::Plane" + or typeId == "PartDesign::CoordinateSystem" + ) + + +def getContainingPart(full_name, selected_object, activeAssemblyOrPart=None): + # full_name is "Assembly.Assembly1.LinkOrPart1.LinkOrBox.Edge16" -> LinkOrPart1 + # or "Assembly.Assembly1.LinkOrPart1.LinkOrBody.pad.Edge16" -> LinkOrPart1 + # or "Assembly.Assembly1.LinkOrPart1.LinkOrBody.Sketch.Edge1" -> LinkOrBody + + if selected_object is None: + App.Console.PrintError("getContainingPart() in UtilsAssembly.py selected_object is None") + return None + + 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 == selected_object: + return selected_object + + if obj.TypeId == "PartDesign::Body" and isBodySubObject(selected_object.TypeId): + if selected_object in obj.OutListRecursive: + return obj + + # Note here we may want to specify a specific behavior for Assembly::AssemblyObject. + if obj.TypeId == "App::Part": + if selected_object in obj.OutListRecursive: + if not activeAssemblyOrPart: + return obj + elif activeAssemblyOrPart in obj.OutListRecursive or obj == activeAssemblyOrPart: + continue + else: + return obj + + elif obj.TypeId == "App::Link": + linked_obj = obj.getLinkedObject() + if linked_obj.TypeId == "PartDesign::Body" and isBodySubObject(selected_object.TypeId): + if selected_object in linked_obj.OutListRecursive: + return obj + if linked_obj.TypeId == "App::Part": + # linked_obj_doc = linked_obj.Document + # selected_obj_in_doc = doc.getObject(selected_object.Name) + if selected_object in linked_obj.OutListRecursive: + if not activeAssemblyOrPart: + return obj + elif (linked_obj.Document == activeAssemblyOrPart.Document) and ( + activeAssemblyOrPart in linked_obj.OutListRecursive + or linked_obj == activeAssemblyOrPart + ): + continue + else: + return obj + + # no container found so we return the object itself. + return selected_object + + +def getObjectInPart(objName, part): + if part.Name == objName: + return part + + if part.TypeId == "App::Link": + part = part.getLinkedObject() + + if part.TypeId in { + "App::Part", + "Assembly::AssemblyObject", + "App::DocumentObjectGroup", + "PartDesign::Body", + }: + for obji in part.OutListRecursive: + if obji.Name == objName: + return obji + + 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): + if targetObj is None: + return App.Placement() + + inContainerBranch = container is None + for rootObj in App.activeDocument().RootObjects: + foundPlacement = getTargetPlacementRelativeTo( + targetObj, rootObj, container, inContainerBranch + ) + if foundPlacement is not None: + return foundPlacement + + 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 +): + 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 in {"App::Part", "Assembly::AssemblyObject", "PartDesign::Body"}: + 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 part == linked_obj or linked_obj is None: + return None # upon loading this can happen for external links. + + if linked_obj.TypeId in {"App::Part", "Assembly::AssemblyObject", "PartDesign::Body"}: + 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 +383,10 @@ def getElementName(full_name): # At minimum "Assembly.Box.edge16". It shouldn't be shorter return "" + # case of PartDesign datums : CoordinateSystem, point, line, plane + if parts[-1] in {"X", "Y", "Z", "Point", "Line", "Plane"}: + return "" + return parts[-1] @@ -147,14 +441,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 +467,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 +497,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 +545,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 @@ -244,3 +592,54 @@ def color_from_unsigned(c): float(int((c >> 16) & 0xFF) / 255), float(int((c >> 8) & 0xFF) / 255), ] + + +def getJointGroup(assembly): + 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) diff --git a/src/Tools/updatecrowdin.py b/src/Tools/updatecrowdin.py index 9a1b728b06..e83e0fe3e8 100755 --- a/src/Tools/updatecrowdin.py +++ b/src/Tools/updatecrowdin.py @@ -111,6 +111,11 @@ locations = [ ], ["App", "../App/Resources/translations", "../App/Resources/App.qrc"], ["Arch", "../Mod/Arch/Resources/translations", "../Mod/Arch/Resources/Arch.qrc"], + [ + "Assembly", + "../Mod/Assembly/Gui/Resources/translations", + "../Mod/Assembly/Gui/Resources/Assembly.qrc", + ], [ "draft", "../Mod/Draft/Resources/translations", diff --git a/src/Tools/updatets.py b/src/Tools/updatets.py index e2e670aac2..d528ed8801 100755 --- a/src/Tools/updatets.py +++ b/src/Tools/updatets.py @@ -67,6 +67,11 @@ directories = [ "workingdir": "./src/Mod/Arch/", "tsdir": "Resources/translations", }, + { + "tsname": "Assembly", + "workingdir": "./src/Mod/Assembly/", + "tsdir": "Gui/Resources/translations", + }, { "tsname": "Draft", "workingdir": "./src/Mod/Draft/", diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f01639d7bd..95758fd826 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -78,6 +78,9 @@ set(TestExecutables Tests_run ) +if(BUILD_ASSEMBLY) + list (APPEND TestExecutables Assembly_tests_run) +endif(BUILD_ASSEMBLY) if(BUILD_MATERIAL) list (APPEND TestExecutables Material_tests_run) endif(BUILD_MATERIAL) diff --git a/tests/src/Base/Vector3D.cpp b/tests/src/Base/Vector3D.cpp index cd4150374b..6020637100 100644 --- a/tests/src/Base/Vector3D.cpp +++ b/tests/src/Base/Vector3D.cpp @@ -316,4 +316,16 @@ TEST(Vector, TestIsParallelShortVectors) EXPECT_FALSE(vec.IsParallel(Base::Vector3d(0.01, 0.02, 0.04), 0.02)); } +TEST(Vector, TestAngleOriented) +{ + Base::Vector3d vec1(0.000001, 0, 0); + Base::Vector3d vec2(0, 0.000001, 0); + Base::Vector3d norm(0, 0, 0.000001); + double angle = vec1.GetAngleOriented(vec2, norm); + EXPECT_EQ(angle, Base::float_traits::pi() * 0.5); + angle = vec2.GetAngleOriented(vec1, norm); + EXPECT_EQ(angle, Base::float_traits::pi() * 1.5); +} + // NOLINTEND + diff --git a/tests/src/Mod/Assembly/App/AssemblyObject.cpp b/tests/src/Mod/Assembly/App/AssemblyObject.cpp new file mode 100644 index 0000000000..2f0f86ef95 --- /dev/null +++ b/tests/src/Mod/Assembly/App/AssemblyObject.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "gtest/gtest.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +class AssemblyObjectTest: public ::testing::Test +{ +protected: + static void SetUpTestSuite() + { + tests::initApplication(); + } + + void SetUp() override + { + _docName = App::GetApplication().getUniqueDocumentName("test"); + auto _doc = App::GetApplication().newDocument(_docName.c_str(), "testUser"); + _assemblyObj = + static_cast(_doc->addObject("Assembly::AssemblyObject")); + _jointGroupObj = static_cast( + _assemblyObj->addObject("Assembly::JointGroup", "jointGroupTest")); + } + + void TearDown() override + { + App::GetApplication().closeDocument(_docName.c_str()); + } + + Assembly::AssemblyObject* getObject() + { + return _assemblyObj; + } + +private: + // TODO: use shared_ptr or something else here? + Assembly::AssemblyObject* _assemblyObj; + Assembly::JointGroup* _jointGroupObj; + std::string _docName; +}; + +TEST_F(AssemblyObjectTest, createAssemblyObject) // NOLINT +{ + // Arrange + + // Act + + // Assert +} diff --git a/tests/src/Mod/Assembly/App/CMakeLists.txt b/tests/src/Mod/Assembly/App/CMakeLists.txt new file mode 100644 index 0000000000..41c6eb9a84 --- /dev/null +++ b/tests/src/Mod/Assembly/App/CMakeLists.txt @@ -0,0 +1,5 @@ +target_sources( + Assembly_tests_run + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/AssemblyObject.cpp +) diff --git a/tests/src/Mod/Assembly/CMakeLists.txt b/tests/src/Mod/Assembly/CMakeLists.txt new file mode 100644 index 0000000000..0b6aaf602a --- /dev/null +++ b/tests/src/Mod/Assembly/CMakeLists.txt @@ -0,0 +1,15 @@ + +target_include_directories(Assembly_tests_run PUBLIC + ${EIGEN3_INCLUDE_DIR} + ${OCC_INCLUDE_DIR} + ${Python3_INCLUDE_DIRS} + ${XercesC_INCLUDE_DIRS} +) + +target_link_libraries(Assembly_tests_run + gtest_main + ${Google_Tests_LIBS} + Assembly +) + +add_subdirectory(App) diff --git a/tests/src/Mod/CMakeLists.txt b/tests/src/Mod/CMakeLists.txt index 597d6e878a..05c9bbe6e2 100644 --- a/tests/src/Mod/CMakeLists.txt +++ b/tests/src/Mod/CMakeLists.txt @@ -1,3 +1,6 @@ +if(BUILD_ASSEMBLY) + add_subdirectory(Assembly) +endif(BUILD_ASSEMBLY) if(BUILD_MATERIAL) add_subdirectory(Material) endif(BUILD_MATERIAL)