1
# SPDX-License-Identifier: LGPL-2.1-or-later
2
# /**************************************************************************
4
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
6
# This file is part of FreeCAD. *
8
# FreeCAD is free software: you can redistribute it and/or modify it *
9
# under the terms of the GNU Lesser General Public License as *
10
# published by the Free Software Foundation, either version 2.1 of the *
11
# License, or (at your option) any later version. *
13
# FreeCAD is distributed in the hope that it will be useful, but *
14
# WITHOUT ANY WARRANTY; without even the implied warranty of *
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
16
# Lesser General Public License for more details. *
18
# You should have received a copy of the GNU Lesser General Public *
19
# License along with FreeCAD. If not, see *
20
# <https://www.gnu.org/licenses/>. *
22
# **************************************************************************/
29
from PySide import QtCore
30
from PySide.QtCore import QT_TRANSLATE_NOOP
33
import FreeCADGui as Gui
35
# translate = App.Qt.translate
37
__title__ = "Assembly Joint object"
39
__url__ = "https://www.freecad.org"
45
translate = App.Qt.translate
47
TranslatedJointTypes = [
48
translate("Assembly", "Fixed"),
49
translate("Assembly", "Revolute"),
50
translate("Assembly", "Cylindrical"),
51
translate("Assembly", "Slider"),
52
translate("Assembly", "Ball"),
53
translate("Assembly", "Distance"),
88
def solveIfAllowed(assembly, storePrev=False):
89
if assembly.Type == "Assembly" and Preferences.preferences().GetBool(
90
"SolveInJointCreation", True
92
assembly.solve(storePrev)
95
# The joint object consists of 2 JCS (joint coordinate systems) and a Joint Type.
96
# A JCS is a placement that is computed (unless it is detached) from :
97
# - An Object name: this is the name of the solid. It can be any Part::Feature solid.
98
# Or a PartDesign Body. Or a App::Link to those. We use the name and not directly the DocumentObject
99
# because the object can be external.
100
# - A Part DocumentObject : This is the lowest level containing part. It can be either the Object itself if it
101
# stands alone. Or a App::Part. Or a App::Link to a App::Part.
103
# Assembly.Assembly1.Part1.Part2.Box : Object is Box, part is 'Part1'
104
# Assembly.Assembly1.LinkToPart1.Part2.Box : Object is Box, part is 'LinkToPart1'
105
# - An element name: This can be either a face, an edge, a vertex or empty. Empty means that the Object placement will be used
106
# - A vertex name: For faces and edges, we need to specify which vertex of said face/edge to use
107
# From these a placement is computed. It is relative to the Object.
109
def __init__(self, joint, type_index):
113
"App::PropertyEnumeration",
116
QT_TRANSLATE_NOOP("App::Property", "The type of the joint"),
118
joint.JointType = JointTypes # sets the list
119
joint.JointType = JointTypes[type_index] # set the initial value
121
# First Joint Connector
123
"App::PropertyString", # Not PropertyLink because they don't support external objects
126
QT_TRANSLATE_NOOP("App::Property", "The first object of the joint"),
133
QT_TRANSLATE_NOOP("App::Property", "The first part of the joint"),
137
"App::PropertyString",
140
QT_TRANSLATE_NOOP("App::Property", "The selected element of the first object"),
144
"App::PropertyString",
147
QT_TRANSLATE_NOOP("App::Property", "The selected vertex of the first object"),
151
"App::PropertyPlacement",
156
"This is the local coordinate system within object1 that will be used for the joint.",
166
"This prevents Placement1 from recomputing, enabling custom positioning of the placement.",
170
# Second Joint Connector
172
"App::PropertyString",
175
QT_TRANSLATE_NOOP("App::Property", "The second object of the joint"),
182
QT_TRANSLATE_NOOP("App::Property", "The second part of the joint"),
186
"App::PropertyString",
189
QT_TRANSLATE_NOOP("App::Property", "The selected element of the second object"),
193
"App::PropertyString",
196
QT_TRANSLATE_NOOP("App::Property", "The selected vertex of the second object"),
200
"App::PropertyPlacement",
205
"This is the local coordinate system within object2 that will be used for the joint.",
215
"This prevents Placement2 from recomputing, enabling custom positioning of the placement.",
220
"App::PropertyFloat",
225
"This is the distance of the joint. It is used only by the distance joint.",
230
"App::PropertyFloat",
235
"This is the rotation of the joint.",
240
"App::PropertyVector",
245
"This is the offset vector of the joint.",
255
"This indicates if the joint is active.",
258
joint.Activated = True
260
self.setJointConnectors(joint, [])
265
def loads(self, state):
268
def getAssembly(self, joint):
269
return joint.InList[0]
271
def setJointType(self, joint, jointType):
272
joint.JointType = jointType
273
joint.Label = jointType.replace(" ", "")
275
def onChanged(self, joint, prop):
276
"""Do something when a property has changed"""
277
# App.Console.PrintMessage("Change property: " + str(prop) + "\n")
279
if prop == "Rotation" or prop == "Offset" or prop == "Distance":
280
# during loading the onchanged may be triggered before full init.
281
if hasattr(joint, "Vertex1"): # so we check Vertex1
282
self.updateJCSPlacements(joint)
283
obj1 = UtilsAssembly.getObjectInPart(joint.Object1, joint.Part1)
284
obj2 = UtilsAssembly.getObjectInPart(joint.Object2, joint.Part2)
285
presolved = self.preSolve(
294
isAssembly = self.getAssembly(joint).Type == "Assembly"
295
if isAssembly and not presolved:
296
solveIfAllowed(self.getAssembly(joint))
298
self.updateJCSPlacements(joint)
300
def execute(self, fp):
301
"""Do something when doing a recomputation, this method is mandatory"""
302
# App.Console.PrintMessage("Recompute Python Box feature\n")
305
def setJointConnectors(self, joint, current_selection):
306
# 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
307
assembly = self.getAssembly(joint)
308
isAssembly = assembly.Type == "Assembly"
310
if len(current_selection) >= 1:
313
self.part1Connected = assembly.isPartConnected(current_selection[0]["part"])
315
self.part1Connected = True
317
joint.Object1 = current_selection[0]["object"].Name
318
joint.Part1 = current_selection[0]["part"]
319
joint.Element1 = current_selection[0]["element_name"]
320
joint.Vertex1 = current_selection[0]["vertex_name"]
321
joint.Placement1 = self.findPlacement(
322
joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1
329
joint.Placement1 = App.Placement()
330
self.partMovedByPresolved = None
332
if len(current_selection) >= 2:
335
self.part2Connected = assembly.isPartConnected(current_selection[1]["part"])
337
self.part2Connected = False
339
joint.Object2 = current_selection[1]["object"].Name
340
joint.Part2 = current_selection[1]["part"]
341
joint.Element2 = current_selection[1]["element_name"]
342
joint.Vertex2 = current_selection[1]["vertex_name"]
343
joint.Placement2 = self.findPlacement(
344
joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True
348
current_selection[0]["object"],
350
current_selection[1]["object"],
354
solveIfAllowed(assembly, True)
356
self.updateJCSPlacements(joint)
363
joint.Placement2 = App.Placement()
368
def updateJCSPlacements(self, joint):
369
if not joint.Detach1:
370
joint.Placement1 = self.findPlacement(
371
joint, joint.Object1, joint.Part1, joint.Element1, joint.Vertex1
374
if not joint.Detach2:
375
joint.Placement2 = self.findPlacement(
376
joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True
380
So here we want to find a placement that corresponds to a local coordinate system that would be placed at the selected vertex.
381
- obj is usually a App::Link to a PartDesign::Body, or primitive, fasteners. But can also be directly the object.1
382
- elt can be a face, an edge or a vertex.
383
- If elt is a vertex, then vtx = elt And placement is vtx coordinates without rotation.
384
- 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.
385
- 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
386
- if elt is a cylindrical face, vtx can also be the center of the arcs of the cylindrical face.
389
def findPlacement(self, joint, objName, part, elt, vtx, isSecond=False):
390
if not objName or not part:
391
return App.Placement()
393
obj = UtilsAssembly.getObjectInPart(objName, part)
394
plc = App.Placement()
397
return App.Placement()
399
if not elt or not vtx:
400
# case of whole parts such as PartDesign::Body or PartDesign::CordinateSystem/Point/Line/Plane.
401
return App.Placement()
403
elt_type, elt_index = UtilsAssembly.extract_type_and_number(elt)
404
vtx_type, vtx_index = UtilsAssembly.extract_type_and_number(vtx)
408
if elt_type == "Vertex":
409
vertex = obj.Shape.Vertexes[elt_index - 1]
410
plc.Base = (vertex.X, vertex.Y, vertex.Z)
411
elif elt_type == "Edge":
412
edge = obj.Shape.Edges[elt_index - 1]
415
# First we find the translation
416
if vtx_type == "Edge" or joint.JointType == "Distance":
417
# In this case the wanted vertex is the center.
418
if curve.TypeId == "Part::GeomCircle":
419
center_point = curve.Location
420
plc.Base = (center_point.x, center_point.y, center_point.z)
421
elif curve.TypeId == "Part::GeomLine":
422
edge_points = UtilsAssembly.getPointsFromVertexes(edge.Vertexes)
423
line_middle = (edge_points[0] + edge_points[1]) * 0.5
424
plc.Base = line_middle
426
vertex = obj.Shape.Vertexes[vtx_index - 1]
427
plc.Base = (vertex.X, vertex.Y, vertex.Z)
429
# Then we find the Rotation
430
if curve.TypeId == "Part::GeomCircle":
431
plc.Rotation = App.Rotation(curve.Rotation)
433
if curve.TypeId == "Part::GeomLine":
435
plane_normal = curve.Direction
436
plane_origin = App.Vector(0, 0, 0)
437
plane = Part.Plane(plane_origin, plane_normal)
438
plc.Rotation = App.Rotation(plane.Rotation)
439
elif elt_type == "Face":
440
face = obj.Shape.Faces[elt_index - 1]
441
surface = face.Surface
443
# First we find the translation
444
if vtx_type == "Face" or joint.JointType == "Distance":
445
if surface.TypeId == "Part::GeomCylinder" or surface.TypeId == "Part::GeomCone":
446
centerOfG = face.CenterOfGravity - surface.Center
447
centerPoint = surface.Center + centerOfG
448
centerPoint = centerPoint + App.Vector().projectToLine(centerOfG, surface.Axis)
449
plc.Base = centerPoint
450
elif surface.TypeId == "Part::GeomTorus" or surface.TypeId == "Part::GeomSphere":
451
plc.Base = surface.Center
453
plc.Base = face.CenterOfGravity
454
elif vtx_type == "Edge":
455
# In this case the edge is a circle/arc and the wanted vertex is its center.
456
edge = face.Edges[vtx_index - 1]
458
if curve.TypeId == "Part::GeomCircle":
459
center_point = curve.Location
460
plc.Base = (center_point.x, center_point.y, center_point.z)
463
surface.TypeId == "Part::GeomCylinder"
464
and curve.TypeId == "Part::GeomBSplineCurve"
466
# handle special case of 2 cylinder intersecting.
467
plc.Base = self.findCylindersIntersection(obj, surface, edge, elt_index)
470
vertex = obj.Shape.Vertexes[vtx_index - 1]
471
plc.Base = (vertex.X, vertex.Y, vertex.Z)
473
# Then we find the Rotation
474
if surface.TypeId == "Part::GeomPlane":
475
plc.Rotation = App.Rotation(surface.Rotation)
477
plc.Rotation = surface.Rotation
479
# Now plc is the placement relative to the origin determined by the object placement.
480
# But it does not take into account Part placements. So if the solid is in a part and
481
# if the part has a placement then plc is wrong.
483
# change plc to be relative to the object placement.
484
plc = obj.Placement.inverse() * plc
486
# post-process of plc for some special cases
487
if elt_type == "Vertex":
488
plc.Rotation = App.Rotation()
490
plane_normal = plc.Rotation.multVec(App.Vector(0, 0, 1))
491
plane_origin = App.Vector(0, 0, 0)
492
plane = Part.Plane(plane_origin, plane_normal)
493
plc.Rotation = App.Rotation(plane.Rotation)
495
# change plc to be relative to the origin of the document.
496
# global_plc = UtilsAssembly.getGlobalPlacement(obj, part)
497
# plc = global_plc * plc
499
# change plc to be relative to the assembly.
500
# assembly = self.getAssembly(joint)
501
# plc = assembly.Placement.inverse() * plc
503
# We apply rotation / reverse / offset it necessary, but only to the second JCS.
505
if joint.Offset.Length != 0.0:
506
plc = self.applyOffsetToPlacement(plc, joint.Offset)
507
if joint.Rotation != 0.0:
508
plc = self.applyRotationToPlacement(plc, joint.Rotation)
512
def applyOffsetToPlacement(self, plc, offset):
513
plc.Base = plc.Base + plc.Rotation.multVec(offset)
516
def applyRotationToPlacement(self, plc, angle):
517
return self.applyRotationToPlacementAlongAxis(plc, angle, App.Vector(0, 0, 1))
519
def applyRotationToPlacementAlongAxis(self, plc, angle, axis):
521
zRotation = App.Rotation(axis, angle)
522
plc.Rotation = rot * zRotation
525
def flipPlacement(self, plc):
526
return self.applyRotationToPlacementAlongAxis(plc, 180, App.Vector(1, 0, 0))
528
def flipOnePart(self, joint):
529
if hasattr(self, "part2Connected") and not self.part2Connected:
530
jcsPlc = UtilsAssembly.getJcsPlcRelativeToPart(
531
joint.Placement2, joint.Object2, joint.Part2
533
globalJcsPlc = UtilsAssembly.getJcsGlobalPlc(
534
joint.Placement2, joint.Object2, joint.Part2
536
jcsPlc = self.flipPlacement(jcsPlc)
537
joint.Part2.Placement = globalJcsPlc * jcsPlc.inverse()
540
jcsPlc = UtilsAssembly.getJcsPlcRelativeToPart(
541
joint.Placement1, joint.Object1, joint.Part1
543
globalJcsPlc = UtilsAssembly.getJcsGlobalPlc(
544
joint.Placement1, joint.Object1, joint.Part1
546
jcsPlc = self.flipPlacement(jcsPlc)
547
joint.Part1.Placement = globalJcsPlc * jcsPlc.inverse()
549
solveIfAllowed(self.getAssembly(joint))
551
def findCylindersIntersection(self, obj, surface, edge, elt_index):
552
for j, facej in enumerate(obj.Shape.Faces):
553
surfacej = facej.Surface
554
if (elt_index - 1) == j or surfacej.TypeId != "Part::GeomCylinder":
557
for edgej in facej.Edges:
559
edgej.Curve.TypeId == "Part::GeomBSplineCurve"
560
and edgej.CenterOfGravity == edge.CenterOfGravity
561
and edgej.Length == edge.Length
563
# we need intersection between the 2 cylinder axis.
564
line1 = Part.Line(surface.Center, surface.Center + surface.Axis)
565
line2 = Part.Line(surfacej.Center, surfacej.Center + surfacej.Axis)
567
res = line1.intersect(line2, Part.Precision.confusion())
570
return App.Vector(res[0].X, res[0].Y, res[0].Z)
571
return surface.Center
573
def preSolve(self, joint, obj1, part1, obj2, part2, savePlc=True):
574
# The goal of this is to put the part in the correct position to avoid wrong placement by the solve.
576
# we actually don't want to match perfectly the JCS, it is best to match them
577
# in the current closest direction, ie either matched or flipped.
578
sameDir = self.areJcsSameDir(joint)
580
if hasattr(self, "part2Connected") and not self.part2Connected:
582
self.partMovedByPresolved = joint.Part2
583
self.presolveBackupPlc = joint.Part2.Placement
585
globalJcsPlc1 = UtilsAssembly.getJcsGlobalPlc(
586
joint.Placement1, joint.Object1, joint.Part1
588
jcsPlc2 = UtilsAssembly.getJcsPlcRelativeToPart(
589
joint.Placement2, joint.Object2, joint.Part2
592
jcsPlc2 = self.flipPlacement(jcsPlc2)
593
joint.Part2.Placement = globalJcsPlc1 * jcsPlc2.inverse()
596
elif hasattr(self, "part1Connected") and not self.part1Connected:
598
self.partMovedByPresolved = joint.Part1
599
self.presolveBackupPlc = joint.Part1.Placement
601
globalJcsPlc2 = UtilsAssembly.getJcsGlobalPlc(
602
joint.Placement2, joint.Object2, joint.Part2
604
jcsPlc1 = UtilsAssembly.getJcsPlcRelativeToPart(
605
joint.Placement1, joint.Object1, joint.Part1
608
jcsPlc1 = self.flipPlacement(jcsPlc1)
609
joint.Part1.Placement = globalJcsPlc2 * jcsPlc1.inverse()
613
def undoPreSolve(self):
614
if self.partMovedByPresolved:
615
self.partMovedByPresolved.Placement = self.presolveBackupPlc
616
self.partMovedByPresolved = None
618
def areJcsSameDir(self, joint):
619
globalJcsPlc1 = UtilsAssembly.getJcsGlobalPlc(joint.Placement1, joint.Object1, joint.Part1)
620
globalJcsPlc2 = UtilsAssembly.getJcsGlobalPlc(joint.Placement2, joint.Object2, joint.Part2)
622
zAxis1 = globalJcsPlc1.Rotation.multVec(App.Vector(0, 0, 1))
623
zAxis2 = globalJcsPlc2.Rotation.multVec(App.Vector(0, 0, 1))
624
return zAxis1.dot(zAxis2) > 0
627
class ViewProviderJoint:
628
def __init__(self, vobj):
629
"""Set this object to the proxy object of the actual view provider"""
633
def attach(self, vobj):
634
"""Setup the scene sub-graph of the view provider, this method is mandatory"""
635
self.axis_thickness = 3
637
view_params = App.ParamGet("User parameter:BaseApp/Preferences/View")
638
param_x_axis_color = view_params.GetUnsigned("AxisXColor", 0xCC333300)
639
param_y_axis_color = view_params.GetUnsigned("AxisYColor", 0x33CC3300)
640
param_z_axis_color = view_params.GetUnsigned("AxisZColor", 0x3333CC00)
642
self.x_axis_so_color = coin.SoBaseColor()
643
self.x_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_x_axis_color))
644
self.y_axis_so_color = coin.SoBaseColor()
645
self.y_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_y_axis_color))
646
self.z_axis_so_color = coin.SoBaseColor()
647
self.z_axis_so_color.rgb.setValue(UtilsAssembly.color_from_unsigned(param_z_axis_color))
649
camera = Gui.ActiveDocument.ActiveView.getCameraNode()
650
self.cameraSensor = coin.SoFieldSensor(self.camera_callback, camera)
651
if isinstance(camera, coin.SoPerspectiveCamera):
652
self.cameraSensor.attach(camera.focalDistance)
653
elif isinstance(camera, coin.SoOrthographicCamera):
654
self.cameraSensor.attach(camera.height)
656
self.app_obj = vobj.Object
658
self.transform1 = coin.SoTransform()
659
self.transform2 = coin.SoTransform()
660
self.transform3 = coin.SoTransform()
662
scaleF = self.get_JCS_size()
663
self.axisScale = coin.SoScale()
664
self.axisScale.scaleFactor.setValue(scaleF, scaleF, scaleF)
666
self.draw_style = coin.SoDrawStyle()
667
self.draw_style.style = coin.SoDrawStyle.LINES
668
self.draw_style.lineWidth = self.axis_thickness
670
self.switch_JCS1 = self.JCS_sep(self.transform1)
671
self.switch_JCS2 = self.JCS_sep(self.transform2)
672
self.switch_JCS_preview = self.JCS_sep(self.transform3)
674
self.pick = coin.SoPickStyle()
675
self.setPickableState(True)
677
self.display_mode = coin.SoType.fromName("SoFCSelection").createInstance()
678
self.display_mode.addChild(self.pick)
679
self.display_mode.addChild(self.switch_JCS1)
680
self.display_mode.addChild(self.switch_JCS2)
681
self.display_mode.addChild(self.switch_JCS_preview)
682
vobj.addDisplayMode(self.display_mode, "Wireframe")
684
def camera_callback(self, *args):
685
scaleF = self.get_JCS_size()
686
self.axisScale.scaleFactor.setValue(scaleF, scaleF, scaleF)
688
def JCS_sep(self, soTransform):
689
JCS = coin.SoAnnotation()
690
JCS.addChild(soTransform)
692
base_plane_sep = self.plane_sep(0.4, 15)
693
X_axis_sep = self.line_sep([0.5, 0, 0], [1, 0, 0], self.x_axis_so_color)
694
Y_axis_sep = self.line_sep([0, 0.5, 0], [0, 1, 0], self.y_axis_so_color)
695
Z_axis_sep = self.line_sep([0, 0, 0], [0, 0, 1], self.z_axis_so_color)
697
JCS.addChild(base_plane_sep)
698
JCS.addChild(X_axis_sep)
699
JCS.addChild(Y_axis_sep)
700
JCS.addChild(Z_axis_sep)
702
switch_JCS = coin.SoSwitch()
703
switch_JCS.addChild(JCS)
704
switch_JCS.whichChild = coin.SO_SWITCH_NONE
707
def line_sep(self, startPoint, endPoint, soColor):
708
line = coin.SoLineSet()
709
line.numVertices.setValue(2)
710
coords = coin.SoCoordinate3()
711
coords.point.setValues(0, [startPoint, endPoint])
713
axis_sep = coin.SoAnnotation()
714
axis_sep.addChild(self.axisScale)
715
axis_sep.addChild(self.draw_style)
716
axis_sep.addChild(soColor)
717
axis_sep.addChild(coords)
718
axis_sep.addChild(line)
721
def plane_sep(self, size, num_vertices):
722
coords = coin.SoCoordinate3()
724
for i in range(num_vertices):
725
angle = float(i) / num_vertices * 2.0 * math.pi
726
x = math.cos(angle) * size
727
y = math.sin(angle) * size
728
coords.point.set1Value(i, x, y, 0)
730
face = coin.SoFaceSet()
731
face.numVertices.setValue(num_vertices)
733
transform = coin.SoTransform()
734
transform.translation.setValue(0, 0, 0)
736
draw_style = coin.SoDrawStyle()
737
draw_style.style = coin.SoDrawStyle.FILLED
739
material = coin.SoMaterial()
740
material.diffuseColor.setValue([0.5, 0.5, 0.5])
741
material.ambientColor.setValue([0.5, 0.5, 0.5])
742
material.specularColor.setValue([0.5, 0.5, 0.5])
743
material.emissiveColor.setValue([0.5, 0.5, 0.5])
744
material.transparency.setValue(0.3)
746
face_sep = coin.SoAnnotation()
747
face_sep.addChild(self.axisScale)
748
face_sep.addChild(transform)
749
face_sep.addChild(draw_style)
750
face_sep.addChild(material)
751
face_sep.addChild(coords)
752
face_sep.addChild(face)
755
def get_JCS_size(self):
756
camera = Gui.ActiveDocument.ActiveView.getCameraNode()
758
# Check if the camera is a perspective camera
759
if isinstance(camera, coin.SoPerspectiveCamera):
760
return camera.focalDistance.getValue() / 20
761
elif isinstance(camera, coin.SoOrthographicCamera):
762
return camera.height.getValue() / 20
764
# Default value if camera type is unknown
767
return camera.height.getValue() / 20
769
def set_JCS_placement(self, soTransform, placement, objName, part):
770
# change plc to be relative to the origin of the document.
771
obj = UtilsAssembly.getObjectInPart(objName, part)
772
global_plc = UtilsAssembly.getGlobalPlacement(obj, part)
773
placement = global_plc * placement
776
soTransform.translation.setValue(t.x, t.y, t.z)
778
r = placement.Rotation.Q
779
soTransform.rotation.setValue(r[0], r[1], r[2], r[3])
781
def updateData(self, joint, prop):
782
"""If a property of the handled feature has changed we have the chance to handle this here"""
783
# joint is the handled feature, prop is the name of the property that has changed
784
if prop == "Placement1":
786
plc = joint.Placement1
787
self.switch_JCS1.whichChild = coin.SO_SWITCH_ALL
790
self.set_JCS_placement(self.transform1, plc, joint.Object1, joint.Part1)
792
self.switch_JCS1.whichChild = coin.SO_SWITCH_NONE
794
if prop == "Placement2":
796
plc = joint.Placement2
797
self.switch_JCS2.whichChild = coin.SO_SWITCH_ALL
800
self.set_JCS_placement(self.transform2, plc, joint.Object2, joint.Part2)
802
self.switch_JCS2.whichChild = coin.SO_SWITCH_NONE
804
def showPreviewJCS(self, visible, placement=None, objName="", part=None):
806
self.switch_JCS_preview.whichChild = coin.SO_SWITCH_ALL
807
self.set_JCS_placement(self.transform3, placement, objName, part)
809
self.switch_JCS_preview.whichChild = coin.SO_SWITCH_NONE
811
def setPickableState(self, state: bool):
812
"""Set JCS selectable or unselectable in 3D view"""
814
self.pick.style.setValue(coin.SoPickStyle.UNPICKABLE)
816
self.pick.style.setValue(coin.SoPickStyle.SHAPE_ON_TOP)
818
def getDisplayModes(self, obj):
819
"""Return a list of display modes."""
821
modes.append("Wireframe")
824
def getDefaultDisplayMode(self):
825
"""Return the name of the default display mode. It must be defined in getDisplayModes."""
828
def onChanged(self, vp, prop):
829
"""Here we can do something when a single property got changed"""
830
# App.Console.PrintMessage("Change property: " + str(prop) + "\n")
831
if prop == "color_X_axis":
832
c = vp.getPropertyByName("color_X_axis")
833
self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
834
if prop == "color_Y_axis":
835
c = vp.getPropertyByName("color_Y_axis")
836
self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
837
if prop == "color_Z_axis":
838
c = vp.getPropertyByName("color_Z_axis")
839
self.x_axis_so_color.rgb.setValue(c[0], c[1], c[2])
842
if self.app_obj.JointType == "Fixed":
843
return ":/icons/Assembly_CreateJointFixed.svg"
844
elif self.app_obj.JointType == "Revolute":
845
return ":/icons/Assembly_CreateJointRevolute.svg"
846
elif self.app_obj.JointType == "Cylindrical":
847
return ":/icons/Assembly_CreateJointCylindrical.svg"
848
elif self.app_obj.JointType == "Slider":
849
return ":/icons/Assembly_CreateJointSlider.svg"
850
elif self.app_obj.JointType == "Ball":
851
return ":/icons/Assembly_CreateJointBall.svg"
852
elif self.app_obj.JointType == "Distance":
853
return ":/icons/Assembly_CreateJointDistance.svg"
855
return ":/icons/Assembly_CreateJoint.svg"
858
"""When saving the document this object gets stored using Python's json module.\
859
Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\
860
to return a tuple of all serializable objects or None."""
863
def loads(self, state):
864
"""When restoring the serialized object from document we have the chance to set some internals here.\
865
Since no data were serialized nothing needs to be done here."""
868
def doubleClicked(self, vobj):
869
assembly = vobj.Object.InList[0]
870
if UtilsAssembly.activeAssembly() != assembly:
871
Gui.ActiveDocument.setEdit(assembly)
873
panel = TaskAssemblyCreateJoint(0, vobj.Object)
874
Gui.Control.showDialog(panel)
877
################ Grounded Joint object #################
881
def __init__(self, joint, obj_to_ground):
889
QT_TRANSLATE_NOOP("App::Property", "The object to ground"),
892
joint.ObjectToGround = obj_to_ground
895
"App::PropertyPlacement",
900
"This is where the part is grounded.",
904
joint.Placement = obj_to_ground.Placement
909
def loads(self, state):
912
def onChanged(self, fp, prop):
913
"""Do something when a property has changed"""
914
# App.Console.PrintMessage("Change property: " + str(prop) + "\n")
917
def execute(self, fp):
918
"""Do something when doing a recomputation, this method is mandatory"""
919
# App.Console.PrintMessage("Recompute Python Box feature\n")
923
class ViewProviderGroundedJoint:
924
def __init__(self, obj):
925
"""Set this object to the proxy object of the actual view provider"""
928
def attach(self, obj):
929
"""Setup the scene sub-graph of the view provider, this method is mandatory"""
932
def updateData(self, fp, prop):
933
"""If a property of the handled feature has changed we have the chance to handle this here"""
934
# fp is the handled feature, prop is the name of the property that has changed
937
def getDisplayModes(self, obj):
938
"""Return a list of display modes."""
939
modes = ["Wireframe"]
942
def getDefaultDisplayMode(self):
943
"""Return the name of the default display mode. It must be defined in getDisplayModes."""
946
def onChanged(self, vp, prop):
947
"""Here we can do something when a single property got changed"""
948
# App.Console.PrintMessage("Change property: " + str(prop) + "\n")
951
def onDelete(self, feature, subelements): # subelements is a tuple of strings
952
# Remove grounded tag.
953
if hasattr(feature.Object, "ObjectToGround"):
954
obj = feature.Object.ObjectToGround
955
if obj is not None and obj.Label.endswith(" 🔒"):
956
obj.Label = obj.Label[:-2]
958
return True # If False is returned the object won't be deleted
961
return ":/icons/Assembly_ToggleGrounded.svg"
964
class MakeJointSelGate:
965
def __init__(self, taskbox, assembly):
966
self.taskbox = taskbox
967
self.assembly = assembly
969
def allow(self, doc, obj, sub):
973
objs_names, element_name = UtilsAssembly.getObjsNamesAndElement(obj.Name, sub)
975
if self.assembly.Name not in objs_names:
976
# Only objects within the assembly.
979
if Gui.Selection.isSelected(obj, sub, Gui.Selection.ResolveMode.NoResolve):
980
# If it's to deselect then it's ok
983
if len(self.taskbox.current_selection) >= 2:
984
# No more than 2 elements can be selected for basic joints.
987
full_obj_name = ".".join(objs_names)
988
full_element_name = full_obj_name + "." + element_name
989
selected_object = UtilsAssembly.getObject(full_element_name)
991
part_containing_selected_object = UtilsAssembly.getContainingPart(
992
full_element_name, selected_object, self.assembly
995
for selection_dict in self.taskbox.current_selection:
996
if selection_dict["part"] == part_containing_selected_object:
997
# Can't join a solid to itself. So the user need to select 2 different parts.
1006
class TaskAssemblyCreateJoint(QtCore.QObject):
1007
def __init__(self, jointTypeIndex, jointObj=None):
1013
self.assembly = UtilsAssembly.activeAssembly()
1014
if not self.assembly:
1015
self.assembly = UtilsAssembly.activePart()
1016
self.activeType = "Part"
1018
self.activeType = "Assembly"
1020
self.view = Gui.activeDocument().activeView()
1021
self.doc = App.ActiveDocument
1023
if not self.assembly or not self.view or not self.doc:
1026
if self.activeType == "Assembly":
1027
self.assembly.ViewObject.EnableMovement = False
1029
self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateJoint.ui")
1031
if self.activeType == "Part":
1032
self.form.setWindowTitle("Match parts")
1033
self.form.jointType.hide()
1034
self.form.jointType.addItems(TranslatedJointTypes)
1035
self.form.jointType.setCurrentIndex(jointTypeIndex)
1036
self.form.jointType.currentIndexChanged.connect(self.onJointTypeChanged)
1038
self.form.distanceSpinbox.valueChanged.connect(self.onDistanceChanged)
1039
self.form.offsetSpinbox.valueChanged.connect(self.onOffsetChanged)
1040
self.form.rotationSpinbox.valueChanged.connect(self.onRotationChanged)
1041
self.form.PushButtonReverse.clicked.connect(self.onReverseClicked)
1044
Gui.Selection.clearSelection()
1045
self.creating = False
1046
self.joint = jointObj
1047
self.jointName = jointObj.Label
1048
App.setActiveTransaction("Edit " + self.jointName + " Joint")
1050
self.updateTaskboxFromJoint()
1051
self.visibilityBackup = self.joint.Visibility
1052
self.joint.Visibility = True
1055
self.creating = True
1056
self.jointName = self.form.jointType.currentText().replace(" ", "")
1057
if self.activeType == "Part":
1058
App.setActiveTransaction("Transform")
1060
App.setActiveTransaction("Create " + self.jointName + " Joint")
1062
self.current_selection = []
1063
self.preselection_dict = None
1065
self.createJointObject()
1066
self.visibilityBackup = False
1067
self.handleInitialSelection()
1069
self.toggleDistanceVisibility()
1070
self.toggleOffsetVisibility()
1071
self.toggleRotationVisibility()
1072
self.toggleReverseVisibility()
1074
self.setJointsPickableState(False)
1076
Gui.Selection.addSelectionGate(
1077
MakeJointSelGate(self, self.assembly), Gui.Selection.ResolveMode.NoResolve
1079
Gui.Selection.addObserver(self, Gui.Selection.ResolveMode.NoResolve)
1080
Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.GreedySelection)
1082
self.callbackMove = self.view.addEventCallback("SoLocation2Event", self.moveMouse)
1083
self.callbackKey = self.view.addEventCallback("SoKeyboardEvent", self.KeyboardEvent)
1086
if len(self.current_selection) != 2:
1087
App.Console.PrintWarning("You need to select 2 elements from 2 separate parts.")
1092
solveIfAllowed(self.assembly)
1093
if self.activeType == "Assembly":
1094
self.joint.Visibility = self.visibilityBackup
1096
self.joint.Document.removeObject(self.joint.Name)
1098
App.closeActiveTransaction()
1103
App.closeActiveTransaction(True)
1104
if not self.creating: # update visibility only if we are editing the joint
1105
self.joint.Visibility = self.visibilityBackup
1108
def deactivate(self):
1112
if self.activeType == "Assembly":
1113
self.assembly.clearUndo()
1114
self.assembly.ViewObject.EnableMovement = True
1116
Gui.Selection.removeSelectionGate()
1117
Gui.Selection.removeObserver(self)
1118
Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.NormalSelection)
1119
Gui.Selection.clearSelection()
1120
self.view.removeEventCallback("SoLocation2Event", self.callbackMove)
1121
self.view.removeEventCallback("SoKeyboardEvent", self.callbackKey)
1122
self.setJointsPickableState(True)
1123
if Gui.Control.activeDialog():
1124
Gui.Control.closeDialog()
1126
def handleInitialSelection(self):
1127
selection = Gui.Selection.getSelectionEx("*", 0)
1130
for sel in selection:
1131
# If you select 2 solids (bodies for example) within an assembly.
1132
# There'll be a single sel but 2 SubElementNames.
1134
if not sel.SubElementNames:
1135
# no subnames, so its a root assembly itself that is selected.
1136
Gui.Selection.removeSelection(sel.Object)
1139
for sub_name in sel.SubElementNames:
1140
# Only objects within the assembly.
1141
objs_names, element_name = UtilsAssembly.getObjsNamesAndElement(
1142
sel.ObjectName, sub_name
1144
if self.assembly.Name not in objs_names:
1145
Gui.Selection.removeSelection(sel.Object, sub_name)
1148
obj_name = sel.ObjectName
1150
full_obj_name = UtilsAssembly.getFullObjName(obj_name, sub_name)
1151
full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
1152
selected_object = UtilsAssembly.getObject(full_element_name)
1153
element_name = UtilsAssembly.getElementName(full_element_name)
1154
part_containing_selected_object = self.getContainingPart(
1155
full_element_name, selected_object
1158
if selected_object == self.assembly:
1159
# do not accept selection of assembly itself
1160
Gui.Selection.removeSelection(sel.Object, sub_name)
1164
len(self.current_selection) == 1
1165
and selected_object == self.current_selection[0]["object"]
1167
# do not select several feature of the same object.
1168
self.current_selection.clear()
1169
Gui.Selection.clearSelection()
1173
"object": selected_object,
1174
"part": part_containing_selected_object,
1175
"element_name": element_name,
1176
"full_element_name": full_element_name,
1177
"full_obj_name": full_obj_name,
1178
"vertex_name": element_name,
1181
self.current_selection.append(selection_dict)
1183
# do not accept initial selection if we don't have 2 selected features
1184
if len(self.current_selection) != 2:
1185
self.current_selection.clear()
1186
Gui.Selection.clearSelection()
1190
def createJointObject(self):
1191
type_index = self.form.jointType.currentIndex()
1193
if self.activeType == "Part":
1194
self.joint = self.assembly.newObject("App::FeaturePython", "Temporary joint")
1196
joint_group = UtilsAssembly.getJointGroup(self.assembly)
1197
self.joint = joint_group.newObject("App::FeaturePython", self.jointName)
1199
Joint(self.joint, type_index)
1200
ViewProviderJoint(self.joint.ViewObject)
1202
def onJointTypeChanged(self, index):
1204
self.joint.Proxy.setJointType(self.joint, JointTypes[self.form.jointType.currentIndex()])
1205
self.toggleDistanceVisibility()
1206
self.toggleOffsetVisibility()
1207
self.toggleRotationVisibility()
1208
self.toggleReverseVisibility()
1210
def onDistanceChanged(self, quantity):
1211
self.joint.Distance = self.form.distanceSpinbox.property("rawValue")
1213
def onOffsetChanged(self, quantity):
1214
self.joint.Offset = App.Vector(0, 0, self.form.offsetSpinbox.property("rawValue"))
1216
def onRotationChanged(self, quantity):
1217
self.joint.Rotation = self.form.rotationSpinbox.property("rawValue")
1219
def onReverseClicked(self):
1220
self.joint.Proxy.flipOnePart(self.joint)
1222
def toggleDistanceVisibility(self):
1223
if JointTypes[self.form.jointType.currentIndex()] in JointUsingDistance:
1224
self.form.distanceLabel.show()
1225
self.form.distanceSpinbox.show()
1227
self.form.distanceLabel.hide()
1228
self.form.distanceSpinbox.hide()
1230
def toggleOffsetVisibility(self):
1231
if JointTypes[self.form.jointType.currentIndex()] in JointUsingOffset:
1232
self.form.offsetLabel.show()
1233
self.form.offsetSpinbox.show()
1235
self.form.offsetLabel.hide()
1236
self.form.offsetSpinbox.hide()
1238
def toggleRotationVisibility(self):
1239
if JointTypes[self.form.jointType.currentIndex()] in JointUsingRotation:
1240
self.form.rotationLabel.show()
1241
self.form.rotationSpinbox.show()
1243
self.form.rotationLabel.hide()
1244
self.form.rotationSpinbox.hide()
1246
def toggleReverseVisibility(self):
1247
if JointTypes[self.form.jointType.currentIndex()] in JointUsingReverse:
1248
self.form.PushButtonReverse.show()
1250
self.form.PushButtonReverse.hide()
1252
def updateTaskboxFromJoint(self):
1253
self.current_selection = []
1254
self.preselection_dict = None
1256
obj1 = UtilsAssembly.getObjectInPart(self.joint.Object1, self.joint.Part1)
1257
obj2 = UtilsAssembly.getObjectInPart(self.joint.Object2, self.joint.Part2)
1261
"part": self.joint.Part1,
1262
"element_name": self.joint.Element1,
1263
"vertex_name": self.joint.Vertex1,
1268
"part": self.joint.Part2,
1269
"element_name": self.joint.Element2,
1270
"vertex_name": self.joint.Vertex2,
1273
self.current_selection.append(selection_dict1)
1274
self.current_selection.append(selection_dict2)
1276
# Add the elements to the selection. Note we cannot do :
1277
# Gui.Selection.addSelection(self.doc.Name, obj1.Name, elName)
1278
# Because obj1 can be external in which case addSelection will fail. And
1279
# Gui.Selection.addSelection(obj1.Document.Name, obj1.Name, elName)
1280
# will not select in the assembly doc.
1281
elName = self.getSubnameForSelection(obj1, self.joint.Part1, self.joint.Element1)
1282
Gui.Selection.addSelection(self.doc.Name, self.joint.Part1.Name, elName)
1284
elName = self.getSubnameForSelection(obj2, self.joint.Part2, self.joint.Element2)
1285
Gui.Selection.addSelection(self.doc.Name, self.joint.Part2.Name, elName)
1287
self.form.distanceSpinbox.setProperty("rawValue", self.joint.Distance)
1288
self.form.offsetSpinbox.setProperty("rawValue", self.joint.Offset.z)
1289
self.form.rotationSpinbox.setProperty("rawValue", self.joint.Rotation)
1291
self.form.jointType.setCurrentIndex(JointTypes.index(self.joint.JointType))
1292
self.updateJointList()
1294
def getSubnameForSelection(self, obj, part, elName):
1295
# We need the subname starting from the part.
1296
# Example for : Assembly.Part1.LinkToPart2.Part3.Body.Tip.Face1
1297
# part is Part1 and obj is Body
1298
# we should get : LinkToPart2.Part3.Body.Tip.Face1
1300
if obj is None or part is None:
1303
if obj.TypeId == "PartDesign::Body":
1304
elName = obj.Tip.Name + "." + elName
1305
elif obj.TypeId == "App::Link":
1306
linked_obj = obj.getLinkedObject()
1307
if linked_obj.TypeId == "PartDesign::Body":
1308
elName = linked_obj.Tip.Name + "." + elName
1310
if obj != part and obj in part.OutListRecursive:
1318
if currentObj != part:
1321
bSub = bSub + currentObj.Name
1323
if currentObj == obj:
1326
if currentObj.TypeId == "App::Link":
1327
currentObj = currentObj.getLinkedObject()
1329
for obji in currentObj.OutList:
1330
if obji == obj or obj in obji.OutListRecursive:
1334
elName = bSub + "." + elName
1337
def updateJoint(self):
1338
# First we build the listwidget
1339
self.updateJointList()
1341
# Then we pass the new list to the join object
1342
self.joint.Proxy.setJointConnectors(self.joint, self.current_selection)
1344
def updateJointList(self):
1345
self.form.featureList.clear()
1346
simplified_names = []
1347
for sel in self.current_selection:
1348
sname = sel["object"].Label
1349
if sel["element_name"] != "":
1350
sname = sname + "." + sel["element_name"]
1351
simplified_names.append(sname)
1352
self.form.featureList.addItems(simplified_names)
1354
def moveMouse(self, info):
1355
if len(self.current_selection) >= 2 or (
1356
len(self.current_selection) == 1
1358
not self.preselection_dict
1359
or self.current_selection[0]["part"] == self.preselection_dict["part"]
1362
self.joint.ViewObject.Proxy.showPreviewJCS(False)
1365
cursor_pos = self.view.getCursorPos()
1366
cursor_info = self.view.getObjectInfo(cursor_pos)
1367
# 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'}
1371
or not self.preselection_dict
1372
# or cursor_info["SubName"] != self.preselection_dict["sub_name"]
1373
# Removed because they are not equal when hovering a line endpoints.
1374
# But we don't actually need to test because if there's no preselection then not cursor is None
1376
self.joint.ViewObject.Proxy.showPreviewJCS(False)
1379
# newPos = self.view.getPoint(*info["Position"]) # This is not what we want, it's not pos on the object but on the focal plane
1381
newPos = App.Vector(cursor_info["x"], cursor_info["y"], cursor_info["z"])
1382
self.preselection_dict["mouse_pos"] = newPos
1384
if self.preselection_dict["element_name"] == "":
1385
self.preselection_dict["vertex_name"] = ""
1387
self.preselection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(
1388
self.preselection_dict
1391
isSecond = len(self.current_selection) == 1
1392
objName = self.preselection_dict["object"].Name
1393
part = self.preselection_dict["part"]
1394
placement = self.joint.Proxy.findPlacement(
1398
self.preselection_dict["element_name"],
1399
self.preselection_dict["vertex_name"],
1402
self.joint.ViewObject.Proxy.showPreviewJCS(True, placement, objName, part)
1403
self.previewJCSVisible = True
1405
# 3D view keyboard handler
1406
def KeyboardEvent(self, info):
1407
if info["State"] == "UP" and info["Key"] == "ESCAPE":
1410
if info["State"] == "UP" and info["Key"] == "RETURN":
1413
def getContainingPart(self, full_element_name, obj):
1414
return UtilsAssembly.getContainingPart(full_element_name, obj, self.assembly)
1416
# selectionObserver stuff
1417
def addSelection(self, doc_name, obj_name, sub_name, mousePos):
1418
full_obj_name = UtilsAssembly.getFullObjName(obj_name, sub_name)
1419
full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
1420
selected_object = UtilsAssembly.getObject(full_element_name)
1421
element_name = UtilsAssembly.getElementName(full_element_name)
1422
part_containing_selected_object = self.getContainingPart(full_element_name, selected_object)
1425
"object": selected_object,
1426
"part": part_containing_selected_object,
1427
"element_name": element_name,
1428
"full_element_name": full_element_name,
1429
"full_obj_name": full_obj_name,
1430
"mouse_pos": App.Vector(mousePos[0], mousePos[1], mousePos[2]),
1432
if element_name == "":
1433
selection_dict["vertex_name"] = ""
1435
selection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(selection_dict)
1437
self.current_selection.append(selection_dict)
1440
# We hide the preview JCS if we just added to the selection
1441
self.joint.ViewObject.Proxy.showPreviewJCS(False)
1443
def removeSelection(self, doc_name, obj_name, sub_name, mousePos=None):
1444
full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
1445
selected_object = UtilsAssembly.getObject(full_element_name)
1446
element_name = UtilsAssembly.getElementName(full_element_name)
1447
part_containing_selected_object = self.getContainingPart(full_element_name, selected_object)
1449
# Find and remove the corresponding dictionary from the combined list
1450
selection_dict_to_remove = None
1451
for selection_dict in self.current_selection:
1452
if selection_dict["part"] == part_containing_selected_object:
1453
selection_dict_to_remove = selection_dict
1456
if selection_dict_to_remove is not None:
1457
self.current_selection.remove(selection_dict_to_remove)
1461
def setPreselection(self, doc_name, obj_name, sub_name):
1463
self.preselection_dict = None
1466
full_obj_name = UtilsAssembly.getFullObjName(obj_name, sub_name)
1467
full_element_name = UtilsAssembly.getFullElementName(obj_name, sub_name)
1468
selected_object = UtilsAssembly.getObject(full_element_name)
1469
element_name = UtilsAssembly.getElementName(full_element_name)
1470
part_containing_selected_object = self.getContainingPart(full_element_name, selected_object)
1472
self.preselection_dict = {
1473
"object": selected_object,
1474
"part": part_containing_selected_object,
1475
"sub_name": sub_name,
1476
"element_name": element_name,
1477
"full_element_name": full_element_name,
1478
"full_obj_name": full_obj_name,
1481
def clearSelection(self, doc_name):
1482
self.current_selection.clear()
1485
def setJointsPickableState(self, state: bool):
1486
"""Make all joints in assembly selectable (True) or unselectable (False) in 3D view"""
1487
if self.activeType == "Assembly":
1488
jointGroup = UtilsAssembly.getJointGroup(self.assembly)
1489
for joint in jointGroup.Group:
1490
if hasattr(joint, "JointType"):
1491
joint.ViewObject.Proxy.setPickableState(state)
1493
for obj in self.assembly.OutList:
1494
if obj.TypeId == "App::FeaturePython" and hasattr(obj, "JointType"):
1495
obj.ViewObject.Proxy.setPickableState(state)