FreeCAD

Форк
0
/
JointObject.py 
1495 строк · 56.7 Кб
1
# SPDX-License-Identifier: LGPL-2.1-or-later
2
# /**************************************************************************
3
#                                                                           *
4
#    Copyright (c) 2023 Ondsel <development@ondsel.com>                     *
5
#                                                                           *
6
#    This file is part of FreeCAD.                                          *
7
#                                                                           *
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.                        *
12
#                                                                           *
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.                        *
17
#                                                                           *
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/>.                                       *
21
#                                                                           *
22
# **************************************************************************/
23

24
import math
25

26
import FreeCAD as App
27
import Part
28

29
from PySide import QtCore
30
from PySide.QtCore import QT_TRANSLATE_NOOP
31

32
if App.GuiUp:
33
    import FreeCADGui as Gui
34

35
# translate = App.Qt.translate
36

37
__title__ = "Assembly Joint object"
38
__author__ = "Ondsel"
39
__url__ = "https://www.freecad.org"
40

41
from pivy import coin
42
import UtilsAssembly
43
import Preferences
44

45
translate = App.Qt.translate
46

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"),
54
]
55

56
JointTypes = [
57
    "Fixed",
58
    "Revolute",
59
    "Cylindrical",
60
    "Slider",
61
    "Ball",
62
    "Distance",
63
]
64

65
JointUsingDistance = [
66
    "Distance",
67
]
68

69
JointUsingOffset = [
70
    "Fixed",
71
    "Revolute",
72
]
73

74
JointUsingRotation = [
75
    "Fixed",
76
    "Slider",
77
]
78

79
JointUsingReverse = [
80
    "Fixed",
81
    "Revolute",
82
    "Cylindrical",
83
    "Slider",
84
    "Distance",
85
]
86

87

88
def solveIfAllowed(assembly, storePrev=False):
89
    if assembly.Type == "Assembly" and Preferences.preferences().GetBool(
90
        "SolveInJointCreation", True
91
    ):
92
        assembly.solve(storePrev)
93

94

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.
102
# For example :
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.
108
class Joint:
109
    def __init__(self, joint, type_index):
110
        joint.Proxy = self
111

112
        joint.addProperty(
113
            "App::PropertyEnumeration",
114
            "JointType",
115
            "Joint",
116
            QT_TRANSLATE_NOOP("App::Property", "The type of the joint"),
117
        )
118
        joint.JointType = JointTypes  # sets the list
119
        joint.JointType = JointTypes[type_index]  # set the initial value
120

121
        # First Joint Connector
122
        joint.addProperty(
123
            "App::PropertyString",  # Not PropertyLink because they don't support external objects
124
            "Object1",
125
            "Joint Connector 1",
126
            QT_TRANSLATE_NOOP("App::Property", "The first object of the joint"),
127
        )
128

129
        joint.addProperty(
130
            "App::PropertyLink",
131
            "Part1",
132
            "Joint Connector 1",
133
            QT_TRANSLATE_NOOP("App::Property", "The first part of the joint"),
134
        )
135

136
        joint.addProperty(
137
            "App::PropertyString",
138
            "Element1",
139
            "Joint Connector 1",
140
            QT_TRANSLATE_NOOP("App::Property", "The selected element of the first object"),
141
        )
142

143
        joint.addProperty(
144
            "App::PropertyString",
145
            "Vertex1",
146
            "Joint Connector 1",
147
            QT_TRANSLATE_NOOP("App::Property", "The selected vertex of the first object"),
148
        )
149

150
        joint.addProperty(
151
            "App::PropertyPlacement",
152
            "Placement1",
153
            "Joint Connector 1",
154
            QT_TRANSLATE_NOOP(
155
                "App::Property",
156
                "This is the local coordinate system within object1 that will be used for the joint.",
157
            ),
158
        )
159

160
        joint.addProperty(
161
            "App::PropertyBool",
162
            "Detach1",
163
            "Joint Connector 1",
164
            QT_TRANSLATE_NOOP(
165
                "App::Property",
166
                "This prevents Placement1 from recomputing, enabling custom positioning of the placement.",
167
            ),
168
        )
169

170
        # Second Joint Connector
171
        joint.addProperty(
172
            "App::PropertyString",
173
            "Object2",
174
            "Joint Connector 2",
175
            QT_TRANSLATE_NOOP("App::Property", "The second object of the joint"),
176
        )
177

178
        joint.addProperty(
179
            "App::PropertyLink",
180
            "Part2",
181
            "Joint Connector 2",
182
            QT_TRANSLATE_NOOP("App::Property", "The second part of the joint"),
183
        )
184

185
        joint.addProperty(
186
            "App::PropertyString",
187
            "Element2",
188
            "Joint Connector 2",
189
            QT_TRANSLATE_NOOP("App::Property", "The selected element of the second object"),
190
        )
