Assembly: Adds a pre-solve when creating joint, preventing wrong orthogonal solutions from solver.

This commit is contained in:
Paddle
2024-01-08 22:12:42 +01:00
committed by PaddleStroke
parent ae0d404c4c
commit fc5a1f1b24
17 changed files with 367 additions and 123 deletions

View File

@@ -162,6 +162,7 @@ App::DocumentObject* AssemblyObject::getJointOfPartConnectingToGround(App::Docum
bool AssemblyObject::isJointConnectingPartToGround(App::DocumentObject* joint, const char* propname)
{
auto* propPart = dynamic_cast<App::PropertyLink*>(joint->getPropertyByName(propname));
if (!propPart) {
return false;
@@ -173,14 +174,32 @@ bool AssemblyObject::isJointConnectingPartToGround(App::DocumentObject* joint, c
return false;
}
// now we disconnect this joint temporarily
propPart->setValue(nullptr);
// to know if a joint is connecting to ground we disable all the other joints
std::vector<App::DocumentObject*> jointsOfPart = getJointsOfPart(part);
std::vector<bool> activatedStates;
for (auto jointi : jointsOfPart) {
if (jointi->getFullName() == joint->getFullName()) {
continue;
}
activatedStates.push_back(getJointActivated(jointi));
setJointActivated(jointi, false);
}
isConnected = isPartConnected(part);
propPart->setValue(part);
// restore activation states
for (auto jointi : jointsOfPart) {
if (jointi->getFullName() == joint->getFullName() || activatedStates.empty()) {
continue;
}
return !isConnected;
setJointActivated(jointi, activatedStates[0]);
activatedStates.erase(activatedStates.begin());
}
return isConnected;
}
std::vector<App::DocumentObject*> AssemblyObject::getJoints(bool updateJCS)
@@ -193,16 +212,20 @@ std::vector<App::DocumentObject*> AssemblyObject::getJoints(bool updateJCS)
}
Base::PyGILStateLocker lock;
for (auto obj : jointGroup->getObjects()) {
if (!obj) {
for (auto joint : jointGroup->getObjects()) {
if (!joint) {
continue;
}
auto proxy = dynamic_cast<App::PropertyPythonObject*>(obj->getPropertyByName("Proxy"));
auto* prop = dynamic_cast<App::PropertyBool*>(joint->getPropertyByName("Activated"));
if (prop && !prop->getValue()) {
continue;
}
auto proxy = dynamic_cast<App::PropertyPythonObject*>(joint->getPropertyByName("Proxy"));
if (proxy) {
Py::Object joint = proxy->getValue();
if (joint.hasAttr("setJointConnectors")) {
joints.push_back(obj);
if (proxy->getValue().hasAttr("setJointConnectors")) {
joints.push_back(joint);
}
}
}
@@ -347,16 +370,37 @@ bool AssemblyObject::isPartConnected(App::DocumentObject* obj)
}
std::vector<App::DocumentObject*> AssemblyObject::getDownstreamParts(App::DocumentObject* part,
int limit)
App::DocumentObject* joint)
{
if (limit > 1000) { // Inifinite loop protection
// First we deactivate the joint
bool state = getJointActivated(joint);
setJointActivated(joint, false);
std::vector<App::DocumentObject*> joints = getJoints(false);
std::set<App::DocumentObject*> connectedParts = {part};
traverseAndMarkConnectedParts(part, connectedParts, joints);
std::vector<App::DocumentObject*> 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<App::DocumentObject*> downstreamParts = {part};
std::string name;
App::DocumentObject* connectingJoint = getJointOfPartConnectingToGround(part, name);
App::DocumentObject* connectingJoint =
getJointOfPartConnectingToGround(part,
name); // ?????????????????????????????? if we remove
// connection to ground then it can't work for tom
std::vector<App::DocumentObject*> jointsOfPart = getJointsOfPart(part);
// remove connectingJoint from jointsOfPart
@@ -365,8 +409,23 @@ std::vector<App::DocumentObject*> AssemblyObject::getDownstreamParts(App::Docume
for (auto joint : jointsOfPart) {
App::DocumentObject* part1 = getLinkObjFromProp(joint, "Part1");
App::DocumentObject* part2 = getLinkObjFromProp(joint, "Part2");
App::DocumentObject* downstreamPart =
part->getFullName() == part1->getFullName() ? part2 : part1;
bool firstIsDown = part->getFullName() == part2->getFullName();
App::DocumentObject* downstreamPart = firstIsDown ? part1 : part2;
Base::Console().Warning("looping\n");
// it is possible that the part is connected to ground by this joint.
// In which case we should not select those parts. To test we disconnect :
auto* propObj = dynamic_cast<App::PropertyLink*>(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<App::DocumentObject*> subDownstreamParts =
getDownstreamParts(downstreamPart, limit);
@@ -376,7 +435,7 @@ std::vector<App::DocumentObject*> AssemblyObject::getDownstreamParts(App::Docume
downstreamParts.push_back(downPart);
}
}
}
}*/
return downstreamParts;
}
@@ -1322,6 +1381,22 @@ void printPlacement(Base::Placement plc, const char* name)
angle);
}
void AssemblyObject::setJointActivated(App::DocumentObject* joint, bool val)
{
auto* propActivated = dynamic_cast<App::PropertyBool*>(joint->getPropertyByName("Activated"));
if (propActivated) {
propActivated->setValue(val);
}
}
bool AssemblyObject::getJointActivated(App::DocumentObject* joint)
{
auto* propActivated = dynamic_cast<App::PropertyBool*>(joint->getPropertyByName("Activated"));
if (propActivated) {
return propActivated->getValue();
}
return false;
}
Base::Placement AssemblyObject::getPlacementFromProp(App::DocumentObject* obj, const char* propName)
{
Base::Placement plc = Base::Placement();
@@ -1494,7 +1569,6 @@ App::DocumentObject* AssemblyObject::getLinkObjFromProp(App::DocumentObject* joi
{
auto* propObj = dynamic_cast<App::PropertyLink*>(joint->getPropertyByName(propLinkName));
if (!propObj) {
Base::Console().Warning("getLinkObjFromProp nullptr\n");
return nullptr;
}
return propObj->getValue();

View File

@@ -136,7 +136,8 @@ public:
bool isPartGrounded(App::DocumentObject* part);
bool isPartConnected(App::DocumentObject* part);
std::vector<App::DocumentObject*> getDownstreamParts(App::DocumentObject* part, int limit = 0);
std::vector<App::DocumentObject*> getDownstreamParts(App::DocumentObject* part,
App::DocumentObject* joint);
std::vector<App::DocumentObject*> getUpstreamParts(App::DocumentObject* part, int limit = 0);
App::DocumentObject* getUpstreamMovingPart(App::DocumentObject* part);
@@ -166,6 +167,8 @@ public:
// see https://forum.freecad.org/viewtopic.php?p=729577#p729577
// getters to get from properties
static void setJointActivated(App::DocumentObject* joint, bool val);
static bool getJointActivated(App::DocumentObject* joint);
static double getJointDistance(App::DocumentObject* joint);
static JointType getJointType(App::DocumentObject* joint);
static const char* getElementFromProp(App::DocumentObject* obj, const char* propName);

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# /****************************************************************************
# /**************************************************************************
# *
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
# *
@@ -19,7 +19,7 @@
# License along with FreeCAD. If not, see *
# <https://www.gnu.org/licenses/>. *
# *
# ***************************************************************************/
# **************************************************************************/
def open(filename):

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# /****************************************************************************
# /**************************************************************************
# *
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
# *
@@ -19,7 +19,7 @@
# License along with FreeCAD. If not, see *
# <https://www.gnu.org/licenses/>. *
# *
# ***************************************************************************/
# **************************************************************************/
import FreeCAD as App
@@ -28,6 +28,9 @@ from PySide.QtCore import QT_TRANSLATE_NOOP
if App.GuiUp:
import FreeCADGui as Gui
import UtilsAssembly
import Preferences
# translate = App.Qt.translate
__title__ = "Assembly Command Create Assembly"
@@ -46,19 +49,32 @@ class CommandCreateAssembly:
"Accel": "A",
"ToolTip": QT_TRANSLATE_NOOP(
"Assembly_CreateAssembly",
"Create an assembly object in the current document.",
"Create an assembly object in the current document or if in the current active assembly if any. One root assembly per file max.",
),
"CmdType": "ForEdit",
}
def IsActive(self):
if Preferences.preferences().GetBool("EnforceOneAssemblyRule", True):
activeAssembly = UtilsAssembly.activeAssembly()
if UtilsAssembly.isThereOneRootAssembly() and not activeAssembly:
return False
return App.ActiveDocument is not None
def Activated(self):
App.setActiveTransaction("Create assembly")
assembly = App.ActiveDocument.addObject("Assembly::AssemblyObject", "Assembly")
activeAssembly = UtilsAssembly.activeAssembly()
if activeAssembly:
assembly = activeAssembly.newObject("Assembly::AssemblyObject", "Assembly")
else:
assembly = App.ActiveDocument.addObject("Assembly::AssemblyObject", "Assembly")
assembly.Type = "Assembly"
Gui.ActiveDocument.setEdit(assembly)
if not activeAssembly:
Gui.ActiveDocument.setEdit(assembly)
assembly.newObject("Assembly::JointGroup", "Joints")
App.closeActiveTransaction()

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# /****************************************************************************
# /**************************************************************************
# *
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
# *
@@ -19,7 +19,7 @@
# License along with FreeCAD. If not, see *
# <https://www.gnu.org/licenses/>. *
# *
# ***************************************************************************/
# **************************************************************************/
import os
import FreeCAD as App
@@ -254,16 +254,15 @@ class CommandToggleGrounded:
# If you select 2 solids (bodies for example) within an assembly.
# There'll be a single sel but 2 SubElementNames.
for sub in sel.SubElementNames:
full_element_name = UtilsAssembly.getFullElementName(sel.ObjectName, sub)
obj = UtilsAssembly.getObject(full_element_name)
part_containing_obj = UtilsAssembly.getContainingPart(full_element_name, obj)
# Only objects within the assembly.
objs_names, element_name = UtilsAssembly.getObjsNamesAndElement(sel.ObjectName, sub)
if assembly.Name not in objs_names:
continue
full_element_name = UtilsAssembly.getFullElementName(sel.ObjectName, sub)
obj = UtilsAssembly.getObject(full_element_name)
part_containing_obj = UtilsAssembly.getContainingPart(full_element_name, obj)
# Check if part is grounded and if so delete the joint.
for joint in joint_group.Group:
if (

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# /****************************************************************************
# /**************************************************************************
# *
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
# *
@@ -19,7 +19,7 @@
# License along with FreeCAD. If not, see *
# <https://www.gnu.org/licenses/>. *
# *
# ***************************************************************************/
# **************************************************************************/
import FreeCAD as App
import UtilsAssembly

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# /****************************************************************************
# /**************************************************************************
# *
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
# *
@@ -19,7 +19,7 @@
# License along with FreeCAD. If not, see *
# <https://www.gnu.org/licenses/>. *
# *
# ***************************************************************************/
# **************************************************************************/
import re
import os

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# /****************************************************************************
# /**************************************************************************
# *
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
# *
@@ -19,7 +19,7 @@
# License along with FreeCAD. If not, see *
# <https://www.gnu.org/licenses/>. *
# *
# ***************************************************************************/
# **************************************************************************/
import os
import FreeCAD as App

View File

@@ -117,7 +117,6 @@ bool ViewProviderAssembly::canDragObject(App::DocumentObject* obj) const
{
Base::Console().Warning("ViewProviderAssembly::canDragObject\n");
if (!obj || obj->getTypeId() == Assembly::JointGroup::getClassTypeId()) {
Base::Console().Warning("so should be false...\n");
return false;
}
@@ -245,6 +244,10 @@ bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInvent
if (enableMovement && getSelectedObjectsWithinAssembly()) {
moveMode = findMoveMode();
if (moveMode == MoveMode::None) {
return false;
}
SbVec3f vec;
if (moveMode == MoveMode::RotationOnPlane
|| moveMode == MoveMode::TranslationOnAxisAndRotationOnePlane) {
@@ -346,8 +349,13 @@ bool ViewProviderAssembly::mouseMove(const SbVec2s& cursorPos, Gui::View3DInvent
}
}
auto* assemblyPart = static_cast<AssemblyObject*>(getObject());
assemblyPart->solve();
ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Mod/Assembly");
bool solveOnMove = hGrp->GetBool("SolveOnMove", true);
if (solveOnMove) {
auto* assemblyPart = static_cast<AssemblyObject*>(getObject());
assemblyPart->solve();
}
}
return false;
}
@@ -542,8 +550,11 @@ ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode()
// actually move A
App::DocumentObject* upstreamPart =
assemblyPart->getUpstreamMovingPart(docsToMove[0].first);
docsToMove.clear();
if (!upstreamPart) {
return MoveMode::None;
}
auto* propPlacement =
dynamic_cast<App::PropertyPlacement*>(upstreamPart->getPropertyByName("Placement"));
if (propPlacement) {
@@ -570,8 +581,7 @@ ViewProviderAssembly::MoveMode ViewProviderAssembly::findMoveMode()
jcsGlobalPlc = global_plc * jcsPlc;
// Add downstream parts so that they move together
auto downstreamParts = assemblyPart->getDownstreamParts(docsToMove[0].first);
docsToMove.clear(); // current [0] is added by the recursive getDownstreamParts.
auto downstreamParts = assemblyPart->getDownstreamParts(docsToMove[0].first, joint);
for (auto part : downstreamParts) {
auto* propPlacement =
dynamic_cast<App::PropertyPlacement*>(part->getPropertyByName("Placement"));
@@ -648,6 +658,18 @@ void ViewProviderAssembly::onSelectionChanged(const Gui::SelectionChanges& msg)
}
}
bool ViewProviderAssembly::onDelete(const std::vector<std::string>& 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) {

View File

@@ -53,6 +53,7 @@ public:
QIcon getIcon() const override;
bool doubleClicked() override;
bool onDelete(const std::vector<std::string>& subNames) override;
/** @name enter/exit edit mode */
//@{
@@ -77,6 +78,7 @@ public:
Rotation,
RotationOnPlane,
TranslationOnAxisAndRotationOnePlane,
None,
};
MoveMode moveMode;

View File

@@ -18,7 +18,7 @@
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
**************************************************************************/
#include "PreCompiled.h"

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# /****************************************************************************
# /**************************************************************************
# *
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
# *
@@ -19,7 +19,7 @@
# License along with FreeCAD. If not, see *
# <https://www.gnu.org/licenses/>. *
# *
# ***************************************************************************/
# **************************************************************************/
# Get the Parameter Group of this module
ParGrp = App.ParamGet("System parameter:Modules").GetGroup("Assembly")

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# /****************************************************************************
# /**************************************************************************
# *
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
# *
@@ -19,7 +19,7 @@
# License along with FreeCAD. If not, see *
# <https://www.gnu.org/licenses/>. *
# *
# ***************************************************************************/
# **************************************************************************/
import Assembly_rc

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# /****************************************************************************
# /**************************************************************************
# *
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
# *
@@ -19,7 +19,7 @@
# License along with FreeCAD. If not, see *
# <https://www.gnu.org/licenses/>. *
# *
# ***************************************************************************/
# **************************************************************************/
import math
@@ -79,12 +79,6 @@ def solveIfAllowed(assembly, storePrev=False):
assembly.solve(storePrev)
def flipPlacement(plc, localXAxis):
flipRot = App.Rotation(localXAxis, 180)
plc.Rotation = plc.Rotation.multiply(flipRot)
return plc
class Joint:
def __init__(self, joint, type_index, assembly):
self.Type = "Joint"
@@ -230,13 +224,14 @@ class Joint:
joint.addProperty(
"App::PropertyBool",
"FirstPartConnected",
"Activated",
"Joint",
QT_TRANSLATE_NOOP(
"App::Property",
"This indicate if the first part was connected to ground at the time of joint creation.",
"This indicate if the joint is active.",
),
)
joint.Activated = True
self.setJointConnectors(joint, [])
@@ -275,7 +270,7 @@ class Joint:
if len(current_selection) >= 1:
joint.Part1 = None
joint.FirstPartConnected = assembly.isPartConnected(current_selection[0]["part"])
self.part1Connected = assembly.isPartConnected(current_selection[0]["part"])
joint.Object1 = current_selection[0]["object"].Name
joint.Part1 = current_selection[0]["part"]
@@ -290,8 +285,12 @@ class Joint:
joint.Element1 = ""
joint.Vertex1 = ""
joint.Placement1 = App.Placement()
self.partMovedByPresolved = None
if len(current_selection) >= 2:
joint.Part2 = None
self.part2Connected = assembly.isPartConnected(current_selection[1]["part"])
joint.Object2 = current_selection[1]["object"].Name
joint.Part2 = current_selection[1]["part"]
joint.Element2 = current_selection[1]["element_name"]
@@ -299,7 +298,13 @@ class Joint:
joint.Placement2 = self.findPlacement(
joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True
)
self.preventOrthogonal(joint)
self.preSolve(
joint,
current_selection[0]["object"],
joint.Part1,
current_selection[1]["object"],
joint.Part2,
)
solveIfAllowed(assembly, True)
else:
@@ -309,6 +314,7 @@ class Joint:
joint.Vertex2 = ""
joint.Placement2 = App.Placement()
assembly.undoSolve()
self.undoPreSolve()
def updateJCSPlacements(self, joint):
if not joint.Detach1:
@@ -455,34 +461,29 @@ class Joint:
plc.Rotation = rot * zRotation
return plc
def flipPart(self, joint):
if joint.FirstPartConnected:
plc = joint.Placement2 # relative to obj
obj = UtilsAssembly.getObjectInPart(joint.Object2, joint.Part2)
def flipPlacement(self, plc):
return self.applyRotationToPlacementAlongAxis(plc, 180, App.Vector(1, 0, 0))
# we need plc to be relative to the containing part
obj_global_plc = UtilsAssembly.getGlobalPlacement(obj, joint.Part2)
part_global_plc = UtilsAssembly.getGlobalPlacement(joint.Part2)
plc = obj_global_plc * plc
plc = part_global_plc.inverse() * plc
jcsXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0))
joint.Part2.Placement = flipPlacement(joint.Part2.Placement, jcsXAxis)
def flipOnePart(self, joint):
if hasattr(self, "part2Connected") and not self.part2Connected:
jcsPlc = UtilsAssembly.getJcsPlcRelativeToPart(
joint.Placement2, joint.Object2, joint.Part2
)
globalJcsPlc = UtilsAssembly.getJcsGlobalPlc(
joint.Placement2, joint.Object2, joint.Part2
)
jcsPlc = self.flipPlacement(jcsPlc)
joint.Part2.Placement = globalJcsPlc * jcsPlc.inverse()
else:
plc = joint.Placement1 # relative to obj
obj = UtilsAssembly.getObjectInPart(joint.Object1, joint.Part1)
# we need plc to be relative to the containing part
obj_global_plc = UtilsAssembly.getGlobalPlacement(obj, joint.Part1)
part_global_plc = UtilsAssembly.getGlobalPlacement(joint.Part1)
plc = obj_global_plc * plc
plc = part_global_plc.inverse() * plc
jcsXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0))
joint.Part1.Placement = flipPlacement(joint.Part1.Placement, jcsXAxis)
jcsPlc = UtilsAssembly.getJcsPlcRelativeToPart(
joint.Placement1, joint.Object1, joint.Part1
)
globalJcsPlc = UtilsAssembly.getJcsGlobalPlc(
joint.Placement1, joint.Object1, joint.Part1
)
jcsPlc = self.flipPlacement(jcsPlc)
joint.Part1.Placement = globalJcsPlc * jcsPlc.inverse()
solveIfAllowed(self.getAssembly(joint))
@@ -508,18 +509,55 @@ class Joint:
return App.Vector(res[0].X, res[0].Y, res[0].Z)
return surface.Center
def preventOrthogonal(self, joint):
zAxis1 = joint.Placement1.Rotation.multVec(App.Vector(0, 0, 1))
zAxis2 = joint.Placement2.Rotation.multVec(App.Vector(0, 0, 1))
if abs(zAxis1.dot(zAxis2)) < Part.Precision.confusion():
if joint.FirstPartConnected:
joint.Part2.Placement = self.applyRotationToPlacementAlongAxis(
joint.Part2.Placement, 30.0, App.Vector(1, 2, 0)
)
else:
joint.Part1.Placement = self.applyRotationToPlacementAlongAxis(
joint.Part1.Placement, 30.0, App.Vector(1, 2, 0)
)
def preSolve(self, joint, obj1, part1, obj2, part2):
# The goal of this is to put the part in the correct position to avoid wrong placement by the solve.
# we actually don't want to match perfectly the JCS, it is best to match them
# in the current closest direction, ie either matched or flipped.
sameDir = self.areJcsSameDir(joint)
if hasattr(self, "part2Connected") and not self.part2Connected:
self.partMovedByPresolved = joint.Part2
self.presolveBackupPlc = joint.Part2.Placement
globalJcsPlc1 = UtilsAssembly.getJcsGlobalPlc(
joint.Placement1, joint.Object1, joint.Part1
)
jcsPlc2 = UtilsAssembly.getJcsPlcRelativeToPart(
joint.Placement2, joint.Object2, joint.Part2
)
if not sameDir:
jcsPlc2 = self.flipPlacement(jcsPlc2)
joint.Part2.Placement = globalJcsPlc1 * jcsPlc2.inverse()
elif hasattr(self, "part1Connected") and not self.part1Connected:
self.partMovedByPresolved = joint.Part1
self.presolveBackupPlc = joint.Part1.Placement
globalJcsPlc2 = UtilsAssembly.getJcsGlobalPlc(
joint.Placement2, joint.Object2, joint.Part2
)
jcsPlc1 = UtilsAssembly.getJcsPlcRelativeToPart(
joint.Placement1, joint.Object1, joint.Part1
)
if not sameDir:
jcsPlc1 = self.flipPlacement(jcsPlc1)
joint.Part1.Placement = globalJcsPlc2 * jcsPlc1.inverse()
def undoPreSolve(
self,
):
if self.partMovedByPresolved:
self.partMovedByPresolved.Placement = self.presolveBackupPlc
self.partMovedByPresolved = None
def areJcsSameDir(self, joint):
globalJcsPlc1 = UtilsAssembly.getJcsGlobalPlc(joint.Placement1, joint.Object1, joint.Part1)
globalJcsPlc2 = UtilsAssembly.getJcsGlobalPlc(joint.Placement2, joint.Object2, joint.Part2)
zAxis1 = globalJcsPlc1.Rotation.multVec(App.Vector(0, 0, 1))
zAxis2 = globalJcsPlc2.Rotation.multVec(App.Vector(0, 0, 1))
return zAxis1.dot(zAxis2) > 0
class ViewProviderJoint:
@@ -567,7 +605,7 @@ class ViewProviderJoint:
self.switch_JCS_preview = self.JCS_sep(self.transform3)
self.pick = coin.SoPickStyle()
self.pick.style.setValue(coin.SoPickStyle.UNPICKABLE)
self.setPickableState(True)
self.display_mode = coin.SoType.fromName("SoFCSelection").createInstance()
self.display_mode.addChild(self.pick)
@@ -682,20 +720,11 @@ class ViewProviderJoint:
if joint.Object2:
plc = joint.Placement2
self.switch_JCS2.whichChild = coin.SO_SWITCH_ALL
# if self.areJCSReversed(joint):
# plc = flipPlacement(plc, App.Vector(1, 0, 0))
self.set_JCS_placement(self.transform2, plc, joint.Object2, joint.Part2)
else:
self.switch_JCS2.whichChild = coin.SO_SWITCH_NONE
def areJCSReversed(self, joint):
zAxis1 = joint.Placement1.Rotation.multVec(App.Vector(0, 0, 1))
zAxis2 = joint.Placement2.Rotation.multVec(App.Vector(0, 0, 1))
sameDir = zAxis1.dot(zAxis2) > 0
return not sameDir
def showPreviewJCS(self, visible, placement=None, objName="", part=None):
if visible:
self.switch_JCS_preview.whichChild = coin.SO_SWITCH_ALL
@@ -763,7 +792,7 @@ class ViewProviderJoint:
def doubleClicked(self, vobj):
assembly = vobj.Object.InList[0]
if UtilsAssembly.activeAssembly() != assembly:
Gui.ActiveDocument.ActiveView.setActiveObject("part", assembly)
Gui.ActiveDocument.setEdit(assembly)
panel = TaskAssemblyCreateJoint(0, vobj.Object)
Gui.Control.showDialog(panel)
@@ -925,16 +954,19 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self.form.rotationSpinbox.valueChanged.connect(self.onRotationChanged)
self.form.PushButtonReverse.clicked.connect(self.onReverseClicked)
Gui.Selection.clearSelection()
if jointObj:
Gui.Selection.clearSelection()
self.creating = False
self.joint = jointObj
self.jointName = jointObj.Label
App.setActiveTransaction("Edit " + self.jointName + " Joint")
self.updateTaskboxFromJoint()
self.visibilityBackup = self.joint.Visibility
self.joint.Visibility = True
else:
self.creating = True
self.jointName = self.form.jointType.currentText().replace(" ", "")
App.setActiveTransaction("Create " + self.jointName + " Joint")
@@ -942,6 +974,8 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self.preselection_dict = None
self.createJointObject()
self.visibilityBackup = False
self.handleInitialSelection()
self.toggleDistanceVisibility()
self.toggleOffsetVisibility()
@@ -964,13 +998,10 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
App.Console.PrintWarning("You need to select 2 elements from 2 separate parts.")
return False
# Hide JSC's when joint is created and enable selection highlighting
# self.joint.ViewObject.Visibility = False
# self.joint.ViewObject.OnTopWhenSelected = "Enabled"
self.deactivate()
solveIfAllowed(self.assembly)
self.joint.Visibility = self.visibilityBackup
App.closeActiveTransaction()
return True
@@ -978,6 +1009,8 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
def reject(self):
self.deactivate()
App.closeActiveTransaction(True)
if not self.creating: # update visibility only if we are editing the joint
self.joint.Visibility = self.visibilityBackup
return True
def deactivate(self):
@@ -996,6 +1029,64 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
if Gui.Control.activeDialog():
Gui.Control.closeDialog()
def handleInitialSelection(self):
selection = Gui.Selection.getSelectionEx("*", 0)
if not selection:
return
for sel in selection:
# If you select 2 solids (bodies for example) within an assembly.
# There'll be a single sel but 2 SubElementNames.
if not sel.SubElementNames:
# no subnames, so its a root assembly itself that is selected.
Gui.Selection.removeSelection(sel.Object)
continue
for sub_name in sel.SubElementNames:
# Only objects within the assembly.
objs_names, element_name = UtilsAssembly.getObjsNamesAndElement(
sel.ObjectName, sub_name
)
if len(self.current_selection) >= 2 or self.assembly.Name not in objs_names:
Gui.Selection.removeSelection(sel.Object, sub_name)
continue
obj_name = sel.ObjectName
full_obj_name = UtilsAssembly.getFullObjName(obj_name, sub_name)
full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
selected_object = UtilsAssembly.getObject(full_element_name)
element_name = UtilsAssembly.getElementName(full_element_name)
part_containing_selected_object = UtilsAssembly.getContainingPart(
full_element_name, selected_object
)
if selected_object == self.assembly:
# do not accept selection of assembly itself
Gui.Selection.removeSelection(sel.Object, sub_name)
continue
if (
len(self.current_selection) == 1
and selected_object == self.current_selection[0]["object"]
):
# do not select several feature of the same object.
Gui.Selection.removeSelection(sel.Object, sub_name)
continue
selection_dict = {
"object": selected_object,
"part": part_containing_selected_object,
"element_name": element_name,
"full_element_name": full_element_name,
"full_obj_name": full_obj_name,
"vertex_name": element_name,
}
self.current_selection.append(selection_dict)
self.updateJoint()
def createJointObject(self):
type_index = self.form.jointType.currentIndex()
@@ -1022,7 +1113,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self.joint.Rotation = self.form.rotationSpinbox.property("rawValue")
def onReverseClicked(self):
self.joint.Proxy.flipPart(self.joint)
self.joint.Proxy.flipOnePart(self.joint)
def toggleDistanceVisibility(self):
if self.form.jointType.currentText() in JointUsingDistance:
@@ -1130,7 +1221,10 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
def moveMouse(self, info):
if len(self.current_selection) >= 2 or (
len(self.current_selection) == 1
and self.current_selection[0]["part"] == self.preselection_dict["part"]
and (
not self.preselection_dict
or self.current_selection[0]["part"] == self.preselection_dict["part"]
)
):
self.joint.ViewObject.Proxy.showPreviewJCS(False)
return

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# /****************************************************************************
# /**************************************************************************
# *
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
# *
@@ -19,7 +19,7 @@
# License along with FreeCAD. If not, see *
# <https://www.gnu.org/licenses/>. *
# *
# ***************************************************************************/
# **************************************************************************/
import FreeCAD
import FreeCADGui

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# /****************************************************************************
# /**************************************************************************
# *
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
# *
@@ -19,7 +19,7 @@
# License along with FreeCAD. If not, see *
# <https://www.gnu.org/licenses/>. *
# *
# ***************************************************************************/
# **************************************************************************/
import TestApp

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# /****************************************************************************
# /**************************************************************************
# *
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
# *
@@ -19,7 +19,7 @@
# License along with FreeCAD. If not, see *
# <https://www.gnu.org/licenses/>. *
# *
# ***************************************************************************/
# **************************************************************************/
import FreeCAD as App
import Part
@@ -237,6 +237,33 @@ def getObjectInPart(objName, part):
return None
# get the placement of Obj relative to its containing Part
# Example : assembly.part1.part2.partn.body1 : placement of Obj relative to part1
def getObjPlcRelativeToPart(objName, part):
obj = getObjectInPart(objName, part)
# we need plc to be relative to the containing part
obj_global_plc = getGlobalPlacement(obj, part)
part_global_plc = getGlobalPlacement(part)
return part_global_plc.inverse() * obj_global_plc
# Example : assembly.part1.part2.partn.body1 : jcsPlc is relative to body1
# This function returns jcsPlc relative to part1
def getJcsPlcRelativeToPart(jcsPlc, objName, part):
obj_relative_plc = getObjPlcRelativeToPart(objName, part)
return obj_relative_plc * jcsPlc
# Return the jcs global placement
def getJcsGlobalPlc(jcsPlc, objName, part):
obj = getObjectInPart(objName, part)
obj_global_plc = getGlobalPlacement(obj, part)
return obj_global_plc * jcsPlc
# The container is used to support cases where the same object appears at several places
# which happens when you have a link to a part.
def getGlobalPlacement(targetObj, container=None):
@@ -249,6 +276,13 @@ def getGlobalPlacement(targetObj, container=None):
return App.Placement()
def isThereOneRootAssembly():
for part in App.activeDocument().RootObjects:
if part.TypeId == "Assembly::AssemblyObject":
return True
return False
def getTargetPlacementRelativeTo(
targetObj, part, container, inContainerBranch, ignorePlacement=False
):