Assembly: Replace Tangent+Parallel+Planar by 'Distance'.

This commit is contained in:
Paddle
2023-11-14 18:39:09 +01:00
committed by PaddleStroke
parent 1d7671942f
commit 2a3284808f
28 changed files with 2716 additions and 399 deletions

View File

@@ -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()) {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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>

View File

@@ -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)

View File

@@ -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

View File

@@ -67,3 +67,9 @@ INSTALL(
DESTINATION
Mod/Assembly/AssemblyTests
)
INSTALL(
FILES
${AssemblyScripts_SRCS}
DESTINATION
Mod/Assembly/Assembly
)

View File

@@ -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()

View File

@@ -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())

View File

@@ -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

View File

@@ -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:

View File

@@ -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()

View File

@@ -37,6 +37,10 @@
#include <sstream>
#include <iostream>
// Qt
#ifndef __QtAll__
#include <Gui/QtAll.h>
#endif
#endif //_PreComp_

View File

@@ -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>

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View 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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)