fix(assembly): extend findPlacement() datum and origin handling (#55)
Some checks failed
Build and Test / build (pull_request) Has been cancelled

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
This commit is contained in:
forbes
2026-02-08 19:53:49 -06:00
parent 7ef7ce1dfc
commit b7374d7b1f

View File

@@ -49,7 +49,9 @@ def activePartOrAssembly():
def activeAssembly(): def activeAssembly():
active_assembly = activePartOrAssembly() 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(): if active_assembly.ViewObject.isInEditMode():
return active_assembly return active_assembly
@@ -59,7 +61,9 @@ def activeAssembly():
def activePart(): def activePart():
active_part = activePartOrAssembly() 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 active_part
return None return None
@@ -120,7 +124,9 @@ def number_of_components_in(assembly):
def isLink(obj): def isLink(obj):
# If element count is not 0, then its a link group in which case the Link # 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. # 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): def isLinkGroup(obj):
@@ -375,7 +381,9 @@ def getGlobalPlacement(ref, targetObj=None):
if not isRefValid(ref, 1): if not isRefValid(ref, 1):
return App.Placement() 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) targetObj = getObject(ref)
if targetObj is None: if targetObj is None:
return App.Placement() return App.Placement()
@@ -520,11 +528,17 @@ def findElementClosestVertex(ref, mousePos):
for i, edge in enumerate(edges): for i, edge in enumerate(edges):
curve = edge.Curve 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.append(curve.Location)
center_points_edge_indexes.append(i) 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. # handle special case of 2 cylinder intersecting.
for j, facej in enumerate(obj.Shape.Faces): for j, facej in enumerate(obj.Shape.Faces):
surfacej = facej.Surface surfacej = facej.Surface
@@ -553,7 +567,9 @@ def findElementClosestVertex(ref, mousePos):
if _type == "Part::GeomCylinder" or _type == "Part::GeomCone": if _type == "Part::GeomCylinder" or _type == "Part::GeomCone":
centerOfG = face.CenterOfGravity - surface.Center centerOfG = face.CenterOfGravity - surface.Center
centerPoint = surface.Center + centerOfG centerPoint = surface.Center + centerOfG
centerPoint = centerPoint + App.Vector().projectToLine(centerOfG, surface.Axis) centerPoint = centerPoint + App.Vector().projectToLine(
centerOfG, surface.Axis
)
face_points.append(centerPoint) face_points.append(centerPoint)
else: else:
face_points.append(face.CenterOfGravity) face_points.append(face.CenterOfGravity)
@@ -623,7 +639,8 @@ def color_from_unsigned(c):
def getJointsOfType(asm, jointTypes): def getJointsOfType(asm, jointTypes):
if not ( if not (
asm.isDerivedFrom("Assembly::AssemblyObject") or asm.isDerivedFrom("Assembly::AssemblyLink") asm.isDerivedFrom("Assembly::AssemblyObject")
or asm.isDerivedFrom("Assembly::AssemblyLink")
): ):
return [] return []
@@ -763,7 +780,9 @@ def getSubMovingParts(obj, partsAsSolid):
if isLink(obj): if isLink(obj):
linked_obj = obj.getLinkedObject() 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 [obj]
return [] return []
@@ -996,7 +1015,7 @@ def findPlacement(ref, ignoreVertex=False):
vtx = getElementName(ref[1][1]) vtx = getElementName(ref[1][1])
if not elt or not vtx: 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.TypeId == "App::Line":
if obj.Role == "X_Axis": if obj.Role == "X_Axis":
return App.Placement(App.Vector(), App.Rotation(0.5, 0.5, 0.5, 0.5)) 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": if obj.Role == "Z_Axis":
return App.Placement(App.Vector(), App.Rotation(-0.5, 0.5, -0.5, 0.5)) 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 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] face = obj.Shape.Faces[0]
surface = face.Surface surface = face.Surface
plc = App.Placement() plc = App.Placement()
@@ -1015,9 +1050,28 @@ def findPlacement(ref, ignoreVertex=False):
if hasattr(surface, "Rotation") and surface.Rotation is not None: if hasattr(surface, "Rotation") and surface.Rotation is not None:
plc.Rotation = App.Rotation(surface.Rotation) plc.Rotation = App.Rotation(surface.Rotation)
return obj.Placement.inverse() * plc 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 # PartDesign datum points
if obj.isDerivedFrom("PartDesign::Point"): 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 obj.Placement
return App.Placement() return App.Placement()
@@ -1080,9 +1134,14 @@ def findPlacement(ref, ignoreVertex=False):
if surface.TypeId == "Part::GeomCylinder": if surface.TypeId == "Part::GeomCylinder":
centerOfG = face.CenterOfGravity - surface.Center centerOfG = face.CenterOfGravity - surface.Center
centerPoint = surface.Center + centerOfG centerPoint = surface.Center + centerOfG
centerPoint = centerPoint + App.Vector().projectToLine(centerOfG, surface.Axis) centerPoint = centerPoint + App.Vector().projectToLine(
centerOfG, surface.Axis
)
plc.Base = centerPoint 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 plc.Base = surface.Center
elif surface.TypeId == "Part::GeomCone": elif surface.TypeId == "Part::GeomCone":
plc.Base = surface.Apex plc.Base = surface.Apex
@@ -1100,7 +1159,8 @@ def findPlacement(ref, ignoreVertex=False):
plc.Base = (center_point.x, center_point.y, center_point.z) plc.Base = (center_point.x, center_point.y, center_point.z)
elif ( 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. # handle special case of 2 cylinder intersecting.
plc.Base = findCylindersIntersection(obj, surface, edge, elt_index) plc.Base = findCylindersIntersection(obj, surface, edge, elt_index)
@@ -1394,13 +1454,16 @@ def generatePropertySettings(documentObject):
commands.append(f"obj.{propertyName} = {propertyValue:.5f}") commands.append(f"obj.{propertyName} = {propertyValue:.5f}")
elif propertyType == "App::PropertyInt" or propertyType == "App::PropertyBool": elif propertyType == "App::PropertyInt" or propertyType == "App::PropertyBool":
commands.append(f"obj.{propertyName} = {propertyValue}") 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}"') commands.append(f'obj.{propertyName} = "{propertyValue}"')
elif propertyType == "App::PropertyPlacement": elif propertyType == "App::PropertyPlacement":
commands.append( commands.append(
f"obj.{propertyName} = App.Placement(" f"obj.{propertyName} = App.Placement("
f"App.Vector({propertyValue.Base.x:.5f},{propertyValue.Base.y:.5f},{propertyValue.Base.z:.5f})," 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": elif propertyType == "App::PropertyXLinkSubHidden":
commands.append( commands.append(