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
# **************************************************************************/
28
import FreeCADGui as Gui
30
import PySide.QtCore as QtCore
31
import PySide.QtGui as QtGui
34
# translate = App.Qt.translate
36
__title__ = "Assembly utilitary functions"
38
__url__ = "https://www.freecad.org"
42
doc = Gui.ActiveDocument
44
if doc is None or doc.ActiveView is None:
47
active_assembly = doc.ActiveView.getActiveObject("part")
49
if active_assembly is not None and active_assembly.Type == "Assembly":
50
return active_assembly
56
doc = Gui.ActiveDocument
58
if doc is None or doc.ActiveView is None:
61
active_part = doc.ActiveView.getActiveObject("part")
63
if active_part is not None and active_part.Type != "Assembly":
69
def isAssemblyCommandActive():
70
return activeAssembly() is not None and not Gui.Control.activeDialog()
73
def isDocTemporary(doc):
74
# Guard against older versions of FreeCad which don't have the Temporary attribute
77
except AttributeError:
82
def assembly_has_at_least_n_parts(n):
83
assembly = activeAssembly()
86
assembly = activePart()
89
for obj in assembly.OutList:
90
# note : groundedJoints comes in the outlist so we filter those out.
91
if hasattr(obj, "Placement") and not hasattr(obj, "ObjectToGround"):
98
def getObject(full_name):
99
# full_name is "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBox.Edge16"
100
# or "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBody.pad.Edge16"
101
# or "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBody.Local_CS.X"
102
# We want either LinkOrBody or LinkOrBox or Local_CS.
103
names = full_name.split(".")
104
doc = App.ActiveDocument
107
App.Console.PrintError(
108
"getObject() in UtilsAssembly.py the object name is too short, at minimum it should be something like 'Assembly.Box.edge16'. It shouldn't be shorter"
114
for i, objName in enumerate(names):
116
prevObj = doc.getObject(objName)
117
if prevObj.TypeId == "App::Link":
118
prevObj = prevObj.getLinkedObject()
122
if prevObj.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}:
123
for obji in prevObj.OutList:
124
if obji.Name == objName:
131
# the last is the element name. So if we are at the last but one name, then it must be the selected
132
if i == len(names) - 2:
135
if obj.TypeId == "App::Link":
136
linked_obj = obj.getLinkedObject()
137
if linked_obj.TypeId == "PartDesign::Body":
138
if i + 1 < len(names):
140
for obji in linked_obj.OutList:
141
if obji.Name == names[i + 1]:
144
if obj2 and isBodySubObject(obj2.TypeId):
147
elif linked_obj.isDerivedFrom("Part::Feature"):
153
elif obj.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}:
157
elif obj.TypeId == "PartDesign::Body":
158
if i + 1 < len(names):
160
for obji in obj.OutList:
161
if obji.Name == names[i + 1]:
164
if obj2 and isBodySubObject(obj2.TypeId):
168
elif obj.isDerivedFrom("Part::Feature"):
169
# primitive, fastener, gear ...
175
def isBodySubObject(typeId):
177
typeId == "Sketcher::SketchObject"
178
or typeId == "PartDesign::Point"
179
or typeId == "PartDesign::Line"
180
or typeId == "PartDesign::Plane"
181
or typeId == "PartDesign::CoordinateSystem"
185
def getContainingPart(full_name, selected_object, activeAssemblyOrPart=None):
186
# full_name is "Assembly.Assembly1.LinkOrPart1.LinkOrBox.Edge16" -> LinkOrPart1
187
# or "Assembly.Assembly1.LinkOrPart1.LinkOrBody.pad.Edge16" -> LinkOrPart1
188
# or "Assembly.Assembly1.LinkOrPart1.LinkOrBody.Sketch.Edge1" -> LinkOrBody
190
if selected_object is None:
191
App.Console.PrintError("getContainingPart() in UtilsAssembly.py selected_object is None")
194
names = full_name.split(".")
195
doc = App.ActiveDocument
197
App.Console.PrintError(
198
"getContainingPart() in UtilsAssembly.py the object name is too short, at minimum it should be something like 'Assembly.Box.edge16'. It shouldn't be shorter"
202
for objName in names:
203
obj = doc.getObject(objName)
208
if obj == selected_object:
209
return selected_object
211
if obj.TypeId == "PartDesign::Body" and isBodySubObject(selected_object.TypeId):
212
if selected_object in obj.OutListRecursive:
215
# Note here we may want to specify a specific behavior for Assembly::AssemblyObject.
216
if obj.TypeId == "App::Part":
217
if selected_object in obj.OutListRecursive:
218
if not activeAssemblyOrPart:
220
elif activeAssemblyOrPart in obj.OutListRecursive or obj == activeAssemblyOrPart:
225
elif obj.TypeId == "App::Link":
226
linked_obj = obj.getLinkedObject()
227
if linked_obj.TypeId == "PartDesign::Body" and isBodySubObject(selected_object.TypeId):
228
if selected_object in linked_obj.OutListRecursive:
230
if linked_obj.TypeId == "App::Part":
231
# linked_obj_doc = linked_obj.Document
232
# selected_obj_in_doc = doc.getObject(selected_object.Name)
233
if selected_object in linked_obj.OutListRecursive:
234
if not activeAssemblyOrPart:
236
elif (linked_obj.Document == activeAssemblyOrPart.Document) and (
237
activeAssemblyOrPart in linked_obj.OutListRecursive
238
or linked_obj == activeAssemblyOrPart
244
# no container found so we return the object itself.
245
return selected_object
248
def getObjectInPart(objName, part):
249
if part.Name == objName:
252
if part.TypeId == "App::Link":
253
part = part.getLinkedObject()
257
"Assembly::AssemblyObject",
258
"App::DocumentObjectGroup",
261
for obji in part.OutListRecursive:
262
if obji.Name == objName:
268
# get the placement of Obj relative to its containing Part
269
# Example : assembly.part1.part2.partn.body1 : placement of Obj relative to part1
270
def getObjPlcRelativeToPart(objName, part):
271
obj = getObjectInPart(objName, part)
273
# we need plc to be relative to the containing part
274
obj_global_plc = getGlobalPlacement(obj, part)
275
part_global_plc = getGlobalPlacement(part)
277
return part_global_plc.inverse() * obj_global_plc
280
# Example : assembly.part1.part2.partn.body1 : jcsPlc is relative to body1
281
# This function returns jcsPlc relative to part1
282
def getJcsPlcRelativeToPart(jcsPlc, objName, part):
283
obj_relative_plc = getObjPlcRelativeToPart(objName, part)
284
return obj_relative_plc * jcsPlc
287
# Return the jcs global placement
288
def getJcsGlobalPlc(jcsPlc, objName, part):
289
obj = getObjectInPart(objName, part)
291
obj_global_plc = getGlobalPlacement(obj, part)
292
return obj_global_plc * jcsPlc
295
# The container is used to support cases where the same object appears at several places
296
# which happens when you have a link to a part.
297
def getGlobalPlacement(targetObj, container=None):
298
if targetObj is None:
299
return App.Placement()
301
inContainerBranch = container is None
302
for rootObj in App.activeDocument().RootObjects:
303
foundPlacement = getTargetPlacementRelativeTo(
304
targetObj, rootObj, container, inContainerBranch
306
if foundPlacement is not None:
307
return foundPlacement
309
return App.Placement()
312
def isThereOneRootAssembly():
313
for part in App.activeDocument().RootObjects:
314
if part.TypeId == "Assembly::AssemblyObject":
319
def getTargetPlacementRelativeTo(
320
targetObj, part, container, inContainerBranch, ignorePlacement=False
322
inContainerBranch = inContainerBranch or (not ignorePlacement and part == container)
324
if targetObj == part and inContainerBranch and not ignorePlacement:
325
return targetObj.Placement
327
if part.TypeId == "App::DocumentObjectGroup":
328
for obj in part.OutList:
329
foundPlacement = getTargetPlacementRelativeTo(
330
targetObj, obj, container, inContainerBranch, ignorePlacement
332
if foundPlacement is not None:
333
return foundPlacement
335
elif part.TypeId in {"App::Part", "Assembly::AssemblyObject", "PartDesign::Body"}:
336
for obj in part.OutList:
337
foundPlacement = getTargetPlacementRelativeTo(
338
targetObj, obj, container, inContainerBranch
340
if foundPlacement is None:
343
# If we were called from a link then we need to ignore this placement as we use the link placement instead.
344
if not ignorePlacement:
345
foundPlacement = part.Placement * foundPlacement
347
return foundPlacement
349
elif part.TypeId == "App::Link":
350
linked_obj = part.getLinkedObject()
351
if part == linked_obj or linked_obj is None:
352
return None # upon loading this can happen for external links.
354
if linked_obj.TypeId in {"App::Part", "Assembly::AssemblyObject", "PartDesign::Body"}:
355
for obj in linked_obj.OutList:
356
foundPlacement = getTargetPlacementRelativeTo(
357
targetObj, obj, container, inContainerBranch
359
if foundPlacement is None:
362
foundPlacement = part.Placement * foundPlacement
363
return foundPlacement
365
foundPlacement = getTargetPlacementRelativeTo(
366
targetObj, linked_obj, container, inContainerBranch, True
369
if foundPlacement is not None and not ignorePlacement:
370
foundPlacement = part.Placement * foundPlacement
372
return foundPlacement
377
def getElementName(full_name):
378
# full_name is "Assembly.Assembly1.Assembly2.Assembly3.Box.Edge16"
379
# We want either Edge16.
380
parts = full_name.split(".")
383
# At minimum "Assembly.Box.edge16". It shouldn't be shorter
386
# case of PartDesign datums : CoordinateSystem, point, line, plane
387
if parts[-1] in {"X", "Y", "Z", "Point", "Line", "Plane"}:
393
def getObjsNamesAndElement(obj_name, sub_name):
394
# if obj_name = "Assembly" and sub_name = "Assembly1.Assembly2.Assembly3.Box.Edge16"
395
# this will return ["Assembly","Assembly1","Assembly2","Assembly3","Box"] and "Edge16"
397
parts = sub_name.split(".")
399
# The last part is always the element name even if empty
400
element_name = parts[-1]
402
# The remaining parts are object names
403
obj_names = parts[:-1]
404
obj_names.insert(0, obj_name)
406
return obj_names, element_name
409
def getFullObjName(obj_name, sub_name):
410
# if obj_name = "Assembly" and sub_name = "Assembly1.Assembly2.Assembly3.Box.Edge16"
411
# this will return "Assembly.Assembly1.Assembly2.Assembly3.Box"
412
objs_names, element_name = getObjsNamesAndElement(obj_name, sub_name)
413
return ".".join(objs_names)
416
def getFullElementName(obj_name, sub_name):
417
# if obj_name = "Assembly" and sub_name = "Assembly1.Assembly2.Assembly3.Box.Edge16"
418
# this will return "Assembly.Assembly1.Assembly2.Assembly3.Box.Edge16"
419
return obj_name + "." + sub_name
422
def extract_type_and_number(element_name):
426
for char in element_name:
428
# If the character is a letter, it's part of the type
431
# If the character is a digit, it's part of the number
432
element_number += char
436
if element_type and element_number:
437
element_number = int(element_number)
438
return element_type, element_number
443
def findElementClosestVertex(selection_dict):
444
obj = selection_dict["object"]
446
mousePos = selection_dict["mouse_pos"]
448
# We need mousePos to be relative to the part containing obj global placement
449
if selection_dict["object"] != selection_dict["part"]:
450
plc = App.Placement()
452
global_plc = getGlobalPlacement(selection_dict["part"])
453
plc = global_plc.inverse() * plc
456
elt_type, elt_index = extract_type_and_number(selection_dict["element_name"])
458
if elt_type == "Vertex":
459
return selection_dict["element_name"]
461
elif elt_type == "Edge":
462
edge = obj.Shape.Edges[elt_index - 1]
464
if curve.TypeId == "Part::GeomCircle":
465
# For centers, as they are not shape vertexes, we return the element name.
466
# For now we only allow selecting the center of arcs / circles.
467
return selection_dict["element_name"]
469
edge_points = getPointsFromVertexes(edge.Vertexes)
471
if curve.TypeId == "Part::GeomLine":
472
# For lines we allow users to select the middle of lines as well.
473
line_middle = (edge_points[0] + edge_points[1]) * 0.5
474
edge_points.append(line_middle)
476
closest_vertex_index, _ = findClosestPointToMousePos(edge_points, mousePos)
478
if curve.TypeId == "Part::GeomLine" and closest_vertex_index == 2:
479
# If line center is closest then we have no vertex name to set so we put element name
480
return selection_dict["element_name"]
482
vertex_name = findVertexNameInObject(edge.Vertexes[closest_vertex_index], obj)
486
elif elt_type == "Face":
487
face = obj.Shape.Faces[elt_index - 1]
488
surface = face.Surface
489
_type = surface.TypeId
490
if _type == "Part::GeomSphere" or _type == "Part::GeomTorus":
491
return selection_dict["element_name"]
493
# Handle the circle/arc edges for their centers
495
center_points_edge_indexes = []
498
for i, edge in enumerate(edges):
500
if curve.TypeId == "Part::GeomCircle" or curve.TypeId == "Part::GeomEllipse":
501
center_points.append(curve.Location)
502
center_points_edge_indexes.append(i)
504
elif _type == "Part::GeomCylinder" and curve.TypeId == "Part::GeomBSplineCurve":
505
# handle special case of 2 cylinder intersecting.
506
for j, facej in enumerate(obj.Shape.Faces):
507
surfacej = facej.Surface
508
if (elt_index - 1) != j and surfacej.TypeId == "Part::GeomCylinder":
509
for edgej in facej.Edges:
510
if edgej.Curve.TypeId == "Part::GeomBSplineCurve":
512
edgej.CenterOfGravity == edge.CenterOfGravity
513
and edgej.Length == edge.Length
515
center_points.append(edgej.CenterOfGravity)
516
center_points_edge_indexes.append(i)
518
if len(center_points) > 0:
519
closest_center_index, closest_center_distance = findClosestPointToMousePos(
520
center_points, mousePos
523
# Handle the face vertexes
526
if _type != "Part::GeomCylinder" and _type != "Part::GeomCone":
527
face_points = getPointsFromVertexes(face.Vertexes)
529
# We also allow users to select the center of gravity.
530
if _type == "Part::GeomCylinder" or _type == "Part::GeomCone":
531
centerOfG = face.CenterOfGravity - surface.Center
532
centerPoint = surface.Center + centerOfG
533
centerPoint = centerPoint + App.Vector().projectToLine(centerOfG, surface.Axis)
534
face_points.append(centerPoint)
536
face_points.append(face.CenterOfGravity)
538
closest_vertex_index, closest_vertex_distance = findClosestPointToMousePos(
539
face_points, mousePos
542
if len(center_points) > 0:
543
if closest_center_distance < closest_vertex_distance:
544
# Note the index here is the index within the face! Not the object.
545
index = center_points_edge_indexes[closest_center_index] + 1
546
return "Edge" + str(index)
548
if _type == "Part::GeomCylinder" or _type == "Part::GeomCone":
549
return selection_dict["element_name"]
551
if closest_vertex_index == len(face.Vertexes):
552
# If center of gravity then we have no vertex name to set so we put element name
553
return selection_dict["element_name"]
555
vertex_name = findVertexNameInObject(face.Vertexes[closest_vertex_index], obj)
562
def getPointsFromVertexes(vertexes):
565
points.append(vtx.Point)
569
def findClosestPointToMousePos(candidates_points, mousePos):
570
closest_point_index = None
571
point_min_length = None
573
for i, point in enumerate(candidates_points):
574
length = (mousePos - point).Length
575
if closest_point_index is None or length < point_min_length:
576
closest_point_index = i
577
point_min_length = length
579
return closest_point_index, point_min_length
582
def findVertexNameInObject(vertex, obj):
583
for i, vtx in enumerate(obj.Shape.Vertexes):
584
if vtx.Point == vertex.Point:
585
return "Vertex" + str(i + 1)
589
def color_from_unsigned(c):
591
float(int((c >> 24) & 0xFF) / 255),
592
float(int((c >> 16) & 0xFF) / 255),
593
float(int((c >> 8) & 0xFF) / 255),
597
def getJointGroup(assembly):
600
for obj in assembly.OutList:
601
if obj.TypeId == "Assembly::JointGroup":
606
joint_group = assembly.newObject("Assembly::JointGroup", "Joints")
611
def isAssemblyGrounded():
612
assembly = activeAssembly()
616
jointGroup = getJointGroup(assembly)
618
for joint in jointGroup.Group:
619
if hasattr(joint, "ObjectToGround"):
625
def removeObjAndChilds(obj):
626
removeObjsAndChilds([obj])
629
def removeObjsAndChilds(objs):
630
def addsubobjs(obj, toremoveset):
631
if obj.TypeId == "App::Origin": # Origins are already handled
635
if obj.TypeId != "App::Link":
636
for subobj in obj.OutList:
637
addsubobjs(subobj, toremoveset)
641
addsubobjs(obj, toremove)
645
obj.Document.removeObject(obj.Name)