Merge pull request #15629 from Ondsel-Development/asm_assembly_link

Assembly: Flexible sub-assemblies.
This commit is contained in:
Yorik van Havre
2024-10-28 17:07:26 +01:00
committed by GitHub
27 changed files with 2201 additions and 219 deletions

View File

@@ -28,6 +28,7 @@
#include <Base/PyObjectBase.h>
#include "AssemblyObject.h"
#include "AssemblyLink.h"
#include "BomObject.h"
#include "BomGroup.h"
#include "JointGroup.h"
@@ -61,6 +62,7 @@ PyMOD_INIT_FUNC(AssemblyApp)
// This function is responsible for adding inherited slots from a type's base class.
Assembly::AssemblyObject ::init();
Assembly::AssemblyLink ::init();
Assembly::BomObject ::init();
Assembly::BomGroup ::init();

View File

@@ -0,0 +1,582 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2024 Ondsel <development@ondsel.com> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include "PreCompiled.h"
#ifndef _PreComp_
#include <cmath>
#include <vector>
#endif
#include <App/Application.h>
#include <App/Document.h>
#include <App/DocumentObjectGroup.h>
#include <App/FeaturePythonPyImp.h>
#include <App/Link.h>
#include <App/PropertyPythonObject.h>
#include <Base/Console.h>
#include <Base/Placement.h>
#include <Base/Rotation.h>
#include <Base/Tools.h>
#include <Base/Interpreter.h>
#include <Mod/Part/App/PartFeature.h>
#include <Mod/Part/App/TopoShape.h>
#include <Mod/PartDesign/App/Body.h>
#include <Mod/Part/App/DatumFeature.h>
#include "AssemblyObject.h"
#include "JointGroup.h"
#include "AssemblyLink.h"
#include "AssemblyLinkPy.h"
namespace PartApp = Part;
using namespace Assembly;
// ================================ Assembly Object ============================
PROPERTY_SOURCE(Assembly::AssemblyLink, App::Part)
AssemblyLink::AssemblyLink()
{
ADD_PROPERTY_TYPE(Rigid,
(true),
"General",
(App::PropertyType)(App::Prop_None),
"If the sub-assembly is set to Rigid, it will act "
"as a rigid body. Else its joints will be taken into account.");
ADD_PROPERTY_TYPE(LinkedObject,
(nullptr),
"General",
(App::PropertyType)(App::Prop_None),
"The linked assembly.");
}
AssemblyLink::~AssemblyLink() = default;
PyObject* AssemblyLink::getPyObject()
{
if (PythonObject.is(Py::_None())) {
// ref counter is set to 1
PythonObject = Py::Object(new AssemblyLinkPy(this), true);
}
return Py::new_reference_to(PythonObject);
}
App::DocumentObjectExecReturn* AssemblyLink::execute()
{
updateContents();
return App::Part::execute();
}
void AssemblyLink::onChanged(const App::Property* prop)
{
if (App::GetApplication().isRestoring()) {
App::Part::onChanged(prop);
return;
}
if (prop == &Rigid) {
Base::Placement movePlc;
if (Rigid.getValue()) {
// movePlc needs to be computed before updateContents.
if (!objLinkMap.empty()) {
auto firstElement = *objLinkMap.begin();
App::DocumentObject* obj = firstElement.first;
App::DocumentObject* link = firstElement.second;
auto* prop =
dynamic_cast<App::PropertyPlacement*>(obj->getPropertyByName("Placement"));
auto* prop2 =
dynamic_cast<App::PropertyPlacement*>(link->getPropertyByName("Placement"));
if (prop && prop2) {
movePlc = prop2->getValue() * prop->getValue().inverse();
}
}
}
updateContents();
auto* propPlc = dynamic_cast<App::PropertyPlacement*>(getPropertyByName("Placement"));
if (!propPlc) {
return;
}
if (!Rigid.getValue()) {
// when the assemblyLink becomes flexible, we need to make sure its placement is
// identity or it's going to mess up moving parts placement within.
Base::Placement plc = propPlc->getValue();
if (!plc.isIdentity()) {
propPlc->setValue(Base::Placement());
// We need to apply the placement of the assembly link to the children or they will
// move.
std::vector<App::DocumentObject*> group = Group.getValues();
for (auto* obj : group) {
if (!obj->isDerivedFrom<App::Part>() && !obj->isDerivedFrom<PartApp::Feature>()
&& !obj->isDerivedFrom<App::Link>()) {
continue;
}
auto* prop =
dynamic_cast<App::PropertyPlacement*>(obj->getPropertyByName("Placement"));
if (prop) {
prop->setValue(plc * prop->getValue());
}
}
AssemblyObject::redrawJointPlacements(getJoints());
}
}
else {
// For the assemblylink not to move to origin, we need to update its placement.
if (!movePlc.isIdentity()) {
propPlc->setValue(movePlc);
}
}
return;
}
App::Part::onChanged(prop);
}
void AssemblyLink::updateContents()
{
synchronizeComponents();
if (isRigid()) {
ensureNoJointGroup();
}
else {
synchronizeJoints();
}
purgeTouched();
}
void AssemblyLink::synchronizeComponents()
{
App::Document* doc = getDocument();
AssemblyObject* assembly = getLinkedAssembly();
if (!assembly) {
return;
}
objLinkMap.clear();
std::vector<App::DocumentObject*> assemblyGroup = assembly->Group.getValues();
std::vector<App::DocumentObject*> assemblyLinkGroup = Group.getValues();
// We check if a component needs to be added to the AssemblyLink
for (auto* obj : assemblyGroup) {
if (!obj->isDerivedFrom<App::Part>() && !obj->isDerivedFrom<PartApp::Feature>()
&& !obj->isDerivedFrom<App::Link>()) {
continue;
}
// Note, the user can have nested sub-assemblies.
// In which case we need to add an AssemblyLink and not a Link.
App::DocumentObject* link = nullptr;
bool found = false;
for (auto* obj2 : assemblyLinkGroup) {
App::DocumentObject* linkedObj;
auto* subAsmLink = dynamic_cast<AssemblyLink*>(obj2);
auto* link2 = dynamic_cast<App::Link*>(obj2);
if (subAsmLink) {
linkedObj = subAsmLink->getLinkedObject2(false); // not recursive
}
else if (link2) {
linkedObj = link2->getLinkedObject(false); // not recursive
}
else {
// We consider only Links and AssemblyLinks in the AssemblyLink.
continue;
}
if (linkedObj == obj) {
found = true;
link = obj2;
break;
}
}
if (!found) {
// Add a link or a AssemblyLink to it in the AssemblyLink.
if (obj->isDerivedFrom<AssemblyLink>()) {
auto* asmLink = static_cast<AssemblyLink*>(obj);
auto* subAsmLink = new AssemblyLink();
doc->addObject(subAsmLink, obj->getNameInDocument());
subAsmLink->LinkedObject.setValue(obj);
subAsmLink->Rigid.setValue(asmLink->Rigid.getValue());
subAsmLink->Label.setValue(obj->Label.getValue());
addObject(subAsmLink);
link = subAsmLink;
}
else {
auto* appLink = new App::Link();
doc->addObject(appLink, obj->getNameInDocument());
appLink->LinkedObject.setValue(obj);
appLink->Label.setValue(obj->Label.getValue());
addObject(appLink);
link = appLink;
}
}
objLinkMap[obj] = link;
// If the assemblyLink is rigid, then we keep the placement synchronized.
if (isRigid()) {
auto* plcProp =
dynamic_cast<App::PropertyPlacement*>(obj->getPropertyByName("Placement"));
auto* plcProp2 =
dynamic_cast<App::PropertyPlacement*>(link->getPropertyByName("Placement"));
if (plcProp && plcProp2) {
if (!plcProp->getValue().isSame(plcProp2->getValue())) {
plcProp2->setValue(plcProp->getValue());
}
}
}
}
// We check if a component needs to be removed from the AssemblyLink
for (auto* link : assemblyLinkGroup) {
// We don't need to update assemblyLinkGroup after the addition since we're not removing
// something we just added.
if (objLinkMap.find(link) != objLinkMap.end()) {
doc->removeObject(link->getNameInDocument());
}
/*if (!link->isDerivedFrom<App::Link>() && !link->isDerivedFrom<AssemblyLink>()) {
// AssemblyLink should contain only Links or assembly links.
continue;
}
auto* linkedObj = link->getLinkedObject(false); // not recursive
bool found = false;
for (auto* obj2 : assemblyGroup) {
if (obj2 == linkedObj) {
found = true;
break;
}
}
if (!found) {
doc->removeObject(link->getNameInDocument());
}*/
}
}
namespace
{
template<typename T>
void copyPropertyIfDifferent(App::DocumentObject* source,
App::DocumentObject* target,
const char* propertyName)
{
auto sourceProp = dynamic_cast<T*>(source->getPropertyByName(propertyName));
auto targetProp = dynamic_cast<T*>(target->getPropertyByName(propertyName));
if (sourceProp && targetProp && sourceProp->getValue() != targetProp->getValue()) {
targetProp->setValue(sourceProp->getValue());
}
}
std::string removeUpToName(const std::string& sub, const std::string& name)
{
size_t pos = sub.find(name);
if (pos != std::string::npos) {
// Move the position to the character after the found substring and the following '.'
pos += name.length() + 1;
if (pos < sub.length()) {
return sub.substr(pos);
}
}
// If s2 is not found in s1, return the original string
return sub;
}
std::string
replaceLastOccurrence(const std::string& str, const std::string& oldStr, const std::string& newStr)
{
size_t pos = str.rfind(oldStr);
if (pos != std::string::npos) {
std::string result = str;
result.replace(pos, oldStr.length(), newStr);
return result;
}
return str;
}
}; // namespace
void AssemblyLink::synchronizeJoints()
{
App::Document* doc = getDocument();
AssemblyObject* assembly = getLinkedAssembly();
if (!assembly) {
return;
}
JointGroup* jGroup = ensureJointGroup();
std::vector<App::DocumentObject*> assemblyJoints =
assembly->getJoints(assembly->isTouched(), false, false);
std::vector<App::DocumentObject*> assemblyLinkJoints = getJoints();
// We delete the excess of joints if any
for (size_t i = assemblyJoints.size(); i < assemblyLinkJoints.size(); ++i) {
doc->removeObject(assemblyLinkJoints[i]->getNameInDocument());
}
// We make sure the joints match.
for (size_t i = 0; i < assemblyJoints.size(); ++i) {
App::DocumentObject* joint = assemblyJoints[i];
App::DocumentObject* lJoint;
if (i < assemblyLinkJoints.size()) {
lJoint = assemblyLinkJoints[i];
}
else {
auto ret = doc->copyObject({joint});
if (ret.size() != 1) {
continue;
}
lJoint = ret[0];
jGroup->addObject(lJoint);
}
// Then we have to check the properties one by one.
copyPropertyIfDifferent<App::PropertyBool>(joint, lJoint, "Activated");
copyPropertyIfDifferent<App::PropertyFloat>(joint, lJoint, "Distance");
copyPropertyIfDifferent<App::PropertyFloat>(joint, lJoint, "Distance2");
copyPropertyIfDifferent<App::PropertyEnumeration>(joint, lJoint, "JointType");
copyPropertyIfDifferent<App::PropertyPlacement>(joint, lJoint, "Offset1");
copyPropertyIfDifferent<App::PropertyPlacement>(joint, lJoint, "Offset2");
copyPropertyIfDifferent<App::PropertyBool>(joint, lJoint, "Detach1");
copyPropertyIfDifferent<App::PropertyBool>(joint, lJoint, "Detach2");
copyPropertyIfDifferent<App::PropertyFloat>(joint, lJoint, "AngleMax");
copyPropertyIfDifferent<App::PropertyFloat>(joint, lJoint, "AngleMin");
copyPropertyIfDifferent<App::PropertyFloat>(joint, lJoint, "LengthMax");
copyPropertyIfDifferent<App::PropertyFloat>(joint, lJoint, "LengthMin");
copyPropertyIfDifferent<App::PropertyBool>(joint, lJoint, "EnableAngleMax");
copyPropertyIfDifferent<App::PropertyBool>(joint, lJoint, "EnableAngleMin");
copyPropertyIfDifferent<App::PropertyBool>(joint, lJoint, "EnableLengthMax");
copyPropertyIfDifferent<App::PropertyBool>(joint, lJoint, "EnableLengthMin");
// The reference needs to be handled specifically
handleJointReference(joint, lJoint, "Reference1");
handleJointReference(joint, lJoint, "Reference2");
}
assemblyLinkJoints = getJoints();
AssemblyObject::recomputeJointPlacements(assemblyLinkJoints);
for (auto* joint : assemblyLinkJoints) {
joint->purgeTouched();
}
}
void AssemblyLink::handleJointReference(App::DocumentObject* joint,
App::DocumentObject* lJoint,
const char* refName)
{
AssemblyObject* assembly = getLinkedAssembly();
auto prop1 = dynamic_cast<App::PropertyXLinkSubHidden*>(joint->getPropertyByName(refName));
auto prop2 = dynamic_cast<App::PropertyXLinkSubHidden*>(lJoint->getPropertyByName(refName));
if (!prop1 || !prop2) {
return;
}
App::DocumentObject* obj1 = nullptr;
App::DocumentObject* obj2 = prop2->getValue();
std::vector<std::string> subs1 = prop1->getSubValues();
std::vector<std::string> subs2 = prop2->getSubValues();
if (subs1.empty()) {
return;
}
// Example :
// Obj1 = docA-Asm1 Subs1 = ["part1.body.pad.face0", "part1.body.pad.vertex1"]
// Obj1 = docA-Part Subs1 = ["Asm1.part1.body.pad.face0", "Asm1.part1.body.pad.vertex1"] // some
// user may put the assembly inside a part... should become : Obj2 = docB-Asm2 Subs2 =
// ["Asm1Link.part1.linkTobody.pad.face0", "Asm1Link.part1.linkTobody.pad.vertex1"] Obj2 =
// docB-Part Sub2 = ["Asm2.Asm1Link.part1.linkTobody.pad.face0",
// "Asm2.Asm1Link.part1.linkTobody.pad.vertex1"]
std::string asmLink = getNameInDocument();
for (auto& sub : subs1) {
// First let's remove 'Asm1' name and everything before if any.
sub = removeUpToName(sub, assembly->getNameInDocument());
// Then we add the assembly link name.
sub = asmLink + "." + sub;
// Then the question is, is there more to prepend? Because the parent assembly may have some
// parents So we check assemblyLink parents and prepend necessary parents.
bool first = true;
std::vector<App::DocumentObject*> inList = getInList();
int limit = 0;
while (!inList.empty() && limit < 20) {
++limit;
bool found = false;
for (auto* obj : inList) {
if (obj->isDerivedFrom<App::Part>()) {
found = true;
if (first) {
first = false;
}
else {
std::string obj1Name = obj1->getNameInDocument();
sub = obj1Name + "." + sub;
}
obj1 = obj;
break;
}
}
if (found) {
inList = obj1->getInList();
}
else {
inList = {};
}
}
// Lastly we need to replace the object name by its link name.
auto* obj = AssemblyObject::getObjFromRef(prop1);
auto* link = objLinkMap[obj];
if (!obj || !link) {
return;
}
std::string objName = obj->getNameInDocument();
std::string linkName = link->getNameInDocument();
sub = replaceLastOccurrence(sub, objName, linkName);
}
// Now obj1 and the subs1 are what should be in obj2 and subs2 if the joint did not changed
if (obj1 != obj2) {
prop2->setValue(obj1);
}
bool changed = false;
for (size_t i = 0; i < subs1.size(); ++i) {
if (i >= subs2.size() || subs1[i] != subs2[i]) {
changed = true;
break;
}
}
if (changed) {
prop2->setSubValues(std::move(subs1));
}
}
void AssemblyLink::ensureNoJointGroup()
{
// Make sure there is no joint group
JointGroup* jGroup = AssemblyObject::getJointGroup(this);
if (jGroup) {
// If there is a joint group, we delete it and its content.
jGroup->removeObjectsFromDocument();
getDocument()->removeObject(jGroup->getNameInDocument());
}
}
JointGroup* AssemblyLink::ensureJointGroup()
{
// Make sure there is a jointGroup
JointGroup* jGroup = AssemblyObject::getJointGroup(this);
if (!jGroup) {
jGroup = new JointGroup();
getDocument()->addObject(jGroup, tr("Joints").toStdString().c_str());
// we want to add jgroup at the start, so we don't use
// addObject(jGroup);
std::vector<DocumentObject*> grp = Group.getValues();
grp.insert(grp.begin(), jGroup);
Group.setValues(grp);
}
return jGroup;
}
App::DocumentObject* AssemblyLink::getLinkedObject2(bool recursive) const
{
auto* obj = LinkedObject.getValue();
auto* assembly = dynamic_cast<AssemblyObject*>(obj);
if (assembly) {
return assembly;
}
else {
auto* assemblyLink = dynamic_cast<AssemblyLink*>(obj);
if (assemblyLink) {
if (recursive) {
return assemblyLink->getLinkedObject2(recursive);
}
else {
return assemblyLink;
}
}
}
return nullptr;
}
AssemblyObject* AssemblyLink::getLinkedAssembly() const
{
return dynamic_cast<AssemblyObject*>(getLinkedObject2());
}
AssemblyObject* AssemblyLink::getParentAssembly() const
{
std::vector<App::DocumentObject*> inList = getInList();
for (auto* obj : inList) {
auto* assembly = dynamic_cast<AssemblyObject*>(obj);
if (assembly) {
return assembly;
}
}
return nullptr;
}
bool AssemblyLink::isRigid()
{
auto* prop = dynamic_cast<App::PropertyBool*>(getPropertyByName("Rigid"));
if (!prop) {
return true;
}
return prop->getValue();
}
std::vector<App::DocumentObject*> AssemblyLink::getJoints()
{
JointGroup* jointGroup = AssemblyObject::getJointGroup(this);
if (!jointGroup) {
return {};
}
return jointGroup->getJoints();
}