191

192
        joint.addProperty(
193
            "App::PropertyString",
194
            "Vertex2",
195
            "Joint Connector 2",
196
            QT_TRANSLATE_NOOP("App::Property", "The selected vertex of the second object"),
197
        )
198

199
        joint.addProperty(
200
            "App::PropertyPlacement",
201
            "Placement2",
202
            "Joint Connector 2",
203
            QT_TRANSLATE_NOOP(
204
                "App::Property",
205
                "This is the local coordinate system within object2 that will be used for the joint.",
206
            ),
207
        )
208

209
        joint.addProperty(
210
            "App::PropertyBool",
211
            "Detach2",
212
            "Joint Connector 2",
213
            QT_TRANSLATE_NOOP(
214
                "App::Property",
215
                "This prevents Placement2 from recomputing, enabling custom positioning of the placement.",
216
            ),
217
        )
218

219
        joint.addProperty(
220
            "App::PropertyFloat",
221
            "Distance",
222
            "Joint",
223
            QT_TRANSLATE_NOOP(
224
                "App::Property",
225
                "This is the distance of the joint. It is used only by the distance joint.",
226
            ),
227
        )
228

229
        joint.addProperty(
230
            "App::PropertyFloat",
231
            "Rotation",
232
            "Joint",
233
            QT_TRANSLATE_NOOP(
234
                "App::Property",
235
                "This is the rotation of the joint.",
236
            ),
237
        )
238

239
        joint.addProperty(
240
            "App::PropertyVector",
241
            "Offset",
242
            "Joint",
243
            QT_TRANSLATE_NOOP(
244
                "App::Property",
245
                "This is the offset vector of the joint.",
246
            ),
247
        )
248

249
        joint.addProperty(
250
            "App::PropertyBool",
251
            "Activated",
252
            "Joint",
253
            QT_TRANSLATE_NOOP(
254
                "App::Property",
255
                "This indicates if the joint is active.",
256
            ),
257
        )
258
        joint.Activated = True
259

260
        self.setJointConnectors(joint, [])
261

262
    def dumps(self):
263
        return None
264

265
    def loads(self, state):
266
        return None
267

268
    def getAssembly(self, joint):
269
        return joint.InList[0]
270

271
    def setJointType(self, joint, jointType):
272
        joint.JointType = jointType
273
        joint.Label = jointType.replace(" ", "")
274

275
    def onChanged(self, joint, prop):
276
        """Do something when a property has changed"""
277
        # App.Console.PrintMessage("Change property: " + str(prop) + "\n")
278

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(
286
                    joint,
287
                    obj1,
288
                    joint.Part1,
289
                    obj2,
290
                    joint.Part2,
291
                    False,
292
                )
293

294
                isAssembly = self.getAssembly(joint).Type == "Assembly"
295
                if isAssembly and not presolved:
296
                    solveIfAllowed(self.getAssembly(joint))
297
                else:
298
                    self.updateJCSPlacements(joint)
299

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")
303
        pass
304

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"
309

310
        if len(current_selection) >= 1:
311
            joint.Part1 = None
312
            if isAssembly:
313
                self.part1Connected = assembly.isPartConnected(current_selection[0]["part"])
314
            else:
315
                self.part1Connected = True
316

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
323
            )
324
        else:
325
            joint.Object1 = ""
326
            joint.Part1 = None
327
            joint.Element1 = ""
328
            joint.Vertex1 = ""
329
            joint.Placement1 = App.Placement()
330
            self.partMovedByPresolved = None
331

332
        if len(current_selection) >= 2:
333
            joint.Part2 = None
334
            if isAssembly:
335
                self.part2Connected = assembly.isPartConnected(current_selection[1]["part"])
336
            else:
337
                self.part2Connected = False
338

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
345
            )
346
            self.preSolve(
347
                joint,
348
                current_selection[0]["object"],
349
                joint.Part1,
350
                current_selection[1]["object"],
351
                joint.Part2,
352
            )
353
            if isAssembly:
354
                solveIfAllowed(assembly, True)
355
            else:
356
                self.updateJCSPlacements(joint)
357

358
        else:
359
            joint.Object2 = ""
360
            joint.Part2 = None
361
            joint.Element2 = ""
362
            joint.Vertex2 = ""
363
            joint.Placement2 = App.Placement()
364
            if isAssembly:
365
                assembly.undoSolve()
366
            self.undoPreSolve()
367

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
372
            )
373

374
        if not joint.Detach2:
375
            joint.Placement2 = self.findPlacement(
376
                joint, joint.Object2, joint.Part2, joint.Element2, joint.Vertex2, True
377
            )
378

379
    """
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.
387
    """
388

