Assembly: Introduce core functionality of assembly workbench.

This commit is contained in:
Paddle
2023-09-20 18:45:47 +02:00
committed by PaddleStroke
parent 13d4cb128a
commit d77cd7acf5
39 changed files with 4229 additions and 391 deletions

View File

@@ -26,6 +26,7 @@ import math
import FreeCAD as App
import Part
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
if App.GuiUp:
@@ -54,8 +55,9 @@ JointTypes = [
class Joint:
def __init__(self, joint, type_index):
self.Type = "Joint"
joint.Proxy = self
self.joint = joint
joint.addProperty(
"App::PropertyEnumeration",
@@ -130,7 +132,18 @@ class Joint:
),
)
self.setJointConnectors([])
self.setJointConnectors(joint, [])
def __getstate__(self):
return self.Type
def __setstate__(self, state):
if state:
self.Type = state
def setJointType(self, joint, jointType):
joint.JointType = jointType
joint.Label = jointType.replace(" ", "")
def onChanged(self, fp, prop):
"""Do something when a property has changed"""
@@ -142,34 +155,34 @@ class Joint:
# App.Console.PrintMessage("Recompute Python Box feature\n")
pass
def setJointConnectors(self, current_selection):
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
if len(current_selection) >= 1:
self.joint.Object1 = current_selection[0]["object"]
self.joint.Element1 = current_selection[0]["element_name"]
self.joint.Vertex1 = current_selection[0]["vertex_name"]
self.joint.Placement1 = self.findPlacement(
self.joint.Object1, self.joint.Element1, self.joint.Vertex1
)
joint.Object1 = current_selection[0]["object"]
joint.Element1 = current_selection[0]["element_name"]
joint.Vertex1 = current_selection[0]["vertex_name"]
joint.Placement1 = self.findPlacement(joint.Object1, joint.Element1, joint.Vertex1)
else:
self.joint.Object1 = None
self.joint.Element1 = ""
self.joint.Vertex1 = ""
self.joint.Placement1 = UtilsAssembly.activeAssembly().Placement
joint.Object1 = None
joint.Element1 = ""
joint.Vertex1 = ""
joint.Placement1 = UtilsAssembly.activeAssembly().Placement
if len(current_selection) >= 2:
self.joint.Object2 = current_selection[1]["object"]
self.joint.Element2 = current_selection[1]["element_name"]
self.joint.Vertex2 = current_selection[1]["vertex_name"]
self.joint.Placement2 = self.findPlacement(
self.joint.Object2, self.joint.Element2, self.joint.Vertex2
)
joint.Object2 = current_selection[1]["object"]
joint.Element2 = current_selection[1]["element_name"]
joint.Vertex2 = current_selection[1]["vertex_name"]
joint.Placement2 = self.findPlacement(joint.Object2, joint.Element2, joint.Vertex2)
else:
self.joint.Object2 = None
self.joint.Element2 = ""
self.joint.Vertex2 = ""
self.joint.Placement2 = UtilsAssembly.activeAssembly().Placement
joint.Object2 = None
joint.Element2 = ""
joint.Vertex2 = ""
joint.Placement2 = UtilsAssembly.activeAssembly().Placement
def updateJCSPlacements(self, joint):
joint.Placement1 = self.findPlacement(joint.Object1, joint.Element1, joint.Vertex1)
joint.Placement2 = self.findPlacement(joint.Object2, joint.Element2, joint.Vertex2)
"""
So here we want to find a placement that corresponds to a local coordinate system that would be placed at the selected vertex.
@@ -182,7 +195,11 @@ class Joint:
"""
def findPlacement(self, obj, elt, vtx):
plc = App.Placement(obj.Placement)
plc = App.Placement()
if not obj or not elt or not vtx:
return App.Placement()
elt_type, elt_index = UtilsAssembly.extract_type_and_number(elt)
vtx_type, vtx_index = UtilsAssembly.extract_type_and_number(vtx)
@@ -234,12 +251,18 @@ class Joint:
if surface.TypeId == "Part::GeomPlane":
plc.Rotation = App.Rotation(surface.Rotation)
# Now plc is the placement in the doc. But we need the placement relative to the solid origin.
return plc
class ViewProviderJoint:
def __init__(self, obj, app_obj):
def __init__(self, vobj):
"""Set this object to the proxy object of the actual view provider"""
vobj.Proxy = self
def attach(self, vobj):
"""Setup the scene sub-graph of the view provider, this method is mandatory"""
self.axis_thickness = 3
view_params = App.ParamGet("User parameter:BaseApp/Preferences/View")
@@ -258,11 +281,8 @@ class ViewProviderJoint:
self.cameraSensor = coin.SoFieldSensor(self.camera_callback, camera)
self.cameraSensor.attach(camera.height)
self.app_obj = app_obj
obj.Proxy = self
self.app_obj = vobj.Object
def attach(self, obj):
"""Setup the scene sub-graph of the view provider, this method is mandatory"""
self.transform1 = coin.SoTransform()
self.transform2 = coin.SoTransform()
self.transform3 = coin.SoTransform()
@@ -275,21 +295,21 @@ class ViewProviderJoint:
self.draw_style.style = coin.SoDrawStyle.LINES
self.draw_style.lineWidth = self.axis_thickness
self.switch_JCS1 = self.JCS_sep(obj, self.transform1)
self.switch_JCS2 = self.JCS_sep(obj, self.transform2)
self.switch_JCS_preview = self.JCS_sep(obj, self.transform3)
self.switch_JCS1 = self.JCS_sep(self.transform1)
self.switch_JCS2 = self.JCS_sep(self.transform2)
self.switch_JCS_preview = self.JCS_sep(self.transform3)
self.display_mode = coin.SoGroup()
self.display_mode.addChild(self.switch_JCS1)
self.display_mode.addChild(self.switch_JCS2)
self.display_mode.addChild(self.switch_JCS_preview)
obj.addDisplayMode(self.display_mode, "Wireframe")
vobj.addDisplayMode(self.display_mode, "Wireframe")
def camera_callback(self, *args):
scaleF = self.get_JCS_size()
self.axisScale.scaleFactor.setValue(scaleF, scaleF, scaleF)
def JCS_sep(self, obj, soTransform):
def JCS_sep(self, soTransform):
pick = coin.SoPickStyle()
pick.style.setValue(coin.SoPickStyle.UNPICKABLE)
@@ -424,21 +444,21 @@ class ViewProviderJoint:
self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
def getIcon(self):
if self.app_obj.getPropertyByName("JointType") == "Fixed":
if self.app_obj.JointType == "Fixed":
return ":/icons/Assembly_CreateJointFixed.svg"
elif self.app_obj.getPropertyByName("JointType") == "Revolute":
elif self.app_obj.JointType == "Revolute":
return ":/icons/Assembly_CreateJointRevolute.svg"
elif self.app_obj.getPropertyByName("JointType") == "Cylindrical":
elif self.app_obj.JointType == "Cylindrical":
return ":/icons/Assembly_CreateJointCylindrical.svg"
elif self.app_obj.getPropertyByName("JointType") == "Slider":
elif self.app_obj.JointType == "Slider":
return ":/icons/Assembly_CreateJointSlider.svg"
elif self.app_obj.getPropertyByName("JointType") == "Ball":
elif self.app_obj.JointType == "Ball":
return ":/icons/Assembly_CreateJointBall.svg"
elif self.app_obj.getPropertyByName("JointType") == "Planar":
elif self.app_obj.JointType == "Planar":
return ":/icons/Assembly_CreateJointPlanar.svg"
elif self.app_obj.getPropertyByName("JointType") == "Parallel":
elif self.app_obj.JointType == "Parallel":
return ":/icons/Assembly_CreateJointParallel.svg"
elif self.app_obj.getPropertyByName("JointType") == "Tangent":
elif self.app_obj.JointType == "Tangent":
return ":/icons/Assembly_CreateJointTangent.svg"
return ":/icons/Assembly_CreateJoint.svg"
@@ -453,3 +473,374 @@ class ViewProviderJoint:
"""When restoring the serialized object from document we have the chance to set some internals here.\
Since no data were serialized nothing needs to be done here."""
return None
def doubleClicked(self, vobj):
panel = TaskAssemblyCreateJoint(0, vobj.Object)
Gui.Control.showDialog(panel)
################ Grounded Joint object #################
class GroundedJoint:
def __init__(self, joint, obj_to_ground):
self.Type = "GoundedJoint"
joint.Proxy = self
self.joint = joint
joint.addProperty(
"App::PropertyLink",
"ObjectToGround",
"Ground",
QT_TRANSLATE_NOOP("App::Property", "The object to ground"),
)
joint.ObjectToGround = obj_to_ground
joint.addProperty(
"App::PropertyPlacement",
"Placement",
"Ground",
QT_TRANSLATE_NOOP(
"App::Property",
"This is where the part is grounded.",
),
)
joint.Placement = obj_to_ground.Placement
def __getstate__(self):
return self.Type
def __setstate__(self, state):
if state:
self.Type = state
def onChanged(self, fp, prop):
"""Do something when a property has changed"""
# App.Console.PrintMessage("Change property: " + str(prop) + "\n")
pass
def execute(self, fp):
"""Do something when doing a recomputation, this method is mandatory"""
# App.Console.PrintMessage("Recompute Python Box feature\n")
pass
class ViewProviderGroundedJoint:
def __init__(self, obj):
"""Set this object to the proxy object of the actual view provider"""
obj.Proxy = self
def attach(self, obj):
"""Setup the scene sub-graph of the view provider, this method is mandatory"""
pass
def updateData(self, fp, 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
pass
def getDisplayModes(self, obj):
"""Return a list of display modes."""
modes = ["Wireframe"]
return modes
def getDefaultDisplayMode(self):
"""Return the name of the default display mode. It must be defined in getDisplayModes."""
return "Wireframe"
def onChanged(self, vp, prop):
"""Here we can do something when a single property got changed"""
# App.Console.PrintMessage("Change property: " + str(prop) + "\n")
pass
def getIcon(self):
return ":/icons/Assembly_ToggleGrounded.svg"
class MakeJointSelGate:
def __init__(self, taskbox, assembly):
self.taskbox = taskbox
self.assembly = assembly
def allow(self, doc, obj, sub):
if not sub:
return False
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.
return False
if Gui.Selection.isSelected(obj, sub, Gui.Selection.ResolveMode.NoResolve):
# If it's to deselect then it's ok
return True
if len(self.taskbox.current_selection) >= 2:
# No more than 2 elements can be selected for basic joints.
return False
full_obj_name = ".".join(objs_names)
full_element_name = full_obj_name + "." + element_name
selected_object = UtilsAssembly.getObject(full_element_name)
for selection_dict in self.taskbox.current_selection:
if selection_dict["object"] == selected_object:
# Can't join a solid to itself. So the user need to select 2 different parts.
return False
return True
class TaskAssemblyCreateJoint(QtCore.QObject):
def __init__(self, jointTypeIndex, jointObj=None):
super().__init__()
self.assembly = UtilsAssembly.activeAssembly()
self.view = Gui.activeDocument().activeView()
self.doc = App.ActiveDocument
if not self.assembly or not self.view or not self.doc:
return
self.assembly.ViewObject.EnableMovement = False
self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateJoint.ui")
self.form.jointType.addItems(JointTypes)
self.form.jointType.setCurrentIndex(jointTypeIndex)
self.form.jointType.currentIndexChanged.connect(self.onJointTypeChanged)
Gui.Selection.clearSelection()
if jointObj:
self.joint = jointObj
self.jointName = jointObj.Label
App.setActiveTransaction("Edit " + self.jointName + " Joint")
self.updateTaskboxFromJoint()
else:
self.jointName = self.form.jointType.currentText().replace(" ", "")
App.setActiveTransaction("Create " + self.jointName + " Joint")
self.current_selection = []
self.preselection_dict = None
self.createJointObject()
Gui.Selection.addSelectionGate(
MakeJointSelGate(self, self.assembly), Gui.Selection.ResolveMode.NoResolve
)
Gui.Selection.addObserver(self, Gui.Selection.ResolveMode.NoResolve)
Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.GreedySelection)
self.callbackMove = self.view.addEventCallback("SoLocation2Event", self.moveMouse)
self.callbackKey = self.view.addEventCallback("SoKeyboardEvent", self.KeyboardEvent)
def accept(self):
if len(self.current_selection) != 2:
App.Console.PrintWarning("You need to select 2 elements from 2 separate parts.")
return False
# Hide JSC's when joint is created and enable selection highlighting
# self.joint.ViewObject.Visibility = False
# self.joint.ViewObject.OnTopWhenSelected = "Enabled"
self.deactivate()
self.assembly.solve()
App.closeActiveTransaction()
return True
def reject(self):
self.deactivate()
App.closeActiveTransaction(True)
return True
def deactivate(self):
self.assembly.ViewObject.EnableMovement = True
Gui.Selection.removeSelectionGate()
Gui.Selection.removeObserver(self)
Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.NormalSelection)
Gui.Selection.clearSelection()
self.view.removeEventCallback("SoLocation2Event", self.callbackMove)
self.view.removeEventCallback("SoKeyboardEvent", self.callbackKey)
if Gui.Control.activeDialog():
Gui.Control.closeDialog()
def createJointObject(self):
type_index = self.form.jointType.currentIndex()
joint_group = UtilsAssembly.getJointGroup(self.assembly)
self.joint = joint_group.newObject("App::FeaturePython", self.jointName)
Joint(self.joint, type_index)
ViewProviderJoint(self.joint.ViewObject)
def onJointTypeChanged(self, index):
self.joint.Proxy.setJointType(self.joint, self.form.jointType.currentText())
def updateTaskboxFromJoint(self):
self.current_selection = []
self.preselection_dict = None
selection_dict1 = {
"object": self.joint.Object1,
"element_name": self.joint.Element1,
"vertex_name": self.joint.Vertex1,
}
selection_dict2 = {
"object": self.joint.Object2,
"element_name": self.joint.Element2,
"vertex_name": self.joint.Vertex2,
}
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(self.joint.Object2, self.joint.Element2)
Gui.Selection.addSelection(self.doc.Name, self.joint.Object2.Name, elName)
self.updateJointList()
def getObjSubNameFromObj(self, obj, elName):
if obj.TypeId == "PartDesign::Body":
return obj.Tip.Name + "." + elName
elif obj.TypeId == "App::Link":
linked_obj = obj.getLinkedObject()
if linked_obj.TypeId == "PartDesign::Body":
return linked_obj.Tip.Name + "." + elName
else:
return elName
else:
return elName
def updateJoint(self):
# First we build the listwidget
self.updateJointList()
# Then we pass the new list to the join object
self.joint.Proxy.setJointConnectors(self.joint, self.current_selection)
def updateJointList(self):
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 + "." + 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"]
):
self.joint.ViewObject.Proxy.showPreviewJCS(False)
return
cursor_pos = self.view.getCursorPos()
cursor_info = self.view.getObjectInfo(cursor_pos)
# cursor_info example {'x': 41.515, 'y': 7.449, 'z': 16.861, 'ParentObject': <Part object>, 'SubName': 'Body002.Pad.Face5', 'Document': 'part3', 'Object': 'Pad', 'Component': 'Face5'}
if (
not cursor_info
or not self.preselection_dict
or cursor_info["SubName"] != self.preselection_dict["sub_name"]
):
self.joint.ViewObject.Proxy.showPreviewJCS(False)
return
# newPos = self.view.getPoint(*info["Position"]) # This is not what we want, it's not pos on the object but on the focal plane
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
)
placement = self.joint.Proxy.findPlacement(
self.preselection_dict["object"],
self.preselection_dict["element_name"],
self.preselection_dict["vertex_name"],
)
self.joint.ViewObject.Proxy.showPreviewJCS(True, placement)
self.previewJCSVisible = True
# 3D view keyboard handler
def KeyboardEvent(self, info):
if info["State"] == "UP" and info["Key"] == "ESCAPE":
self.reject()
if info["State"] == "UP" and info["Key"] == "RETURN":
self.accept()
# selectionObserver stuff
def addSelection(self, doc_name, obj_name, sub_name, mousePos):
full_obj_name = UtilsAssembly.getFullObjName(obj_name, sub_name)
full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
selected_object = UtilsAssembly.getObject(full_element_name)
element_name = UtilsAssembly.getElementName(full_element_name)
selection_dict = {
"object": 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)
self.current_selection.append(selection_dict)
self.updateJoint()
def removeSelection(self, doc_name, obj_name, sub_name, mousePos=None):
full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
selected_object = UtilsAssembly.getObject(full_element_name)
element_name = UtilsAssembly.getElementName(full_element_name)
# 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:
selection_dict_to_remove = selection_dict
break
if selection_dict_to_remove is not None:
self.current_selection.remove(selection_dict_to_remove)
self.updateJoint()
def setPreselection(self, doc_name, obj_name, sub_name):
if not sub_name:
self.preselection_dict = None
return
full_obj_name = UtilsAssembly.getFullObjName(obj_name, sub_name)
full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
selected_object = UtilsAssembly.getObject(full_element_name)
element_name = UtilsAssembly.getElementName(full_element_name)
self.preselection_dict = {
"object": selected_object,
"sub_name": sub_name,
"element_name": element_name,
"full_element_name": full_element_name,
"full_obj_name": full_obj_name,
}
def clearSelection(self, doc_name):
self.current_selection.clear()
self.updateJoint()