View File

@@ -0,0 +1,96 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2024 Ondsel <development@ondsel.com> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef ASSEMBLY_AssemblyLink_H
#define ASSEMBLY_AssemblyLink_H
#include <unordered_map>
#include <Mod/Assembly/AssemblyGlobal.h>
#include <App/FeaturePython.h>
#include <App/Part.h>
#include <App/PropertyLinks.h>
namespace Assembly
{
class AssemblyObject;
class JointGroup;
class AssemblyExport AssemblyLink: public App::Part
{
PROPERTY_HEADER_WITH_OVERRIDE(Assembly::AssemblyLink);
public:
AssemblyLink();
~AssemblyLink() override;
PyObject* getPyObject() override;
/// returns the type name of the ViewProvider
const char* getViewProviderName() const override
{
return "AssemblyGui::ViewProviderAssemblyLink";
}
App::DocumentObjectExecReturn* execute() override;
// The linked assembly is the AssemblyObject that this AssemblyLink pseudo-links to recursively.
AssemblyObject* getLinkedAssembly() const;
// The parent assembly is the main assembly in which the linked assembly is contained
AssemblyObject* getParentAssembly() const;
// Overriding DocumentObject::getLinkedObject is giving bugs
// This function returns the linked object, either an AssemblyObject or an AssemblyLink
App::DocumentObject* getLinkedObject2(bool recurse = true) const;
bool isRigid();
void updateContents();
void synchronizeComponents();
void synchronizeJoints();
void handleJointReference(App::DocumentObject* joint,
App::DocumentObject* lJoint,
const char* refName);
void ensureNoJointGroup();
JointGroup* ensureJointGroup();
std::vector<App::DocumentObject*> getJoints();
App::PropertyXLink LinkedObject;
App::PropertyBool Rigid;
std::unordered_map<App::DocumentObject*, App::DocumentObject*> objLinkMap;
protected:
/// get called by the container whenever a property has been changed
void onChanged(const App::Property* prop) override;
};
} // namespace Assembly
#endif // ASSEMBLY_AssemblyLink_H

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<GenerateModel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="generateMetaModel_Module.xsd">
<PythonExport
Father="PartPy"
Name="AssemblyLinkPy"
Twin="AssemblyLink"
TwinPointer="AssemblyLink"
Include="Mod/Assembly/App/AssemblyLink.h"
Namespace="Assembly"
FatherInclude="App/PartPy.h"
FatherNamespace="App">
<Documentation>
<Author Licence="LGPL" Name="Ondsel" EMail="development@ondsel.com" />
<UserDocu>This class handles document objects in Assembly</UserDocu>
</Documentation>
<CustomAttributes />
</PythonExport>
</GenerateModel>

