Assembly: Replace Tangent+Parallel+Planar by 'Distance'.
This commit is contained in:
@@ -1341,7 +1341,7 @@ void StdCmdDelete::activated(int iMsg)
|
||||
ViewProviderDocumentObject *vpedit = nullptr;
|
||||
if(editDoc)
|
||||
vpedit = dynamic_cast<ViewProviderDocumentObject*>(editDoc->getInEdit());
|
||||
if(vpedit) {
|
||||
if(vpedit && !vpedit->acceptDeletionsInEdit()) {
|
||||
for(auto &sel : Selection().getSelectionEx(editDoc->getDocument()->getName())) {
|
||||
if(sel.getObject() == vpedit->getObject()) {
|
||||
if (!sel.getSubNames().empty()) {
|
||||
|
||||
@@ -465,7 +465,7 @@ QStringList DlgSettingsWorkbenchesImp::getDisabledWorkbenches()
|
||||
ParameterGrp::handle hGrp;
|
||||
|
||||
hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Workbenches");
|
||||
disabled_wbs = QString::fromStdString(hGrp->GetASCII("Disabled", "NoneWorkbench,TestWorkbench,AssemblyWorkbench"));
|
||||
disabled_wbs = QString::fromStdString(hGrp->GetASCII("Disabled", "NoneWorkbench,TestWorkbench"));
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
|
||||
unfiltered_disabled_wbs_list = disabled_wbs.split(QLatin1String(","), Qt::SkipEmptyParts);
|
||||
#else
|
||||
|
||||
@@ -94,6 +94,8 @@ public:
|
||||
App::DocumentObject *getObject() const {return pcObject;}
|
||||
/// Asks the view provider if the given object can be deleted.
|
||||
bool canDelete(App::DocumentObject* obj) const override;
|
||||
/// Ask the view provider if it accepts object deletions while in edit
|
||||
virtual bool acceptDeletionsInEdit() { return false; }
|
||||
/// Get the GUI document to this ViewProvider object
|
||||
Gui::Document* getDocument() const;
|
||||
/// Get the python wrapper for that ViewProvider
|
||||
|
||||
@@ -621,7 +621,7 @@ class Addon:
|
||||
wbName = self.get_workbench_name()
|
||||
|
||||
# Add the wb to the list of disabled if it was not already
|
||||
disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench,AssemblyWorkbench")
|
||||
disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench")
|
||||
# print(f"start disabling {disabled_wbs}")
|
||||
disabled_wbs_list = disabled_wbs.split(",")
|
||||
if not (wbName in disabled_wbs_list):
|
||||
@@ -652,7 +652,7 @@ class Addon:
|
||||
def remove_from_disabled_wbs(self, wbName: str):
|
||||
pref = fci.ParamGet("User parameter:BaseApp/Preferences/Workbenches")
|
||||
|
||||
disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench,AssemblyWorkbench")
|
||||
disabled_wbs = pref.GetString("Disabled", "NoneWorkbench,TestWorkbench")
|
||||
# print(f"start enabling : {disabled_wbs}")
|
||||
disabled_wbs_list = disabled_wbs.split(",")
|
||||
disabled_wbs = ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,10 @@
|
||||
#ifndef ASSEMBLY_AssemblyObject_H
|
||||
#define ASSEMBLY_AssemblyObject_H
|
||||
|
||||
|
||||
#include <GeomAbs_CurveType.hxx>
|
||||
#include <GeomAbs_SurfaceType.hxx>
|
||||
|
||||
#include <Mod/Assembly/AssemblyGlobal.h>
|
||||
|
||||
#include <App/FeaturePython.h>
|
||||
@@ -49,6 +53,8 @@ class Rotation;
|
||||
namespace Assembly
|
||||
{
|
||||
|
||||
class JointGroup;
|
||||
|
||||
// This enum has to be the same as the one in JointObject.py
|
||||
enum class JointType
|
||||
{
|
||||
@@ -57,9 +63,7 @@ enum class JointType
|
||||
Cylindrical,
|
||||
Slider,
|
||||
Ball,
|
||||
Planar,
|
||||
Parallel,
|
||||
Tangent
|
||||
Distance
|
||||
};
|
||||
|
||||
class AssemblyExport AssemblyObject: public App::Part
|
||||
@@ -78,35 +82,78 @@ public:
|
||||
return "AssemblyGui::ViewProviderAssembly";
|
||||
}
|
||||
|
||||
int solve();
|
||||
int solve(bool enableRedo = false);
|
||||
void savePlacementsForUndo();
|
||||
void undoSolve();
|
||||
void clearUndo();
|
||||
void exportAsASMT(std::string fileName);
|
||||
std::shared_ptr<MbD::ASMTAssembly> makeMbdAssembly();
|
||||
std::shared_ptr<MbD::ASMTPart>
|
||||
makeMbdPart(std::string& name, Base::Placement plc = Base::Placement(), double mass = 1.0);
|
||||
std::shared_ptr<MbD::ASMTPart> getMbDPart(App::DocumentObject* obj);
|
||||
std::shared_ptr<MbD::ASMTMarker> makeMbdMarker(std::string& name, Base::Placement& plc);
|
||||
std::shared_ptr<MbD::ASMTJoint> makeMbdJoint(App::DocumentObject* joint);
|
||||
std::shared_ptr<MbD::ASMTJoint> makeMbdJointOfType(JointType jointType);
|
||||
std::vector<std::shared_ptr<MbD::ASMTJoint>> makeMbdJoint(App::DocumentObject* joint);
|
||||
std::shared_ptr<MbD::ASMTJoint> makeMbdJointOfType(App::DocumentObject* joint,
|
||||
JointType jointType);
|
||||
std::shared_ptr<MbD::ASMTJoint> makeMbdJointDistance(App::DocumentObject* joint);
|
||||
std::shared_ptr<MbD::ASMTJoint> makeMbdJointDistanceFaceVertex(App::DocumentObject* joint);
|
||||
std::shared_ptr<MbD::ASMTJoint> makeMbdJointDistanceEdgeVertex(App::DocumentObject* joint);
|
||||
std::shared_ptr<MbD::ASMTJoint> makeMbdJointDistanceFaceEdge(App::DocumentObject* joint);
|
||||
std::shared_ptr<MbD::ASMTJoint> makeMbdJointDistanceEdgeEdge(App::DocumentObject* joint);
|
||||
std::shared_ptr<MbD::ASMTJoint> makeMbdJointDistanceFaceFace(App::DocumentObject* joint);
|
||||
|
||||
std::string handleOneSideOfJoint(App::DocumentObject* joint,
|
||||
JointType jointType,
|
||||
const char* propObjLinkName,
|
||||
const char* propPlcName);
|
||||
void fixGroundedPart(App::DocumentObject* obj, Base::Placement& plc, std::string& jointName);
|
||||
bool fixGroundedParts();
|
||||
void jointParts(std::vector<App::DocumentObject*> joints);
|
||||
std::vector<App::DocumentObject*> getJoints();
|
||||
Base::Placement getPlacementFromProp(App::DocumentObject* obj, const char* propName);
|
||||
std::vector<App::DocumentObject*> getGroundedJoints();
|
||||
void fixGroundedPart(App::DocumentObject* obj, Base::Placement& plc, std::string& jointName);
|
||||
std::vector<App::DocumentObject*> fixGroundedParts();
|
||||
|
||||
void removeUnconnectedJoints(std::vector<App::DocumentObject*>& joints,
|
||||
std::vector<App::DocumentObject*> groundedObjs);
|
||||
void traverseAndMarkConnectedParts(App::DocumentObject* currentPart,
|
||||
std::set<App::DocumentObject*>& connectedParts,
|
||||
const std::vector<App::DocumentObject*>& joints);
|
||||
std::vector<App::DocumentObject*>
|
||||
getConnectedParts(App::DocumentObject* part, const std::vector<App::DocumentObject*>& joints);
|
||||
|
||||
JointGroup* getJointGroup();
|
||||
|
||||
void swapJCS(App::DocumentObject* joint);
|
||||
|
||||
void setNewPlacements();
|
||||
void recomputeJointPlacements(std::vector<App::DocumentObject*> joints);
|
||||
|
||||
bool isPartConnected(App::DocumentObject* obj);
|
||||
|
||||
double getObjMass(App::DocumentObject* obj);
|
||||
void setObjMasses(std::vector<std::pair<App::DocumentObject*, double>> objectMasses);
|
||||
|
||||
bool isEdgeType(App::DocumentObject* obj, const char* elName, GeomAbs_CurveType type);
|
||||
bool isFaceType(App::DocumentObject* obj, const char* elName, GeomAbs_SurfaceType type);
|
||||
double getFaceRadius(App::DocumentObject* obj, const char* elName);
|
||||
double getEdgeRadius(App::DocumentObject* obj, const char* elName);
|
||||
|
||||
// getters to get from properties
|
||||
double getJointDistance(App::DocumentObject* joint);
|
||||
JointType getJointType(App::DocumentObject* joint);
|
||||
const char* getElementFromProp(App::DocumentObject* obj, const char* propName);
|
||||
std::string getElementTypeFromProp(App::DocumentObject* obj, const char* propName);
|
||||
Base::Placement getPlacementFromProp(App::DocumentObject* obj, const char* propName);
|
||||
App::DocumentObject* getLinkObjFromProp(App::DocumentObject* joint, const char* propName);
|
||||
App::DocumentObject* getLinkedObjFromProp(App::DocumentObject* joint, const char* propName);
|
||||
|
||||
private:
|
||||
std::shared_ptr<MbD::ASMTAssembly> mbdAssembly;
|
||||
|
||||
std::unordered_map<App::DocumentObject*, std::shared_ptr<MbD::ASMTPart>> objectPartMap;
|
||||
std::vector<std::pair<App::DocumentObject*, double>> objMasses;
|
||||
|
||||
std::vector<std::pair<App::DocumentObject*, Base::Placement>> previousPositions;
|
||||
|
||||
// void handleChangedPropertyType(Base::XMLReader &reader, const char *TypeName, App::Property
|
||||
// *prop) override;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,13 @@
|
||||
<UserDocu>
|
||||
Solve the assembly and update part placements.
|
||||
|
||||
solve()
|
||||
solve(enableRedo=False) -> int
|
||||
|
||||
Args:
|
||||
enableRedo: Whether the solve save the initial position of parts
|
||||
to enable undoing it even without a transaction.
|
||||
Defaults to `False` ie the solve cannot be undone if called
|
||||
outside of a transaction.
|
||||
|
||||
Returns:
|
||||
0 in case of success, otherwise the following codes in this order of
|
||||
@@ -32,6 +38,41 @@
|
||||
</UserDocu>
|
||||
</Documentation>
|
||||
</Methode>
|
||||
<Methode Name="undoSolve">
|
||||
<Documentation>
|
||||
<UserDocu>
|
||||
Undo the last solve of the assembly and return part placements to their initial position.
|
||||
|
||||
undoSolve()
|
||||
|
||||
Returns: None
|
||||
</UserDocu>
|
||||
</Documentation>
|
||||
</Methode>
|
||||
<Methode Name="clearUndo">
|
||||
<Documentation>
|
||||
<UserDocu>
|
||||
Clear the registered undo positions.
|
||||
|
||||
clearUndo()
|
||||
|
||||
Returns: None
|
||||
</UserDocu>
|
||||
</Documentation>
|
||||
</Methode>
|
||||
<Methode Name="isPartConnected">
|
||||
<Documentation>
|
||||
<UserDocu>
|
||||
Check if a part is connected to the ground through joints.
|
||||
|
||||
isPartConnected(obj) -> bool
|
||||
|
||||
Args: document object to check.
|
||||
|
||||
Returns: True if part is connected to ground
|
||||
</UserDocu>
|
||||
</Documentation>
|
||||
</Methode>
|
||||
<Methode Name="exportAsASMT">
|
||||
<Documentation>
|
||||
<UserDocu>
|
||||
|
||||
@@ -46,12 +46,55 @@ int AssemblyObjectPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*
|
||||
}
|
||||
|
||||
PyObject* AssemblyObjectPy::solve(PyObject* args)
|
||||
{
|
||||
PyObject* enableUndoPy;
|
||||
bool enableUndo;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "O!", &PyBool_Type, &enableUndoPy)) {
|
||||
PyErr_Clear();
|
||||
if (!PyArg_ParseTuple(args, "")) {
|
||||
return nullptr;
|
||||
}
|
||||
else {
|
||||
enableUndo = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
enableUndo = Base::asBoolean(enableUndoPy);
|
||||
}
|
||||
|
||||
int ret = this->getAssemblyObjectPtr()->solve(enableUndo);
|
||||
return Py_BuildValue("i", ret);
|
||||
}
|
||||
|
||||
PyObject* AssemblyObjectPy::undoSolve(PyObject* args)
|
||||
{
|
||||
if (!PyArg_ParseTuple(args, "")) {
|
||||
return nullptr;
|
||||
}
|
||||
int ret = this->getAssemblyObjectPtr()->solve();
|
||||
return Py_BuildValue("i", ret);
|
||||
this->getAssemblyObjectPtr()->undoSolve();
|
||||
Py_Return;
|
||||
}
|
||||
|
||||
PyObject* AssemblyObjectPy::clearUndo(PyObject* args)
|
||||
{
|
||||
if (!PyArg_ParseTuple(args, "")) {
|
||||
return nullptr;
|
||||
}
|
||||
this->getAssemblyObjectPtr()->clearUndo();
|
||||
Py_Return;
|
||||
}
|
||||
|
||||
PyObject* AssemblyObjectPy::isPartConnected(PyObject* args)
|
||||
{
|
||||
PyObject* pyobj;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "O", &pyobj)) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* obj = static_cast<App::DocumentObjectPy*>(pyobj)->getDocumentObjectPtr();
|
||||
bool ok = this->getAssemblyObjectPtr()->isPartConnected(obj);
|
||||
return Py_BuildValue("O", (ok ? Py_True : Py_False));
|
||||
}
|
||||
|
||||
PyObject* AssemblyObjectPy::exportAsASMT(PyObject* args)
|
||||
|
||||
@@ -42,5 +42,10 @@
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <BRepAdaptor_Curve.hxx>
|
||||
#include <BRepAdaptor_Surface.hxx>
|
||||
#include <TopoDS.hxx>
|
||||
#include <TopoDS_Face.hxx>
|
||||
|
||||
#endif // _PreComp_
|
||||
#endif // ASSEMBLY_PRECOMPILED_H
|
||||
|
||||
@@ -67,3 +67,9 @@ INSTALL(
|
||||
DESTINATION
|
||||
Mod/Assembly/AssemblyTests
|
||||
)
|
||||
INSTALL(
|
||||
FILES
|
||||
${AssemblyScripts_SRCS}
|
||||
DESTINATION
|
||||
Mod/Assembly/Assembly
|
||||
)
|
||||
|
||||
@@ -58,7 +58,7 @@ class CommandCreateAssembly:
|
||||
App.setActiveTransaction("Create assembly")
|
||||
assembly = App.ActiveDocument.addObject("Assembly::AssemblyObject", "Assembly")
|
||||
assembly.Type = "Assembly"
|
||||
Gui.ActiveDocument.ActiveView.setActiveObject("part", assembly)
|
||||
Gui.ActiveDocument.setEdit(assembly)
|
||||
assembly.newObject("Assembly::JointGroup", "Joints")
|
||||
App.closeActiveTransaction()
|
||||
|
||||
|
||||
@@ -42,6 +42,18 @@ __author__ = "Ondsel"
|
||||
__url__ = "https://www.freecad.org"
|
||||
|
||||
|
||||
def isCreateJointActive():
|
||||
return UtilsAssembly.isAssemblyGrounded() and UtilsAssembly.assembly_has_at_least_n_parts(2)
|
||||
|
||||
|
||||
def activateJoint(index):
|
||||
if JointObject.activeTask:
|
||||
JointObject.activeTask.reject()
|
||||
|
||||
panel = TaskAssemblyCreateJoint(index)
|
||||
Gui.Control.showDialog(panel)
|
||||
|
||||
|
||||
class CommandCreateJointFixed:
|
||||
def __init__(self):
|
||||
pass
|
||||
@@ -51,7 +63,7 @@ class CommandCreateJointFixed:
|
||||
return {
|
||||
"Pixmap": "Assembly_CreateJointFixed",
|
||||
"MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointFixed", "Create Fixed Joint"),
|
||||
"Accel": "F",
|
||||
"Accel": "J",
|
||||
"ToolTip": "<p>"
|
||||
+ QT_TRANSLATE_NOOP(
|
||||
"Assembly_CreateJointFixed",
|
||||
@@ -62,11 +74,10 @@ class CommandCreateJointFixed:
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return UtilsAssembly.activeAssembly() is not None
|
||||
return isCreateJointActive()
|
||||
|
||||
def Activated(self):
|
||||
panel = TaskAssemblyCreateJoint(0)
|
||||
Gui.Control.showDialog(panel)
|
||||
activateJoint(0)
|
||||
|
||||
|
||||
class CommandCreateJointRevolute:
|
||||
@@ -89,11 +100,10 @@ class CommandCreateJointRevolute:
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return UtilsAssembly.activeAssembly() is not None
|
||||
return isCreateJointActive()
|
||||
|
||||
def Activated(self):
|
||||
panel = TaskAssemblyCreateJoint(1)
|
||||
Gui.Control.showDialog(panel)
|
||||
activateJoint(1)
|
||||
|
||||
|
||||
class CommandCreateJointCylindrical:
|
||||
@@ -118,11 +128,10 @@ class CommandCreateJointCylindrical:
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return UtilsAssembly.activeAssembly() is not None
|
||||
return isCreateJointActive()
|
||||
|
||||
def Activated(self):
|
||||
panel = TaskAssemblyCreateJoint(2)
|
||||
Gui.Control.showDialog(panel)
|
||||
activateJoint(2)
|
||||
|
||||
|
||||
class CommandCreateJointSlider:
|
||||
@@ -145,11 +154,10 @@ class CommandCreateJointSlider:
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return UtilsAssembly.activeAssembly() is not None
|
||||
return isCreateJointActive()
|
||||
|
||||
def Activated(self):
|
||||
panel = TaskAssemblyCreateJoint(3)
|
||||
Gui.Control.showDialog(panel)
|
||||
activateJoint(3)
|
||||
|
||||
|
||||
class CommandCreateJointBall:
|
||||
@@ -172,92 +180,37 @@ class CommandCreateJointBall:
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return UtilsAssembly.activeAssembly() is not None
|
||||
return isCreateJointActive()
|
||||
|
||||
def Activated(self):
|
||||
panel = TaskAssemblyCreateJoint(4)
|
||||
Gui.Control.showDialog(panel)
|
||||
activateJoint(4)
|
||||
|
||||
|
||||
class CommandCreateJointPlanar:
|
||||
class CommandCreateJointDistance:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def GetResources(self):
|
||||
|
||||
return {
|
||||
"Pixmap": "Assembly_CreateJointPlanar",
|
||||
"MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointPlanar", "Create Planar Joint"),
|
||||
"Accel": "P",
|
||||
"Pixmap": "Assembly_CreateJointDistance",
|
||||
"MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointDistance", "Create Distance Joint"),
|
||||
"Accel": "D",
|
||||
"ToolTip": "<p>"
|
||||
+ QT_TRANSLATE_NOOP(
|
||||
"Assembly_CreateJointPlanar",
|
||||
"Create a Planar Joint: Ensures two selected features are in the same plane, restricting movement to that plane.",
|
||||
"Assembly_CreateJointDistance",
|
||||
"Create a Distance Joint: Depending on your selection this tool will apply different constraints.",
|
||||
)
|
||||
+ "</p>",
|
||||
"CmdType": "ForEdit",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return UtilsAssembly.activeAssembly() is not None
|
||||
# return False
|
||||
return isCreateJointActive()
|
||||
|
||||
def Activated(self):
|
||||
panel = TaskAssemblyCreateJoint(5)
|
||||
Gui.Control.showDialog(panel)
|
||||
|
||||
|
||||
class CommandCreateJointParallel:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def GetResources(self):
|
||||
|
||||
return {
|
||||
"Pixmap": "Assembly_CreateJointParallel",
|
||||
"MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointParallel", "Create Parallel Joint"),
|
||||
"Accel": "L",
|
||||
"ToolTip": "<p>"
|
||||
+ QT_TRANSLATE_NOOP(
|
||||
"Assembly_CreateJointParallel",
|
||||
"Create a Parallel Joint: Aligns two features to be parallel, constraining relative movement to parallel translations.",
|
||||
)
|
||||
+ "</p>",
|
||||
"CmdType": "ForEdit",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return UtilsAssembly.activeAssembly() is not None
|
||||
|
||||
def Activated(self):
|
||||
panel = TaskAssemblyCreateJoint(6)
|
||||
Gui.Control.showDialog(panel)
|
||||
|
||||
|
||||
class CommandCreateJointTangent:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def GetResources(self):
|
||||
|
||||
return {
|
||||
"Pixmap": "Assembly_CreateJointTangent",
|
||||
"MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointTangent", "Create Tangent Joint"),
|
||||
"Accel": "T",
|
||||
"ToolTip": "<p>"
|
||||
+ QT_TRANSLATE_NOOP(
|
||||
"Assembly_CreateJointTangent",
|
||||
"Create a Tangent Joint: Forces two features to be tangent, restricting movement to smooth transitions along their contact surface.",
|
||||
)
|
||||
+ "</p>",
|
||||
"CmdType": "ForEdit",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return UtilsAssembly.activeAssembly() is not None
|
||||
|
||||
def Activated(self):
|
||||
panel = TaskAssemblyCreateJoint(7)
|
||||
Gui.Control.showDialog(panel)
|
||||
activateJoint(5)
|
||||
|
||||
|
||||
class CommandToggleGrounded:
|
||||
@@ -269,18 +222,21 @@ class CommandToggleGrounded:
|
||||
return {
|
||||
"Pixmap": "Assembly_ToggleGrounded",
|
||||
"MenuText": QT_TRANSLATE_NOOP("Assembly_ToggleGrounded", "Toggle grounded"),
|
||||
"Accel": "F",
|
||||
"Accel": "G",
|
||||
"ToolTip": "<p>"
|
||||
+ QT_TRANSLATE_NOOP(
|
||||
"Assembly_ToggleGrounded",
|
||||
"Toggle the grounded state of a part. Grounding a part permanently locks its position in the assembly, preventing any movement or rotation. You need at least one grounded part per assembly.",
|
||||
"Grounding a part permanently locks its position in the assembly, preventing any movement or rotation. You need at least one grounded part before starting to assemble.",
|
||||
)
|
||||
+ "</p>",
|
||||
"CmdType": "ForEdit",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return UtilsAssembly.activeAssembly() is not None
|
||||
return (
|
||||
UtilsAssembly.isAssemblyCommandActive()
|
||||
and UtilsAssembly.assembly_has_at_least_n_parts(1)
|
||||
)
|
||||
|
||||
def Activated(self):
|
||||
assembly = UtilsAssembly.activeAssembly()
|
||||
@@ -301,20 +257,33 @@ class CommandToggleGrounded:
|
||||
|
||||
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
|
||||
|
||||
# Check if part is grounded and if so delete the joint.
|
||||
for joint in joint_group.Group:
|
||||
if hasattr(joint, "ObjectToGround") and joint.ObjectToGround == obj:
|
||||
if (
|
||||
hasattr(joint, "ObjectToGround")
|
||||
and joint.ObjectToGround == part_containing_obj
|
||||
):
|
||||
# Remove grounded tag.
|
||||
if part_containing_obj.Label.endswith(" 🔒"):
|
||||
part_containing_obj.Label = part_containing_obj.Label[:-2]
|
||||
doc = App.ActiveDocument
|
||||
doc.removeObject(joint.Name)
|
||||
doc.recompute()
|
||||
return
|
||||
|
||||
# Create groundedJoint.
|
||||
|
||||
part_containing_obj.Label = part_containing_obj.Label + " 🔒"
|
||||
ground = joint_group.newObject("App::FeaturePython", "GroundedJoint")
|
||||
JointObject.GroundedJoint(ground, obj)
|
||||
JointObject.GroundedJoint(ground, part_containing_obj)
|
||||
JointObject.ViewProviderGroundedJoint(ground.ViewObject)
|
||||
Gui.Selection.clearSelection()
|
||||
App.closeActiveTransaction()
|
||||
|
||||
|
||||
@@ -325,6 +294,4 @@ if App.GuiUp:
|
||||
Gui.addCommand("Assembly_CreateJointCylindrical", CommandCreateJointCylindrical())
|
||||
Gui.addCommand("Assembly_CreateJointSlider", CommandCreateJointSlider())
|
||||
Gui.addCommand("Assembly_CreateJointBall", CommandCreateJointBall())
|
||||
Gui.addCommand("Assembly_CreateJointPlanar", CommandCreateJointPlanar())
|
||||
Gui.addCommand("Assembly_CreateJointParallel", CommandCreateJointParallel())
|
||||
Gui.addCommand("Assembly_CreateJointTangent", CommandCreateJointTangent())
|
||||
Gui.addCommand("Assembly_CreateJointDistance", CommandCreateJointDistance())
|
||||
|
||||
@@ -54,7 +54,7 @@ class CommandExportASMT:
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return UtilsAssembly.activeAssembly() is not None
|
||||
return UtilsAssembly.isAssemblyCommandActive() and UtilsAssembly.isAssemblyGrounded()
|
||||
|
||||
def Activated(self):
|
||||
document = App.ActiveDocument
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
# *
|
||||
# ***************************************************************************/
|
||||
|
||||
import re
|
||||
import os
|
||||
import FreeCAD as App
|
||||
|
||||
@@ -31,6 +32,7 @@ if App.GuiUp:
|
||||
from PySide import QtCore, QtGui, QtWidgets
|
||||
|
||||
import UtilsAssembly
|
||||
import Preferences
|
||||
|
||||
# translate = App.Qt.translate
|
||||
|
||||
@@ -44,22 +46,30 @@ class CommandInsertLink:
|
||||
pass
|
||||
|
||||
def GetResources(self):
|
||||
tooltip = "<p>Insert a Link into the assembly. "
|
||||
tooltip += "This will create dynamic links to parts/bodies/primitives/assemblies."
|
||||
tooltip += "To insert external objects, make sure that the file "
|
||||
tooltip += "is <b>open in the current session</b></p>"
|
||||
tooltip += "<p>Press shift to add several links while clicking on the view."
|
||||
|
||||
return {
|
||||
"Pixmap": "Assembly_InsertLink",
|
||||
"MenuText": QT_TRANSLATE_NOOP("Assembly_InsertLink", "Insert Link"),
|
||||
"Accel": "I",
|
||||
"ToolTip": QT_TRANSLATE_NOOP("Assembly_InsertLink", tooltip),
|
||||
"ToolTip": "<p>"
|
||||
+ QT_TRANSLATE_NOOP(
|
||||
"Assembly_InsertLink",
|
||||
"Insert a Link into the currently active assembly. This will create dynamic links to parts/bodies/primitives/assemblies. To insert external objects, make sure that the file is <b>open in the current session</b>",
|
||||
)
|
||||
+ "</p><p><ul><li>"
|
||||
+ QT_TRANSLATE_NOOP("Assembly_InsertLink", "Insert by left clicking items in the list.")
|
||||
+ "</li><li>"
|
||||
+ QT_TRANSLATE_NOOP("Assembly_InsertLink", "Undo by right clicking items in the list.")
|
||||
+ "</li><li>"
|
||||
+ QT_TRANSLATE_NOOP(
|
||||
"Assembly_InsertLink",
|
||||
"Press shift to add several links while clicking on the view.",
|
||||
)
|
||||
+ "</li></ul></p>",
|
||||
"CmdType": "ForEdit",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return UtilsAssembly.activeAssembly() is not None
|
||||
return UtilsAssembly.isAssemblyCommandActive()
|
||||
|
||||
def Activated(self):
|
||||
assembly = UtilsAssembly.activeAssembly()
|
||||
@@ -81,6 +91,10 @@ class TaskAssemblyInsertLink(QtCore.QObject):
|
||||
|
||||
self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyInsertLink.ui")
|
||||
self.form.installEventFilter(self)
|
||||
self.form.partList.installEventFilter(self)
|
||||
|
||||
pref = Preferences.preferences()
|
||||
self.form.CheckBox_InsertInParts.setChecked(pref.GetBool("InsertInParts", True))
|
||||
|
||||
# Actions
|
||||
self.form.openFileButton.clicked.connect(self.openFiles)
|
||||
@@ -89,28 +103,37 @@ class TaskAssemblyInsertLink(QtCore.QObject):
|
||||
|
||||
self.allParts = []
|
||||
self.partsDoc = []
|
||||
self.numberOfAddedParts = 0
|
||||
self.translation = 0
|
||||
self.partMoving = False
|
||||
self.totalTranslation = App.Vector()
|
||||
|
||||
self.insertionStack = [] # used to handle cancellation of insertions.
|
||||
|
||||
self.buildPartList()
|
||||
|
||||
App.setActiveTransaction("Insert Link")
|
||||
|
||||
def accept(self):
|
||||
App.closeActiveTransaction()
|
||||
self.deactivated()
|
||||
|
||||
if self.partMoving:
|
||||
self.endMove()
|
||||
|
||||
App.closeActiveTransaction()
|
||||
return True
|
||||
|
||||
def reject(self):
|
||||
App.closeActiveTransaction(True)
|
||||
self.deactivated()
|
||||
|
||||
if self.partMoving:
|
||||
self.dismissPart()
|
||||
|
||||
App.closeActiveTransaction(True)
|
||||
return True
|
||||
|
||||
def deactivated(self):
|
||||
if self.partMoving:
|
||||
self.endMove()
|
||||
self.doc.removeObject(self.createdLink.Name)
|
||||
pref = Preferences.preferences()
|
||||
pref.SetBool("InsertInParts", self.form.CheckBox_InsertInParts.isChecked())
|
||||
|
||||
def buildPartList(self):
|
||||
self.allParts.clear()
|
||||
@@ -136,7 +159,7 @@ class TaskAssemblyInsertLink(QtCore.QObject):
|
||||
self.allParts.append(obj)
|
||||
self.partsDoc.append(doc)
|
||||
|
||||
for obj in doc.findObjects("PartDesign::Body"):
|
||||
for obj in doc.findObjects("Part::Feature"):
|
||||
# but only those at top level (not nested inside other containers)
|
||||
if obj.getParentGeoFeatureGroup() is None:
|
||||
self.allParts.append(obj)
|
||||
@@ -145,7 +168,7 @@ class TaskAssemblyInsertLink(QtCore.QObject):
|
||||
self.form.partList.clear()
|
||||
for part in self.allParts:
|
||||
newItem = QtGui.QListWidgetItem()
|
||||
newItem.setText(part.Document.Name + " - " + part.Name)
|
||||
newItem.setText(part.Label + " (" + part.Document.Name + ".FCStd)")
|
||||
newItem.setIcon(part.ViewObject.Icon)
|
||||
self.form.partList.addItem(newItem)
|
||||
|
||||
@@ -193,23 +216,82 @@ class TaskAssemblyInsertLink(QtCore.QObject):
|
||||
|
||||
# check that the current document had been saved or that it's the same document as that of the selected part
|
||||
if not self.doc.FileName != "" and not self.doc == selectedPart.Document:
|
||||
print("The current document must be saved before inserting an external part")
|
||||
return
|
||||
msgBox = QtWidgets.QMessageBox()
|
||||
msgBox.setIcon(QtWidgets.QMessageBox.Warning)
|
||||
msgBox.setText("The current document must be saved before inserting external parts.")
|
||||
msgBox.setWindowTitle("Save Document")
|
||||
saveButton = msgBox.addButton("Save", QtWidgets.QMessageBox.AcceptRole)
|
||||
cancelButton = msgBox.addButton("Cancel", QtWidgets.QMessageBox.RejectRole)
|
||||
|
||||
self.createdLink = self.assembly.newObject("App::Link", selectedPart.Name)
|
||||
self.createdLink.LinkedObject = selectedPart
|
||||
self.createdLink.Placement.Base = self.getTranslationVec(selectedPart)
|
||||
self.createdLink.recompute()
|
||||
msgBox.exec_()
|
||||
|
||||
self.numberOfAddedParts += 1
|
||||
if not (msgBox.clickedButton() == saveButton and Gui.ActiveDocument.saveAs()):
|
||||
return
|
||||
|
||||
objectWhereToInsert = self.assembly
|
||||
|
||||
if self.form.CheckBox_InsertInParts.isChecked() and selectedPart.TypeId != "App::Part":
|
||||
objectWhereToInsert = self.assembly.newObject("App::Part", "Part_" + selectedPart.Label)
|
||||
|
||||
createdLink = objectWhereToInsert.newObject("App::Link", selectedPart.Label)
|
||||
createdLink.LinkedObject = selectedPart
|
||||
createdLink.recompute()
|
||||
|
||||
addedObject = createdLink
|
||||
if self.form.CheckBox_InsertInParts.isChecked() and selectedPart.TypeId != "App::Part":
|
||||
addedObject = objectWhereToInsert
|
||||
|
||||
insertionDict = {}
|
||||
insertionDict["item"] = item
|
||||
insertionDict["addedObject"] = addedObject
|
||||
self.insertionStack.append(insertionDict)
|
||||
self.increment_counter(item)
|
||||
|
||||
translation = self.getTranslationVec(addedObject)
|
||||
insertionDict["translation"] = translation
|
||||
self.totalTranslation += translation
|
||||
addedObject.Placement.Base = self.totalTranslation
|
||||
|
||||
# highlight the link
|
||||
Gui.Selection.clearSelection()
|
||||
Gui.Selection.addSelection(self.doc.Name, self.assembly.Name, self.createdLink.Name + ".")
|
||||
Gui.Selection.addSelection(self.doc.Name, addedObject.Name, "")
|
||||
|
||||
# Start moving the part if user brings mouse on view
|
||||
self.initMove()
|
||||
|
||||
self.form.partList.setItemSelected(item, False)
|
||||
|
||||
def increment_counter(self, item):
|
||||
text = item.text()
|
||||
match = re.search(r"(\d+) inserted$", text)
|
||||
|
||||
if match:
|
||||
# Counter exists, increment it
|
||||
counter = int(match.group(1)) + 1
|
||||
new_text = re.sub(r"\d+ inserted$", f"{counter} inserted", text)
|
||||
else:
|
||||
# Counter does not exist, add it
|
||||
new_text = f"{text} : 1 inserted"
|
||||
|
||||
item.setText(new_text)
|
||||
|
||||
def decrement_counter(self, item):
|
||||
text = item.text()
|
||||
match = re.search(r"(\d+) inserted$", text)
|
||||
|
||||
if match:
|
||||
counter = int(match.group(1)) - 1
|
||||
if counter > 0:
|
||||
# Update the counter
|
||||
new_text = re.sub(r"\d+ inserted$", f"{counter} inserted", text)
|
||||
elif counter == 0:
|
||||
# Remove the counter part from the text
|
||||
new_text = re.sub(r" : \d+ inserted$", "", text)
|
||||
else:
|
||||
return
|
||||
|
||||
item.setText(new_text)
|
||||
|
||||
def initMove(self):
|
||||
self.callbackMove = self.view.addEventCallback("SoLocation2Event", self.moveMouse)
|
||||
self.callbackClick = self.view.addEventCallback("SoMouseButtonEvent", self.clickMouse)
|
||||
@@ -230,42 +312,83 @@ class TaskAssemblyInsertLink(QtCore.QObject):
|
||||
|
||||
def moveMouse(self, info):
|
||||
newPos = self.view.getPoint(*info["Position"])
|
||||
self.createdLink.Placement.Base = newPos
|
||||
self.insertionStack[-1]["addedObject"].Placement.Base = newPos
|
||||
|
||||
def clickMouse(self, info):
|
||||
if info["Button"] == "BUTTON1" and info["State"] == "DOWN":
|
||||
Gui.Selection.clearSelection()
|
||||
if info["ShiftDown"]:
|
||||
# Create a new link and moves this one now
|
||||
currentPos = self.createdLink.Placement.Base
|
||||
selectedPart = self.createdLink.LinkedObject
|
||||
self.createdLink = self.assembly.newObject("App::Link", selectedPart.Name)
|
||||
self.createdLink.LinkedObject = selectedPart
|
||||
self.createdLink.Placement.Base = currentPos
|
||||
addedObject = self.insertionStack[-1]["addedObject"]
|
||||
currentPos = addedObject.Placement.Base
|
||||
selectedPart = addedObject
|
||||
if addedObject.TypeId == "App::Link":
|
||||
selectedPart = addedObject.LinkedObject
|
||||
|
||||
addedObject = self.assembly.newObject("App::Link", selectedPart.Label)
|
||||
addedObject.LinkedObject = selectedPart
|
||||
addedObject.Placement.Base = currentPos
|
||||
|
||||
insertionDict = {}
|
||||
insertionDict["translation"] = App.Vector()
|
||||
insertionDict["item"] = self.insertionStack[-1]["item"]
|
||||
insertionDict["addedObject"] = addedObject
|
||||
self.insertionStack.append(insertionDict)
|
||||
|
||||
else:
|
||||
self.endMove()
|
||||
|
||||
elif info["Button"] == "BUTTON2" and info["State"] == "DOWN":
|
||||
self.dismissPart()
|
||||
|
||||
# 3D view keyboard handler
|
||||
def KeyboardEvent(self, info):
|
||||
if info["State"] == "UP" and info["Key"] == "ESCAPE":
|
||||
self.endMove()
|
||||
self.doc.removeObject(self.createdLink.Name)
|
||||
self.dismissPart()
|
||||
|
||||
def dismissPart(self):
|
||||
self.endMove()
|
||||
stack_item = self.insertionStack.pop()
|
||||
self.totalTranslation -= stack_item["translation"]
|
||||
UtilsAssembly.removeObjAndChilds(stack_item["addedObject"])
|
||||
self.decrement_counter(stack_item["item"])
|
||||
|
||||
# Taskbox keyboard event handler
|
||||
def eventFilter(self, watched, event):
|
||||
if watched == self.form and event.type() == QtCore.QEvent.KeyPress:
|
||||
if event.key() == QtCore.Qt.Key_Escape and self.partMoving:
|
||||
self.endMove()
|
||||
self.doc.removeObject(self.createdLink.Name)
|
||||
self.dismissPart()
|
||||
return True # Consume the event
|
||||
|
||||
if event.type() == QtCore.QEvent.ContextMenu and watched is self.form.partList:
|
||||
item = watched.itemAt(event.pos())
|
||||
|
||||
if item:
|
||||
# Iterate through the insertionStack in reverse
|
||||
for i in reversed(range(len(self.insertionStack))):
|
||||
stack_item = self.insertionStack[i]
|
||||
|
||||
if stack_item["item"] == item:
|
||||
if self.partMoving:
|
||||
self.endMove()
|
||||
|
||||
self.totalTranslation -= stack_item["translation"]
|
||||
UtilsAssembly.removeObjAndChilds(stack_item["addedObject"])
|
||||
self.decrement_counter(item)
|
||||
del self.insertionStack[i]
|
||||
self.form.partList.setItemSelected(item, False)
|
||||
|
||||
return True
|
||||
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def getTranslationVec(self, part):
|
||||
bb = part.Shape.BoundBox
|
||||
if bb:
|
||||
self.translation += (bb.XMax + bb.YMax + bb.ZMax) * 0.15
|
||||
translation = (bb.XMax + bb.YMax + bb.ZMax) * 0.15
|
||||
else:
|
||||
self.translation += 10
|
||||
return App.Vector(self.translation, self.translation, self.translation)
|
||||
translation = 10
|
||||
return App.Vector(translation, translation, translation)
|
||||
|
||||
|
||||
if App.GuiUp:
|
||||
|
||||
@@ -49,7 +49,7 @@ class CommandSolveAssembly:
|
||||
return {
|
||||
"Pixmap": "Assembly_SolveAssembly",
|
||||
"MenuText": QT_TRANSLATE_NOOP("Assembly_SolveAssembly", "Solve Assembly"),
|
||||
"Accel": "F",
|
||||
"Accel": "Z",
|
||||
"ToolTip": "<p>"
|
||||
+ QT_TRANSLATE_NOOP(
|
||||
"Assembly_SolveAssembly",
|
||||
@@ -60,7 +60,7 @@ class CommandSolveAssembly:
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return UtilsAssembly.activeAssembly() is not None
|
||||
return UtilsAssembly.isAssemblyCommandActive() and UtilsAssembly.isAssemblyGrounded()
|
||||
|
||||
def Activated(self):
|
||||
assembly = UtilsAssembly.activeAssembly()
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
|
||||
// Qt
|
||||
#ifndef __QtAll__
|
||||
#include <Gui/QtAll.h>
|
||||
#endif
|
||||
|
||||
#endif //_PreComp_
|
||||
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
<RCC>
|
||||
<qresource>
|
||||
<qresource prefix="/">
|
||||
<file>icons/Assembly_InsertLink.svg</file>
|
||||
<file>icons/preferences-assembly.svg</file>
|
||||
<file>icons/Assembly_ToggleGrounded.svg</file>
|
||||
<file>icons/Assembly_CreateJointBall.svg</file>
|
||||
<file>icons/Assembly_CreateJointCylindrical.svg</file>
|
||||
<file>icons/Assembly_CreateJointFixed.svg</file>
|
||||
<file>icons/Assembly_CreateJointParallel.svg</file>
|
||||
<file>icons/Assembly_CreateJointPlanar.svg</file>
|
||||
<file>icons/Assembly_CreateJointRevolute.svg</file>
|
||||
<file>icons/Assembly_CreateJointSlider.svg</file>
|
||||
<file>icons/Assembly_CreateJointTangent.svg</file>
|
||||
<file>icons/Assembly_ExportASMT.svg</file>
|
||||
<file>icons/Assembly_SolveAssembly.svg</file>
|
||||
<file>panels/TaskAssemblyCreateJoint.ui</file>
|
||||
<file>panels/TaskAssemblyInsertLink.ui</file>
|
||||
<file>preferences/Assembly.ui</file>
|
||||
<file>icons/Assembly_CreateJointDistance.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
615
src/Mod/Assembly/Gui/Resources/icons/Assembly_SolveAssembly.svg
Normal file
615
src/Mod/Assembly/Gui/Resources/icons/Assembly_SolveAssembly.svg
Normal file
@@ -0,0 +1,615 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
id="svg3559"
|
||||
version="1.1"
|
||||
inkscape:version="1.1-beta1 (77e7b44db3, 2021-03-28)"
|
||||
sodipodi:docname="Assembly_SolveAssembly.svg"
|
||||
viewBox="0 0 64 64"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs3561">
|
||||
<linearGradient
|
||||
id="linearGradient4383-3"
|
||||
inkscape:collect="always">
|
||||
<stop
|
||||
id="stop73188"
|
||||
offset="0"
|
||||
style="stop-color:#3465a4;stop-opacity:1" />
|
||||
<stop
|
||||
id="stop73190"
|
||||
offset="1"
|
||||
style="stop-color:#729fcf;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4383-3"
|
||||
id="linearGradient4389-0"
|
||||
x1="27.243532"
|
||||
y1="54.588112"
|
||||
x2="21.243532"
|
||||
y2="30.588112"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(-1.243533,-2.588112)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4393-9"
|
||||
id="linearGradient4399-7"
|
||||
x1="48.714352"
|
||||
y1="45.585785"
|
||||
x2="40.714352"
|
||||
y2="24.585787"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(1.2856487,1.4142136)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient4393-9">
|
||||
<stop
|
||||
style="stop-color:#204a87;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop4395-8" />
|
||||
<stop
|
||||
style="stop-color:#3465a4;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop4397-1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3774">
|
||||
<stop
|
||||
style="stop-color:#4e9a06;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop3776" />
|
||||
<stop
|
||||
style="stop-color:#8ae234;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3778" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient8662"
|
||||
id="radialGradient1503"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(2.0414388,0,0,0.73027218,-169.35231,-78.023792)"
|
||||
cx="24.837126"
|
||||
cy="36.421127"
|
||||
fx="24.837126"
|
||||
fy="36.421127"
|
||||
r="15.644737" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient8662">
|
||||
<stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop8664" />
|
||||
<stop
|
||||
style="stop-color:#000000;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop8666" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2831"
|
||||
id="linearGradient1486"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.370336,0,0,1.3589114,0.02150968,-18.214919)"
|
||||
x1="13.478554"
|
||||
y1="10.612206"
|
||||
x2="15.419417"
|
||||
y2="19.115122" />
|
||||
<linearGradient
|
||||
id="linearGradient2831">
|
||||
<stop
|
||||
style="stop-color:#3465a4;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop2833" />
|
||||
<stop
|
||||
id="stop2855"
|
||||
offset="0.33333334"
|
||||
style="stop-color:#5b86be;stop-opacity:1;" />
|
||||
<stop
|
||||
style="stop-color:#83a8d8;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop2835" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2847"
|
||||
id="linearGradient1488"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-1.370336,0,0,-1.3589114,64.512944,44.464873)"
|
||||
x1="37.128052"
|
||||
y1="29.729605"
|
||||
x2="37.065414"
|
||||
y2="26.194071" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient2847">
|
||||
<stop
|
||||
style="stop-color:#3465a4;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop2849" />
|
||||
<stop
|
||||
style="stop-color:#3465a4;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop2851" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3063"
|
||||
id="linearGradient3858"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="42.703487"
|
||||
y1="20.547306"
|
||||
x2="26.605606"
|
||||
y2="33.634254" />
|
||||
<linearGradient
|
||||
id="linearGradient3063">
|
||||
<stop
|
||||
id="stop3065"
|
||||
offset="0"
|
||||
style="stop-color:#729fcf;stop-opacity:1" />
|
||||
<stop
|
||||
id="stop3067"
|
||||
offset="1"
|
||||
style="stop-color:#204a87;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2380"
|
||||
id="linearGradient3034"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="41.791897"
|
||||
y1="20.134634"
|
||||
x2="23.705669"
|
||||
y2="34.083359" />
|
||||
<linearGradient
|
||||
id="linearGradient2380">
|
||||
<stop
|
||||
style="stop-color:#729fcf;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2382" />
|
||||
<stop
|
||||
style="stop-color:#3465a4;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop2384" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2380"
|
||||
id="linearGradient3034-4"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="26.221533"
|
||||
y1="31.125586"
|
||||
x2="46.731483"
|
||||
y2="21.766298" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2831"
|
||||
id="linearGradient962"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.370336,0,0,1.3589114,0.02150968,-18.214919)"
|
||||
x1="13.478554"
|
||||
y1="10.612206"
|
||||
x2="15.419417"
|
||||
y2="19.115122" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2847"
|
||||
id="linearGradient964"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-1.370336,0,0,-1.3589114,64.512944,44.464873)"
|
||||
x1="37.128052"
|
||||
y1="29.729605"
|
||||
x2="37.065414"
|
||||
y2="26.194071" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3063"
|
||||
id="linearGradient966"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="42.703487"
|
||||
y1="20.547306"
|
||||
x2="26.605606"
|
||||
y2="33.634254" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient69056-7"
|
||||
id="linearGradient949"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(-1.2435,-2.5881)"
|
||||
x1="27.243999"
|
||||
y1="54.588001"
|
||||
x2="22.243999"
|
||||
y2="40.588001" />
|
||||
<linearGradient
|
||||
id="linearGradient69056-7"
|
||||
x1="27.243999"
|
||||
x2="22.243999"
|
||||
y1="54.588001"
|
||||
y2="40.588001"
|
||||
gradientTransform="translate(-1.2435,-2.5881)"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-color="#c4a000"
|
||||
offset="0"
|
||||
id="stop14" />
|
||||
<stop
|
||||
stop-color="#fce94f"
|
||||
offset="1"
|
||||
id="stop16" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4399-70"
|
||||
id="linearGradient951"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(1.2856,1.4142)"
|
||||
x1="48.714001"
|
||||
y1="45.585999"
|
||||
x2="44.714001"
|
||||
y2="34.585999" />
|
||||
<linearGradient
|
||||
id="linearGradient4399-70"
|
||||
x1="48.714001"
|
||||
x2="44.714001"
|
||||
y1="45.585999"
|
||||
y2="34.585999"
|
||||
gradientTransform="translate(1.2856,1.4142)"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-color="#c4a000"
|
||||
offset="0"
|
||||
id="stop8" />
|
||||
<stop
|
||||
stop-color="#edd400"
|
||||
offset="1"
|
||||
id="stop10" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient69709"
|
||||
x1="20.243999"
|
||||
x2="17.243999"
|
||||
y1="37.588001"
|
||||
y2="27.587999"
|
||||
gradientTransform="matrix(1,-0.026667,0,1,81.696,-5.3735)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
xlink:href="#linearGradient4383-3" />
|
||||
<linearGradient
|
||||
id="linearGradient69717"
|
||||
x1="50.714001"
|
||||
x2="48.714001"
|
||||
y1="25.586"
|
||||
y2="20.586"
|
||||
gradientTransform="translate(61.2256,1.0356)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
xlink:href="#linearGradient4383-3" />
|
||||
<linearGradient
|
||||
id="linearGradient4389-9"
|
||||
x1="20.243999"
|
||||
x2="17.243999"
|
||||
y1="37.588001"
|
||||
y2="27.587999"
|
||||
gradientTransform="translate(-1.2435,-2.5881)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
xlink:href="#linearGradient3774" />
|
||||
<linearGradient
|
||||
id="linearGradient69042-0"
|
||||
x1="48.714001"
|
||||
x2="44.714001"
|
||||
y1="45.585999"
|
||||
y2="34.585999"
|
||||
gradientTransform="translate(-12.714,-17.586)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
xlink:href="#linearGradient3774" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient69056-7"
|
||||
id="linearGradient920"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(-41.2435,-2.5881)"
|
||||
x1="20.243999"
|
||||
y1="37.588001"
|
||||
x2="17.243999"
|
||||
y2="27.587999" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4399-70"
|
||||
id="linearGradient922"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(-52.714,-17.586)"
|
||||
x1="48.714001"
|
||||
y1="45.585999"
|
||||
x2="44.714001"
|
||||
y2="34.585999" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="5"
|
||||
inkscape:cx="44.1"
|
||||
inkscape:cy="9.1"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
inkscape:document-units="px"
|
||||
inkscape:grid-bbox="true"
|
||||
inkscape:window-width="3686"
|
||||
inkscape:window-height="1571"
|
||||
inkscape:window-x="145"
|
||||
inkscape:window-y="-9"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-global="true"
|
||||
objecttolerance="10.0"
|
||||
gridtolerance="10.0"
|
||||
guidetolerance="10.0"
|
||||
inkscape:pagecheckerboard="0">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid3007"
|
||||
empspacing="4"
|
||||
visible="true"
|
||||
enabled="true"
|
||||
snapvisiblegridlinesonly="true"
|
||||
spacingx="0.5"
|
||||
spacingy="0.5" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata3564">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>Path-Stock</dc:title>
|
||||
<dc:date>2015-07-04</dc:date>
|
||||
<dc:relation>https://www.freecadweb.org/wiki/index.php?title=Artwork</dc:relation>
|
||||
<dc:publisher>
|
||||
<cc:Agent>
|
||||
<dc:title>FreeCAD</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:publisher>
|
||||
<dc:identifier>FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-Stock.svg</dc:identifier>
|
||||
<dc:rights>
|
||||
<cc:Agent>
|
||||
<dc:title>FreeCAD LGPL2+</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:rights>
|
||||
<cc:license>https://www.gnu.org/copyleft/lesser.html</cc:license>
|
||||
<dc:contributor>
|
||||
<cc:Agent>
|
||||
<dc:title>[agryson] Alexander Gryson</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:contributor>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="layer1"
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer">
|
||||
<g
|
||||
id="g1124"
|
||||
transform="matrix(1.0964113,0,0,1.0964113,-13.226965,60.643745)">
|
||||
<g
|
||||
id="g40"
|
||||
style="stroke-width:2"
|
||||
transform="translate(9.249999,-58.229485)">
|
||||
<path
|
||||
d="M 9,49 V 35 l 28,10 v 14 z"
|
||||
id="path30"
|
||||
style="fill:url(#linearGradient949);stroke:#302b00;stroke-linejoin:round"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="M 37,59 V 45 L 55,28 v 13 z"
|
||||
id="path32"
|
||||
style="fill:url(#linearGradient951);stroke:#302b00;stroke-linejoin:round"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="M 11.008,47.606 11,37.9997 l 24,8 0.0081,10.185 z"
|
||||
id="path34"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:none;stroke:#fce94f" />
|
||||
<path
|
||||
d="M 39.005,54.168 39,45.9998 l 14,-13 0.0021,7.1768 z"
|
||||
id="path36"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:none;stroke:#edd400" />
|
||||
<path
|
||||
d="M 23,40 42,23 55,28 37,45 Z"
|
||||
id="path38"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#fce94f;stroke:#302b00;stroke-linejoin:round" />
|
||||
</g>
|
||||
<g
|
||||
id="g943"
|
||||
transform="translate(-50.750001,-58.229485)">
|
||||
<path
|
||||
d="m 91,33.5 -0.02739,-14.214 12.967,4.3352 v 14.5 z"
|
||||
id="path54"
|
||||
style="fill:url(#linearGradient69709);stroke:#0b1521;stroke-width:2;stroke-linejoin:round"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 92.927,32.029 0.04731,-10.141 8.9272,3.29 0.0781,10.042 z"
|
||||
id="path56"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:none;stroke:#729fcf;stroke-width:2" />
|
||||
<path
|
||||
d="m 103.94,38.121 v -14.5 l 11,-9 L 115,28 Z"
|
||||
id="path58"
|
||||
style="fill:url(#linearGradient69717);stroke:#0b1521;stroke-width:2;stroke-linejoin:round"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 105.94,33.621 v -9 l 7,-6 -0.0122,8.5816 z"
|
||||
id="path60"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:none;stroke:#729fcf;stroke-width:2" />
|
||||
<path
|
||||
d="M 90.973,19.286 102,9.9998 l 12.94,4.6214 -11,9 z"
|
||||
id="path62"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#729fcf;stroke:#0b1521;stroke-width:2;stroke-linejoin:round" />
|
||||
</g>
|
||||
<g
|
||||
id="g963"
|
||||
transform="translate(49.249999,-58.229485)">
|
||||
<g
|
||||
style="stroke:#172a04;stroke-width:2;stroke-linejoin:round"
|
||||
transform="translate(-40)"
|
||||
id="g48">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:url(#linearGradient4389-9)"
|
||||
id="path42"
|
||||
d="M 9,35 V 21 l 14,5 v 14 z" />
|
||||
<path
|
||||
style="fill:#8ae234"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path44"
|
||||
d="M 9,21 28.585,5.209 42,10.0001 l -19,16 z" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:url(#linearGradient69042-0)"
|
||||
id="path46"
|
||||
d="M 23,40 V 26 l 7.9726,-6.7138 0.02739,13.714 z" />
|
||||
</g>
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:url(#linearGradient920);fill-opacity:1;stroke:#172a04;stroke-width:2;stroke-linejoin:round"
|
||||
id="path912"
|
||||
d="M -31,35 V 21 l 14,5 v 14 z" />
|
||||
<path
|
||||
style="fill:#fce94f;fill-opacity:1;stroke:#172a04;stroke-width:2;stroke-linejoin:round;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path914"
|
||||
d="M -31,21 -11.415,5.209 2,10.0001 l -19,16 z" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:url(#linearGradient922);fill-opacity:1;stroke:#172a04;stroke-width:2;stroke-linejoin:round"
|
||||
id="path916"
|
||||
d="M -17,40 V 26 l 7.9726,-6.7138 0.02739,13.714 z" />
|
||||
<path
|
||||
d="m -15,36 v -9 l 4,-3.5 v 9 z"
|
||||
id="path50"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:none;stroke:#fce94f;stroke-width:2;stroke-opacity:1" />
|
||||
<path
|
||||
d="m -29.049,33.746 0.08695,-9.9796 9.9568,3.5229 -0.02105,9.9613 z"
|
||||
id="path52"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:none;stroke:#fce94f;stroke-width:2;stroke-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g1230"
|
||||
transform="matrix(0.82819734,0,0,0.82819734,-66.264643,5.39994)">
|
||||
<ellipse
|
||||
transform="scale(-1)"
|
||||
id="path8660"
|
||||
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.383333;fill:url(#radialGradient1503);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.66662;marker:none"
|
||||
inkscape:r_cx="true"
|
||||
inkscape:r_cy="true"
|
||||
cx="-118.64883"
|
||||
cy="-51.426441"
|
||||
rx="31.937773"
|
||||
ry="11.424921" />
|
||||
<g
|
||||
id="g3863"
|
||||
transform="translate(85.809699,15.628782)">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc"
|
||||
inkscape:r_cy="true"
|
||||
inkscape:r_cx="true"
|
||||
id="path2865"
|
||||
d="m 27,-3.6915582 c 0,0 -12.247378,-0.8493196 -8.478954,13.4192502 H 7.986588 c 0,0 0.685168,-16.137073 19.013412,-13.4192502 z"
|
||||
style="color:#000000;display:block;overflow:visible;visibility:visible;fill:url(#linearGradient962);fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient964);stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none" />
|
||||
<g
|
||||
style="fill:url(#linearGradient966);fill-opacity:1;stroke:#204a87;stroke-width:0.732809;stroke-opacity:1"
|
||||
inkscape:r_cy="true"
|
||||
inkscape:r_cx="true"
|
||||
transform="matrix(-0.79349441,-0.66481753,-0.67040672,0.78687903,77.66003,0.94046451)"
|
||||
id="g1878">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:r_cy="true"
|
||||
inkscape:r_cx="true"
|
||||
style="color:#000000;display:block;overflow:visible;visibility:visible;fill:url(#linearGradient3034);fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:1.9334;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none"
|
||||
d="M 44.306783,50.229694 C 62.821497,35.818859 49.664587,13.411704 22.462411,12.49765 L 22.113843,3.1515478 7.6245439,20.496754 22.714328,33.219189 c 0,0 -0.251917,-9.88122 -0.251917,-9.88122 18.82976,0.998977 32.981627,14.071729 21.844372,26.891725 z"
|
||||
id="path1880"
|
||||
sodipodi:nodetypes="ccccccc" />
|
||||
</g>
|
||||
<g
|
||||
id="g2805"
|
||||
transform="matrix(-0.69686517,-0.58385766,-0.58876622,0.69105539,72.350404,1.0127423)"
|
||||
inkscape:r_cx="true"
|
||||
inkscape:r_cy="true"
|
||||
style="fill:none;stroke:#729fcf;stroke-width:0.732809;stroke-opacity:1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccc"
|
||||
id="path2807"
|
||||
d="M 52.368857,42.344789 C 57.336994,33.465615 49.176003,12.601866 19.05552,12.672851 L 18.677956,5.6633463 7.4378077,19.282655 19.129354,29.167094 18.807724,20.554957 c 18.244937,0.381972 33.804002,9.457851 33.561133,21.789832 z"
|
||||
style="color:#000000;display:block;overflow:visible;visibility:visible;fill:none;stroke:#729fcf;stroke-width:2.20149;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:21;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none"
|
||||
inkscape:r_cx="true"
|
||||
inkscape:r_cy="true" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="rotate(180,75.898143,22.314391)"
|
||||
id="g3863-0">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc"
|
||||
inkscape:r_cy="true"
|
||||
inkscape:r_cx="true"
|
||||
id="path2865-3"
|
||||
d="m 27,-3.6915582 c 0,0 -12.247378,-0.8493196 -8.478954,13.4192502 H 7.986588 c 0,0 0.685168,-16.137073 19.013412,-13.4192502 z"
|
||||
style="color:#000000;display:block;overflow:visible;visibility:visible;fill:url(#linearGradient1486);fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient1488);stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none" />
|
||||
<g
|
||||
style="fill:url(#linearGradient3858);fill-opacity:1;stroke:#204a87;stroke-width:0.732809;stroke-opacity:1"
|
||||
inkscape:r_cy="true"
|
||||
inkscape:r_cx="true"
|
||||
transform="matrix(-0.79349441,-0.66481753,-0.67040672,0.78687903,77.66003,0.94046451)"
|
||||
id="g1878-6">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:r_cy="true"
|
||||
inkscape:r_cx="true"
|
||||
style="color:#000000;display:block;overflow:visible;visibility:visible;fill:url(#linearGradient3034-4);fill-opacity:1;fill-rule:nonzero;stroke:#204a87;stroke-width:1.9334;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none"
|
||||
d="M 44.306783,50.229694 C 62.821497,35.818859 49.664587,13.411704 22.462411,12.49765 L 22.113843,3.1515478 7.6245439,20.496754 22.714328,33.219189 c 0,0 -0.251917,-9.88122 -0.251917,-9.88122 18.82976,0.998977 32.981627,14.071729 21.844372,26.891725 z"
|
||||
id="path1880-2"
|
||||
sodipodi:nodetypes="ccccccc" />
|
||||
</g>
|
||||
<g
|
||||
id="g2805-4"
|
||||
transform="matrix(-0.69686517,-0.58385766,-0.58876622,0.69105539,72.350404,1.0127423)"
|
||||
inkscape:r_cx="true"
|
||||
inkscape:r_cy="true"
|
||||
style="fill:none;stroke:#729fcf;stroke-width:0.732809;stroke-opacity:1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccc"
|
||||
id="path2807-5"
|
||||
d="M 52.368857,42.344789 C 57.864671,33.591679 49.176003,12.601866 19.05552,12.672851 L 18.677956,5.6633463 7.4378077,19.282655 19.129354,29.167094 18.807724,20.554957 c 18.244937,0.381972 33.804002,9.457851 33.561133,21.789832 z"
|
||||
style="color:#000000;display:block;overflow:visible;visibility:visible;fill:none;stroke:#729fcf;stroke-width:2.20149;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:21;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none"
|
||||
inkscape:r_cx="true"
|
||||
inkscape:r_cy="true" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 22 KiB |
@@ -20,8 +20,107 @@
|
||||
<item row="1" column="0">
|
||||
<widget class="QListWidget" name="featureList"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<layout class="QHBoxLayout" name="hLayoutDistance">
|
||||
<item>
|
||||
<widget class="QLabel" name="distanceLabel">
|
||||
<property name="text">
|
||||
<string>Distance</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Gui::QuantitySpinBox" name="distanceSpinbox">
|
||||
<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="3" column="0">
|
||||
<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="4" column="0">
|
||||
<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>
|
||||
<item row="5" column="0">
|
||||
<widget class="QToolButton" name="PushButtonReverse">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Reverse the direction of the joint.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Reverse</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="Resources/resource.qrc">
|
||||
<normaloff>:/icons/button_sort.svg</normaloff>:/icons/button_sort.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>Gui::QuantitySpinBox</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>Gui/QuantitySpinBox.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
||||
@@ -42,8 +42,34 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="Gui::PrefCheckBox" name="CheckBox_InsertInParts">
|
||||
<property name="toolTip">
|
||||
<string>If checked, the selected object will be inserted inside a Part container, unless it is already a Part.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Insert as part</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="prefEntry" stdset="0">
|
||||
<cstring>InsertInParts</cstring>
|
||||
</property>
|
||||
<property name="prefPath" stdset="0">
|
||||
<cstring>Mod/Assembly</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>Gui::PrefCheckBox</class>
|
||||
<extends>QCheckBox</extends>
|
||||
<header>Gui/PrefWidgets.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
/****************************************************************************
|
||||
* *
|
||||
* Copyright (c) 2023 Ondsel <development@ondsel.com> *
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "PreCompiled.h"
|
||||
|
||||
#ifndef _PreComp_
|
||||
#include <QMessageBox>
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
@@ -35,11 +36,12 @@
|
||||
#include <App/Part.h>
|
||||
#include <Gui/Application.h>
|
||||
#include <Gui/BitmapFactory.h>
|
||||
#include <Gui/Command.h>
|
||||
#include <Gui/CommandT.h>
|
||||
#include <Gui/MDIView.h>
|
||||
#include <Gui/View3DInventor.h>
|
||||
#include <Gui/View3DInventorViewer.h>
|
||||
#include <Mod/Assembly/App/AssemblyObject.h>
|
||||
#include <Mod/Assembly/App/JointGroup.h>
|
||||
#include <Mod/PartDesign/App/Body.h>
|
||||
|
||||
#include "ViewProviderAssembly.h"
|
||||
@@ -80,6 +82,62 @@ bool ViewProviderAssembly::doubleClicked()
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// else if a solid is removed, remove associated joints if any.
|
||||
bool prompted = false;
|
||||
auto* assemblyPart = static_cast<AssemblyObject*>(getObject());
|
||||
std::vector<App::DocumentObject*> joints = assemblyPart->getJoints();
|
||||
|
||||
// Combine the joints and groundedJoints vectors into one for simplicity.
|
||||
std::vector<App::DocumentObject*> allJoints = assemblyPart->getJoints();
|
||||
std::vector<App::DocumentObject*> groundedJoints = assemblyPart->getGroundedJoints();
|
||||
allJoints.insert(allJoints.end(), groundedJoints.begin(), groundedJoints.end());
|
||||
|
||||
Gui::Command::openCommand(tr("Delete associated joints").toStdString().c_str());
|
||||
for (auto joint : allJoints) {
|
||||
// Assume getLinkObjFromProp can return nullptr if the property doesn't exist.
|
||||
App::DocumentObject* obj1 = assemblyPart->getLinkObjFromProp(joint, "Part1");
|
||||
App::DocumentObject* obj2 = assemblyPart->getLinkObjFromProp(joint, "Part2");
|
||||
App::DocumentObject* obj3 = assemblyPart->getLinkObjFromProp(joint, "ObjectToGround");
|
||||
if (obj == obj1 || obj == obj2 || obj == obj3) {
|
||||
if (!prompted) {
|
||||
prompted = true;
|
||||
QMessageBox msgBox;
|
||||
msgBox.setText(tr("The object is associated to one or more joints."));
|
||||
msgBox.setInformativeText(
|
||||
tr("Do you want to move the object and delete associated joints?"));
|
||||
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
|
||||
msgBox.setDefaultButton(QMessageBox::No);
|
||||
int ret = msgBox.exec();
|
||||
|
||||
if (ret == QMessageBox::No) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Gui::Command::doCommand(Gui::Command::Gui,
|
||||
"App.activeDocument().removeObject('%s')",
|
||||
joint->getNameInDocument());
|
||||
}
|
||||
}
|
||||
Gui::Command::commitCommand();
|
||||
|
||||
// Remove grounded tag if any. (as it is not done in jointObject.py onDelete)
|
||||
std::string label = obj->Label.getValue();
|
||||
|
||||
if (label.size() >= 4 && label.substr(label.size() - 2) == " 🔒") {
|
||||
label = label.substr(0, label.size() - 2);
|
||||
obj->Label.setValue(label.c_str());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ViewProviderAssembly::setEdit(int ModNum)
|
||||
{
|
||||
@@ -308,35 +366,53 @@ App::DocumentObject* ViewProviderAssembly::getObjectFromSubNames(std::vector<std
|
||||
// For example we want box in "box.face1"
|
||||
return appDoc->getObject(subNames[0].c_str());
|
||||
}
|
||||
else {
|
||||
objName = subNames[subNames.size() - 3];
|
||||
|
||||
// From here subnames is at least 3 and can be more. There are several cases to consider :
|
||||
// bodyOrLink.pad.face1 -> bodyOrLink should be the moving
|
||||
// entity partOrLink.bodyOrLink.pad.face1 -> partOrLink should be the
|
||||
// moving entity partOrLink.box.face1 -> partOrLink should
|
||||
// be the moving entity partOrLink1...ParOrLinkn.bodyOrLink.pad.face1 -> partOrLink1
|
||||
// should be the moving entity assembly1.partOrLink1...ParOrLinkn.bodyOrLink.pad.face1 ->
|
||||
// partOrLink1 should be the moving entity assembly1.boxOrLink1.face1 -> boxOrLink1 should be
|
||||
// the moving entity
|
||||
|
||||
for (auto objName : subNames) {
|
||||
App::DocumentObject* obj = appDoc->getObject(objName.c_str());
|
||||
if (!obj) {
|
||||
return nullptr;
|
||||
continue;
|
||||
}
|
||||
if (obj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) {
|
||||
|
||||
if (obj->getTypeId().isDerivedFrom(AssemblyObject::getClassTypeId())) {
|
||||
continue;
|
||||
}
|
||||
else if (obj->getTypeId().isDerivedFrom(App::Part::getClassTypeId())
|
||||
|| obj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) {
|
||||
return obj;
|
||||
}
|
||||
else if (obj->getTypeId().isDerivedFrom(App::Link::getClassTypeId())) {
|
||||
|
||||
App::Link* link = dynamic_cast<App::Link*>(obj);
|
||||
|
||||
App::DocumentObject* linkedObj = link->getLinkedObject(true);
|
||||
if (!linkedObj) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (linkedObj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) {
|
||||
if (linkedObj->getTypeId().isDerivedFrom(App::Part::getClassTypeId())
|
||||
|| linkedObj->getTypeId().isDerivedFrom(PartDesign::Body::getClassTypeId())) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
// then its neither a body or a link to a body.
|
||||
objName = subNames[subNames.size() - 2];
|
||||
return appDoc->getObject(objName.c_str());
|
||||
}
|
||||
|
||||
// then its neither a part or body or a link to a part or body. So it is something like
|
||||
// assembly.box.face1
|
||||
objName = subNames[subNames.size() - 2];
|
||||
return appDoc->getObject(objName.c_str());
|
||||
}
|
||||
|
||||
void ViewProviderAssembly::initMove(Base::Vector3d& mousePosition)
|
||||
{
|
||||
Gui::Command::openCommand(tr("Move part").toStdString().c_str());
|
||||
partMoving = true;
|
||||
|
||||
// prevent selection while moving
|
||||
@@ -376,6 +452,8 @@ void ViewProviderAssembly::endMove()
|
||||
|
||||
auto* assemblyPart = static_cast<AssemblyObject*>(getObject());
|
||||
assemblyPart->setObjMasses({});
|
||||
|
||||
Gui::Command::commitCommand();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
#ifndef ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H
|
||||
#define ASSEMBLYGUI_VIEWPROVIDER_ViewProviderAssembly_H
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include <Mod/Assembly/AssemblyGlobal.h>
|
||||
|
||||
#include <Gui/Selection.h>
|
||||
@@ -40,6 +42,7 @@ namespace AssemblyGui
|
||||
class AssemblyGuiExport ViewProviderAssembly: public Gui::ViewProviderPart,
|
||||
public Gui::SelectionObserver
|
||||
{
|
||||
Q_DECLARE_TR_FUNCTIONS(AssemblyGui::ViewProviderAssembly)
|
||||
PROPERTY_HEADER_WITH_OVERRIDE(AssemblyGui::ViewProviderAssembly);
|
||||
|
||||
public:
|
||||
@@ -57,6 +60,14 @@ public:
|
||||
void unsetEdit(int ModNum) override;
|
||||
bool isInEditMode();
|
||||
|
||||
/// Ask the view provider if it accepts object deletions while in edit
|
||||
bool acceptDeletionsInEdit() override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool canDragObject(App::DocumentObject*) const override;
|
||||
|
||||
App::DocumentObject* getActivePart();
|
||||
|
||||
/// is called when the provider is in edit and the mouse is moved
|
||||
|
||||
@@ -47,3 +47,9 @@ QIcon ViewProviderJointGroup::getIcon() const
|
||||
{
|
||||
return Gui::BitmapFactory().pixmap("Assembly_CreateJointFixed.svg");
|
||||
}
|
||||
|
||||
// Make the joint group impossible to delete.
|
||||
bool ViewProviderJointGroup::onDelete(const std::vector<std::string>& subNames)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,22 @@ public:
|
||||
/// deliver the icon shown in the tree view. Override from ViewProvider.h
|
||||
QIcon getIcon() const override;
|
||||
|
||||
// Prevent dragging of the joints and dropping things inside the joint group.
|
||||
bool canDragObjects() const override
|
||||
{
|
||||
return false;
|
||||
};
|
||||
bool canDropObjects() const override
|
||||
{
|
||||
return false;
|
||||
};
|
||||
bool canDragAndDropObject(App::DocumentObject*) const override
|
||||
{
|
||||
return false;
|
||||
};
|
||||
|
||||
bool onDelete(const std::vector<std::string>& subNames) override;
|
||||
|
||||
// protected:
|
||||
/// get called by the container whenever a property has been changed
|
||||
// void onChanged(const App::Property* prop) override;
|
||||
|
||||
@@ -90,9 +90,7 @@ class AssemblyWorkbench(Workbench):
|
||||
"Assembly_CreateJointCylindrical",
|
||||
"Assembly_CreateJointSlider",
|
||||
"Assembly_CreateJointBall",
|
||||
"Assembly_CreateJointPlanar",
|
||||
"Assembly_CreateJointParallel",
|
||||
"Assembly_CreateJointTangent",
|
||||
"Assembly_CreateJointDistance",
|
||||
]
|
||||
|
||||
self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Assembly"), cmdlist)
|
||||
|
||||
@@ -47,14 +47,40 @@ JointTypes = [
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Cylindrical"),
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Slider"),
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Ball"),
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Planar"),
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Parallel"),
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Tangent"),
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Distance"),
|
||||
]
|
||||
|
||||
JointUsingDistance = [
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Distance"),
|
||||
]
|
||||
|
||||
JointUsingOffset = [
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"),
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Revolute"),
|
||||
]
|
||||
|
||||
JointUsingRotation = [
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"),
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Slider"),
|
||||
]
|
||||
|
||||
JointUsingReverse = [
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"),
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Revolute"),
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Cylindrical"),
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Slider"),
|
||||
QT_TRANSLATE_NOOP("AssemblyJoint", "Distance"),
|
||||
]
|
||||
|
||||
|
||||
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):
|
||||
def __init__(self, joint, type_index, assembly):
|
||||
self.Type = "Joint"
|
||||
|
||||
joint.Proxy = self
|
||||
@@ -76,6 +102,13 @@ class Joint:
|
||||
QT_TRANSLATE_NOOP("App::Property", "The first object of the joint"),
|
||||
)
|
||||
|
||||
joint.addProperty(
|
||||
"App::PropertyLink",
|
||||
"Part1",
|
||||
"Joint Connector 1",
|
||||
QT_TRANSLATE_NOOP("App::Property", "The first part of the joint"),
|
||||
)
|
||||
|
||||
joint.addProperty(
|
||||
"App::PropertyString",
|
||||
"Element1",
|
||||
@@ -108,6 +141,13 @@ class Joint:
|
||||
QT_TRANSLATE_NOOP("App::Property", "The second object of the joint"),
|
||||
)
|
||||
|
||||
joint.addProperty(
|
||||
"App::PropertyLink",
|
||||
"Part2",
|
||||
"Joint Connector 2",
|
||||
QT_TRANSLATE_NOOP("App::Property", "The second part of the joint"),
|
||||
)
|
||||
|
||||
joint.addProperty(
|
||||
"App::PropertyString",
|
||||
"Element2",
|
||||
@@ -132,6 +172,46 @@ class Joint:
|
||||
),
|
||||
)
|
||||
|
||||
joint.addProperty(
|
||||
"App::PropertyFloat",
|
||||
"Distance",
|
||||
"Joint",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"This is the distance of the joint. It is used only by the distance joint.",
|
||||
),
|
||||
)
|
||||
|
||||
joint.addProperty(
|
||||
"App::PropertyFloat",
|
||||
"Rotation",
|
||||
"Joint",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"This is the rotation of the joint.",
|
||||
),
|
||||
)
|
||||
|
||||
joint.addProperty(
|
||||
"App::PropertyVector",
|
||||
"Offset",
|
||||
"Joint",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"This is the offset vector of the joint.",
|
||||
),
|
||||
)
|
||||
|
||||
joint.addProperty(
|
||||
"App::PropertyBool",
|
||||
"FirstPartConnected",
|
||||
"Joint",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"This indicate if the first part was connected to ground at the time of joint creation.",
|
||||
),
|
||||
)
|
||||
|
||||
self.setJointConnectors(joint, [])
|
||||
|
||||
def __getstate__(self):
|
||||
@@ -141,14 +221,22 @@ class Joint:
|
||||
if state:
|
||||
self.Type = state
|
||||
|
||||
def getAssembly(self, joint):
|
||||
return joint.InList[0]
|
||||
|
||||
def setJointType(self, joint, jointType):
|
||||
joint.JointType = jointType
|
||||
joint.Label = jointType.replace(" ", "")
|
||||
|
||||
def onChanged(self, fp, prop):
|
||||
def onChanged(self, joint, prop):
|
||||
"""Do something when a property has changed"""
|
||||
# App.Console.PrintMessage("Change property: " + str(prop) + "\n")
|
||||
pass
|
||||
|
||||
if prop == "Rotation" or prop == "Offset" or prop == "Distance":
|
||||
if hasattr(
|
||||
joint, "Vertex1"
|
||||
): # during loading the onchanged may be triggered before full init.
|
||||
self.getAssembly(joint).solve()
|
||||
|
||||
def execute(self, fp):
|
||||
"""Do something when doing a recomputation, this method is mandatory"""
|
||||
@@ -157,32 +245,51 @@ class Joint:
|
||||
|
||||
def setJointConnectors(self, joint, current_selection):
|
||||
# current selection is a vector of strings like "Assembly.Assembly1.Assembly2.Body.Pad.Edge16" including both what selection return as obj_name and obj_sub
|
||||
assembly = self.getAssembly(joint)
|
||||
|
||||
if len(current_selection) >= 1:
|
||||
joint.Part1 = None
|
||||
joint.FirstPartConnected = assembly.isPartConnected(current_selection[0]["part"])
|
||||
|
||||
joint.Object1 = current_selection[0]["object"]
|
||||
joint.Part1 = current_selection[0]["part"]
|
||||
joint.Element1 = current_selection[0]["element_name"]
|
||||
joint.Vertex1 = current_selection[0]["vertex_name"]
|
||||
joint.Placement1 = self.findPlacement(joint.Object1, joint.Element1, joint.Vertex1)
|
||||
joint.Placement1 = self.findPlacement(
|
||||
joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1
|
||||
)
|
||||
else:
|
||||
joint.Object1 = None
|
||||
joint.Part1 = None
|
||||
joint.Element1 = ""
|
||||
joint.Vertex1 = ""
|
||||
joint.Placement1 = UtilsAssembly.activeAssembly().Placement
|
||||
joint.Placement1 = App.Placement()
|
||||
|
||||
if len(current_selection) >= 2:
|
||||
joint.Object2 = current_selection[1]["object"]
|
||||
joint.Part2 = current_selection[1]["part"]
|
||||
joint.Element2 = current_selection[1]["element_name"]
|
||||
joint.Vertex2 = current_selection[1]["vertex_name"]
|
||||
joint.Placement2 = self.findPlacement(joint.Object2, joint.Element2, joint.Vertex2)
|
||||
joint.Placement2 = self.findPlacement(
|
||||
joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True
|
||||
)
|
||||
assembly.solve(True)
|
||||
|
||||
else:
|
||||
joint.Object2 = None
|
||||
joint.Part2 = None
|
||||
joint.Element2 = ""
|
||||
joint.Vertex2 = ""
|
||||
joint.Placement2 = UtilsAssembly.activeAssembly().Placement
|
||||
joint.Placement2 = App.Placement()
|
||||
assembly.undoSolve()
|
||||
|
||||
def updateJCSPlacements(self, joint):
|
||||
joint.Placement1 = self.findPlacement(joint.Object1, joint.Element1, joint.Vertex1)
|
||||
joint.Placement2 = self.findPlacement(joint.Object2, joint.Element2, joint.Vertex2)
|
||||
joint.Placement1 = self.findPlacement(
|
||||
joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1
|
||||
)
|
||||
joint.Placement2 = self.findPlacement(
|
||||
joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True
|
||||
)
|
||||
|
||||
"""
|
||||
So here we want to find a placement that corresponds to a local coordinate system that would be placed at the selected vertex.
|
||||
@@ -194,12 +301,19 @@ class Joint:
|
||||
- if elt is a cylindrical face, vtx can also be the center of the arcs of the cylindrical face.
|
||||
"""
|
||||
|
||||
def findPlacement(self, obj, elt, vtx):
|
||||
def findPlacement(self, joint, obj, part, elt, vtx, isSecond=False):
|
||||
assembly = self.getAssembly(joint)
|
||||
plc = App.Placement()
|
||||
|
||||
if not obj or not elt or not vtx:
|
||||
if not obj:
|
||||
return App.Placement()
|
||||
|
||||
if not elt or not vtx:
|
||||
# case of whole parts such as PartDesign::Body or PartDesign::CordinateSystem.
|
||||
plc = UtilsAssembly.getGlobalPlacement(obj, part)
|
||||
plc = assembly.Placement.inverse() * plc
|
||||
return plc
|
||||
|
||||
elt_type, elt_index = UtilsAssembly.extract_type_and_number(elt)
|
||||
vtx_type, vtx_index = UtilsAssembly.extract_type_and_number(vtx)
|
||||
|
||||
@@ -211,11 +325,15 @@ class Joint:
|
||||
curve = edge.Curve
|
||||
|
||||
# First we find the translation
|
||||
if vtx_type == "Edge":
|
||||
# In this case the edge is a circle/arc and the wanted vertex is its center.
|
||||
if vtx_type == "Edge" or joint.JointType == "Distance":
|
||||
# In this case the wanted vertex is the center.
|
||||
if curve.TypeId == "Part::GeomCircle":
|
||||
center_point = curve.Location
|
||||
plc.Base = (center_point.x, center_point.y, center_point.z)
|
||||
elif curve.TypeId == "Part::GeomLine":
|
||||
edge_points = UtilsAssembly.getPointsFromVertexes(edge.Vertexes)
|
||||
line_middle = (edge_points[0] + edge_points[1]) * 0.5
|
||||
plc.Base = line_middle
|
||||
else:
|
||||
vertex = obj.Shape.Vertexes[vtx_index - 1]
|
||||
plc.Base = (vertex.X, vertex.Y, vertex.Z)
|
||||
@@ -229,31 +347,113 @@ class Joint:
|
||||
plane_origin = App.Vector(0, 0, 0)
|
||||
plane = Part.Plane(plane_origin, plane_normal)
|
||||
plc.Rotation = App.Rotation(plane.Rotation)
|
||||
|
||||
elif elt_type == "Face":
|
||||
face = obj.Shape.Faces[elt_index - 1]
|
||||
surface = face.Surface
|
||||
|
||||
# First we find the translation
|
||||
if vtx_type == "Edge":
|
||||
if vtx_type == "Face" or joint.JointType == "Distance":
|
||||
if surface.TypeId == "Part::GeomCylinder" or surface.TypeId == "Part::GeomCone":
|
||||
centerOfG = face.CenterOfGravity - surface.Center
|
||||
centerPoint = surface.Center + centerOfG
|
||||
centerPoint = centerPoint + App.Vector().projectToLine(centerOfG, surface.Axis)
|
||||
plc.Base = centerPoint
|
||||
elif surface.TypeId == "Part::GeomTorus" or surface.TypeId == "Part::GeomSphere":
|
||||
plc.Base = surface.Center
|
||||
else:
|
||||
plc.Base = face.CenterOfGravity
|
||||
elif vtx_type == "Edge":
|
||||
# In this case the edge is a circle/arc and the wanted vertex is its center.
|
||||
circleOrArc = face.Edges[vtx_index - 1]
|
||||
curve = circleOrArc.Curve
|
||||
edge = face.Edges[vtx_index - 1]
|
||||
curve = edge.Curve
|
||||
if curve.TypeId == "Part::GeomCircle":
|
||||
center_point = curve.Location
|
||||
plc.Base = (center_point.x, center_point.y, center_point.z)
|
||||
|
||||
elif (
|
||||
surface.TypeId == "Part::GeomCylinder"
|
||||
and curve.TypeId == "Part::GeomBSplineCurve"
|
||||
):
|
||||
# handle special case of 2 cylinder intersecting.
|
||||
plc.Base = self.findCylindersIntersection(obj, surface, edge, elt_index)
|
||||
|
||||
else:
|
||||
vertex = obj.Shape.Vertexes[vtx_index - 1]
|
||||
plc.Base = (vertex.X, vertex.Y, vertex.Z)
|
||||
|
||||
# Then we find the Rotation
|
||||
surface = face.Surface
|
||||
if surface.TypeId == "Part::GeomPlane":
|
||||
plc.Rotation = App.Rotation(surface.Rotation)
|
||||
else:
|
||||
plc.Rotation = surface.Rotation
|
||||
|
||||
# Now plc is the placement relative to the origin determined by the object placement.
|
||||
# But it does not take into account Part placements. So if the solid is in a part and
|
||||
# if the part has a placement then plc is wrong.
|
||||
|
||||
# change plc to be relative to the object placement.
|
||||
plc = obj.Placement.inverse() * plc
|
||||
|
||||
# change plc to be relative to the origin of the document.
|
||||
global_plc = UtilsAssembly.getGlobalPlacement(obj, part)
|
||||
plc = global_plc * plc
|
||||
|
||||
# change plc to be relative to the assembly.
|
||||
plc = assembly.Placement.inverse() * plc
|
||||
|
||||
# We apply rotation / reverse / offset it necessary, but only to the second JCS.
|
||||
if isSecond:
|
||||
if joint.Offset.Length != 0.0:
|
||||
plc = self.applyOffsetToPlacement(plc, joint.Offset)
|
||||
if joint.Rotation != 0.0:
|
||||
plc = self.applyRotationToPlacement(plc, joint.Rotation)
|
||||
|
||||
# Now plc is the placement in the doc. But we need the placement relative to the solid origin.
|
||||
return plc
|
||||
|
||||
def applyOffsetToPlacement(self, plc, offset):
|
||||
plc.Base = plc.Base + plc.Rotation.multVec(offset)
|
||||
return plc
|
||||
|
||||
def applyRotationToPlacement(self, plc, angle):
|
||||
rot = plc.Rotation
|
||||
zRotation = App.Rotation(App.Vector(0, 0, 1), angle)
|
||||
rot = rot.multiply(zRotation)
|
||||
plc.Rotation = rot
|
||||
return plc
|
||||
|
||||
def flipPart(self, joint):
|
||||
if joint.FirstPartConnected:
|
||||
plc = joint.Part2.Placement.inverse() * joint.Placement2
|
||||
localXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0))
|
||||
joint.Part2.Placement = flipPlacement(joint.Part2.Placement, localXAxis)
|
||||
else:
|
||||
plc = joint.Part1.Placement.inverse() * joint.Placement1
|
||||
localXAxis = plc.Rotation.multVec(App.Vector(1, 0, 0))
|
||||
joint.Part1.Placement = flipPlacement(joint.Part1.Placement, localXAxis)
|
||||
self.getAssembly(joint).solve()
|
||||
|
||||
def findCylindersIntersection(self, obj, surface, edge, elt_index):
|
||||
for j, facej in enumerate(obj.Shape.Faces):
|
||||
surfacej = facej.Surface
|
||||
if (elt_index - 1) == j or surfacej.TypeId != "Part::GeomCylinder":
|
||||
continue
|
||||
|
||||
for edgej in facej.Edges:
|
||||
if (
|
||||
edgej.Curve.TypeId == "Part::GeomBSplineCurve"
|
||||
and edgej.CenterOfGravity == edge.CenterOfGravity
|
||||
and edgej.Length == edge.Length
|
||||
):
|
||||
# we need intersection between the 2 cylinder axis.
|
||||
line1 = Part.Line(surface.Center, surface.Center + surface.Axis)
|
||||
line2 = Part.Line(surfacej.Center, surfacej.Center + surfacej.Axis)
|
||||
|
||||
res = line1.intersect(line2, Part.Precision.confusion())
|
||||
|
||||
if res:
|
||||
return App.Vector(res[0].X, res[0].Y, res[0].Z)
|
||||
return surface.Center
|
||||
|
||||
|
||||
class ViewProviderJoint:
|
||||
def __init__(self, vobj):
|
||||
@@ -394,25 +594,34 @@ class ViewProviderJoint:
|
||||
r = placement.Rotation.Q
|
||||
soTransform.rotation.setValue(r[0], r[1], r[2], r[3])
|
||||
|
||||
def updateData(self, fp, prop):
|
||||
def updateData(self, joint, prop):
|
||||
"""If a property of the handled feature has changed we have the chance to handle this here"""
|
||||
# fp is the handled feature, prop is the name of the property that has changed
|
||||
# joint is the handled feature, prop is the name of the property that has changed
|
||||
if prop == "Placement1":
|
||||
plc = fp.getPropertyByName("Placement1")
|
||||
if fp.getPropertyByName("Object1"):
|
||||
plc = joint.getPropertyByName("Placement1")
|
||||
if joint.getPropertyByName("Object1"):
|
||||
self.switch_JCS1.whichChild = coin.SO_SWITCH_ALL
|
||||
self.set_JCS_placement(self.transform1, plc)
|
||||
else:
|
||||
self.switch_JCS1.whichChild = coin.SO_SWITCH_NONE
|
||||
|
||||
if prop == "Placement2":
|
||||
plc = fp.getPropertyByName("Placement2")
|
||||
if fp.getPropertyByName("Object2"):
|
||||
plc = joint.getPropertyByName("Placement2")
|
||||
if joint.getPropertyByName("Object2"):
|
||||
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)
|
||||
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):
|
||||
if visible:
|
||||
self.switch_JCS_preview.whichChild = coin.SO_SWITCH_ALL
|
||||
@@ -454,12 +663,8 @@ class ViewProviderJoint:
|
||||
return ":/icons/Assembly_CreateJointSlider.svg"
|
||||
elif self.app_obj.JointType == "Ball":
|
||||
return ":/icons/Assembly_CreateJointBall.svg"
|
||||
elif self.app_obj.JointType == "Planar":
|
||||
return ":/icons/Assembly_CreateJointPlanar.svg"
|
||||
elif self.app_obj.JointType == "Parallel":
|
||||
return ":/icons/Assembly_CreateJointParallel.svg"
|
||||
elif self.app_obj.JointType == "Tangent":
|
||||
return ":/icons/Assembly_CreateJointTangent.svg"
|
||||
elif self.app_obj.JointType == "Distance":
|
||||
return ":/icons/Assembly_CreateJointDistance.svg"
|
||||
|
||||
return ":/icons/Assembly_CreateJoint.svg"
|
||||
|
||||
@@ -475,6 +680,10 @@ class ViewProviderJoint:
|
||||
return None
|
||||
|
||||
def doubleClicked(self, vobj):
|
||||
assembly = vobj.Object.InList[0]
|
||||
if UtilsAssembly.activeAssembly() != assembly:
|
||||
Gui.ActiveDocument.ActiveView.setActiveObject("part", assembly)
|
||||
|
||||
panel = TaskAssemblyCreateJoint(0, vobj.Object)
|
||||
Gui.Control.showDialog(panel)
|
||||
|
||||
@@ -555,6 +764,15 @@ class ViewProviderGroundedJoint:
|
||||
# App.Console.PrintMessage("Change property: " + str(prop) + "\n")
|
||||
pass
|
||||
|
||||
def onDelete(self, feature, subelements): # subelements is a tuple of strings
|
||||
# Remove grounded tag.
|
||||
if hasattr(feature.Object, "ObjectToGround"):
|
||||
obj = feature.Object.ObjectToGround
|
||||
if obj.Label.endswith(" 🔒"):
|
||||
obj.Label = obj.Label[:-2]
|
||||
|
||||
return True # If False is returned the object won't be deleted
|
||||
|
||||
def getIcon(self):
|
||||
return ":/icons/Assembly_ToggleGrounded.svg"
|
||||
|
||||
@@ -570,8 +788,8 @@ class MakeJointSelGate:
|
||||
|
||||
objs_names, element_name = UtilsAssembly.getObjsNamesAndElement(obj.Name, sub)
|
||||
|
||||
if self.assembly.Name not in objs_names or element_name == "":
|
||||
# Only objects within the assembly. And not whole objects, only elements.
|
||||
if self.assembly.Name not in objs_names:
|
||||
# Only objects within the assembly.
|
||||
return False
|
||||
|
||||
if Gui.Selection.isSelected(obj, sub, Gui.Selection.ResolveMode.NoResolve):
|
||||
@@ -585,19 +803,28 @@ class MakeJointSelGate:
|
||||
full_obj_name = ".".join(objs_names)
|
||||
full_element_name = full_obj_name + "." + element_name
|
||||
selected_object = UtilsAssembly.getObject(full_element_name)
|
||||
part_containing_selected_object = UtilsAssembly.getContainingPart(
|
||||
full_element_name, selected_object
|
||||
)
|
||||
|
||||
for selection_dict in self.taskbox.current_selection:
|
||||
if selection_dict["object"] == selected_object:
|
||||
if selection_dict["part"] == part_containing_selected_object:
|
||||
# Can't join a solid to itself. So the user need to select 2 different parts.
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
activeTask = None
|
||||
|
||||
|
||||
class TaskAssemblyCreateJoint(QtCore.QObject):
|
||||
def __init__(self, jointTypeIndex, jointObj=None):
|
||||
super().__init__()
|
||||
|
||||
global activeTask
|
||||
activeTask = self
|
||||
|
||||
self.assembly = UtilsAssembly.activeAssembly()
|
||||
self.view = Gui.activeDocument().activeView()
|
||||
self.doc = App.ActiveDocument
|
||||
@@ -612,6 +839,10 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
|
||||
self.form.jointType.addItems(JointTypes)
|
||||
self.form.jointType.setCurrentIndex(jointTypeIndex)
|
||||
self.form.jointType.currentIndexChanged.connect(self.onJointTypeChanged)
|
||||
self.form.distanceSpinbox.valueChanged.connect(self.onDistanceChanged)
|
||||
self.form.offsetSpinbox.valueChanged.connect(self.onOffsetChanged)
|
||||
self.form.rotationSpinbox.valueChanged.connect(self.onRotationChanged)
|
||||
self.form.PushButtonReverse.clicked.connect(self.onReverseClicked)
|
||||
|
||||
Gui.Selection.clearSelection()
|
||||
|
||||
@@ -631,6 +862,11 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
|
||||
|
||||
self.createJointObject()
|
||||
|
||||
self.toggleDistanceVisibility()
|
||||
self.toggleOffsetVisibility()
|
||||
self.toggleRotationVisibility()
|
||||
self.toggleReverseVisibility()
|
||||
|
||||
Gui.Selection.addSelectionGate(
|
||||
MakeJointSelGate(self, self.assembly), Gui.Selection.ResolveMode.NoResolve
|
||||
)
|
||||
@@ -662,6 +898,10 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
|
||||
return True
|
||||
|
||||
def deactivate(self):
|
||||
global activeTask
|
||||
activeTask = None
|
||||
self.assembly.clearUndo()
|
||||
|
||||
self.assembly.ViewObject.EnableMovement = True
|
||||
Gui.Selection.removeSelectionGate()
|
||||
Gui.Selection.removeObserver(self)
|
||||
@@ -678,11 +918,57 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
|
||||
joint_group = UtilsAssembly.getJointGroup(self.assembly)
|
||||
|
||||
self.joint = joint_group.newObject("App::FeaturePython", self.jointName)
|
||||
Joint(self.joint, type_index)
|
||||
Joint(self.joint, type_index, self.assembly)
|
||||
ViewProviderJoint(self.joint.ViewObject)
|
||||
|
||||
def onJointTypeChanged(self, index):
|
||||
self.joint.Proxy.setJointType(self.joint, self.form.jointType.currentText())
|
||||
self.toggleDistanceVisibility()
|
||||
self.toggleOffsetVisibility()
|
||||
self.toggleRotationVisibility()
|
||||
self.toggleReverseVisibility()
|
||||
|
||||
def onDistanceChanged(self, quantity):
|
||||
self.joint.Distance = self.form.distanceSpinbox.property("rawValue")
|
||||
|
||||
def onOffsetChanged(self, quantity):
|
||||
self.joint.Offset = App.Vector(0, 0, self.form.offsetSpinbox.property("rawValue"))
|
||||
|
||||
def onRotationChanged(self, quantity):
|
||||
self.joint.Rotation = self.form.rotationSpinbox.property("rawValue")
|
||||
|
||||
def onReverseClicked(self):
|
||||
self.joint.Proxy.flipPart(self.joint)
|
||||
|
||||
def toggleDistanceVisibility(self):
|
||||
if self.form.jointType.currentText() in JointUsingDistance:
|
||||
self.form.distanceLabel.show()
|
||||
self.form.distanceSpinbox.show()
|
||||
else:
|
||||
self.form.distanceLabel.hide()
|
||||
self.form.distanceSpinbox.hide()
|
||||
|
||||
def toggleOffsetVisibility(self):
|
||||
if self.form.jointType.currentText() in JointUsingOffset:
|
||||
self.form.offsetLabel.show()
|
||||
self.form.offsetSpinbox.show()
|
||||
else:
|
||||
self.form.offsetLabel.hide()
|
||||
self.form.offsetSpinbox.hide()
|
||||
|
||||
def toggleRotationVisibility(self):
|
||||
if self.form.jointType.currentText() in JointUsingRotation:
|
||||
self.form.rotationLabel.show()
|
||||
self.form.rotationSpinbox.show()
|
||||
else:
|
||||
self.form.rotationLabel.hide()
|
||||
self.form.rotationSpinbox.hide()
|
||||
|
||||
def toggleReverseVisibility(self):
|
||||
if self.form.jointType.currentText() in JointUsingReverse:
|
||||
self.form.PushButtonReverse.show()
|
||||
else:
|
||||
self.form.PushButtonReverse.hide()
|
||||
|
||||
def updateTaskboxFromJoint(self):
|
||||
self.current_selection = []
|
||||
@@ -690,12 +976,14 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
|
||||
|
||||
selection_dict1 = {
|
||||
"object": self.joint.Object1,
|
||||
"part": self.joint.Part1,
|
||||
"element_name": self.joint.Element1,
|
||||
"vertex_name": self.joint.Vertex1,
|
||||
}
|
||||
|
||||
selection_dict2 = {
|
||||
"object": self.joint.Object2,
|
||||
"part": self.joint.Part2,
|
||||
"element_name": self.joint.Element2,
|
||||
"vertex_name": self.joint.Vertex2,
|
||||
}
|
||||
@@ -712,9 +1000,17 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
|
||||
elName = self.getObjSubNameFromObj(self.joint.Object2, self.joint.Element2)
|
||||
Gui.Selection.addSelection(self.doc.Name, self.joint.Object2.Name, elName)
|
||||
|
||||
self.form.distanceSpinbox.setProperty("rawValue", self.joint.Distance)
|
||||
self.form.offsetSpinbox.setProperty("rawValue", self.joint.Offset.z)
|
||||
self.form.rotationSpinbox.setProperty("rawValue", self.joint.Rotation)
|
||||
|
||||
self.form.jointType.setCurrentIndex(JointTypes.index(self.joint.JointType))
|
||||
self.updateJointList()
|
||||
|
||||
def getObjSubNameFromObj(self, obj, elName):
|
||||
if obj is None:
|
||||
return elName
|
||||
|
||||
if obj.TypeId == "PartDesign::Body":
|
||||
return obj.Tip.Name + "." + elName
|
||||
elif obj.TypeId == "App::Link":
|
||||
@@ -738,14 +1034,16 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
|
||||
simplified_names = []
|
||||
for sel in self.current_selection:
|
||||
# TODO: ideally we probably want to hide the feature name in case of PartDesign bodies. ie body.face12 and not body.pad2.face12
|
||||
sname = sel["object"].Label + "." + sel["element_name"]
|
||||
sname = sel["object"].Label
|
||||
if sel["element_name"] != "":
|
||||
sname = sname + "." + sel["element_name"]
|
||||
simplified_names.append(sname)
|
||||
self.form.featureList.addItems(simplified_names)
|
||||
|
||||
def moveMouse(self, info):
|
||||
if len(self.current_selection) >= 2 or (
|
||||
len(self.current_selection) == 1
|
||||
and self.current_selection[0]["object"] == self.preselection_dict["object"]
|
||||
and self.current_selection[0]["part"] == self.preselection_dict["part"]
|
||||
):
|
||||
self.joint.ViewObject.Proxy.showPreviewJCS(False)
|
||||
return
|
||||
@@ -767,14 +1065,22 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
|
||||
newPos = App.Vector(cursor_info["x"], cursor_info["y"], cursor_info["z"])
|
||||
self.preselection_dict["mouse_pos"] = newPos
|
||||
|
||||
self.preselection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(
|
||||
self.preselection_dict
|
||||
)
|
||||
if self.preselection_dict["element_name"] == "":
|
||||
self.preselection_dict["vertex_name"] = ""
|
||||
else:
|
||||
self.preselection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(
|
||||
self.preselection_dict
|
||||
)
|
||||
|
||||
isSecond = len(self.current_selection) == 1
|
||||
|
||||
placement = self.joint.Proxy.findPlacement(
|
||||
self.joint,
|
||||
self.preselection_dict["object"],
|
||||
self.preselection_dict["part"],
|
||||
self.preselection_dict["element_name"],
|
||||
self.preselection_dict["vertex_name"],
|
||||
isSecond,
|
||||
)
|
||||
self.joint.ViewObject.Proxy.showPreviewJCS(True, placement)
|
||||
self.previewJCSVisible = True
|
||||
@@ -793,15 +1099,22 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
|
||||
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
|
||||
)
|
||||
|
||||
selection_dict = {
|
||||
"object": selected_object,
|
||||
"part": part_containing_selected_object,
|
||||
"element_name": element_name,
|
||||
"full_element_name": full_element_name,
|
||||
"full_obj_name": full_obj_name,
|
||||
"mouse_pos": App.Vector(mousePos[0], mousePos[1], mousePos[2]),
|
||||
}
|
||||
selection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(selection_dict)
|
||||
if element_name == "":
|
||||
selection_dict["vertex_name"] = ""
|
||||
else:
|
||||
selection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(selection_dict)
|
||||
|
||||
self.current_selection.append(selection_dict)
|
||||
self.updateJoint()
|
||||
@@ -810,11 +1123,14 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
|
||||
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
|
||||
)
|
||||
|
||||
# Find and remove the corresponding dictionary from the combined list
|
||||
selection_dict_to_remove = None
|
||||
for selection_dict in self.current_selection:
|
||||
if selection_dict["object"] == selected_object:
|
||||
if selection_dict["part"] == part_containing_selected_object:
|
||||
selection_dict_to_remove = selection_dict
|
||||
break
|
||||
|
||||
@@ -832,9 +1148,13 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
|
||||
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
|
||||
)
|
||||
|
||||
self.preselection_dict = {
|
||||
"object": selected_object,
|
||||
"part": part_containing_selected_object,
|
||||
"sub_name": sub_name,
|
||||
"element_name": element_name,
|
||||
"full_element_name": full_element_name,
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# ***************************************************************************/
|
||||
|
||||
import FreeCAD as App
|
||||
import Part
|
||||
|
||||
if App.GuiUp:
|
||||
import FreeCADGui as Gui
|
||||
@@ -36,17 +37,35 @@ __url__ = "https://www.freecad.org"
|
||||
def activeAssembly():
|
||||
doc = Gui.ActiveDocument
|
||||
|
||||
if doc is None or doc.ActiveView is None:
|
||||
return None
|
||||
|
||||
active_assembly = doc.ActiveView.getActiveObject("part")
|
||||
|
||||
if active_assembly is not None and active_assembly.Type == "Assembly":
|
||||
return active_assembly
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def activePart():
|
||||
doc = Gui.ActiveDocument
|
||||
|
||||
if doc is None or doc.ActiveView is None:
|
||||
return None
|
||||
|
||||
active_part = doc.ActiveView.getActiveObject("part")
|
||||
|
||||
if active_part is not None and active_part.Type == "Assembly":
|
||||
if active_part is not None and active_part.Type != "Assembly":
|
||||
return active_part
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def isAssemblyCommandActive():
|
||||
return activeAssembly() is not None and not Gui.Control.activeDialog()
|
||||
|
||||
|
||||
def isDocTemporary(doc):
|
||||
# Guard against older versions of FreeCad which don't have the Temporary attribute
|
||||
try:
|
||||
@@ -56,19 +75,39 @@ def isDocTemporary(doc):
|
||||
return temp
|
||||
|
||||
|
||||
def assembly_has_at_least_n_parts(n):
|
||||
assembly = activeAssembly()
|
||||
i = 0
|
||||
if not assembly:
|
||||
return False
|
||||
for obj in assembly.OutList:
|
||||
# note : groundedJoints comes in the outlist so we filter those out.
|
||||
if hasattr(obj, "Placement") and not hasattr(obj, "ObjectToGround"):
|
||||
i = i + 1
|
||||
if i == n:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def getObject(full_name):
|
||||
# full_name is "Assembly.Assembly1.Assembly2.Assembly3.Box.Edge16"
|
||||
# or "Assembly.Assembly1.Assembly2.Assembly3.Body.pad.Edge16"
|
||||
# We want either Body or Box.
|
||||
parts = full_name.split(".")
|
||||
# full_name is "Assembly.Assembly1.LinkOrPart1.Box.Edge16"
|
||||
# or "Assembly.Assembly1.LinkOrPart1.Body.pad.Edge16"
|
||||
# or "Assembly.Assembly1.LinkOrPart1.Body.Local_CS.X"
|
||||
# We want either Body or Box or Local_CS.
|
||||
names = full_name.split(".")
|
||||
doc = App.ActiveDocument
|
||||
if len(parts) < 3:
|
||||
if len(names) < 3:
|
||||
App.Console.PrintError(
|
||||
"getObject() in UtilsAssembly.py the object name is too short, at minimum it should be something like 'Assembly.Box.edge16'. It shouldn't be shorter"
|
||||
)
|
||||
return None
|
||||
|
||||
obj = doc.getObject(parts[-3]) # So either 'Body', or 'Assembly'
|
||||
obj = doc.getObject(names[-2])
|
||||
|
||||
if obj and obj.TypeId == "PartDesign::CoordinateSystem":
|
||||
return doc.getObject(names[-2])
|
||||
|
||||
obj = doc.getObject(names[-3]) # So either 'Body', or 'Assembly'
|
||||
|
||||
if not obj:
|
||||
return None
|
||||
@@ -80,8 +119,116 @@ def getObject(full_name):
|
||||
if linked_obj.TypeId == "PartDesign::Body":
|
||||
return obj
|
||||
|
||||
else: # primitive, fastener, gear ... or link to primitive, fastener, gear...
|
||||
return doc.getObject(parts[-2])
|
||||
# primitive, fastener, gear ... or link to primitive, fastener, gear...
|
||||
return doc.getObject(names[-2])
|
||||
|
||||
|
||||
def getContainingPart(full_name, selected_object):
|
||||
# full_name is "Assembly.Assembly1.LinkOrPart1.Box.Edge16"
|
||||
# or "Assembly.Assembly1.LinkOrPart1.Body.pad.Edge16"
|
||||
# We want either Body or Box.
|
||||
names = full_name.split(".")
|
||||
doc = App.ActiveDocument
|
||||
if len(names) < 3:
|
||||
App.Console.PrintError(
|
||||
"getContainingPart() in UtilsAssembly.py the object name is too short, at minimum it should be something like 'Assembly.Box.edge16'. It shouldn't be shorter"
|
||||
)
|
||||
return None
|
||||
|
||||
for objName in names:
|
||||
obj = doc.getObject(objName)
|
||||
|
||||
if not obj:
|
||||
continue
|
||||
|
||||
if (
|
||||
obj.TypeId == "PartDesign::Body"
|
||||
and selected_object.TypeId == "PartDesign::CoordinateSystem"
|
||||
):
|
||||
if obj.hasObject(selected_object, True):
|
||||
return obj
|
||||
|
||||
# Note here we may want to specify a specific behavior for Assembly::AssemblyObject.
|
||||
if obj.TypeId == "App::Part":
|
||||
if obj.hasObject(selected_object, True):
|
||||
return obj
|
||||
|
||||
elif obj.TypeId == "App::Link":
|
||||
linked_obj = obj.getLinkedObject()
|
||||
if linked_obj.TypeId == "App::Part":
|
||||
if linked_obj.hasObject(selected_object, True):
|
||||
return obj
|
||||
|
||||
# no container found so we return the object itself.
|
||||
return selected_object
|
||||
|
||||
|
||||
# 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):
|
||||
inContainerBranch = container is None
|
||||
for part in App.activeDocument().RootObjects:
|
||||
foundPlacement = getTargetPlacementRelativeTo(targetObj, part, container, inContainerBranch)
|
||||
if foundPlacement is not None:
|
||||
return foundPlacement
|
||||
|
||||
return App.Placement()
|
||||
|
||||
|
||||
def getTargetPlacementRelativeTo(
|
||||
targetObj, part, container, inContainerBranch, ignorePlacement=False
|
||||
):
|
||||
inContainerBranch = inContainerBranch or (not ignorePlacement and part == container)
|
||||
|
||||
if targetObj == part and inContainerBranch and not ignorePlacement:
|
||||
return targetObj.Placement
|
||||
|
||||
if part.TypeId == "App::DocumentObjectGroup":
|
||||
for obj in part.OutList:
|
||||
foundPlacement = getTargetPlacementRelativeTo(
|
||||
targetObj, obj, container, inContainerBranch, ignorePlacement
|
||||
)
|
||||
if foundPlacement is not None:
|
||||
return foundPlacement
|
||||
|
||||
elif part.TypeId == "App::Part" or part.TypeId == "Assembly::AssemblyObject":
|
||||
for obj in part.OutList:
|
||||
foundPlacement = getTargetPlacementRelativeTo(
|
||||
targetObj, obj, container, inContainerBranch
|
||||
)
|
||||
if foundPlacement is None:
|
||||
continue
|
||||
|
||||
# If we were called from a link then we need to ignore this placement as we use the link placement instead.
|
||||
if not ignorePlacement:
|
||||
foundPlacement = part.Placement * foundPlacement
|
||||
|
||||
return foundPlacement
|
||||
|
||||
elif part.TypeId == "App::Link":
|
||||
linked_obj = part.getLinkedObject()
|
||||
|
||||
if linked_obj.TypeId == "App::Part" or linked_obj.TypeId == "Assembly::AssemblyObject":
|
||||
for obj in linked_obj.OutList:
|
||||
foundPlacement = getTargetPlacementRelativeTo(
|
||||
targetObj, obj, container, inContainerBranch
|
||||
)
|
||||
if foundPlacement is None:
|
||||
continue
|
||||
|
||||
foundPlacement = part.Placement * foundPlacement
|
||||
return foundPlacement
|
||||
|
||||
foundPlacement = getTargetPlacementRelativeTo(
|
||||
targetObj, linked_obj, container, inContainerBranch, True
|
||||
)
|
||||
|
||||
if foundPlacement is not None and not ignorePlacement:
|
||||
foundPlacement = part.Placement * foundPlacement
|
||||
|
||||
return foundPlacement
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def getElementName(full_name):
|
||||
@@ -93,6 +240,10 @@ def getElementName(full_name):
|
||||
# At minimum "Assembly.Box.edge16". It shouldn't be shorter
|
||||
return ""
|
||||
|
||||
# case of PartDesign::CoordinateSystem
|
||||
if parts[-1] == "X" or parts[-1] == "Y" or parts[-1] == "Z":
|
||||
return ""
|
||||
|
||||
return parts[-1]
|
||||
|
||||
|
||||
@@ -147,14 +298,25 @@ def extract_type_and_number(element_name):
|
||||
|
||||
|
||||
def findElementClosestVertex(selection_dict):
|
||||
obj = selection_dict["object"]
|
||||
|
||||
mousePos = selection_dict["mouse_pos"]
|
||||
|
||||
# We need mousePos to be relative to the part containing obj global placement
|
||||
if selection_dict["object"] != selection_dict["part"]:
|
||||
plc = App.Placement()
|
||||
plc.Base = mousePos
|
||||
global_plc = getGlobalPlacement(selection_dict["part"])
|
||||
plc = global_plc.inverse() * plc
|
||||
mousePos = plc.Base
|
||||
|
||||
elt_type, elt_index = extract_type_and_number(selection_dict["element_name"])
|
||||
|
||||
if elt_type == "Vertex":
|
||||
return selection_dict["element_name"]
|
||||
|
||||
elif elt_type == "Edge":
|
||||
edge = selection_dict["object"].Shape.Edges[elt_index - 1]
|
||||
|
||||
edge = obj.Shape.Edges[elt_index - 1]
|
||||
curve = edge.Curve
|
||||
if curve.TypeId == "Part::GeomCircle":
|
||||
# For centers, as they are not shape vertexes, we return the element name.
|
||||
@@ -162,17 +324,28 @@ def findElementClosestVertex(selection_dict):
|
||||
return selection_dict["element_name"]
|
||||
|
||||
edge_points = getPointsFromVertexes(edge.Vertexes)
|
||||
closest_vertex_index, _ = findClosestPointToMousePos(
|
||||
edge_points, selection_dict["mouse_pos"]
|
||||
)
|
||||
vertex_name = findVertexNameInObject(
|
||||
edge.Vertexes[closest_vertex_index], selection_dict["object"]
|
||||
)
|
||||
|
||||
if curve.TypeId == "Part::GeomLine":
|
||||
# For lines we allow users to select the middle of lines as well.
|
||||
line_middle = (edge_points[0] + edge_points[1]) * 0.5
|
||||
edge_points.append(line_middle)
|
||||
|
||||
closest_vertex_index, _ = findClosestPointToMousePos(edge_points, mousePos)
|
||||
|
||||
if curve.TypeId == "Part::GeomLine" and closest_vertex_index == 2:
|
||||
# If line center is closest then we have no vertex name to set so we put element name
|
||||
return selection_dict["element_name"]
|
||||
|
||||
vertex_name = findVertexNameInObject(edge.Vertexes[closest_vertex_index], obj)
|
||||
|
||||
return vertex_name
|
||||
|
||||
elif elt_type == "Face":
|
||||
face = selection_dict["object"].Shape.Faces[elt_index - 1]
|
||||
face = obj.Shape.Faces[elt_index - 1]
|
||||
surface = face.Surface
|
||||
_type = surface.TypeId
|
||||
if _type == "Part::GeomSphere" or _type == "Part::GeomTorus":
|
||||
return selection_dict["element_name"]
|
||||
|
||||
# Handle the circle/arc edges for their centers
|
||||
center_points = []
|
||||
@@ -181,19 +354,46 @@ def findElementClosestVertex(selection_dict):
|
||||
|
||||
for i, edge in enumerate(edges):
|
||||
curve = edge.Curve
|
||||
if curve.TypeId == "Part::GeomCircle":
|
||||
if curve.TypeId == "Part::GeomCircle" or curve.TypeId == "Part::GeomEllipse":
|
||||
center_points.append(curve.Location)
|
||||
center_points_edge_indexes.append(i)
|
||||
|
||||
elif _type == "Part::GeomCylinder" and curve.TypeId == "Part::GeomBSplineCurve":
|
||||
# handle special case of 2 cylinder intersecting.
|
||||
for j, facej in enumerate(obj.Shape.Faces):
|
||||
surfacej = facej.Surface
|
||||
if (elt_index - 1) != j and surfacej.TypeId == "Part::GeomCylinder":
|
||||
for edgej in facej.Edges:
|
||||
if edgej.Curve.TypeId == "Part::GeomBSplineCurve":
|
||||
if (
|
||||
edgej.CenterOfGravity == edge.CenterOfGravity
|
||||
and edgej.Length == edge.Length
|
||||
):
|
||||
center_points.append(edgej.CenterOfGravity)
|
||||
center_points_edge_indexes.append(i)
|
||||
|
||||
if len(center_points) > 0:
|
||||
closest_center_index, closest_center_distance = findClosestPointToMousePos(
|
||||
center_points, selection_dict["mouse_pos"]
|
||||
center_points, mousePos
|
||||
)
|
||||
|
||||
# Hendle the face vertexes
|
||||
face_points = getPointsFromVertexes(face.Vertexes)
|
||||
# Handle the face vertexes
|
||||
face_points = []
|
||||
|
||||
if _type != "Part::GeomCylinder" and _type != "Part::GeomCone":
|
||||
face_points = getPointsFromVertexes(face.Vertexes)
|
||||
|
||||
# We also allow users to select the center of gravity.
|
||||
if _type == "Part::GeomCylinder" or _type == "Part::GeomCone":
|
||||
centerOfG = face.CenterOfGravity - surface.Center
|
||||
centerPoint = surface.Center + centerOfG
|
||||
centerPoint = centerPoint + App.Vector().projectToLine(centerOfG, surface.Axis)
|
||||
face_points.append(centerPoint)
|
||||
else:
|
||||
face_points.append(face.CenterOfGravity)
|
||||
|
||||
closest_vertex_index, closest_vertex_distance = findClosestPointToMousePos(
|
||||
face_points, selection_dict["mouse_pos"]
|
||||
face_points, mousePos
|
||||
)
|
||||
|
||||
if len(center_points) > 0:
|
||||
@@ -202,9 +402,14 @@ def findElementClosestVertex(selection_dict):
|
||||
index = center_points_edge_indexes[closest_center_index] + 1
|
||||
return "Edge" + str(index)
|
||||
|
||||
vertex_name = findVertexNameInObject(
|
||||
face.Vertexes[closest_vertex_index], selection_dict["object"]
|
||||
)
|
||||
if _type == "Part::GeomCylinder" or _type == "Part::GeomCone":
|
||||
return selection_dict["element_name"]
|
||||
|
||||
if closest_vertex_index == len(face.Vertexes):
|
||||
# If center of gravity then we have no vertex name to set so we put element name
|
||||
return selection_dict["element_name"]
|
||||
|
||||
vertex_name = findVertexNameInObject(face.Vertexes[closest_vertex_index], obj)
|
||||
|
||||
return vertex_name
|
||||
|
||||
@@ -247,8 +452,51 @@ def color_from_unsigned(c):
|
||||
|
||||
|
||||
def getJointGroup(assembly):
|
||||
joint_group = assembly.getObject("Joints")
|
||||
joint_group = None
|
||||
|
||||
for obj in assembly.OutList:
|
||||
if obj.TypeId == "Assembly::JointGroup":
|
||||
joint_group = obj
|
||||
break
|
||||
|
||||
if not joint_group:
|
||||
joint_group = assembly.newObject("Assembly::JointGroup", "Joints")
|
||||
|
||||
return joint_group
|
||||
|
||||
|
||||
def isAssemblyGrounded():
|
||||
assembly = activeAssembly()
|
||||
if not assembly:
|
||||
return False
|
||||
|
||||
jointGroup = getJointGroup(assembly)
|
||||
|
||||
for joint in jointGroup.Group:
|
||||
if hasattr(joint, "ObjectToGround"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def removeObjAndChilds(obj):
|
||||
removeObjsAndChilds([obj])
|
||||
|
||||
|
||||
def removeObjsAndChilds(objs):
|
||||
def addsubobjs(obj, toremoveset):
|
||||
if obj.TypeId == "App::Origin": # Origins are already handled
|
||||
return
|
||||
|
||||
toremoveset.add(obj)
|
||||
if obj.TypeId != "App::Link":
|
||||
for subobj in obj.OutList:
|
||||
addsubobjs(subobj, toremoveset)
|
||||
|
||||
toremove = set()
|
||||
for obj in objs:
|
||||
addsubobjs(obj, toremove)
|
||||
|
||||
for obj in toremove:
|
||||
if obj:
|
||||
obj.Document.removeObject(obj.Name)
|
||||
|
||||
Reference in New Issue
Block a user