Assembly: Add support to external objects. (And various fixes)

This commit is contained in:
Paddle
2023-12-19 09:21:46 +01:00
committed by PaddleStroke
parent 2a3284808f
commit f306515b28
5 changed files with 213 additions and 67 deletions

View File

@@ -27,6 +27,9 @@
#include <BRepAdaptor_Surface.hxx>
#include <TopoDS.hxx>
#include <TopoDS_Face.hxx>
#include <gp_Circ.hxx>
#include <gp_Cylinder.hxx>
#include <gp_Sphere.hxx>
#include <cmath>
#include <vector>
#include <unordered_map>
@@ -36,6 +39,7 @@
#include <App/Document.h>
#include <App/DocumentObjectGroup.h>
#include <App/FeaturePythonPyImp.h>
#include <App/Link.h>
#include <App/PropertyPythonObject.h>
#include <Base/Console.h>
#include <Base/Placement.h>
@@ -444,8 +448,8 @@ std::shared_ptr<ASMTJoint> AssemblyObject::makeMbdJointDistanceEdgeEdge(App::Doc
{
const char* elt1 = getElementFromProp(joint, "Element1");
const char* elt2 = getElementFromProp(joint, "Element2");
auto* obj1 = getLinkedObjFromProp(joint, "Object1");
auto* obj2 = getLinkedObjFromProp(joint, "Object2");
auto* obj1 = getLinkedObjFromNameProp(joint, "Object1", "Part1");
auto* obj2 = getLinkedObjFromNameProp(joint, "Object2", "Part2");
if (isEdgeType(obj1, elt1, GeomAbs_Line) || isEdgeType(obj2, elt2, GeomAbs_Line)) {
if (!isEdgeType(obj1, elt1, GeomAbs_Line)) {
@@ -492,8 +496,8 @@ std::shared_ptr<ASMTJoint> AssemblyObject::makeMbdJointDistanceFaceFace(App::Doc
{
const char* elt1 = getElementFromProp(joint, "Element1");
const char* elt2 = getElementFromProp(joint, "Element2");
auto* obj1 = getLinkedObjFromProp(joint, "Object1");
auto* obj2 = getLinkedObjFromProp(joint, "Object2");
auto* obj1 = getLinkedObjFromNameProp(joint, "Object1", "Part1");
auto* obj2 = getLinkedObjFromNameProp(joint, "Object2", "Part2");
if (isFaceType(obj1, elt1, GeomAbs_Plane) || isFaceType(obj2, elt2, GeomAbs_Plane)) {
if (!isFaceType(obj1, elt1, GeomAbs_Plane)) {
@@ -623,7 +627,7 @@ std::shared_ptr<ASMTJoint>
AssemblyObject::makeMbdJointDistanceFaceVertex(App::DocumentObject* joint)
{
const char* elt1 = getElementFromProp(joint, "Element1");
auto* obj1 = getLinkedObjFromProp(joint, "Object1");
auto* obj1 = getLinkedObjFromNameProp(joint, "Object1", "Part1");
if (isFaceType(obj1, elt1, GeomAbs_Plane)) {
auto mbdJoint = CREATE<ASMTPointInPlaneJoint>::With();
@@ -654,7 +658,7 @@ std::shared_ptr<ASMTJoint>
AssemblyObject::makeMbdJointDistanceEdgeVertex(App::DocumentObject* joint)
{
const char* elt1 = getElementFromProp(joint, "Element1");
auto* obj1 = getLinkedObjFromProp(joint, "Object1");
auto* obj1 = getLinkedObjFromNameProp(joint, "Object1", "Part1");
if (isEdgeType(obj1, elt1, GeomAbs_Line)) { // Point on line joint.
auto mbdJoint = CREATE<ASMTCylSphJoint>::With();
@@ -676,7 +680,7 @@ AssemblyObject::makeMbdJointDistanceEdgeVertex(App::DocumentObject* joint)
std::shared_ptr<ASMTJoint> AssemblyObject::makeMbdJointDistanceFaceEdge(App::DocumentObject* joint)
{
const char* elt2 = getElementFromProp(joint, "Element2");
auto* obj2 = getLinkedObjFromProp(joint, "Object2");
auto* obj2 = getLinkedObjFromNameProp(joint, "Object2", "Part2");
if (isEdgeType(obj2, elt2, GeomAbs_Line)) {
// Make line in plane joint.
@@ -705,16 +709,16 @@ AssemblyObject::makeMbdJoint(App::DocumentObject* joint)
return {};
}
std::string fullMarkerName1 = handleOneSideOfJoint(joint, jointType, "Part1", "Placement1");
std::string fullMarkerName2 = handleOneSideOfJoint(joint, jointType, "Part2", "Placement2");
std::string fullMarkerName1 = handleOneSideOfJoint(joint, "Part1", "Placement1");
std::string fullMarkerName2 = handleOneSideOfJoint(joint, "Part2", "Placement2");
mbdJoint->setMarkerI(fullMarkerName1);
mbdJoint->setMarkerJ(fullMarkerName2);
return {mbdJoint};
}
std::string AssemblyObject::handleOneSideOfJoint(App::DocumentObject* joint,
JointType jointType,
const char* propLinkName,
const char* propPlcName)
{
@@ -843,10 +847,10 @@ void AssemblyObject::swapJCS(App::DocumentObject* joint)
propPlacement1->setValue(propPlacement2->getValue());
propPlacement2->setValue(temp);
}
auto propObject1 = dynamic_cast<App::PropertyLink*>(joint->getPropertyByName("Object1"));
auto propObject2 = dynamic_cast<App::PropertyLink*>(joint->getPropertyByName("Object2"));
auto propObject1 = dynamic_cast<App::PropertyString*>(joint->getPropertyByName("Object1"));
auto propObject2 = dynamic_cast<App::PropertyString*>(joint->getPropertyByName("Object2"));
if (propObject1 && propObject2) {
auto temp = propObject1->getValue();
auto temp = std::string(propObject1->getValue());
propObject1->setValue(propObject2->getValue());
propObject2->setValue(temp);
}
@@ -885,6 +889,10 @@ void AssemblyObject::savePlacementsForUndo()
void AssemblyObject::undoSolve()
{
if (previousPositions.size() == 0) {
return;
}
for (auto& pair : previousPositions) {
App::DocumentObject* obj = pair.first;
if (!obj) {
@@ -1004,7 +1012,7 @@ bool AssemblyObject::isFaceType(App::DocumentObject* obj,
GeomAbs_SurfaceType type)
{
auto base = static_cast<PartApp::Feature*>(obj);
const PartApp::TopoShape& TopShape = base->Shape.getShape();
PartApp::TopoShape TopShape = base->Shape.getShape();
// Check for valid face types
TopoDS_Face face = TopoDS::Face(TopShape.getSubShape(elName));
@@ -1127,10 +1135,41 @@ App::DocumentObject* AssemblyObject::getLinkObjFromProp(App::DocumentObject* joi
return propObj->getValue();
}
App::DocumentObject* AssemblyObject::getLinkedObjFromProp(App::DocumentObject* joint,
const char* propLinkName)
App::DocumentObject* AssemblyObject::getLinkedObjFromNameProp(App::DocumentObject* joint,
const char* pObjName,
const char* pPart)
{
return getLinkObjFromProp(joint, propLinkName)->getLinkedObject(true);
auto* propObjName = dynamic_cast<App::PropertyString*>(joint->getPropertyByName(pObjName));
if (!propObjName) {
return nullptr;
}
std::string objName = std::string(propObjName->getValue());
App::DocumentObject* containingPart = getLinkObjFromProp(joint, pPart);
if (!containingPart) {
return nullptr;
}
if (objName == containingPart->getNameInDocument()) {
return containingPart->getLinkedObject(true);
}
if (containingPart->getTypeId().isDerivedFrom(App::Link::getClassTypeId())) {
App::Link* link = dynamic_cast<App::Link*>(containingPart);
containingPart = link->getLinkedObject(true);
if (!containingPart) {
return nullptr;
}
}
for (auto obj : containingPart->getOutList()) {
if (objName == obj->getNameInDocument()) {
return obj->getLinkedObject(true);
}
}
return nullptr;
}
Base::Placement AssemblyObject::getPlacementFromProp(App::DocumentObject* obj, const char* propName)

View File

@@ -103,7 +103,6 @@ public:
std::shared_ptr<MbD::ASMTJoint> makeMbdJointDistanceFaceFace(App::DocumentObject* joint);
std::string handleOneSideOfJoint(App::DocumentObject* joint,
JointType jointType,
const char* propObjLinkName,
const char* propPlcName);
void jointParts(std::vector<App::DocumentObject*> joints);
@@ -144,7 +143,8 @@ public:
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);
App::DocumentObject*
getLinkedObjFromNameProp(App::DocumentObject* joint, const char* pObjName, const char* pPart);
private:
std::shared_ptr<MbD::ASMTAssembly> mbdAssembly;

View File

@@ -72,7 +72,8 @@ bool ViewProviderAssembly::doubleClicked()
{
if (isInEditMode()) {
// Part is already 'Active' so we exit edit mode.
Gui::Command::doCommand(Gui::Command::Gui, "Gui.activeDocument().resetEdit()");
// Gui::Command::doCommand(Gui::Command::Gui, "Gui.activeDocument().resetEdit()");
Gui::Application::Instance->activeDocument()->resetEdit();
}
else {
// Part is not 'Active' so we enter edit mode to make it so.
@@ -160,6 +161,16 @@ void ViewProviderAssembly::unsetEdit(int ModNum)
partMoving = false;
docsToMove = {};
// Check if the view is still active before trying to deactivate the assembly.
auto activeDoc = Gui::Application::Instance->activeDocument();
if (!activeDoc) {
return;
}
auto activeView = activeDoc->getActiveView();
if (!activeView) {
return;
}
// Set the part as not 'Activated' ie not bold in the tree.
Gui::Command::doCommand(Gui::Command::Gui,
"Gui.ActiveDocument.ActiveView.setActiveObject('%s', None)",

View File

@@ -40,6 +40,7 @@ __url__ = "https://www.freecad.org"
from pivy import coin
import UtilsAssembly
import Preferences
JointTypes = [
QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"),
@@ -73,6 +74,11 @@ JointUsingReverse = [
]
def solveIfAllowed(assembly, storePrev=False):
if Preferences.preferences().GetBool("SolveInJointCreation", True):
assembly.solve(storePrev)
def flipPlacement(plc, localXAxis):
flipRot = App.Rotation(localXAxis, 180)
plc.Rotation = plc.Rotation.multiply(flipRot)
@@ -96,10 +102,10 @@ class Joint:
# First Joint Connector
joint.addProperty(
"App::PropertyLink",
"App::PropertyString", # Not PropertyLink because they don't support external objects
"Object1",
"Joint Connector 1",
QT_TRANSLATE_NOOP("App::Property", "The first object of the joint"),
QT_TRANSLATE_NOOP("App::Property", "The name of the first object of the joint"),
)
joint.addProperty(
@@ -133,12 +139,22 @@ class Joint:
),
)
joint.addProperty(
"App::PropertyBool",
"Detach1",
"Joint Connector 1",
QT_TRANSLATE_NOOP(
"App::Property",
"This prevent Placement1 from recomputing, enabling custom positioning of the placement.",
),
)
# Second Joint Connector
joint.addProperty(
"App::PropertyLink",
"App::PropertyString",
"Object2",
"Joint Connector 2",
QT_TRANSLATE_NOOP("App::Property", "The second object of the joint"),
QT_TRANSLATE_NOOP("App::Property", "The name of the second object of the joint"),
)
joint.addProperty(
@@ -172,6 +188,16 @@ class Joint:
),
)
joint.addProperty(
"App::PropertyBool",
"Detach2",
"Joint Connector 2",
QT_TRANSLATE_NOOP(
"App::Property",
"This prevent Placement2 from recomputing, enabling custom positioning of the placement.",
),
)
joint.addProperty(
"App::PropertyFloat",
"Distance",
@@ -236,7 +262,7 @@ class Joint:
if hasattr(
joint, "Vertex1"
): # during loading the onchanged may be triggered before full init.
self.getAssembly(joint).solve()
solveIfAllowed(self.getAssembly(joint))
def execute(self, fp):
"""Do something when doing a recomputation, this method is mandatory"""
@@ -251,7 +277,7 @@ class Joint:
joint.Part1 = None
joint.FirstPartConnected = assembly.isPartConnected(current_selection[0]["part"])
joint.Object1 = current_selection[0]["object"]
joint.Object1 = current_selection[0]["object"].Name
joint.Part1 = current_selection[0]["part"]
joint.Element1 = current_selection[0]["element_name"]
joint.Vertex1 = current_selection[0]["vertex_name"]
@@ -259,24 +285,24 @@ class Joint:
joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1
)
else:
joint.Object1 = None
joint.Object1 = ""
joint.Part1 = None
joint.Element1 = ""
joint.Vertex1 = ""
joint.Placement1 = App.Placement()
if len(current_selection) >= 2:
joint.Object2 = current_selection[1]["object"]
joint.Object2 = current_selection[1]["object"].Name
joint.Part2 = current_selection[1]["part"]
joint.Element2 = current_selection[1]["element_name"]
joint.Vertex2 = current_selection[1]["vertex_name"]
joint.Placement2 = self.findPlacement(
joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True
)
assembly.solve(True)
solveIfAllowed(assembly, True)
else:
joint.Object2 = None
joint.Object2 = ""
joint.Part2 = None
joint.Element2 = ""
joint.Vertex2 = ""
@@ -284,12 +310,15 @@ class Joint:
assembly.undoSolve()
def updateJCSPlacements(self, joint):
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
)
if not joint.Detach1:
joint.Placement1 = self.findPlacement(
joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1
)
if not joint.Detach2:
joint.Placement2 = self.findPlacement(
joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True
)
"""
So here we want to find a placement that corresponds to a local coordinate system that would be placed at the selected vertex.
@@ -301,7 +330,11 @@ class Joint:
- if elt is a cylindrical face, vtx can also be the center of the arcs of the cylindrical face.
"""
def findPlacement(self, joint, obj, part, elt, vtx, isSecond=False):
def findPlacement(self, joint, objName, part, elt, vtx, isSecond=False):
if not objName or not part:
return App.Placement()
obj = UtilsAssembly.getObjectInPart(objName, part)
assembly = self.getAssembly(joint)
plc = App.Placement()
@@ -430,7 +463,8 @@ class Joint:
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()
solveIfAllowed(self.getAssembly(joint))
def findCylindersIntersection(self, obj, surface, edge, elt_index):
for j, facej in enumerate(obj.Shape.Faces):
@@ -887,7 +921,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self.deactivate()
self.assembly.solve()
solveIfAllowed(self.assembly)
App.closeActiveTransaction()
return True
@@ -974,15 +1008,18 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self.current_selection = []
self.preselection_dict = None
obj1 = UtilsAssembly.getObjectInPart(self.joint.Object1, self.joint.Part1)
obj2 = UtilsAssembly.getObjectInPart(self.joint.Object2, self.joint.Part2)
selection_dict1 = {
"object": self.joint.Object1,
"object": obj1,
"part": self.joint.Part1,
"element_name": self.joint.Element1,
"vertex_name": self.joint.Vertex1,
}
selection_dict2 = {
"object": self.joint.Object2,
"object": obj2,
"part": self.joint.Part2,
"element_name": self.joint.Element2,
"vertex_name": self.joint.Vertex2,
@@ -991,14 +1028,15 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self.current_selection.append(selection_dict1)
self.current_selection.append(selection_dict2)
elName = self.getObjSubNameFromObj(self.joint.Object1, self.joint.Element1)
"""print(
f"Gui.Selection.addSelection('{self.doc.Name}', '{self.joint.Object1.Name}', '{elName}')"
)"""
Gui.Selection.addSelection(self.doc.Name, self.joint.Object1.Name, elName)
elName = self.getObjSubNameFromObj(obj1, self.joint.Element1)
if obj1 != self.joint.Part1:
elName = obj1.Name + "." + elName
Gui.Selection.addSelection(self.doc.Name, self.joint.Part1.Name, elName)
elName = self.getObjSubNameFromObj(self.joint.Object2, self.joint.Element2)
Gui.Selection.addSelection(self.doc.Name, self.joint.Object2.Name, elName)
elName = self.getObjSubNameFromObj(obj2, self.joint.Element2)
if obj2 != self.joint.Part2:
elName = obj2.Name + "." + elName
Gui.Selection.addSelection(self.doc.Name, self.joint.Part2.Name, elName)
self.form.distanceSpinbox.setProperty("rawValue", self.joint.Distance)
self.form.offsetSpinbox.setProperty("rawValue", self.joint.Offset.z)
@@ -1033,7 +1071,6 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
self.form.featureList.clear()
simplified_names = []
for sel in self.current_selection:
# TODO: ideally we probably want to hide the feature name in case of PartDesign bodies. ie body.face12 and not body.pad2.face12
sname = sel["object"].Label
if sel["element_name"] != "":
sname = sname + "." + sel["element_name"]
@@ -1076,7 +1113,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject):
placement = self.joint.Proxy.findPlacement(
self.joint,
self.preselection_dict["object"],
self.preselection_dict["object"].Name,
self.preselection_dict["part"],
self.preselection_dict["element_name"],
self.preselection_dict["vertex_name"],

View File

@@ -90,43 +90,82 @@ def assembly_has_at_least_n_parts(n):
def getObject(full_name):
# 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.
# full_name is "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBox.Edge16"
# or "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBody.pad.Edge16"
# or "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBody.Local_CS.X"
# We want either LinkOrBody or LinkOrBox or Local_CS.
names = full_name.split(".")
doc = App.ActiveDocument
if len(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(names[-2])
prevObj = None
if obj and obj.TypeId == "PartDesign::CoordinateSystem":
return doc.getObject(names[-2])
for i, objName in enumerate(names):
if i == 0:
prevObj = doc.getObject(objName)
if prevObj.TypeId == "App::Link":
prevObj = prevObj.getLinkedObject()
continue
obj = doc.getObject(names[-3]) # So either 'Body', or 'Assembly'
obj = None
if prevObj.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}:
for obji in prevObj.OutList:
if obji.Name == objName:
obj = obji
break
if not obj:
return None
if obj is None:
return None
if obj.TypeId == "PartDesign::Body":
return obj
elif obj.TypeId == "App::Link":
linked_obj = obj.getLinkedObject()
if linked_obj.TypeId == "PartDesign::Body":
# the last is the element name. So if we are at the last but one name, then it must be the selected
if i == len(names) - 2:
return obj
# primitive, fastener, gear ... or link to primitive, fastener, gear...
return doc.getObject(names[-2])
if obj.TypeId == "App::Link":
linked_obj = obj.getLinkedObject()
if linked_obj.TypeId == "PartDesign::Body":
if i + 1 < len(names):
obj2 = doc.getObject(names[i + 1])
if obj2 and obj2.TypeId == "PartDesign::CoordinateSystem":
return obj2
return obj
elif linked_obj.isDerivedFrom("Part::Feature"):
return obj
else:
prevObj = linked_obj
continue
elif obj.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}:
prevObj = obj
continue
elif obj.TypeId == "PartDesign::Body":
if i + 1 < len(names):
obj2 = doc.getObject(names[i + 1])
if obj2 and obj2.TypeId == "PartDesign::CoordinateSystem":
return obj2
return obj
elif obj.isDerivedFrom("Part::Feature"):
# primitive, fastener, gear ...
return obj
return None
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.
if selected_object is None:
App.Console.PrintError("getContainingPart() in UtilsAssembly.py selected_object is None")
return None
names = full_name.split(".")
doc = App.ActiveDocument
if len(names) < 3:
@@ -141,6 +180,9 @@ def getContainingPart(full_name, selected_object):
if not obj:
continue
if obj == selected_object:
return selected_object
if (
obj.TypeId == "PartDesign::Body"
and selected_object.TypeId == "PartDesign::CoordinateSystem"
@@ -156,6 +198,8 @@ def getContainingPart(full_name, selected_object):
elif obj.TypeId == "App::Link":
linked_obj = obj.getLinkedObject()
if linked_obj.TypeId == "App::Part":
# linked_obj_doc = linked_obj.Document
# selected_obj_in_doc = doc.getObject(selected_object.Name)
if linked_obj.hasObject(selected_object, True):
return obj
@@ -163,6 +207,21 @@ def getContainingPart(full_name, selected_object):
return selected_object
def getObjectInPart(objName, part):
if part.Name == objName:
return part
if part.TypeId == "App::Link":
part = part.getLinkedObject()
if part.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}:
for obji in part.OutList:
if obji.Name == objName:
return obji
return None
# 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):