Assembly : Initial implementation of 'create joint' command.

This commit is contained in:
Paddle
2023-09-14 16:18:28 +02:00
parent c0185ad95c
commit 04a951aeb3
9 changed files with 1165 additions and 6 deletions

View File

@@ -8,7 +8,9 @@ set(Assembly_Scripts
Init.py
CommandCreateAssembly.py
CommandInsertLink.py
CommandCreateJoint.py
TestAssemblyWorkbench.py
JointObject.py
Preferences.py
AssemblyImport.py
UtilsAssembly.py

View File

@@ -59,6 +59,7 @@ class CommandCreateAssembly:
assembly = App.ActiveDocument.addObject("App::Part", "Assembly")
assembly.Type = "Assembly"
Gui.ActiveDocument.ActiveView.setActiveObject("part", assembly)
assembly.newObject("App::DocumentObjectGroup", "Joints")
App.closeActiveTransaction()

View File

@@ -0,0 +1,505 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# /****************************************************************************
# *
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
# *
# This file is part of FreeCAD. *
# *
# FreeCAD is free software: you can redistribute it and/or modify it *
# under the terms of the GNU Lesser General Public License as *
# published by the Free Software Foundation, either version 2.1 of the *
# License, or (at your option) any later version. *
# *
# FreeCAD is distributed in the hope that it will be useful, but *
# WITHOUT ANY WARRANTY; without even the implied warranty of *
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
# Lesser General Public License for more details. *
# *
# You should have received a copy of the GNU Lesser General Public *
# License along with FreeCAD. If not, see *
# <https://www.gnu.org/licenses/>. *
# *
# ***************************************************************************/
import os
import FreeCAD as App
from PySide.QtCore import QT_TRANSLATE_NOOP
if App.GuiUp:
import FreeCADGui as Gui
from PySide import QtCore, QtGui, QtWidgets
import JointObject
import UtilsAssembly
import Assembly_rc
# translate = App.Qt.translate
__title__ = "Assembly Commands to Create Joints"
__author__ = "Ondsel"
__url__ = "https://www.freecad.org"
class CommandCreateJointFixed:
def __init__(self):
pass
def GetResources(self):
return {
"Pixmap": "Assembly_CreateJointFixed",
"MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointFixed", "Create Fixed Joint"),
"Accel": "F",
"ToolTip": QT_TRANSLATE_NOOP(
"Assembly_CreateJointFixed",
"<p>Create a Fixed Joint: Permanently locks two parts together, preventing any movement or rotation.</p>",
),
"CmdType": "ForEdit",
}
def IsActive(self):
return UtilsAssembly.activeAssembly() is not None
def Activated(self):
assembly = UtilsAssembly.activeAssembly()
if not assembly:
return
view = Gui.activeDocument().activeView()
self.panel = TaskAssemblyCreateJoint(assembly, view, 0)
Gui.Control.showDialog(self.panel)
class CommandCreateJointRevolute:
def __init__(self):
pass
def GetResources(self):
return {
"Pixmap": "Assembly_CreateJointRevolute",
"MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointRevolute", "Create Revolute Joint"),
"Accel": "R",
"ToolTip": QT_TRANSLATE_NOOP(
"Assembly_CreateJointRevolute",
"<p>Create a Revolute Joint: Allows rotation around a single axis between selected parts.</p>",
),
"CmdType": "ForEdit",
}
def IsActive(self):
return UtilsAssembly.activeAssembly() is not None
def Activated(self):
assembly = UtilsAssembly.activeAssembly()
if not assembly:
return
view = Gui.activeDocument().activeView()
self.panel = TaskAssemblyCreateJoint(assembly, view, 1)
Gui.Control.showDialog(self.panel)
class CommandCreateJointCylindrical:
def __init__(self):
pass
def GetResources(self):
return {
"Pixmap": "Assembly_CreateJointCylindrical",
"MenuText": QT_TRANSLATE_NOOP(
"Assembly_CreateJointCylindrical", "Create Cylindrical Joint"
),
"Accel": "C",
"ToolTip": QT_TRANSLATE_NOOP(
"Assembly_CreateJointCylindrical",
"<p>Create a Cylindrical Joint: Enables rotation along one axis while permitting movement along the same axis between assembled parts.</p>",
),
"CmdType": "ForEdit",
}
def IsActive(self):
return UtilsAssembly.activeAssembly() is not None
def Activated(self):
assembly = UtilsAssembly.activeAssembly()
if not assembly:
return
view = Gui.activeDocument().activeView()
self.panel = TaskAssemblyCreateJoint(assembly, view, 2)
Gui.Control.showDialog(self.panel)
class CommandCreateJointSlider:
def __init__(self):
pass
def GetResources(self):
return {
"Pixmap": "Assembly_CreateJointSlider",
"MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointSlider", "Create Slider Joint"),
"Accel": "S",
"ToolTip": QT_TRANSLATE_NOOP(
"Assembly_CreateJointSlider",
"<p>Create a Slider Joint: Allows linear movement along a single axis but restricts rotation between selected parts.</p>",
),
"CmdType": "ForEdit",
}
def IsActive(self):
return UtilsAssembly.activeAssembly() is not None
def Activated(self):
assembly = UtilsAssembly.activeAssembly()
if not assembly:
return
view = Gui.activeDocument().activeView()
self.panel = TaskAssemblyCreateJoint(assembly, view, 3)
Gui.Control.showDialog(self.panel)
class CommandCreateJointBall:
def __init__(self):
pass
def GetResources(self):
return {
"Pixmap": "Assembly_CreateJointBall",
"MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointBall", "Create Ball Joint"),
"Accel": "B",
"ToolTip": QT_TRANSLATE_NOOP(
"Assembly_CreateJointBall",
"<p>Create a Ball Joint: Connects parts at a point, allowing unrestricted movement as long as the connection points remain in contact.</p>",
),
"CmdType": "ForEdit",
}
def IsActive(self):
return UtilsAssembly.activeAssembly() is not None
def Activated(self):
assembly = UtilsAssembly.activeAssembly()
if not assembly:
return
view = Gui.activeDocument().activeView()
self.panel = TaskAssemblyCreateJoint(assembly, view, 4)
Gui.Control.showDialog(self.panel)
class CommandCreateJointPlanar:
def __init__(self):
pass
def GetResources(self):
return {
"Pixmap": "Assembly_CreateJointPlanar",
"MenuText": QT_TRANSLATE_NOOP("Assembly_CreateJointPlanar", "Create Planar Joint"),
"Accel": "P",
"ToolTip": QT_TRANSLATE_NOOP(
"Assembly_CreateJointPlanar",
"<p>Create a Planar Joint: Ensures two selected features are in the same plane, restricting movement to that plane.</p>",
),
"CmdType": "ForEdit",
}
def IsActive(self):
return UtilsAssembly.activeAssembly() is not None
def Activated(self):
assembly = UtilsAssembly.activeAssembly()
if not assembly:
return
view = Gui.activeDocument().activeView()
self.panel = TaskAssemblyCreateJoint(assembly, view, 5)
Gui.Control.showDialog(self.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": QT_TRANSLATE_NOOP(
"Assembly_CreateJointParallel",
"<p>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):
assembly = UtilsAssembly.activeAssembly()
if not assembly:
return
view = Gui.activeDocument().activeView()
self.panel = TaskAssemblyCreateJoint(assembly, view, 6)
Gui.Control.showDialog(self.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": QT_TRANSLATE_NOOP(
"Assembly_CreateJointTangent",
"<p>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):
assembly = UtilsAssembly.activeAssembly()
if not assembly:
return
view = Gui.activeDocument().activeView()
self.panel = TaskAssemblyCreateJoint(assembly, view, 7)
Gui.Control.showDialog(self.panel)
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)
for selection_dict in self.taskbox.current_selection:
if selection_dict["full_obj_name"] == full_obj_name:
# 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, assembly, view, jointTypeIndex):
super().__init__()
self.assembly = assembly
self.view = view
self.doc = App.ActiveDocument
self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateJoint.ui")
self.form.jointType.addItems(JointObject.JointTypes)
self.form.jointType.setCurrentIndex(jointTypeIndex)
Gui.Selection.clearSelection()
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.current_selection = []
self.preselection_dict = None
self.callbackMove = self.view.addEventCallback("SoLocation2Event", self.moveMouse)
self.callbackKey = self.view.addEventCallback("SoKeyboardEvent", self.KeyboardEvent)
App.setActiveTransaction("Create joint")
self.createJointObject()
def accept(self):
if len(self.current_selection) != 2:
App.Console.PrintWarning("You need to select 2 elements from 2 separate parts.")
return False
self.deactivate()
App.closeActiveTransaction()
return True
def reject(self):
self.deactivate()
App.closeActiveTransaction(True)
return True
def deactivate(self):
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 = self.assembly.getObject("Joints")
if not joint_group:
joint_group = self.assembly.newObject("App::DocumentObjectGroup", "Joints")
self.joint = joint_group.newObject("App::FeaturePython", "Joint")
JointObject.Joint(self.joint, type_index)
JointObject.ViewProviderJoint(self.joint.ViewObject, self.joint)
def updateJoint(self):
# First we build the listwidget
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["full_element_name"].split(self.assembly.Name + ".", 1)[-1]
simplified_names.append(sname)
self.form.featureList.addItems(simplified_names)
# Then we pass the new list to the join object
self.joint.Proxy.setJointConnectors(self.current_selection)
def moveMouse(self, info):
if len(self.current_selection) >= 2 or (
len(self.current_selection) == 1
and self.current_selection[0]["full_element_name"]
== self.preselection_dict["full_element_name"]
):
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)
# 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["full_element_name"] == full_element_name:
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()
if App.GuiUp:
Gui.addCommand("Assembly_CreateJointFixed", CommandCreateJointFixed())
Gui.addCommand("Assembly_CreateJointRevolute", CommandCreateJointRevolute())
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())