389
    def findPlacement(self, joint, objName, part, elt, vtx, isSecond=False):
390
        if not objName or not part:
391
            return App.Placement()
392

393
        obj = UtilsAssembly.getObjectInPart(objName, part)
394
        plc = App.Placement()
395

396
        if not obj:
397
            return App.Placement()
398

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

403
        elt_type, elt_index = UtilsAssembly.extract_type_and_number(elt)
404
        vtx_type, vtx_index = UtilsAssembly.extract_type_and_number(vtx)
405

406
        isLine = False
407

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]
413
            curve = edge.Curve
414

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
425
            else:
426
                vertex = obj.Shape.Vertexes[vtx_index - 1]
427
                plc.Base = (vertex.X, vertex.Y, vertex.Z)
428

429
            # Then we find the Rotation
430
            if curve.TypeId == "Part::GeomCircle":
431
                plc.Rotation = App.Rotation(curve.Rotation)
432

433
            if curve.TypeId == "Part::GeomLine":
434
                isLine = True
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
442

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
452
                else:
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]
457
                curve = edge.Curve
458
                if curve.TypeId == "Part::GeomCircle":
459
                    center_point = curve.Location
460
                    plc.Base = (center_point.x, center_point.y, center_point.z)
461

462
                elif (
463
                    surface.TypeId == "Part::GeomCylinder"
464
                    and curve.TypeId == "Part::GeomBSplineCurve"
465
                ):
466
                    # handle special case of 2 cylinder intersecting.
467
                    plc.Base = self.findCylindersIntersection(obj, surface, edge, elt_index)
468

469
            else:
470
                vertex = obj.Shape.Vertexes[vtx_index - 1]
471
                plc.Base = (vertex.X, vertex.Y, vertex.Z)
472

473
            # Then we find the Rotation
474
            if surface.TypeId == "Part::GeomPlane":
475
                plc.Rotation = App.Rotation(surface.Rotation)
476
            else:
477
                plc.Rotation = surface.Rotation
478

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.
482

483
        # change plc to be relative to the object placement.
484
        plc = obj.Placement.inverse() * plc
485

486
        # post-process of plc for some special cases
487
        if elt_type == "Vertex":
488
            plc.Rotation = App.Rotation()
489
        elif isLine:
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)
494

495
        # change plc to be relative to the origin of the document.
496
        # global_plc = UtilsAssembly.getGlobalPlacement(obj, part)
497
        # plc = global_plc * plc
498

499
        # change plc to be relative to the assembly.
500
        # assembly = self.getAssembly(joint)
501
        # plc = assembly.Placement.inverse() * plc
502

503
        # We apply rotation / reverse / offset it necessary, but only to the second JCS.
504
        if isSecond:
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)
509

510
        return plc
511

512
    def applyOffsetToPlacement(self, plc, offset):
513
        plc.Base = plc.Base + plc.Rotation.multVec(offset)
514
        return plc
515

516
    def applyRotationToPlacement(self, plc, angle):
517
        return self.applyRotationToPlacementAlongAxis(plc, angle, App.Vector(0, 0, 1))
518

519
    def applyRotationToPlacementAlongAxis(self, plc, angle, axis):
520
        rot = plc.Rotation
521
        zRotation = App.Rotation(axis, angle)
522
        plc.Rotation = rot * zRotation
523
        return plc
524

525
    def flipPlacement(self, plc):
526
        return self.applyRotationToPlacementAlongAxis(plc, 180, App.Vector(1, 0, 0))
527

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
532
            )
533
            globalJcsPlc = UtilsAssembly.getJcsGlobalPlc(
534
                joint.Placement2, joint.Object2, joint.Part2
535
            )
536
            jcsPlc = self.flipPlacement(jcsPlc)
537
            joint.Part2.Placement = globalJcsPlc * jcsPlc.inverse()
538

539
        else:
540
            jcsPlc = UtilsAssembly.getJcsPlcRelativeToPart(
541
                joint.Placement1, joint.Object1, joint.Part1
542
            )
543
            globalJcsPlc = UtilsAssembly.getJcsGlobalPlc(
544
                joint.Placement1, joint.Object1, joint.Part1
545
            )
546
            jcsPlc = self.flipPlacement(jcsPlc)
547
            joint.Part1.Placement = globalJcsPlc * jcsPlc.inverse()
548

549
        solveIfAllowed(self.getAssembly(joint))
550

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":
555
                continue
556

557
            for edgej in facej.Edges:
558
                if (
559
                    edgej.Curve.TypeId == "Part::GeomBSplineCurve"
560
                    and edgej.CenterOfGravity == edge.CenterOfGravity
561
                    and edgej.Length == edge.Length
562
                ):
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)
566

567
                    res = line1.intersect(line2, Part.Precision.confusion())
568

569
                    if res:
570
                        return App.Vector(res[0].X, res[0].Y, res[0].Z)
571
        return surface.Center
572

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.
575

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

580
        if hasattr(self, "part2Connected") and not self.part2Connected:
581
            if savePlc:
582
                self.partMovedByPresolved = joint.Part2
583
                self.presolveBackupPlc = joint.Part2.Placement
584

585
            globalJcsPlc1 = UtilsAssembly.getJcsGlobalPlc(
586
                joint.Placement1, joint.Object1, joint.Part1
587
            )
588
            jcsPlc2 = UtilsAssembly.getJcsPlcRelativeToPart(
589
                joint.Placement2, joint.Object2, joint.Part2
590
            )
591
            if not sameDir:
592
                jcsPlc2 = self.flipPlacement(jcsPlc2)
593
            joint.Part2.Placement = globalJcsPlc1 * jcsPlc2.inverse()
594
            return True
595

596
        elif hasattr(self, "part1Connected") and not self.part1Connected:
597
            if savePlc:
598
                self.partMovedByPresolved = joint.Part1
599
                self.presolveBackupPlc = joint.Part1.Placement
600

601
            globalJcsPlc2 = UtilsAssembly.getJcsGlobalPlc(
602
                joint.Placement2, joint.Object2, joint.Part2
603
            )
604
            jcsPlc1 = UtilsAssembly.getJcsPlcRelativeToPart(
605
                joint.Placement1, joint.Object1, joint.Part1
606
            )
607
            if not sameDir:
608
                jcsPlc1 = self.flipPlacement(jcsPlc1)
609
            joint.Part1.Placement = globalJcsPlc2 * jcsPlc1.inverse()
610
            return True
611
        return False
612

613
    def undoPreSolve(self):
614
        if self.partMovedByPresolved:
615
            self.partMovedByPresolved.Placement = self.presolveBackupPlc
616
            self.partMovedByPresolved = None
617

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

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
625

626

627
class ViewProviderJoint:
628
    def __init__(self, vobj):
629
        """Set this object to the proxy object of the actual view provider"""
630

631
        vobj.Proxy = self
632

633
    def attach(self, vobj):
634
        """Setup the scene sub-graph of the view provider, this method is mandatory"""
635
        self.axis_thickness = 3
636

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

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

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

656
        self.app_obj = vobj.Object
657

658
        self.transform1 = coin.SoTransform()
659
        self.transform2 = coin.SoTransform()
660
        self.transform3 = coin.SoTransform()
661

662
        scaleF = self.get_JCS_size()
663
        self.axisScale = coin.SoScale()
664
        self.axisScale.scaleFactor.setValue(scaleF, scaleF, scaleF)
665

666
        self.draw_style = coin.SoDrawStyle()
667
        self.draw_style.style = coin.SoDrawStyle.LINES
668
        self.draw_style.lineWidth = self.axis_thickness
669

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

674
        self.pick = coin.SoPickStyle()
675
        self.setPickableState(True)
676

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")
683

684
    def camera_callback(self, *args):
685
        scaleF = self.get_JCS_size()
686
        self.axisScale.scaleFactor.setValue(scaleF, scaleF, scaleF)
687

688
    def JCS_sep(self, soTransform):
689
        JCS = coin.SoAnnotation()
690
        JCS.addChild(soTransform)
691

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

697
        JCS.addChild(base_plane_sep)
698
        JCS.addChild(X_axis_sep)
699
        JCS.addChild(Y_axis_sep)
700
        JCS.addChild(Z_axis_sep)
701

702
        switch_JCS = coin.SoSwitch()
703
        switch_JCS.addChild(JCS)
704
        switch_JCS.whichChild = coin.SO_SWITCH_NONE
705
        return switch_JCS
706

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])
712

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)
719
        return axis_sep
720

721
    def plane_sep(self, size, num_vertices):
722
        coords = coin.SoCoordinate3()
723

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

730
        face = coin.SoFaceSet()
731
        face.numVertices.setValue(num_vertices)
732

733
        transform = coin.SoTransform()
734
        transform.translation.setValue(0, 0, 0)
735

736
        draw_style = coin.SoDrawStyle()
737
        draw_style.style = coin.SoDrawStyle.FILLED
738

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

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)
753
        return face_sep
754

755
    def get_JCS_size(self):
756
        camera = Gui.ActiveDocument.ActiveView.getCameraNode()
757

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
763
        else:
764
            # Default value if camera type is unknown
765
            return 10
766

767
        return camera.height.getValue() / 20
768

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
774

775
        t = placement.Base
776
        soTransform.translation.setValue(t.x, t.y, t.z)
777

778
        r = placement.Rotation.Q
779
        soTransform.rotation.setValue(r[0], r[1], r[2], r[3])
780

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":
785
            if joint.Object1:
786
                plc = joint.Placement1
787
                self.switch_JCS1.whichChild = coin.SO_SWITCH_ALL
788

789
                if joint.Part1:
790
                    self.set_JCS_placement(self.transform1, plc, joint.Object1, joint.Part1)
791
            else:
792
                self.switch_JCS1.whichChild = coin.SO_SWITCH_NONE
793

794
        if prop == "Placement2":
795
            if joint.Object2:
796
                plc = joint.Placement2
797
                self.switch_JCS2.whichChild = coin.SO_SWITCH_ALL
798

799
                if joint.Part2:
800
                    self.set_JCS_placement(self.transform2, plc, joint.Object2, joint.Part2)
801
            else:
802
                self.switch_JCS2.whichChild = coin.SO_SWITCH_NONE
803

804
    def showPreviewJCS(self, visible, placement=None, objName="", part=None):
805
        if visible:
806
            self.switch_JCS_preview.whichChild = coin.SO_SWITCH_ALL
807
            self.set_JCS_placement(self.transform3, placement, objName, part)
808
        else:
809
            self.switch_JCS_preview.whichChild = coin.SO_SWITCH_NONE
810

811
    def setPickableState(self, state: bool):
812
        """Set JCS selectable or unselectable in 3D view"""
813
        if not state:
814
            self.pick.style.setValue(coin.SoPickStyle.UNPICKABLE)
815
        else:
816
            self.pick.style.setValue(coin.SoPickStyle.SHAPE_ON_TOP)
817

818
    def getDisplayModes(self, obj):
819
        """Return a list of display modes."""
820
        modes = []
821
        modes.append("Wireframe")
822
        return modes
823

824
    def getDefaultDisplayMode(self):
825
        """Return the name of the default display mode. It must be defined in getDisplayModes."""
826
        return "Wireframe"
827

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])
840

841
    def getIcon(self):
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"
854

855
        return ":/icons/Assembly_CreateJoint.svg"
856

857
    def dumps(self):
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."""
861
        return None
862

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."""
866
        return None
867

868
    def doubleClicked(self, vobj):
869
        assembly = vobj.Object.InList[0]
870
        if UtilsAssembly.activeAssembly() != assembly:
871
            Gui.ActiveDocument.setEdit(assembly)
872

873
        panel = TaskAssemblyCreateJoint(0, vobj.Object)
874
        Gui.Control.showDialog(panel)
875

876

877
################ Grounded Joint object #################
878

879

880
class GroundedJoint:
881
    def __init__(self, joint, obj_to_ground):
882
        joint.Proxy = self
883
        self.joint = joint
884

885
        joint.addProperty(
886
            "App::PropertyLink",
887
            "ObjectToGround",
888
            "Ground",
889
            QT_TRANSLATE_NOOP("App::Property", "The object to ground"),
890
        )
891

892
        joint.ObjectToGround = obj_to_ground
893

894
        joint.addProperty(
895
            "App::PropertyPlacement",
896
            "Placement",
897
            "Ground",
898
            QT_TRANSLATE_NOOP(
899
                "App::Property",
900
                "This is where the part is grounded.",
901
            ),
902
        )
903

904
        joint.Placement = obj_to_ground.Placement
905

906
    def dumps(self):
907
        return None
908

909
    def loads(self, state):
910
        return None
911

912
    def onChanged(self, fp, prop):
913
        """Do something when a property has changed"""
914
        # App.Console.PrintMessage("Change property: " + str(prop) + "\n")
915
        pass
916

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")
920
        pass
921

922

923
class ViewProviderGroundedJoint:
924
    def __init__(self, obj):
925
        """Set this object to the proxy object of the actual view provider"""
926
        obj.Proxy = self
927

928
    def attach(self, obj):
929
        """Setup the scene sub-graph of the view provider, this method is mandatory"""
930
        pass
931

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
935
        pass
936

937
    def getDisplayModes(self, obj):
938
        """Return a list of display modes."""
939
        modes = ["Wireframe"]
940
        return modes
941

942
    def getDefaultDisplayMode(self):
943
        """Return the name of the default display mode. It must be defined in getDisplayModes."""
944
        return "Wireframe"
945

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")
949
        pass
950

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]
957

958
        return True  # If False is returned the object won't be deleted
959

960
    def getIcon(self):
961
        return ":/icons/Assembly_ToggleGrounded.svg"
962

963

964
class MakeJointSelGate:
965
    def __init__(self, taskbox, assembly):
966
        self.taskbox = taskbox
967
        self.assembly = assembly
968

969
    def allow(self, doc, obj, sub):
970
        if not sub:
971
            return False
972

973
        objs_names, element_name = UtilsAssembly.getObjsNamesAndElement(obj.Name, sub)
974

975
        if self.assembly.Name not in objs_names:
