Assembly: Property change from Object/Part to Reference

This commit is contained in:
PaddleStroke
2024-07-12 08:36:14 +02:00
committed by Yorik van Havre
parent 828e85963e
commit 8d3e3acd11
9 changed files with 1217 additions and 1057 deletions

View File

@@ -95,32 +95,27 @@ def assembly_has_at_least_n_parts(n):
return False
def getObject(full_name):
# full_name is "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBox.Edge16"
# or "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBody.pad.Edge16"
def getObject(ref):
if len(ref) != 2:
return None
subs = ref[1]
if len(subs) < 1:
return None
sub_name = subs[0]
# sub_name is "LinkOrAssembly1.LinkOrPart1.LinkOrBox.Edge16"
# or "LinkOrAssembly1.LinkOrPart1.LinkOrBody.pad.Edge16"
# or "Assembly.LinkOrAssembly1.LinkOrPart1.LinkOrBody.Local_CS.X"
# We want either LinkOrBody or LinkOrBox or Local_CS.
names = full_name.split(".")
doc = App.ActiveDocument
names = sub_name.split(".")
if len(names) < 3:
if len(names) < 2:
return None
prevObj = None
doc = ref[0].Document
for i, objName in enumerate(names):
if i == 0:
prevObj = doc.getObject(objName)
if prevObj.TypeId == "App::Link":
prevObj = prevObj.getLinkedObject()
continue
obj = None
if prevObj.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}:
for obji in prevObj.OutList:
if obji.Name == objName:
obj = obji
break
for i, obj_name in enumerate(names):
obj = doc.getObject(obj_name)
if obj is None:
return None
@@ -129,26 +124,7 @@ def getObject(full_name):
if i == len(names) - 2:
return obj
if obj.TypeId == "App::Link":
linked_obj = obj.getLinkedObject()
if linked_obj.TypeId == "PartDesign::Body":
if i + 1 < len(names):
obj2 = None
for obji in linked_obj.OutList:
if obji.Name == names[i + 1]:
obj2 = obji
break
if obj2 and isBodySubObject(obj2.TypeId):
return obj2
return obj
elif linked_obj.isDerivedFrom("Part::Feature"):
return obj
else:
prevObj = linked_obj
continue
elif obj.TypeId in {"App::Part", "Assembly::AssemblyObject", "App::DocumentObjectGroup"}:
prevObj = obj
if obj.TypeId in {"App::Part", "Assembly::AssemblyObject"}:
continue
elif obj.TypeId == "PartDesign::Body":
@@ -166,6 +142,24 @@ def getObject(full_name):
# primitive, fastener, gear ...
return obj
elif obj.TypeId == "App::Link":
linked_obj = obj.getLinkedObject()
if linked_obj.TypeId == "PartDesign::Body":
if i + 1 < len(names):
obj2 = None
for obji in linked_obj.OutList:
if obji.Name == names[i + 1]:
obj2 = obji
break
if obj2 and isBodySubObject(obj2.TypeId):
return obj2
return obj
elif linked_obj.isDerivedFrom("Part::Feature"):
return obj
else:
doc = linked_obj.Document
continue
return None
@@ -179,6 +173,7 @@ def isBodySubObject(typeId):
)
# To be deprecated. CommandCreateView needs to stop using it.
def getContainingPart(full_name, selected_object, activeAssemblyOrPart=None):
# full_name is "Assembly.Assembly1.LinkOrPart1.LinkOrBox.Edge16" -> LinkOrPart1
# or "Assembly.Assembly1.LinkOrPart1.LinkOrBody.pad.Edge16" -> LinkOrPart1
@@ -243,6 +238,7 @@ def getContainingPart(full_name, selected_object, activeAssemblyOrPart=None):
return selected_object
# To be deprecated. Kept for migrationScript.
def getObjectInPart(objName, part):
if part is None:
return None
@@ -266,43 +262,85 @@ def getObjectInPart(objName, part):
return None
# get the placement of Obj relative to its containing Part
# Used by migrationScript.
def getRootPath(obj, part):
sels = obj.Parents
for sel in sels:
rootObj = sel[0]
# The part and the rootObj should be in the same doc
if rootObj.Document.Name != part.Document.Name:
continue
path = sel[1]
# we need to check that the part name is in the list.
names = path.split(".")
if part.Name not in names:
continue
# for bodies we need to add the tip to the path.
if obj.TypeId == "PartDesign::Body":
path.append(obj.Tip.Name + ".")
return rootObj, path
return None, ""
# get the placement of Obj relative to its moving Part
# Example : assembly.part1.part2.partn.body1 : placement of Obj relative to part1
def getObjPlcRelativeToPart(obj, part):
# we need plc to be relative to the containing part
obj_global_plc = getGlobalPlacement(obj, part)
part_global_plc = getGlobalPlacement(part)
def getObjPlcRelativeToPart(assembly, ref):
# we need plc to be relative to the moving part
moving_part = getMovingPart(assembly, ref)
obj_global_plc = getGlobalPlacement(ref)
part_global_plc = getGlobalPlacement(ref, moving_part)
return part_global_plc.inverse() * obj_global_plc
# Example : assembly.part1.part2.partn.body1 : jcsPlc is relative to body1
# This function returns jcsPlc relative to part1
def getJcsPlcRelativeToPart(jcsPlc, obj, part):
obj_relative_plc = getObjPlcRelativeToPart(obj, part)
def getJcsPlcRelativeToPart(assembly, jcsPlc, ref):
obj_relative_plc = getObjPlcRelativeToPart(assembly, ref)
return obj_relative_plc * jcsPlc
# Return the jcs global placement
def getJcsGlobalPlc(jcsPlc, obj, part):
obj_global_plc = getGlobalPlacement(obj, part)
def getJcsGlobalPlc(jcsPlc, ref):
obj_global_plc = getGlobalPlacement(ref)
return obj_global_plc * jcsPlc
# The container is used to support cases where the same object appears at several places
# which happens when you have a link to a part.
def getGlobalPlacement(targetObj, container=None):
def getGlobalPlacement(ref, targetObj=None):
if not isRefValid(ref, 1):
return App.Placement()
if targetObj is None: # If no targetObj is given, we consider it's the getObject(ref)
targetObj = getObject(ref)
if targetObj is None:
return App.Placement()
inContainerBranch = container is None
for rootObj in App.activeDocument().RootObjectsIgnoreLinks:
foundPlacement = getTargetPlacementRelativeTo(
targetObj, rootObj, container, inContainerBranch
)
if foundPlacement is not None:
return foundPlacement
rootObj = ref[0]
names = ref[1][0].split(".")
doc = rootObj.Document
plc = rootObj.Placement
for objName in names:
obj = doc.getObject(objName)
if not obj:
continue
plc = plc * obj.Placement
if obj == targetObj:
return plc
if obj.TypeId == "App::Link":
linked_obj = obj.getLinkedObject()
doc = linked_obj.Document # in case its an external link.
# If targetObj has not been found there's a problem
return App.Placement()
@@ -313,71 +351,13 @@ def isThereOneRootAssembly():
return False
def getTargetPlacementRelativeTo(
targetObj, part, container, inContainerBranch, ignorePlacement=False
):
inContainerBranch = inContainerBranch or (not ignorePlacement and part == container)
if targetObj == part and inContainerBranch and not ignorePlacement:
return targetObj.Placement
if part.TypeId == "App::DocumentObjectGroup":
for obj in part.OutList:
foundPlacement = getTargetPlacementRelativeTo(
targetObj, obj, container, inContainerBranch, ignorePlacement
)
if foundPlacement is not None:
return foundPlacement
elif part.TypeId in {"App::Part", "Assembly::AssemblyObject", "PartDesign::Body"}:
for obj in part.OutList:
foundPlacement = getTargetPlacementRelativeTo(
targetObj, obj, container, inContainerBranch
)
if foundPlacement is None:
continue
# If we were called from a link then we need to ignore this placement as we use the link placement instead.
if not ignorePlacement:
foundPlacement = part.Placement * foundPlacement
return foundPlacement
elif part.TypeId == "App::Link":
linked_obj = part.getLinkedObject()
if part == linked_obj or linked_obj is None:
return None # upon loading this can happen for external links.
if linked_obj.TypeId in {"App::Part", "Assembly::AssemblyObject", "PartDesign::Body"}:
for obj in linked_obj.OutList:
foundPlacement = getTargetPlacementRelativeTo(
targetObj, obj, container, inContainerBranch
)
if foundPlacement is None:
continue
foundPlacement = part.Placement * foundPlacement
return foundPlacement
foundPlacement = getTargetPlacementRelativeTo(
targetObj, linked_obj, container, inContainerBranch, True
)
if foundPlacement is not None and not ignorePlacement:
foundPlacement = part.Placement * foundPlacement
return foundPlacement
return None
def getElementName(full_name):
# full_name is "Assembly.Assembly1.Assembly2.Assembly3.Box.Edge16"
# We want either Edge16.
parts = full_name.split(".")
if len(parts) < 3:
# At minimum "Assembly.Box.edge16". It shouldn't be shorter
if len(parts) < 2:
# At minimum "Box.edge16". It shouldn't be shorter
return ""
# case of PartDesign datums : CoordinateSystem, point, line, plane
@@ -452,23 +432,27 @@ def extract_type_and_number(element_name):
return None, None
def findElementClosestVertex(selection_dict):
obj = selection_dict["object"]
def findElementClosestVertex(assembly, ref, mousePos):
element_name = getElementName(ref[1][0])
if element_name == "":
return ""
mousePos = selection_dict["mouse_pos"]
moving_part = getMovingPart(assembly, ref)
obj = getObject(ref)
# We need mousePos to be relative to the part containing obj global placement
if selection_dict["object"] != selection_dict["part"]:
if obj != moving_part:
plc = App.Placement()
plc.Base = mousePos
global_plc = getGlobalPlacement(selection_dict["part"])
plc = global_plc.inverse() * plc
global_plc = getGlobalPlacement(ref)
plc = global_plc.inverse() * plc # We make it relative to obj Origin
plc = obj.Placement * plc # Make plc in the same lcs as obj
mousePos = plc.Base
elt_type, elt_index = extract_type_and_number(selection_dict["element_name"])
elt_type, elt_index = extract_type_and_number(element_name)
if elt_type == "Vertex":
return selection_dict["element_name"]
return element_name
elif elt_type == "Edge":
edge = obj.Shape.Edges[elt_index - 1]
@@ -476,7 +460,7 @@ def findElementClosestVertex(selection_dict):
if curve.TypeId == "Part::GeomCircle":
# For centers, as they are not shape vertexes, we return the element name.
# For now we only allow selecting the center of arcs / circles.
return selection_dict["element_name"]
return element_name
edge_points = getPointsFromVertexes(edge.Vertexes)
@@ -489,7 +473,7 @@ def findElementClosestVertex(selection_dict):
if curve.TypeId == "Part::GeomLine" and closest_vertex_index == 2:
# If line center is closest then we have no vertex name to set so we put element name
return selection_dict["element_name"]
return element_name
vertex_name = findVertexNameInObject(edge.Vertexes[closest_vertex_index], obj)
@@ -500,7 +484,7 @@ def findElementClosestVertex(selection_dict):
surface = face.Surface
_type = surface.TypeId
if _type == "Part::GeomSphere" or _type == "Part::GeomTorus":
return selection_dict["element_name"]
return element_name
# Handle the circle/arc edges for their centers
center_points = []
@@ -558,11 +542,11 @@ def findElementClosestVertex(selection_dict):
return "Edge" + str(index)
if _type == "Part::GeomCylinder" or _type == "Part::GeomCone":
return selection_dict["element_name"]
return element_name
if closest_vertex_index == len(face.Vertexes):
# If center of gravity then we have no vertex name to set so we put element name
return selection_dict["element_name"]
return element_name
vertex_name = findVertexNameInObject(face.Vertexes[closest_vertex_index], obj)
@@ -785,10 +769,10 @@ def getObjMassAndCom(obj, containingPart=None):
return 0, App.Vector(0, 0, 0)
def getCenterOfBoundingBox(objs, parts):
def getCenterOfBoundingBox(objs, refs):
i = 0
center = App.Vector()
for obj, part in zip(objs, parts):
for obj, ref in zip(objs, refs):
viewObject = obj.ViewObject
if viewObject is None:
continue
@@ -796,15 +780,15 @@ def getCenterOfBoundingBox(objs, parts):
if boundingBox is None:
continue
bboxCenter = boundingBox.Center
if part != obj:
# bboxCenter does not take into account obj global placement
plc = App.Placement(bboxCenter, App.Rotation())
# change plc to be relative to the object placement.
plc = obj.Placement.inverse() * plc
# change plc to be relative to the origin of the document.
global_plc = getGlobalPlacement(obj, part)
plc = global_plc * plc
bboxCenter = plc.Base
# bboxCenter does not take into account obj global placement
plc = App.Placement(bboxCenter, App.Rotation())
# change plc to be relative to the object placement.
plc = obj.Placement.inverse() * plc
# change plc to be relative to the origin of the document.
global_plc = getGlobalPlacement(ref, obj)
plc = global_plc * plc
bboxCenter = plc.Base
center = center + bboxCenter
i = i + 1
@@ -880,9 +864,15 @@ So here we want to find a placement that corresponds to a local coordinate syste
"""
def findPlacement(obj, part, elt, vtx, ignoreVertex=False):
if not obj or not part:
def findPlacement(ref, ignoreVertex=False):
if not isRefValid(ref, 2):
return App.Placement()
obj = getObject(ref)
if not obj:
return App.Placement()
elt = getElementName(ref[1][0])
vtx = getElementName(ref[1][1])
# case of origin objects.
if elt == "X_Axis" or elt == "YZ_Plane":
@@ -999,6 +989,17 @@ def findPlacement(obj, part, elt, vtx, ignoreVertex=False):
return plc
def isRefValid(ref, number_sub):
if ref is None:
return False
if len(ref) != 2:
return False
if len(ref[1]) < number_sub:
return False
return True
def round_vector(v, decimals=10):
"""Round each component of the vector to a specified number of decimal places."""
return App.Vector(round(v.x, decimals), round(v.y, decimals), round(v.z, decimals))
@@ -1045,8 +1046,8 @@ def getAssemblyShapes(assembly):
def getJointDistance(joint):
plc1 = getJcsGlobalPlc(joint.Placement1, joint.Object1, joint.Part1)
plc2 = getJcsGlobalPlc(joint.Placement2, joint.Object2, joint.Part2)
plc1 = getJcsGlobalPlc(joint.Placement1, joint.Reference1)
plc2 = getJcsGlobalPlc(joint.Placement2, joint.Reference2)
# Find the sign
sign = 1
@@ -1058,10 +1059,112 @@ def getJointDistance(joint):
def getJointXYAngle(joint):
plc1 = getJcsGlobalPlc(joint.Placement1, joint.Object1, joint.Part1)
plc2 = getJcsGlobalPlc(joint.Placement2, joint.Object2, joint.Part2)
plc1 = getJcsGlobalPlc(joint.Placement1, joint.Reference1)
plc2 = getJcsGlobalPlc(joint.Placement2, joint.Reference2)
plc3 = plc1.inverse() * plc2 # plc3 is plc2 relative to plc1
x_axis = plc3.Rotation.multVec(App.Vector(1, 0, 0))
return math.atan2(x_axis.y, x_axis.x)
def getMovingPart(assembly, ref):
# ref can be :
# [assembly, ['box.edge1', 'box.vertex2']]
# [Part, ['Assembly.box.edge1', 'Assembly.box.vertex2']]
# [assembly, ['Body.Pad.edge1', 'Body.Pad.vertex2']]
if assembly is None or ref is None or len(ref) != 2:
return None
obj = ref[0]
subs = ref[1]
if subs is None or len(subs) < 1:
return None
sub = ref[1][0] # All subs should have the same object paths.
names = [obj.Name] + sub.split(".")
try:
index = names.index(assembly.Name)
# Get the sublist starting after the after the assembly (in case of Part1/Assembly/...)
names = names[index + 1 :]
except ValueError:
return None
doc = assembly.Document
if len(names) < 2:
App.Console.PrintError(
"getMovingPart() in UtilsAssembly.py the object name is too short, at minimum it should be something like ['Box','edge16']. It shouldn't be shorter"
)
return None
for objName in names:
obj = doc.getObject(objName)
if not obj:
continue
return obj
return None
def truncateSubAtFirst(sub, target):
# target=part1 & sub=asm.part1.link1.part1.obj -> asm.part1.
names = sub.split(".")
sub = ""
for name in names:
sub = sub + name + "."
if name == target:
break
return sub
def truncateSubAtLast(sub, target):
# target=part1 & sub=asm.part1.link1.part1.obj -> asm.part1.link1.part1.
names = sub.split(".")
sub = ""
target_indices = [i for i, name in enumerate(names) if name == target]
if target_indices:
last_index = target_indices[-1]
for i, name in enumerate(names):
sub += name + "."
if i == last_index:
break
return sub
def swapElNameInSubname(sub_name, new_elName):
# turns assembly.box.edge1 into assembly.box.new_elName
names = sub_name.split(".")
# Replace the last element
names[-1] = new_elName
# Join the names back together
modified_sub = ".".join(names)
return modified_sub
def addVertexToReference(ref, vertex_name):
# Turns [obj, ['box.face1']] and 'vertex1' into [obj, ['box.face1', 'box.vertex1']]
if len(ref) == 2:
subs = ref[1]
if len(subs) > 0:
sub_name = subs[0]
vertex_full_sub = swapElNameInSubname(sub_name, vertex_name)
if len(subs) == 2: # Update the vertex sub
subs[1] = vertex_full_sub
else:
subs.append(vertex_full_sub)
ref = [ref[0], subs]
return ref