View File

@@ -0,0 +1,47 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2024 Ondsel <development@ondsel.com> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include "PreCompiled.h"
// inclusion of the generated files (generated out of AssemblyLink.xml)
#include "AssemblyLinkPy.h"
#include "AssemblyLinkPy.cpp"
using namespace Assembly;
// returns a string which represents the object e.g. when printed in python
std::string AssemblyLinkPy::representation() const
{
return {"<Assembly link>"};
}
PyObject* AssemblyLinkPy::getCustomAttributes(const char* /*attr*/) const
{
return nullptr;
}
int AssemblyLinkPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/)
{
return 0;
}

View File

@@ -82,6 +82,7 @@
#include <OndselSolver/ASMTTime.h>
#include <OndselSolver/ASMTConstantGravity.h>
#include "AssemblyLink.h"
#include "AssemblyObject.h"
#include "AssemblyObjectPy.h"
#include "JointGroup.h"
@@ -445,6 +446,7 @@ void AssemblyObject::redrawJointPlacement(App::DocumentObject* joint)
void AssemblyObject::recomputeJointPlacements(std::vector<App::DocumentObject*> joints)
{
// The Placement1 and Placement2 of each joint needs to be updated as the parts moved.
Base::PyGILStateLocker lock;
for (auto* joint : joints) {
if (!joint) {
continue;
@@ -1773,17 +1775,17 @@ void AssemblyObject::setObjMasses(std::vector<std::pair<App::DocumentObject*, do
objMasses = objectMasses;
}
std::vector<AssemblyObject*> AssemblyObject::getSubAssemblies()
std::vector<AssemblyLink*> AssemblyObject::getSubAssemblies()
{
std::vector<AssemblyObject*> subAssemblies = {};
std::vector<AssemblyLink*> subAssemblies = {};
App::Document* doc = getDocument();
std::vector<DocumentObject*> assemblies =
doc->getObjectsOfType(Assembly::AssemblyObject::getClassTypeId());
doc->getObjectsOfType(Assembly::AssemblyLink::getClassTypeId());
for (auto assembly : assemblies) {
if (hasObject(assembly)) {
subAssemblies.push_back(dynamic_cast<AssemblyObject*>(assembly));
subAssemblies.push_back(dynamic_cast<AssemblyLink*>(assembly));
}
}
@@ -2465,6 +2467,14 @@ App::DocumentObject* AssemblyObject::getMovingPartFromRef(App::DocumentObject* o
continue;
}
// We ignore dynamic sub-assemblies.
if (obj->isDerivedFrom<Assembly::AssemblyLink>()) {
auto* pRigid = dynamic_cast<App::PropertyBool*>(obj->getPropertyByName("Rigid"));
if (pRigid && !pRigid->getValue()) {
continue;
}
}
return obj;
}

View File

@@ -59,6 +59,7 @@ class Rotation;
namespace Assembly
{
class AssemblyLink;
class JointGroup;
class ViewGroup;
@@ -241,7 +242,7 @@ public:
double getObjMass(App::DocumentObject* obj);
void setObjMasses(std::vector<std::pair<App::DocumentObject*, double>> objectMasses);
std::vector<AssemblyObject*> getSubAssemblies();
std::vector<AssemblyLink*> getSubAssemblies();
void updateGroundedJointsPlacements();
private:

View File

@@ -19,6 +19,7 @@ set(Assembly_LIBS
)
generate_from_xml(AssemblyObjectPy)
generate_from_xml(AssemblyLinkPy)
generate_from_xml(BomObjectPy)
generate_from_xml(BomGroupPy)
generate_from_xml(JointGroupPy)
@@ -27,6 +28,8 @@ generate_from_xml(ViewGroupPy)
SET(Python_SRCS
AssemblyObjectPy.xml
AssemblyObjectPyImp.cpp
AssemblyLinkPy.xml
AssemblyLinkPyImp.cpp
BomObjectPy.xml
BomObjectPyImp.cpp
BomGroupPy.xml
@@ -49,6 +52,8 @@ SOURCE_GROUP("Module" FILES ${Module_SRCS})
SET(Assembly_SRCS
AssemblyObject.cpp
AssemblyObject.h
AssemblyLink.cpp
AssemblyLink.h
BomObject.cpp
BomObject.h
BomGroup.cpp

View File

@@ -53,3 +53,31 @@ PyObject* JointGroup::getPyObject()
}
return Py::new_reference_to(PythonObject);
}
std::vector<App::DocumentObject*> JointGroup::getJoints()
{
std::vector<App::DocumentObject*> joints = {};
Base::PyGILStateLocker lock;
for (auto joint : getObjects()) {
if (!joint) {
continue;
}
auto* prop = dynamic_cast<App::PropertyBool*>(joint->getPropertyByName("Activated"));
if (!prop || !prop->getValue()) {
// Filter grounded joints and deactivated joints.
continue;
}
auto proxy = dynamic_cast<App::PropertyPythonObject*>(joint->getPropertyByName("Proxy"));
if (proxy) {
if (proxy->getValue().hasAttr("setJointConnectors")) {
joints.push_back(joint);
}
}
}
return joints;
}

View File

@@ -49,6 +49,8 @@ public:
{
return "AssemblyGui::ViewProviderJointGroup";
}
std::vector<App::DocumentObject*> getJoints();
};

View File

@@ -17,6 +17,7 @@ set(Assembly_Scripts
JointObject.py
Preferences.py
AssemblyImport.py
SoSwitchMarker.py
UtilsAssembly.py
)

View File

@@ -99,6 +99,7 @@ class TaskAssemblyInsertLink(QtCore.QObject):
pref = Preferences.preferences()
self.form.CheckBox_ShowOnlyParts.setChecked(pref.GetBool("InsertShowOnlyParts", False))
self.form.CheckBox_RigidSubAsm.setChecked(pref.GetBool("InsertRigidSubAssemblies", True))
# Actions
self.form.openFileButton.clicked.connect(self.openFiles)
@@ -166,6 +167,7 @@ class TaskAssemblyInsertLink(QtCore.QObject):
def deactivated(self):
pref = Preferences.preferences()
pref.SetBool("InsertShowOnlyParts", self.form.CheckBox_ShowOnlyParts.isChecked())
pref.SetBool("InsertRigidSubAssemblies", self.form.CheckBox_RigidSubAsm.isChecked())
Gui.Selection.clearSelection()
def buildPartList(self):
@@ -357,7 +359,16 @@ class TaskAssemblyInsertLink(QtCore.QObject):
print(selectedPart.Document.Name)
documentItem.setText(0, f"{newDocName}.FCStd")"""
addedObject = self.assembly.newObject("App::Link", selectedPart.Label)
if selectedPart.isDerivedFrom("Assembly::AssemblyObject"):
objType = "Assembly::AssemblyLink"
else:
objType = "App::Link"
addedObject = self.assembly.newObject(objType, selectedPart.Label)
if selectedPart.isDerivedFrom("Assembly::AssemblyObject"):
addedObject.Rigid = self.form.CheckBox_RigidSubAsm.isChecked()
# set placement of the added object to the center of the screen.
view = Gui.activeView()
x, y = view.getSize()

View File

@@ -28,6 +28,7 @@
#include <Base/PyObjectBase.h>
#include "ViewProviderAssembly.h"
#include "ViewProviderAssemblyLink.h"
#include "ViewProviderBom.h"
#include "ViewProviderBomGroup.h"
#include "ViewProviderJointGroup.h"
@@ -60,6 +61,7 @@ PyMOD_INIT_FUNC(AssemblyGui)
// This function is responsible for adding inherited slots from a type's base class.
AssemblyGui::ViewProviderAssembly::init();
AssemblyGui::ViewProviderAssemblyLink::init();
AssemblyGui::ViewProviderBom::init();
AssemblyGui::ViewProviderBomGroup::init();
AssemblyGui::ViewProviderJointGroup::init();

View File

@@ -40,6 +40,8 @@ SET(AssemblyGui_SRCS_Module
PreCompiled.h
ViewProviderAssembly.cpp
ViewProviderAssembly.h
ViewProviderAssemblyLink.cpp
ViewProviderAssemblyLink.h
ViewProviderBom.cpp
ViewProviderBom.h
ViewProviderBomGroup.cpp

View File

@@ -1,5 +1,7 @@
<RCC>
<qresource prefix="/">
<file>icons/Assembly_AssemblyLink.svg</file>
<file>icons/Assembly_AssemblyLinkRigid.svg</file>
<file>icons/Assembly_InsertLink.svg</file>
<file>icons/preferences-assembly.svg</file>
<file>icons/Assembly_ToggleGrounded.svg</file>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -82,52 +82,121 @@
</layout>
</item>
<item row="4" column="0" colspan="2">
<layout class="QHBoxLayout" name="hLayout">
<item>
<widget class="QLabel" name="offsetLabel">
<property name="text">
<string>Offset</string>
</property>
</widget>
</item>
<item>
<widget class="Gui::QuantitySpinBox" name="offsetSpinbox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="unit" stdset="0">
<string notr="true">mm</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="0" colspan="2">
<layout class="QHBoxLayout" name="hLayoutRotation">
<item>
<widget class="QLabel" name="rotationLabel">
<property name="text">
<string>Rotation</string>
</property>
</widget>
</item>
<item>
<widget class="Gui::QuantitySpinBox" name="rotationSpinbox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="unit" stdset="0">
<string notr="true">deg</string>
</property>
</widget>
</item>
</layout>
<widget class="QGroupBox" name="groupBox_offsets">
<property name="title">
<string>Attachement offsets</string>
</property>
<layout class="QVBoxLayout" name="layout_offset_group">
<item>
<widget class="QTabWidget" name="offsetTabs">
<widget class="QWidget" name="tabSimple">
<attribute name="title">
<string>Simple</string>
</attribute>
<layout class="QVBoxLayout" name="layout_tabSimple">
<item>
<layout class="QHBoxLayout" name="hLayout">
<item>
<widget class="QLabel" name="offsetLabel">
<property name="text">
<string>Offset</string>
</property>
</widget>
</item>
<item>
<widget class="Gui::QuantitySpinBox" name="offsetSpinbox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="unit" stdset="0">
<string notr="true">mm</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="hLayoutRotation">
<item>
<widget class="QLabel" name="rotationLabel">
<property name="text">
<string>Rotation</string>
</property>
</widget>
</item>
<item>
<widget class="Gui::QuantitySpinBox" name="rotationSpinbox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="unit" stdset="0">
<string notr="true">deg</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabAdvanced">
<attribute name="title">
<string>Advanced</string>
</attribute>
<layout class="QVBoxLayout" name="layout_tabAdvanced">
<item>
<layout class="QHBoxLayout" name="hLayoutOffset1">
<item>
<widget class="QLabel" name="offset1Label">
<property name="text">
<string>Offset1</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="offset1Button">
<property name="toolTip">
<string>By clicking this button, you can set the attachement offset of the first marker (coordinate system) of the joint.</string>
</property>
<property name="text">
<string></string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="hLayoutOffset2">
<item>
<widget class="QLabel" name="offset2Label">
<property name="text">
<string>Offset2</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="offset2Button">
<property name="toolTip">
<string>By clicking this button, you can set the attachement offset of the second marker (coordinate system) of the joint.</string>
</property>
<property name="text">
<string></string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</item>
<item row="6" column="0">
<widget class="QToolButton" name="PushButtonReverse">

View File

@@ -68,6 +68,28 @@
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="Gui::PrefCheckBox" name="CheckBox_RigidSubAsm">
<property name="toolTip">
<string>If checked, the inserted sub-assemblies will not be flexible.
Rigid means that the sub-assembly will be considered as a solid.
Flexible means that the sub-assembly joints will be taken into account in the main assembly.
You can change this property of sub-assemblies at any time by right clicking them.</string>
</property>
<property name="text">
<string>Rigid sub-assemblies</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="prefEntry" stdset="0">
<cstring>InsertRigidSubAssemblies</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Assembly</cstring>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>

View File

@@ -63,6 +63,7 @@
#include <Mod/Assembly/App/AssemblyUtils.h>
#include <Mod/Assembly/App/JointGroup.h>
#include <Mod/Assembly/App/ViewGroup.h>
#include <Mod/Assembly/App/BomGroup.h>
#include <Mod/PartDesign/App/Body.h>
#include "ViewProviderAssembly.h"
@@ -1024,7 +1025,7 @@ bool ViewProviderAssembly::onDelete(const std::vector<std::string>& subNames)
for (auto obj : getObject()->getOutList()) {
if (obj->getTypeId() == Assembly::JointGroup::getClassTypeId()
|| obj->getTypeId() == Assembly::ViewGroup::getClassTypeId()
/* || obj->getTypeId() == Assembly::BomGroup::getClassTypeId()*/) {
|| obj->getTypeId() == Assembly::BomGroup::getClassTypeId()) {
// Delete the group content first.
Gui::Command::doCommand(Gui::Command::Doc,

View File

@@ -0,0 +1,150 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2024 Ondsel <development@ondsel.com> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include "PreCompiled.h"
#ifndef _PreComp_
#include <QAction>
#include <QMenu>
#include <vector>
#include <sstream>
#include <iostream>
#endif
#include <App/Link.h>
#include <App/Document.h>
#include <App/DocumentObject.h>
#include <App/Part.h>
#include <Gui/ActionFunction.h>
#include <Gui/Application.h>
#include <Gui/BitmapFactory.h>
#include <Gui/CommandT.h>
#include <Gui/MainWindow.h>
#include <Mod/Assembly/App/AssemblyObject.h>
#include <Mod/Assembly/App/AssemblyLink.h>
#include "ViewProviderAssembly.h"
#include "ViewProviderAssemblyLink.h"
using namespace Assembly;
using namespace AssemblyGui;
PROPERTY_SOURCE(AssemblyGui::ViewProviderAssemblyLink, Gui::ViewProviderPart)
ViewProviderAssemblyLink::ViewProviderAssemblyLink()
{}
ViewProviderAssemblyLink::~ViewProviderAssemblyLink() = default;
QIcon ViewProviderAssemblyLink::getIcon() const
{
auto* assembly = dynamic_cast<Assembly::AssemblyLink*>(getObject());
if (assembly->isRigid()) {
return Gui::BitmapFactory().pixmap("Assembly_AssemblyLinkRigid.svg");
}
else {
return Gui::BitmapFactory().pixmap("Assembly_AssemblyLink.svg");
}
}
bool ViewProviderAssemblyLink::setEdit(int mode)
{
auto* assemblyLink = dynamic_cast<Assembly::AssemblyLink*>(getObject());
if (!assemblyLink->isRigid() && mode == (int)ViewProvider::Transform) {
Base::Console().UserTranslatedNotification(
"Flexible sub-assemblies cannot be transformed.");
return true;
}
return ViewProviderPart::setEdit(mode);
}
bool ViewProviderAssemblyLink::doubleClicked()
{
auto* link = dynamic_cast<AssemblyLink*>(getObject());
if (!link) {
return true;
}
auto* assembly = link->getLinkedAssembly();
auto* vpa =
dynamic_cast<ViewProviderAssembly*>(Gui::Application::Instance->getViewProvider(assembly));
if (!vpa) {
return true;
}
return vpa->doubleClicked();
}
bool ViewProviderAssemblyLink::onDelete(const std::vector<std::string>& subNames)
{
Q_UNUSED(subNames)
Base::Console().Warning("onDelete\n");
Gui::Command::doCommand(Gui::Command::Doc,
"App.getDocument(\"%s\").getObject(\"%s\").removeObjectsFromDocument()",
getObject()->getDocument()->getName(),
getObject()->getNameInDocument());
// getObject()->purgeTouched();
return ViewProviderPart::onDelete(subNames);
}
void ViewProviderAssemblyLink::setupContextMenu(QMenu* menu, QObject* receiver, const char* member)
{
auto func = new Gui::ActionFunction(menu);
QAction* act;
auto* assemblyLink = dynamic_cast<Assembly::AssemblyLink*>(getObject());
if (assemblyLink->isRigid()) {
act = menu->addAction(QObject::tr("Turn flexible"));
act->setToolTip(QObject::tr(
"Your sub-assembly is currently rigid. This will make it flexible instead."));
}
else {
act = menu->addAction(QObject::tr("Turn rigid"));
act->setToolTip(QObject::tr(
"Your sub-assembly is currently flexible. This will make it rigid instead."));
}
func->trigger(act, [this]() {
auto* assemblyLink = dynamic_cast<Assembly::AssemblyLink*>(getObject());
Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Toggle Rigid"));
Gui::cmdAppObjectArgs(assemblyLink,
"Rigid = %s",
assemblyLink->Rigid.getValue() ? "False" : "True");
Gui::Command::commitCommand();
Gui::Selection().clearSelection();
});
Q_UNUSED(receiver)
Q_UNUSED(member)
}

View File

@@ -0,0 +1,81 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2024 Ondsel <development@ondsel.com> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssemblyLink_H
#define ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssemblyLink_H
#include <QCoreApplication>
#include <Mod/Assembly/AssemblyGlobal.h>
#include <Gui/ViewProviderPart.h>
namespace AssemblyGui
{
class AssemblyGuiExport ViewProviderAssemblyLink: public Gui::ViewProviderPart
{
Q_DECLARE_TR_FUNCTIONS(AssemblyGui::ViewProviderAssemblyLink)
PROPERTY_HEADER_WITH_OVERRIDE(AssemblyGui::ViewProviderAssemblyLink);
public:
ViewProviderAssemblyLink();
~ViewProviderAssemblyLink() override;
/// deliver the icon shown in the tree view. Override from ViewProvider.h
QIcon getIcon() const override;
bool setEdit(int ModNum) override;
bool doubleClicked() override;
// When the assembly link is deleted, we delete all its content as well.
bool onDelete(const std::vector<std::string>& subNames) override;
// Prevent deletion of the link assembly's content.
bool canDelete(App::DocumentObject*) const override
{
return false;
};
// Prevent drag/drop of objects within the assembly link.
bool canDragObjects() const override
{
return false;
};
bool canDropObjects() const override
{
return false;
};
bool canDragAndDropObject(App::DocumentObject*) const override
{
return false;
};
void setupContextMenu(QMenu*, QObject*, const char*) override;
};
} // namespace AssemblyGui
#endif // ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssemblyLink_H

View File

@@ -47,10 +47,3 @@ QIcon ViewProviderJointGroup::getIcon() const
{
return Gui::BitmapFactory().pixmap("Assembly_JointGroup.svg");
}
// Make the joint group impossible to delete.
bool ViewProviderJointGroup::onDelete(const std::vector<std::string>& subNames)
{
Q_UNUSED(subNames);
return false;
}

View File

@@ -57,7 +57,11 @@ public:
return false;
};
bool onDelete(const std::vector<std::string>& subNames) override;
// Make the joint group impossible to delete.
bool onDelete(const std::vector<std::string>&) override
{
return false;
};
// protected:
/// get called by the container whenever a property has been changed

View File

@@ -40,6 +40,8 @@ from pivy import coin
import UtilsAssembly
import Preferences
from SoSwitchMarker import SoSwitchMarker
translate = App.Qt.translate
TranslatedJointTypes = [
@@ -800,134 +802,18 @@ class ViewProviderJoint:
def attach(self, vobj):
"""Setup the scene sub-graph of the view provider, this method is mandatory"""
self.axis_thickness = 3
self.scaleFactor = 20
view_params = App.ParamGet("User parameter:BaseApp/Preferences/View")
param_x_axis_color = view_params.GetUnsigned("AxisXColor", 0xCC333300)
param_y_axis_color = view_params.GetUnsigned("AxisYColor", 0x33CC3300)
param_z_axis_color = view_params.GetUnsigned("AxisZColor", 0x3333CC00)
self.x_axis_so_color = coin.SoBaseColor()
self.x_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_x_axis_color))
self.y_axis_so_color = coin.SoBaseColor()
self.y_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_y_axis_color))
self.z_axis_so_color = coin.SoBaseColor()
self.z_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_z_axis_color))
self.app_obj = vobj.Object
app_doc = self.app_obj.Document
self.gui_doc = Gui.getDocument(app_doc)
self.transform1 = coin.SoTransform()
self.transform2 = coin.SoTransform()
self.transform3 = coin.SoTransform()
self.draw_style = coin.SoDrawStyle()
self.draw_style.style = coin.SoDrawStyle.LINES
self.draw_style.lineWidth = self.axis_thickness
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.pick = coin.SoPickStyle()
self.setPickableState(True)
self.switch_JCS1 = SoSwitchMarker(vobj)
self.switch_JCS2 = SoSwitchMarker(vobj)
self.switch_JCS_preview = SoSwitchMarker(vobj)
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)
vobj.addDisplayMode(self.display_mode, "Wireframe")
def JCS_sep(self, soTransform):
JCS = coin.SoAnnotation()
JCS.addChild(soTransform)
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)
Y_axis_sep = self.line_sep([0, 0.5, 0], [0, 1, 0], self.y_axis_so_color)
Z_axis_sep = self.line_sep([0, 0, 0], [0, 0, 1], self.z_axis_so_color)
JCS.addChild(base_plane_sep)
JCS.addChild(X_axis_sep)
JCS.addChild(Y_axis_sep)
JCS.addChild(Z_axis_sep)
switch_JCS = coin.SoSwitch()
switch_JCS.addChild(JCS)
switch_JCS.whichChild = coin.SO_SWITCH_NONE
return switch_JCS
def line_sep(self, startPoint, endPoint, soColor):
line = coin.SoLineSet()
line.numVertices.setValue(2)
coords = coin.SoCoordinate3()
coords.point.setValues(0, [startPoint, endPoint])
axis_sep = coin.SoAnnotation()
axis_sep.addChild(self.draw_style)
axis_sep.addChild(soColor)
axis_sep.addChild(coords)
axis_sep.addChild(line)
scale = coin.SoType.fromName("SoShapeScale").createInstance()
scale.setPart("shape", axis_sep)
scale.scaleFactor = self.scaleFactor
return scale
def plane_sep(self, size, num_vertices):
coords = coin.SoCoordinate3()
for i in range(num_vertices):
angle = float(i) / num_vertices * 2.0 * math.pi
x = math.cos(angle) * size
y = math.sin(angle) * size
coords.point.set1Value(i, x, y, 0)
face = coin.SoFaceSet()
face.numVertices.setValue(num_vertices)
transform = coin.SoTransform()
transform.translation.setValue(0, 0, 0)
draw_style = coin.SoDrawStyle()
draw_style.style = coin.SoDrawStyle.FILLED
material = coin.SoMaterial()
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(transform)
face_sep.addChild(draw_style)
face_sep.addChild(material)
face_sep.addChild(coords)
face_sep.addChild(face)
scale = coin.SoType.fromName("SoShapeScale").createInstance()
scale.setPart("shape", face_sep)
scale.scaleFactor = self.scaleFactor
return scale
def set_JCS_placement(self, soTransform, placement, ref):
# change plc to be relative to the origin of the document.
global_plc = UtilsAssembly.getGlobalPlacement(ref)
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, joint, prop):
"""If a property of the handled feature has changed we have the chance to handle this here"""
# joint is the handled feature, prop is the name of the property that has changed
@@ -936,7 +822,7 @@ class ViewProviderJoint:
plc = joint.Placement1
self.switch_JCS1.whichChild = coin.SO_SWITCH_ALL
self.set_JCS_placement(self.transform1, plc, joint.Reference1)
self.switch_JCS1.set_marker_placement(plc, joint.Reference1)
else:
self.switch_JCS1.whichChild = coin.SO_SWITCH_NONE
@@ -945,23 +831,22 @@ class ViewProviderJoint:
plc = joint.Placement2
self.switch_JCS2.whichChild = coin.SO_SWITCH_ALL
self.set_JCS_placement(self.transform2, plc, joint.Reference2)
self.switch_JCS2.set_marker_placement(plc, joint.Reference2)
else:
self.switch_JCS2.whichChild = coin.SO_SWITCH_NONE
def showPreviewJCS(self, visible, placement=None, ref=None):
if visible:
self.switch_JCS_preview.whichChild = coin.SO_SWITCH_ALL
self.set_JCS_placement(self.transform3, placement, ref)
self.switch_JCS_preview.set_marker_placement(placement, ref)
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)
self.switch_JCS1.setPickableState(state)
self.switch_JCS2.setPickableState(state)
self.switch_JCS_preview.setPickableState(state)
def getDisplayModes(self, obj):
"""Return a list of display modes."""
@@ -976,15 +861,10 @@ class ViewProviderJoint:
def onChanged(self, vp, prop):
"""Here we can do something when a single property got changed"""
# App.Console.PrintMessage("Change property: " + str(prop) + "\n")
if prop == "color_X_axis":
c = vp.getPropertyByName("color_X_axis")
self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
if prop == "color_Y_axis":
c = vp.getPropertyByName("color_Y_axis")
self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
if prop == "color_Z_axis":
c = vp.getPropertyByName("color_Z_axis")
self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
if prop == "color_X_axis" or prop == "color_Y_axis" or prop == "color_Z_axis":
self.switch_JCS1.onChanged(vp, prop)
self.switch_JCS2.onChanged(vp, prop)
self.switch_JCS_preview.onChanged(vp, prop)
def getIcon(self):
if self.app_obj.JointType == "Fixed":
@@ -1041,7 +921,10 @@ class ViewProviderJoint:
self.gui_doc.setEdit(assembly)
panel = TaskAssemblyCreateJoint(0, vobj.Object)
Gui.Control.showDialog(panel)
dialog = Gui.Control.showDialog(panel)
if dialog is not None:
dialog.setAutoCloseOnTransactionChange(True)
dialog.setDocumentName(App.ActiveDocument.Name)
return True
@@ -1296,6 +1179,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
global activeTask
activeTask = self
self.blockOffsetRotation = False
self.assembly = UtilsAssembly.activeAssembly()
if not self.assembly:
@@ -1333,7 +1217,8 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self.form.distanceSpinbox2.valueChanged.connect(self.onDistance2Changed)
self.form.offsetSpinbox.valueChanged.connect(self.onOffsetChanged)
self.form.rotationSpinbox.valueChanged.connect(self.onRotationChanged)
self.form.PushButtonReverse.clicked.connect(self.onReverseClicked)
self.form.offset1Button.clicked.connect(self.onOffset1Clicked)
self.form.offset2Button.clicked.connect(self.onOffset2Clicked)
self.form.limitCheckbox1.stateChanged.connect(self.adaptUi)
self.form.limitCheckbox2.stateChanged.connect(self.adaptUi)
@@ -1347,6 +1232,8 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self.form.reverseRotCheckbox.setChecked(self.jType == "Gears")
self.form.reverseRotCheckbox.stateChanged.connect(self.reverseRotToggled)
self.form.offsetTabs.currentChanged.connect(self.on_offset_tab_changed)
if jointObj:
Gui.Selection.clearSelection()
self.creating = False
@@ -1380,7 +1267,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
# before handleInitialSelection tries to solve.
self.handleInitialSelection()
self.setJointsPickableState(False)
UtilsAssembly.setJointsPickableState(self.doc, False)
Gui.Selection.addSelectionGate(
MakeJointSelGate(self, self.assembly), Gui.Selection.ResolveMode.NoResolve
@@ -1441,7 +1328,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
Gui.Selection.clearSelection()
self.view.removeEventCallback("SoLocation2Event", self.callbackMove)
self.view.removeEventCallback("SoKeyboardEvent", self.callbackKey)
self.setJointsPickableState(True)
UtilsAssembly.setJointsPickableState(self.doc, True)
if Gui.Control.activeDialog():
Gui.Control.closeDialog()
@@ -1509,9 +1396,15 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self.joint.Distance2 = self.form.distanceSpinbox2.property("rawValue")
def onOffsetChanged(self, quantity):
if self.blockOffsetRotation:
return
self.joint.Offset2.Base = App.Vector(0, 0, self.form.offsetSpinbox.property("rawValue"))
def onRotationChanged(self, quantity):
if self.blockOffsetRotation:
return
yaw = self.form.rotationSpinbox.property("rawValue")
ypr = self.joint.Offset2.Rotation.getYawPitchRoll()
self.joint.Offset2.Rotation.setYawPitchRoll(yaw, ypr[1], ypr[2])
@@ -1652,6 +1545,34 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
else:
self.form.groupBox_limits.hide()
self.updateOffsetWidgets()
def updateOffsetWidgets(self):
# Makes sure the values in both the simplified and advanced tabs are sync.
pos = self.joint.Offset1.Base
self.form.offset1Button.setText(f"({pos.x}, {pos.y}, {pos.z})")
pos = self.joint.Offset2.Base
self.form.offset2Button.setText(f"({pos.x}, {pos.y}, {pos.z})")
self.blockOffsetRotation = True
self.form.offsetSpinbox.setProperty("rawValue", pos.z)
self.form.rotationSpinbox.setProperty(
"rawValue", self.joint.Offset2.Rotation.getYawPitchRoll()[0]
)
self.blockOffsetRotation = False
def on_offset_tab_changed(self):
self.updateOffsetWidgets()
def onOffset1Clicked(self):
UtilsAssembly.openEditingPlacementDialog(self.joint, "Offset1")
self.updateOffsetWidgets()
def onOffset2Clicked(self):
UtilsAssembly.openEditingPlacementDialog(self.joint, "Offset2")
self.updateOffsetWidgets()
def updateTaskboxFromJoint(self):
self.refs = []
self.presel_ref = None
@@ -1752,7 +1673,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
# newPos = self.view.getPoint(*info["Position"]) is not OK: it's not pos on the object but on the focal plane
newPos = App.Vector(cursor_info["x"], cursor_info["y"], cursor_info["z"])
vertex_name = UtilsAssembly.findElementClosestVertex(self.assembly, ref, newPos)
vertex_name = UtilsAssembly.findElementClosestVertex(ref, newPos)
ref = UtilsAssembly.addVertexToReference(ref, vertex_name)
@@ -1828,7 +1749,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
# Selection is acceptable so add it
mousePos = App.Vector(mousePos[0], mousePos[1], mousePos[2])
vertex_name = UtilsAssembly.findElementClosestVertex(self.assembly, ref, mousePos)
vertex_name = UtilsAssembly.findElementClosestVertex(ref, mousePos)
# add the vertex name to the reference
ref = UtilsAssembly.addVertexToReference(ref, vertex_name)
@@ -1866,15 +1787,3 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
def clearSelection(self, doc_name):
self.refs.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)

View File

@@ -0,0 +1,181 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# /**************************************************************************
# *
# Copyright (c) 2024 Ondsel <development@ondsel.com> *
# *
# 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 *
# <https://www.gnu.org/licenses/>. *
# *
# **************************************************************************/
import math
import FreeCAD as App
if App.GuiUp:
import FreeCADGui as Gui
__title__ = "Assembly Marker Inventor object"
__author__ = "Ondsel"
__url__ = "https://www.freecad.org"
from pivy import coin
import UtilsAssembly
import Preferences
class SoSwitchMarker(coin.SoSwitch):
def __init__(self, vobj):
super().__init__() # Initialize the SoSwitch base class
self.axis_thickness = 3
self.scaleFactor = 20
view_params = App.ParamGet("User parameter:BaseApp/Preferences/View")
param_x_axis_color = view_params.GetUnsigned("AxisXColor", 0xCC333300)
param_y_axis_color = view_params.GetUnsigned("AxisYColor", 0x33CC3300)
param_z_axis_color = view_params.GetUnsigned("AxisZColor", 0x3333CC00)
self.x_axis_so_color = coin.SoBaseColor()
self.x_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_x_axis_color))
self.y_axis_so_color = coin.SoBaseColor()
self.y_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_y_axis_color))
self.z_axis_so_color = coin.SoBaseColor()
self.z_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_z_axis_color))
self.app_obj = vobj.Object
app_doc = self.app_obj.Document
self.gui_doc = Gui.getDocument(app_doc)
self.transform = coin.SoTransform()
self.draw_style = coin.SoDrawStyle()
self.draw_style.style = coin.SoDrawStyle.LINES
self.draw_style.lineWidth = self.axis_thickness
self.pick = coin.SoPickStyle()
self.setPickableState(True)
JCS = coin.SoAnnotation()
JCS.addChild(self.transform)
JCS.addChild(self.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)
Y_axis_sep = self.line_sep([0, 0.5, 0], [0, 1, 0], self.y_axis_so_color)
Z_axis_sep = self.line_sep([0, 0, 0], [0, 0, 1], self.z_axis_so_color)
JCS.addChild(base_plane_sep)
JCS.addChild(X_axis_sep)
JCS.addChild(Y_axis_sep)
JCS.addChild(Z_axis_sep)
switch_JCS = coin.SoSwitch()
self.addChild(JCS)
self.whichChild = coin.SO_SWITCH_NONE
def line_sep(self, startPoint, endPoint, soColor):
line = coin.SoLineSet()
line.numVertices.setValue(2)
coords = coin.SoCoordinate3()
coords.point.setValues(0, [startPoint, endPoint])
axis_sep = coin.SoAnnotation()
axis_sep.addChild(self.draw_style)
axis_sep.addChild(soColor)
axis_sep.addChild(coords)
axis_sep.addChild(line)
scale = coin.SoType.fromName("SoShapeScale").createInstance()
scale.setPart("shape", axis_sep)
scale.scaleFactor = self.scaleFactor
return scale
def plane_sep(self, size, num_vertices):
coords = coin.SoCoordinate3()
for i in range(num_vertices):
angle = float(i) / num_vertices * 2.0 * math.pi
x = math.cos(angle) * size
y = math.sin(angle) * size
coords.point.set1Value(i, x, y, 0)
face = coin.SoFaceSet()
face.numVertices.setValue(num_vertices)
transform = coin.SoTransform()
transform.translation.setValue(0, 0, 0)
draw_style = coin.SoDrawStyle()
draw_style.style = coin.SoDrawStyle.FILLED
material = coin.SoMaterial()
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(transform)
face_sep.addChild(draw_style)
face_sep.addChild(material)
face_sep.addChild(coords)
face_sep.addChild(face)
scale = coin.SoType.fromName("SoShapeScale").createInstance()
scale.setPart("shape", face_sep)
scale.scaleFactor = self.scaleFactor
return scale
def set_marker_placement(self, placement, ref):
# change plc to be relative to the origin of the document.
global_plc = UtilsAssembly.getGlobalPlacement(ref)
placement = global_plc * placement
t = placement.Base
self.transform.translation.setValue(t.x, t.y, t.z)
r = placement.Rotation.Q
self.transform.rotation.setValue(r[0], r[1], r[2], r[3])
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 show_marker(self, visible, placement=None, ref=None):
if visible:
self.whichChild = coin.SO_SWITCH_ALL
self.set_marker_placement(placement, ref)
else:
self.whichChild = coin.SO_SWITCH_NONE
def onChanged(self, vp, prop):
if prop == "color_X_axis":
c = vp.getPropertyByName("color_X_axis")
self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
if prop == "color_Y_axis":
c = vp.getPropertyByName("color_Y_axis")
self.y_axis_so_color.rgb.setValue(c[0], c[1], c[2])
if prop == "color_Z_axis":
c = vp.getPropertyByName("color_Z_axis")
self.z_axis_so_color.rgb.setValue(c[0], c[1], c[2])

View File

@@ -28,12 +28,10 @@ import Part
if App.GuiUp:
import FreeCADGui as Gui
import PySide.QtCore as QtCore
import PySide.QtGui as QtGui
from PySide import QtCore, QtGui, QtWidgets
# translate = App.Qt.translate
translate = App.Qt.translate
__title__ = "Assembly utilitary functions"
__author__ = "Ondsel"
@@ -392,7 +390,7 @@ def extract_type_and_number(element_name):
return None, None
def findElementClosestVertex(assembly, ref, mousePos):
def findElementClosestVertex(ref, mousePos):
element_name = getElementName(ref[1][0])
if element_name == "":
return ""
@@ -788,6 +786,36 @@ def findCylindersIntersection(obj, surface, edge, elt_index):
return surface.Center
def openEditingPlacementDialog(obj, propName):
task_placement = Gui.TaskPlacement()
dialog = task_placement.form
# Connect to the placement property
task_placement.setPlacement(getattr(obj, propName))
task_placement.setSelection([obj])
task_placement.setPropertyName(propName)
task_placement.bindObject()
task_placement.setIgnoreTransactions(True)
dialog.findChild(QtWidgets.QPushButton, "selectedVertex").hide()
dialog.exec_()
def setPickableState(obj, state: bool):
vobj = obj.ViewObject
if hasattr(vobj, "Proxy"):
proxy = vobj.Proxy
if hasattr(proxy, "setPickableState"):
proxy.setPickableState(state)
def setJointsPickableState(doc, state: bool):
"""Make all joints in document selectable (True) or unselectable (False) in 3D view"""
for obj in doc.Objects:
if obj.TypeId == "App::FeaturePython" and hasattr(obj, "JointType"):
setPickableState(obj, state)
def applyOffsetToPlacement(plc, offset):
plc.Base = plc.Base + plc.Rotation.multVec(offset)
return plc
@@ -820,6 +848,23 @@ def arePlacementZParallel(plc1, plc2):
return zAxis1.cross(zAxis2).Length < 1e-06
def removeTNPFromSubname(doc_name, obj_name, sub_name):
rootObj = App.getDocument(doc_name).getObject(obj_name)
resolved = rootObj.resolveSubElement(sub_name)
element_name_TNP = resolved[1]
element_name = resolved[2]
# Preprocess the sub_name to remove the TNP string
# We do this because after we need to add the vertex_name as well.
# And the names will be resolved anyway after.
if len(element_name_TNP.split(".")) == 2:
names = sub_name.split(".")
names.pop(-2) # remove the TNP string
sub_name = ".".join(names)
return sub_name
"""
So here we want to find a placement that corresponds to a local coordinate system that would be placed at the selected vertex.
- obj is usually a App::Link to a PartDesign::Body, or primitive, fasteners. But can also be directly the object.1
@@ -1101,6 +1146,10 @@ def getMovingPart(assembly, ref):
if obj.TypeId == "App::DocumentObjectGroup":
continue # we ignore groups.
# We ignore dynamic sub-assemblies.
if obj.isDerivedFrom("Assembly::AssemblyLink") and obj.Rigid == False:
continue
# If it is a LinkGroup then we skip it
if isLinkGroup(obj):
continue