976
            # Only objects within the assembly.
977
            return False
978

979
        if Gui.Selection.isSelected(obj, sub, Gui.Selection.ResolveMode.NoResolve):
980
            # If it's to deselect then it's ok
981
            return True
982

983
        if len(self.taskbox.current_selection) >= 2:
984
            # No more than 2 elements can be selected for basic joints.
985
            return False
986

987
        full_obj_name = ".".join(objs_names)
988
        full_element_name = full_obj_name + "." + element_name
989
        selected_object = UtilsAssembly.getObject(full_element_name)
990

991
        part_containing_selected_object = UtilsAssembly.getContainingPart(
992
            full_element_name, selected_object, self.assembly
993
        )
994

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.
998
                return False
999

1000
        return True
1001

1002

1003
activeTask = None
1004

1005

1006
class TaskAssemblyCreateJoint(QtCore.QObject):
1007
    def __init__(self, jointTypeIndex, jointObj=None):
1008
        super().__init__()
1009

1010
        global activeTask
1011
        activeTask = self
1012

1013
        self.assembly = UtilsAssembly.activeAssembly()
1014
        if not self.assembly:
1015
            self.assembly = UtilsAssembly.activePart()
1016
            self.activeType = "Part"
1017
        else:
1018
            self.activeType = "Assembly"
1019

1020
        self.view = Gui.activeDocument().activeView()
1021
        self.doc = App.ActiveDocument
1022

1023
        if not self.assembly or not self.view or not self.doc:
1024
            return
1025

1026
        if self.activeType == "Assembly":
1027
            self.assembly.ViewObject.EnableMovement = False
1028

1029
        self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateJoint.ui")
1030

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

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

1043
        if jointObj:
1044
            Gui.Selection.clearSelection()
1045
            self.creating = False
1046
            self.joint = jointObj
1047
            self.jointName = jointObj.Label
1048
            App.setActiveTransaction("Edit " + self.jointName + " Joint")
1049

1050
            self.updateTaskboxFromJoint()
1051
            self.visibilityBackup = self.joint.Visibility
1052
            self.joint.Visibility = True
1053

1054
        else:
1055
            self.creating = True
1056
            self.jointName = self.form.jointType.currentText().replace(" ", "")
1057
            if self.activeType == "Part":
1058
                App.setActiveTransaction("Transform")
1059
            else:
1060
                App.setActiveTransaction("Create " + self.jointName + " Joint")
1061

1062
            self.current_selection = []
1063
            self.preselection_dict = None
1064

1065
            self.createJointObject()
1066
            self.visibilityBackup = False
1067
            self.handleInitialSelection()
1068

1069
        self.toggleDistanceVisibility()
1070
        self.toggleOffsetVisibility()
1071
        self.toggleRotationVisibility()
1072
        self.toggleReverseVisibility()
1073

1074
        self.setJointsPickableState(False)
1075

1076
        Gui.Selection.addSelectionGate(
1077
            MakeJointSelGate(self, self.assembly), Gui.Selection.ResolveMode.NoResolve
1078
        )
1079
        Gui.Selection.addObserver(self, Gui.Selection.ResolveMode.NoResolve)
1080
        Gui.Selection.setSelectionStyle(Gui.Selection.SelectionStyle.GreedySelection)
1081

1082
        self.callbackMove = self.view.addEventCallback("SoLocation2Event", self.moveMouse)
1083
        self.callbackKey = self.view.addEventCallback("SoKeyboardEvent", self.KeyboardEvent)
1084

1085
    def accept(self):
1086
        if len(self.current_selection) != 2:
1087
            App.Console.PrintWarning("You need to select 2 elements from 2 separate parts.")
1088
            return False
1089

1090
        self.deactivate()
1091

1092
        solveIfAllowed(self.assembly)
1093
        if self.activeType == "Assembly":
1094
            self.joint.Visibility = self.visibilityBackup
1095
        else:
1096
            self.joint.Document.removeObject(self.joint.Name)
1097

1098
        App.closeActiveTransaction()
1099
        return True
1100

1101
    def reject(self):
1102
        self.deactivate()
1103
        App.closeActiveTransaction(True)
1104
        if not self.creating:  # update visibility only if we are editing the joint
1105
            self.joint.Visibility = self.visibilityBackup
1106
        return True
1107

1108
    def deactivate(self):
1109
        global activeTask
1110
        activeTask = None
1111

1112
        if self.activeType == "Assembly":
1113
            self.assembly.clearUndo()
1114
            self.assembly.ViewObject.EnableMovement = True
1115

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

1126
    def handleInitialSelection(self):
1127
        selection = Gui.Selection.getSelectionEx("*", 0)
1128
        if not selection:
1129
            return
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.
1133

1134
            if not sel.SubElementNames:
1135
                # no subnames, so its a root assembly itself that is selected.
1136
                Gui.Selection.removeSelection(sel.Object)
1137
                continue
1138

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
1143
                )
1144
                if self.assembly.Name not in objs_names:
1145
                    Gui.Selection.removeSelection(sel.Object, sub_name)
1146
                    continue
1147

1148
                obj_name = sel.ObjectName
1149

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
1156
                )
1157

1158
                if selected_object == self.assembly:
1159
                    # do not accept selection of assembly itself
1160
                    Gui.Selection.removeSelection(sel.Object, sub_name)
1161
                    continue
1162

1163
                if (
1164
                    len(self.current_selection) == 1
1165
                    and selected_object == self.current_selection[0]["object"]
1166
                ):
1167
                    # do not select several feature of the same object.
1168
                    self.current_selection.clear()
1169
                    Gui.Selection.clearSelection()
1170
                    return
1171

1172
                selection_dict = {
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,
1179
                }
1180

1181
                self.current_selection.append(selection_dict)
1182

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()
1187
        else:
1188
            self.updateJoint()
1189

1190
    def createJointObject(self):
1191
        type_index = self.form.jointType.currentIndex()
1192

1193
        if self.activeType == "Part":
1194
            self.joint = self.assembly.newObject("App::FeaturePython", "Temporary joint")
1195
        else:
1196
            joint_group = UtilsAssembly.getJointGroup(self.assembly)
1197
            self.joint = joint_group.newObject("App::FeaturePython", self.jointName)
1198

1199
        Joint(self.joint, type_index)
1200
        ViewProviderJoint(self.joint.ViewObject)
1201

1202
    def onJointTypeChanged(self, index):
1203

1204
        self.joint.Proxy.setJointType(self.joint, JointTypes[self.form.jointType.currentIndex()])
1205
        self.toggleDistanceVisibility()
1206
        self.toggleOffsetVisibility()
1207
        self.toggleRotationVisibility()
1208
        self.toggleReverseVisibility()
1209

1210
    def onDistanceChanged(self, quantity):
1211
        self.joint.Distance = self.form.distanceSpinbox.property("rawValue")
1212

1213
    def onOffsetChanged(self, quantity):
1214
        self.joint.Offset = App.Vector(0, 0, self.form.offsetSpinbox.property("rawValue"))
1215

1216
    def onRotationChanged(self, quantity):
1217
        self.joint.Rotation = self.form.rotationSpinbox.property("rawValue")
1218

1219
    def onReverseClicked(self):
1220
        self.joint.Proxy.flipOnePart(self.joint)
1221

1222
    def toggleDistanceVisibility(self):
1223
        if JointTypes[self.form.jointType.currentIndex()] in JointUsingDistance:
1224
            self.form.distanceLabel.show()
1225
            self.form.distanceSpinbox.show()
1226
        else:
1227
            self.form.distanceLabel.hide()
1228
            self.form.distanceSpinbox.hide()
1229

1230
    def toggleOffsetVisibility(self):
1231
        if JointTypes[self.form.jointType.currentIndex()] in JointUsingOffset:
1232
            self.form.offsetLabel.show()
1233
            self.form.offsetSpinbox.show()
1234
        else:
1235
            self.form.offsetLabel.hide()
1236
            self.form.offsetSpinbox.hide()
1237

1238
    def toggleRotationVisibility(self):
1239
        if JointTypes[self.form.jointType.currentIndex()] in JointUsingRotation:
1240
            self.form.rotationLabel.show()
1241
            self.form.rotationSpinbox.show()
1242
        else:
1243
            self.form.rotationLabel.hide()
1244
            self.form.rotationSpinbox.hide()
1245

1246
    def toggleReverseVisibility(self):
1247
        if JointTypes[self.form.jointType.currentIndex()] in JointUsingReverse:
1248
            self.form.PushButtonReverse.show()
1249
        else:
1250
            self.form.PushButtonReverse.hide()
1251

1252
    def updateTaskboxFromJoint(self):
1253
        self.current_selection = []
1254
        self.preselection_dict = None
1255

1256
        obj1 = UtilsAssembly.getObjectInPart(self.joint.Object1, self.joint.Part1)
1257
        obj2 = UtilsAssembly.getObjectInPart(self.joint.Object2, self.joint.Part2)
1258

1259
        selection_dict1 = {
1260
            "object": obj1,
1261
            "part": self.joint.Part1,
1262
            "element_name": self.joint.Element1,
1263
            "vertex_name": self.joint.Vertex1,
1264
        }
1265

1266
        selection_dict2 = {
1267
            "object": obj2,
1268
            "part": self.joint.Part2,
1269
            "element_name": self.joint.Element2,
1270
            "vertex_name": self.joint.Vertex2,
1271
        }