View File

@@ -31,6 +31,7 @@ if App.GuiUp:
from PySide import QtCore, QtGui, QtWidgets
import UtilsAssembly
# translate = App.Qt.translate
__title__ = "Assembly Command Insert Link"
@@ -120,9 +121,17 @@ class TaskAssemblyInsertLink(QtCore.QObject):
if UtilsAssembly.isDocTemporary(doc):
continue
# Build list of current assembly's parents, including the current assembly itself
parents = self.assembly.Parents
if parents:
root_parent, sub = parents[0]
parents_names, _ = UtilsAssembly.getObjsNamesAndElement(root_parent.Name, sub)
else:
parents_names = [self.assembly.Name]
for obj in doc.findObjects("App::Part"):
# we don't want to link to itself
if obj != self.assembly:
# we don't want to link to itself or parents.
if obj.Name not in parents_names:
self.allParts.append(obj)
self.partsDoc.append(doc)

View File

@@ -2,8 +2,6 @@
<qresource>
<file>icons/Assembly_InsertLink.svg</file>
<file>icons/preferences-assembly.svg</file>
<file>panels/TaskAssemblyInsertLink.ui</file>
<file>preferences/Assembly.ui</file>
<file>icons/Assembly_CreateJointBall.svg</file>
<file>icons/Assembly_CreateJointCylindrical.svg</file>
<file>icons/Assembly_CreateJointFixed.svg</file>
@@ -12,5 +10,8 @@
<file>icons/Assembly_CreateJointRevolute.svg</file>
<file>icons/Assembly_CreateJointSlider.svg</file>
<file>icons/Assembly_CreateJointTangent.svg</file>
<file>panels/TaskAssemblyCreateJoint.ui</file>
<file>panels/TaskAssemblyInsertLink.ui</file>
<file>preferences/Assembly.ui</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TaskAssemblyCreateJoint</class>
<widget class="QWidget" name="TaskAssemblyCreateJoint">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>376</width>
<height>387</height>
</rect>
</property>
<property name="windowTitle">
<string>Create Joint</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QComboBox" name="jointType"/>
</item>
<item row="1" column="0">
<widget class="QListWidget" name="featureList"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -65,7 +65,7 @@ class AssemblyWorkbench(Workbench):
# load the builtin modules
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
import CommandCreateAssembly, CommandInsertLink
import CommandCreateAssembly, CommandInsertLink, CommandCreateJoint
from Preferences import PreferencesPage
# from Preferences import preferences
@@ -77,12 +77,23 @@ class AssemblyWorkbench(Workbench):
# build commands list
cmdlist = ["Assembly_CreateAssembly", "Assembly_InsertLink"]
cmdListJoints = [
"Assembly_CreateJointFixed",
"Assembly_CreateJointRevolute",
"Assembly_CreateJointCylindrical",
"Assembly_CreateJointSlider",
"Assembly_CreateJointBall",
"Assembly_CreateJointPlanar",
"Assembly_CreateJointParallel",
"Assembly_CreateJointTangent",
]
self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Assembly"), cmdlist)
self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Assembly Joints"), cmdListJoints)
self.appendMenu(
[QT_TRANSLATE_NOOP("Workbench", "&Assembly")],
cmdlist + ["Separator"],
cmdlist + ["Separator"] + cmdListJoints,
)
print("Assembly workbench loaded")

