From b7374d7b1fc1d023fbb25ee409a820ae42d4a9ea Mon Sep 17 00:00:00 2001 From: forbes Date: Sun, 8 Feb 2026 19:53:49 -0600 Subject: [PATCH] fix(assembly): extend findPlacement() datum and origin handling (#55) Add support for missing datum and origin object types in the Assembly solver's findPlacement() function: - App::Plane: handle XY/XZ/YZ origin planes with correct rotations - App::Point: handle origin point (identity placement) - PartDesign::Line: extract edge midpoint and direction from shape - PartDesign::Point: extract vertex position from shape geometry instead of returning raw obj.Placement - Add obj.Shape.isNull() validation to all PartDesign datum branches - PartDesign::Plane falls back to obj.Placement on invalid shape instead of falling through to App.Placement() Closes #55 --- src/Mod/Assembly/UtilsAssembly.py | 97 +++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 17 deletions(-) diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index 68eddfffbc..16aff1c3f1 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -49,7 +49,9 @@ def activePartOrAssembly(): def activeAssembly(): active_assembly = activePartOrAssembly() - if active_assembly is not None and active_assembly.isDerivedFrom("Assembly::AssemblyObject"): + if active_assembly is not None and active_assembly.isDerivedFrom( + "Assembly::AssemblyObject" + ): if active_assembly.ViewObject.isInEditMode(): return active_assembly @@ -59,7 +61,9 @@ def activeAssembly(): def activePart(): active_part = activePartOrAssembly() - if active_part is not None and not active_part.isDerivedFrom("Assembly::AssemblyObject"): + if active_part is not None and not active_part.isDerivedFrom( + "Assembly::AssemblyObject" + ): return active_part return None @@ -120,7 +124,9 @@ def number_of_components_in(assembly): def isLink(obj): # If element count is not 0, then its a link group in which case the Link # is a container and it's the LinkElement that is linking to external doc. - return (obj.TypeId == "App::Link" and obj.ElementCount == 0) or obj.TypeId == "App::LinkElement" + return ( + obj.TypeId == "App::Link" and obj.ElementCount == 0 + ) or obj.TypeId == "App::LinkElement" def isLinkGroup(obj): @@ -375,7 +381,9 @@ 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) + 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() @@ -520,11 +528,17 @@ def findElementClosestVertex(ref, mousePos): for i, edge in enumerate(edges): curve = edge.Curve - if curve.TypeId == "Part::GeomCircle" or curve.TypeId == "Part::GeomEllipse": + if ( + curve.TypeId == "Part::GeomCircle" + or curve.TypeId == "Part::GeomEllipse" + ): center_points.append(curve.Location) center_points_edge_indexes.append(i) - elif _type == "Part::GeomCylinder" and curve.TypeId == "Part::GeomBSplineCurve": + elif ( + _type == "Part::GeomCylinder" + and curve.TypeId == "Part::GeomBSplineCurve" + ): # handle special case of 2 cylinder intersecting. for j, facej in enumerate(obj.Shape.Faces): surfacej = facej.Surface @@ -553,7 +567,9 @@ def findElementClosestVertex(ref, mousePos): if _type == "Part::GeomCylinder" or _type == "Part::GeomCone": centerOfG = face.CenterOfGravity - surface.Center centerPoint = surface.Center + centerOfG - centerPoint = centerPoint + App.Vector().projectToLine(centerOfG, surface.Axis) + centerPoint = centerPoint + App.Vector().projectToLine( + centerOfG, surface.Axis + ) face_points.append(centerPoint) else: face_points.append(face.CenterOfGravity) @@ -623,7 +639,8 @@ def color_from_unsigned(c): def getJointsOfType(asm, jointTypes): if not ( - asm.isDerivedFrom("Assembly::AssemblyObject") or asm.isDerivedFrom("Assembly::AssemblyLink") + asm.isDerivedFrom("Assembly::AssemblyObject") + or asm.isDerivedFrom("Assembly::AssemblyLink") ): return [] @@ -763,7 +780,9 @@ def getSubMovingParts(obj, partsAsSolid): if isLink(obj): linked_obj = obj.getLinkedObject() - if linked_obj.isDerivedFrom("App::Part") or linked_obj.isDerivedFrom("Part::Feature"): + if linked_obj.isDerivedFrom("App::Part") or linked_obj.isDerivedFrom( + "Part::Feature" + ): return [obj] return [] @@ -996,7 +1015,7 @@ def findPlacement(ref, ignoreVertex=False): vtx = getElementName(ref[1][1]) if not elt or not vtx: - # case of whole parts such as PartDesign::Body or App/PartDesign::CordinateSystem/Point/Line/Plane. + # Origin objects (App::Line, App::Plane, App::Point) if obj.TypeId == "App::Line": if obj.Role == "X_Axis": return App.Placement(App.Vector(), App.Rotation(0.5, 0.5, 0.5, 0.5)) @@ -1005,9 +1024,25 @@ def findPlacement(ref, ignoreVertex=False): if obj.Role == "Z_Axis": return App.Placement(App.Vector(), App.Rotation(-0.5, 0.5, -0.5, 0.5)) - # PartDesign datum planes (including ZTools datums like ZPlane_Mid, ZPlane_Offset) + if obj.TypeId == "App::Plane": + if obj.Role == "XY_Plane": + return App.Placement() + if obj.Role == "XZ_Plane": + return App.Placement( + App.Vector(), App.Rotation(App.Vector(1, 0, 0), -90) + ) + if obj.Role == "YZ_Plane": + return App.Placement( + App.Vector(), App.Rotation(App.Vector(0, 1, 0), 90) + ) + return App.Placement() + + if obj.TypeId == "App::Point": + return App.Placement() + + # PartDesign datum planes if obj.isDerivedFrom("PartDesign::Plane"): - if hasattr(obj, "Shape") and obj.Shape.Faces: + if hasattr(obj, "Shape") and not obj.Shape.isNull() and obj.Shape.Faces: face = obj.Shape.Faces[0] surface = face.Surface plc = App.Placement() @@ -1015,9 +1050,28 @@ def findPlacement(ref, ignoreVertex=False): if hasattr(surface, "Rotation") and surface.Rotation is not None: plc.Rotation = App.Rotation(surface.Rotation) return obj.Placement.inverse() * plc + return obj.Placement + + # PartDesign datum lines + if obj.isDerivedFrom("PartDesign::Line"): + if hasattr(obj, "Shape") and not obj.Shape.isNull() and obj.Shape.Edges: + edge = obj.Shape.Edges[0] + points = getPointsFromVertexes(edge.Vertexes) + mid = (points[0] + points[1]) * 0.5 + direction = round_vector(edge.Curve.Direction) + plane = Part.Plane(App.Vector(), direction) + plc = App.Placement() + plc.Base = mid + plc.Rotation = App.Rotation(plane.Rotation) + return obj.Placement.inverse() * plc + return obj.Placement # PartDesign datum points if obj.isDerivedFrom("PartDesign::Point"): + if hasattr(obj, "Shape") and not obj.Shape.isNull() and obj.Shape.Vertexes: + plc = App.Placement() + plc.Base = obj.Shape.Vertexes[0].Point + return obj.Placement.inverse() * plc return obj.Placement return App.Placement() @@ -1080,9 +1134,14 @@ def findPlacement(ref, ignoreVertex=False): if surface.TypeId == "Part::GeomCylinder": centerOfG = face.CenterOfGravity - surface.Center centerPoint = surface.Center + centerOfG - centerPoint = centerPoint + App.Vector().projectToLine(centerOfG, surface.Axis) + centerPoint = centerPoint + App.Vector().projectToLine( + centerOfG, surface.Axis + ) plc.Base = centerPoint - elif surface.TypeId == "Part::GeomTorus" or surface.TypeId == "Part::GeomSphere": + elif ( + surface.TypeId == "Part::GeomTorus" + or surface.TypeId == "Part::GeomSphere" + ): plc.Base = surface.Center elif surface.TypeId == "Part::GeomCone": plc.Base = surface.Apex @@ -1100,7 +1159,8 @@ def findPlacement(ref, ignoreVertex=False): plc.Base = (center_point.x, center_point.y, center_point.z) elif ( - surface.TypeId == "Part::GeomCylinder" and curve.TypeId == "Part::GeomBSplineCurve" + surface.TypeId == "Part::GeomCylinder" + and curve.TypeId == "Part::GeomBSplineCurve" ): # handle special case of 2 cylinder intersecting. plc.Base = findCylindersIntersection(obj, surface, edge, elt_index) @@ -1394,13 +1454,16 @@ def generatePropertySettings(documentObject): commands.append(f"obj.{propertyName} = {propertyValue:.5f}") elif propertyType == "App::PropertyInt" or propertyType == "App::PropertyBool": commands.append(f"obj.{propertyName} = {propertyValue}") - elif propertyType == "App::PropertyString" or propertyType == "App::PropertyEnumeration": + elif ( + propertyType == "App::PropertyString" + or propertyType == "App::PropertyEnumeration" + ): commands.append(f'obj.{propertyName} = "{propertyValue}"') elif propertyType == "App::PropertyPlacement": commands.append( f"obj.{propertyName} = App.Placement(" f"App.Vector({propertyValue.Base.x:.5f},{propertyValue.Base.y:.5f},{propertyValue.Base.z:.5f})," - f"App.Rotation(*{[round(n,5) for n in propertyValue.Rotation.getYawPitchRoll()]}))" + f"App.Rotation(*{[round(n, 5) for n in propertyValue.Rotation.getYawPitchRoll()]}))" ) elif propertyType == "App::PropertyXLinkSubHidden": commands.append( -- 2.49.1