1272

1273
        self.current_selection.append(selection_dict1)
1274
        self.current_selection.append(selection_dict2)
1275

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

1284
        elName = self.getSubnameForSelection(obj2, self.joint.Part2, self.joint.Element2)
1285
        Gui.Selection.addSelection(self.doc.Name, self.joint.Part2.Name, elName)
1286

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

1291
        self.form.jointType.setCurrentIndex(JointTypes.index(self.joint.JointType))
1292
        self.updateJointList()
1293

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
1299

1300
        if obj is None or part is None:
1301
            return elName
1302

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
1309

1310
        if obj != part and obj in part.OutListRecursive:
1311
            bSub = ""
1312
            currentObj = part
1313

1314
            limit = 0
1315
            while limit < 1000:
1316
                limit = limit + 1
1317

1318
                if currentObj != part:
1319
                    if bSub != "":
1320
                        bSub = bSub + "."
1321
                    bSub = bSub + currentObj.Name
1322

1323
                if currentObj == obj:
1324
                    break
1325

1326
                if currentObj.TypeId == "App::Link":
1327
                    currentObj = currentObj.getLinkedObject()
1328

1329
                for obji in currentObj.OutList:
1330
                    if obji == obj or obj in obji.OutListRecursive:
1331
                        currentObj = obji
1332
                        break
1333

1334
            elName = bSub + "." + elName
1335
        return elName
1336

1337
    def updateJoint(self):
1338
        # First we build the listwidget
1339
        self.updateJointList()
1340

1341
        # Then we pass the new list to the join object
1342
        self.joint.Proxy.setJointConnectors(self.joint, self.current_selection)
1343

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

1354
    def moveMouse(self, info):
1355
        if len(self.current_selection) >= 2 or (
1356
            len(self.current_selection) == 1
1357
            and (
1358
                not self.preselection_dict
1359
                or self.current_selection[0]["part"] == self.preselection_dict["part"]
1360
            )
1361
        ):
1362
            self.joint.ViewObject.Proxy.showPreviewJCS(False)
1363
            return
1364

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'}
1368

1369
        if (
1370
            not cursor_info
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
1375
        ):
1376
            self.joint.ViewObject.Proxy.showPreviewJCS(False)
1377
            return
1378

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
1380

1381
        newPos = App.Vector(cursor_info["x"], cursor_info["y"], cursor_info["z"])
1382
        self.preselection_dict["mouse_pos"] = newPos
1383

1384
        if self.preselection_dict["element_name"] == "":
1385
            self.preselection_dict["vertex_name"] = ""
1386
        else:
1387
            self.preselection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(
1388
                self.preselection_dict
1389
            )
1390

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(
1395
            self.joint,
1396
            objName,
1397
            part,
1398
            self.preselection_dict["element_name"],
1399
            self.preselection_dict["vertex_name"],
1400
            isSecond,
1401
        )
1402
        self.joint.ViewObject.Proxy.showPreviewJCS(True, placement, objName, part)
1403
        self.previewJCSVisible = True
1404

1405
    # 3D view keyboard handler
1406
    def KeyboardEvent(self, info):
1407
        if info["State"] == "UP" and info["Key"] == "ESCAPE":
1408
            self.reject()
1409

1410
        if info["State"] == "UP" and info["Key"] == "RETURN":
1411
            self.accept()
1412

1413
    def getContainingPart(self, full_element_name, obj):
1414
        return UtilsAssembly.getContainingPart(full_element_name, obj, self.assembly)
1415

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

1424
        selection_dict = {
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]),
1431
        }
1432
        if element_name == "":
1433
            selection_dict["vertex_name"] = ""
1434
        else:
1435
            selection_dict["vertex_name"] = UtilsAssembly.findElementClosestVertex(selection_dict)
1436

1437
        self.current_selection.append(selection_dict)
1438
        self.updateJoint()
1439

1440
        # We hide the preview JCS if we just added to the selection
1441
        self.joint.ViewObject.Proxy.showPreviewJCS(False)
1442

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

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
1454
                break
1455

1456
        if selection_dict_to_remove is not None:
1457
            self.current_selection.remove(selection_dict_to_remove)
1458

1459
        self.updateJoint()
1460

1461
    def setPreselection(self, doc_name, obj_name, sub_name):
1462
        if not sub_name:
1463
            self.preselection_dict = None
1464
            return
1465

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

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,
1479
        }
1480

1481
    def clearSelection(self, doc_name):
1482
        self.current_selection.clear()
1483
        self.updateJoint()
1484

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)
1492
        else:
1493
            for obj in self.assembly.OutList:
1494
                if obj.TypeId == "App::FeaturePython" and hasattr(obj, "JointType"):
1495
                    obj.ViewObject.Proxy.setPickableState(state)
1496

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.