View File

@@ -0,0 +1,421 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# /****************************************************************************
# *
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
# *
# This file is part of FreeCAD. *
# *
# FreeCAD is free software: you can redistribute it and/or modify it *
# under the terms of the GNU Lesser General Public License as *
# published by the Free Software Foundation, either version 2.1 of the *
# License, or (at your option) any later version. *
# *
# FreeCAD is distributed in the hope that it will be useful, but *
# WITHOUT ANY WARRANTY; without even the implied warranty of *
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
# Lesser General Public License for more details. *
# *
# You should have received a copy of the GNU Lesser General Public *
# License along with FreeCAD. If not, see *
# <https://www.gnu.org/licenses/>. *
# *
# ***************************************************************************/
import FreeCAD as App
import Part
from PySide.QtCore import QT_TRANSLATE_NOOP
if App.GuiUp:
import FreeCADGui as Gui
# translate = App.Qt.translate
__title__ = "Assembly Joint object"
__author__ = "Ondsel"
__url__ = "https://www.freecad.org"
from pivy import coin
import UtilsAssembly
JointTypes = [
QT_TRANSLATE_NOOP("AssemblyJoint", "Fixed"),
QT_TRANSLATE_NOOP("AssemblyJoint", "Revolute"),
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"),
]
class Joint:
def __init__(self, joint, type_index):
joint.Proxy = self
self.joint = joint
joint.addProperty(
"App::PropertyEnumeration",
"JointType",
"Joint",
QT_TRANSLATE_NOOP("App::Property", "The type of the joint"),
)
joint.JointType = JointTypes # sets the list
joint.JointType = JointTypes[type_index] # set the initial value
# First Joint Connector
joint.addProperty(
"App::PropertyLink",
"Object1",
"Joint Connector 1",
QT_TRANSLATE_NOOP("App::Property", "The first object of the joint"),
)
joint.addProperty(
"App::PropertyString",
"Element1",
"Joint Connector 1",
QT_TRANSLATE_NOOP("App::Property", "The selected element of the first object"),
)
joint.addProperty(
"App::PropertyString",
"Vertex1",
"Joint Connector 1",
QT_TRANSLATE_NOOP("App::Property", "The selected vertex of the first object"),
)
joint.addProperty(
"App::PropertyPlacement",
"Placement1",
"Joint Connector 1",
QT_TRANSLATE_NOOP(
"App::Property",
"This is the local coordinate system within the object1 that will be used to joint.",
),
)
# Second Joint Connector
joint.addProperty(
"App::PropertyLink",
"Object2",
"Joint Connector 2",
QT_TRANSLATE_NOOP("App::Property", "The second object of the joint"),
)
joint.addProperty(
"App::PropertyString",
"Element2",
"Joint Connector 2",
QT_TRANSLATE_NOOP("App::Property", "The selected element of the second object"),
)
joint.addProperty(
"App::PropertyString",
"Vertex2",
"Joint Connector 2",
QT_TRANSLATE_NOOP("App::Property", "The selected vertex of the second object"),
)
joint.addProperty(
"App::PropertyPlacement",
"Placement2",
"Joint Connector 2",
QT_TRANSLATE_NOOP(
"App::Property",
"This is the local coordinate system within the object2 that will be used to joint.",
),
)
self.setJointConnectors([])
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
def setJointConnectors(self, 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
)
else:
self.joint.Object1 = None
self.joint.Element1 = ""
self.joint.Vertex1 = ""
self.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
)
else:
self.joint.Object2 = None
self.joint.Element2 = ""
self.joint.Vertex2 = ""
self.joint.Placement2 = UtilsAssembly.activeAssembly().Placement
"""
So here we want to find a placement that corresponds to a local coordinate system that would be placed at the selected vertex.
- obj is usually a App::Link to a PartDesign::Body, or primitive, fasteners. But can also be directly the object.1
- elt can be a face, an edge or a vertex.
- If elt is a vertex, then vtx = elt And placement is vtx coordinates without rotation.
- if elt is an edge, then vtx = edge start/end vertex depending on which is closer. If elt is an arc or circle, vtx can also be the center. The rotation is the plane normal to the line positioned at vtx. Or for arcs/circle, the plane of the arc.
- if elt is a plane face, vtx is the face vertex (to the list of vertex we need to add arc/circle centers) the closer to the mouse. The placement is the plane rotation positioned at vtx
- 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):
plc = App.Placement(obj.Placement)
elt_type, elt_index = UtilsAssembly.extract_type_and_number(elt)
vtx_type, vtx_index = UtilsAssembly.extract_type_and_number(vtx)
if elt_type == "Vertex":
vertex = obj.Shape.Vertexes[elt_index - 1]
plc.Base = (vertex.X, vertex.Y, vertex.Z)
elif elt_type == "Edge":
edge = obj.Shape.Edges[elt_index - 1]
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 curve.TypeId == "Part::GeomCircle":
center_point = curve.Location
plc.Base = (center_point.x, center_point.y, center_point.z)
else:
vertex = obj.Shape.Vertexes[vtx_index - 1]
plc.Base = (vertex.X, vertex.Y, vertex.Z)
# Then we find the Rotation
if curve.TypeId == "Part::GeomCircle":
plc.Rotation = App.Rotation(curve.Rotation)
if curve.TypeId == "Part::GeomLine":
plane_normal = curve.Direction
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]
# 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.
circleOrArc = face.Edges[vtx_index - 1]
curve = circleOrArc.Curve
if curve.TypeId == "Part::GeomCircle":
center_point = curve.Location
plc.Base = (center_point.x, center_point.y, center_point.z)
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)
return plc
class ViewProviderJoint:
def __init__(self, obj, app_obj):
"""Set this object to the proxy object of the actual view provider"""
obj.addProperty(
"App::PropertyColor", "color_X_axis", "JCS", "Joint coordinate system X axis color"
)
obj.addProperty(
"App::PropertyColor", "color_Y_axis", "JCS", "Joint coordinate system Y axis color"
)
obj.addProperty(
"App::PropertyColor", "color_Z_axis", "JCS", "Joint coordinate system Z axis color"
)
obj.addProperty(
"App::PropertyInteger",
"axis_thickness",
"JCS",
"Joint cordinate system X axis thickness",
)
obj.color_X_axis = (1.0, 0.0, 0.0)
obj.color_Y_axis = (0.0, 1.0, 0.0)
obj.color_Z_axis = (0.0, 0.0, 1.0)
obj.axis_thickness = 2
self.x_axis_so_color = coin.SoBaseColor()
self.x_axis_so_color.rgb.setValue(1.0, 0.0, 0.0)
self.y_axis_so_color = coin.SoBaseColor()
self.y_axis_so_color.rgb.setValue(0.0, 1.0, 0.0)
self.z_axis_so_color = coin.SoBaseColor()
self.z_axis_so_color.rgb.setValue(0.0, 0.0, 1.0)
self.app_obj = app_obj
obj.Proxy = self
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()
scaleF = self.get_JCS_size()
self.axisScale = coin.SoScale()
self.axisScale.scaleFactor.setValue(scaleF, scaleF, scaleF)
self.draw_style = coin.SoDrawStyle()
self.draw_style.style = coin.SoDrawStyle.LINES
self.draw_style.lineWidth = obj.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.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")
def JCS_sep(self, obj, soTransform):
pick = coin.SoPickStyle()
pick.style.setValue(coin.SoPickStyle.UNPICKABLE)
JCS = coin.SoAnnotation()
JCS.addChild(soTransform)
JCS.addChild(pick)
X_axis_sep = self.line_sep([0, 0, 0], [1, 0, 0], self.x_axis_so_color)
Y_axis_sep = self.line_sep([0, 0, 0], [0, 1, 0], self.y_axis_so_color)
Z_axis_sep = self.line_sep([0, 0, 0], [0, 0, 1], self.z_axis_so_color)
JCS.addChild(X_axis_sep)
JCS.addChild(Y_axis_sep)
JCS.addChild(Z_axis_sep)
switch_JCS = coin.SoSwitch()
switch_JCS.addChild(JCS)
switch_JCS.whichChild = coin.SO_SWITCH_NONE
return switch_JCS
def line_sep(self, startPoint, endPoint, soColor):
line = coin.SoLineSet()
line.numVertices.setValue(2)
coords = coin.SoCoordinate3()
coords.point.setValues(0, [startPoint, endPoint])
axis_sep = coin.SoAnnotation()
axis_sep.addChild(self.axisScale)
axis_sep.addChild(self.draw_style)
axis_sep.addChild(soColor)
axis_sep.addChild(coords)
axis_sep.addChild(line)
return axis_sep
def get_JCS_size(self):
camera = Gui.ActiveDocument.ActiveView.getCameraNode()
if not camera:
return 10
return camera.height.getValue() / 10
def set_JCS_placement(self, soTransform, placement):
t = placement.Base
soTransform.translation.setValue(t.x, t.y, t.z)
r = placement.Rotation.Q
soTransform.rotation.setValue(r[0], r[1], r[2], r[3])
def updateData(self, 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
if prop == "Placement1":
plc = fp.getPropertyByName("Placement1")
if fp.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"):
self.switch_JCS2.whichChild = coin.SO_SWITCH_ALL
self.set_JCS_placement(self.transform2, plc)
else:
self.switch_JCS2.whichChild = coin.SO_SWITCH_NONE
def showPreviewJCS(self, visible, placement=None):
if visible:
self.switch_JCS_preview.whichChild = coin.SO_SWITCH_ALL
self.set_JCS_placement(self.transform3, placement)
else:
self.switch_JCS_preview.whichChild = coin.SO_SWITCH_NONE
def getDisplayModes(self, obj):
"""Return a list of display modes."""
modes = []
modes.append("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")
if prop == "color_X_axis":
c = vp.getPropertyByName("color_X_axis")
self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
if prop == "color_Y_axis":
c = vp.getPropertyByName("color_Y_axis")
self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
if prop == "color_Z_axis":
c = vp.getPropertyByName("color_Z_axis")
self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
def getIcon(self):
if self.app_obj.getPropertyByName("JointType") == "Fixed":
return ":/icons/Assembly_CreateJointFixed.svg"
elif self.app_obj.getPropertyByName("JointType") == "Revolute":
return ":/icons/Assembly_CreateJointRevolute.svg"
elif self.app_obj.getPropertyByName("JointType") == "Cylindrical":
return ":/icons/Assembly_CreateJointCylindrical.svg"
elif self.app_obj.getPropertyByName("JointType") == "Slider":
return ":/icons/Assembly_CreateJointSlider.svg"
elif self.app_obj.getPropertyByName("JointType") == "Ball":
return ":/icons/Assembly_CreateJointBall.svg"
elif self.app_obj.getPropertyByName("JointType") == "Planar":
return ":/icons/Assembly_CreateJointPlanar.svg"
elif self.app_obj.getPropertyByName("JointType") == "Parallel":
return ":/icons/Assembly_CreateJointParallel.svg"
elif self.app_obj.getPropertyByName("JointType") == "Tangent":
return ":/icons/Assembly_CreateJointTangent.svg"
return ":/icons/Assembly_CreateJoint.svg"
def __getstate__(self):
"""When saving the document this object gets stored using Python's json module.\
Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\
to return a tuple of all serializable objects or None."""
return None
def __setstate__(self, state):
"""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

View File

@@ -54,3 +54,185 @@ def isDocTemporary(doc):
except AttributeError:
temp = False
return temp
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(".")
doc = App.ActiveDocument
if len(parts) < 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'
if not obj:
return None
if obj.TypeId == "PartDesign::Body":
return obj
elif obj.TypeId == "App::Link":
linked_obj = obj.getLinkedObject()
if linked_obj.TypeId == "PartDesign::Body":
return obj
else: # primitive, fastener, gear ... or link to primitive, fastener, gear...
return doc.getObject(parts[-2])
def getElementName(full_name):
# full_name is "Assembly.Assembly1.Assembly2.Assembly3.Box.Edge16"
# We want either Edge16.
parts = full_name.split(".")
if len(parts) < 3:
# At minimum "Assembly.Box.edge16". It shouldn't be shorter
return ""
return parts[-1]
def getObjsNamesAndElement(obj_name, sub_name):
# if obj_name = "Assembly" and sub_name = "Assembly1.Assembly2.Assembly3.Box.Edge16"
# this will return ["Assembly","Assembly1","Assembly2","Assembly3","Box"] and "Edge16"
parts = sub_name.split(".")
# The last part is always the element name even if empty
element_name = parts[-1]
# The remaining parts are object names
obj_names = parts[:-1]
obj_names.insert(0, obj_name)
return obj_names, element_name
def getFullObjName(obj_name, sub_name):
# if obj_name = "Assembly" and sub_name = "Assembly1.Assembly2.Assembly3.Box.Edge16"
# this will return "Assembly.Assembly1.Assembly2.Assembly3.Box"
objs_names, element_name = getObjsNamesAndElement(obj_name, sub_name)
return ".".join(objs_names)
def getFullElementName(obj_name, sub_name):
# if obj_name = "Assembly" and sub_name = "Assembly1.Assembly2.Assembly3.Box.Edge16"
# this will return "Assembly.Assembly1.Assembly2.Assembly3.Box.Edge16"
return obj_name + "." + sub_name
def extract_type_and_number(element_name):
element_type = ""
element_number = ""
for char in element_name:
if char.isalpha():
# If the character is a letter, it's part of the type
element_type += char
elif char.isdigit():
# If the character is a digit, it's part of the number
element_number += char
else:
break
if element_type and element_number:
element_number = int(element_number)
return element_type, element_number
else:
return None, None
def findElementClosestVertex(selection_dict):
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]
curve = edge.Curve
if curve.TypeId == "Part::GeomCircle":
# For centers, as they are not shape vertexes, we return the element name.
# For now we only allow selecting the center of arcs / circles.
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"]
)
return vertex_name
elif elt_type == "Face":
face = selection_dict["object"].Shape.Faces[elt_index - 1]
# Handle the circle/arc edges for their centers
center_points = []
center_points_edge_indexes = []
edges = face.Edges
for i, edge in enumerate(edges):
curve = edge.Curve
if curve.TypeId == "Part::GeomCircle":
center_points.append(curve.Location)
center_points_edge_indexes.append(i)
if len(center_points) > 0:
closest_center_index, closest_center_distance = findClosestPointToMousePos(
center_points, selection_dict["mouse_pos"]
)
# Hendle the face vertexes
face_points = getPointsFromVertexes(face.Vertexes)
closest_vertex_index, closest_vertex_distance = findClosestPointToMousePos(
face_points, selection_dict["mouse_pos"]
)
if len(center_points) > 0:
if closest_center_distance < closest_vertex_distance:
# Note the index here is the index within the face! Not the object.
index = center_points_edge_indexes[closest_center_index] + 1
return "Edge" + str(index)
vertex_name = findVertexNameInObject(
face.Vertexes[closest_vertex_index], selection_dict["object"]
)
return vertex_name
return ""
def getPointsFromVertexes(vertexes):
points = []
for vtx in vertexes:
points.append(vtx.Point)
return points
def findClosestPointToMousePos(candidates_points, mousePos):
closest_point_index = None
point_min_length = None
for i, point in enumerate(candidates_points):
length = (mousePos - point).Length
if closest_point_index is None or length < point_min_length:
closest_point_index = i
point_min_length = length
return closest_point_index, point_min_length
def findVertexNameInObject(vertex, obj):
for i, vtx in enumerate(obj.Shape.Vertexes):
if vtx.Point == vertex.Point:
return "Vertex" + str(i + 1)
return ""