FreeCAD_assembly3/utils.py
Zheng, Lei b9e05500c2 assembly: migrate AsmElementLink or restore
Also implement auto naming in AsmElement/AsmElementLink
2019-01-14 08:36:36 +08:00

614 lines
20 KiB
Python

'''
Collection of helper function to extract geometry properties from OCC elements
Most of the functions are borrowed directly from assembly2lib.py or lib3D.py in
assembly2
'''
import math
from collections import namedtuple
import FreeCAD, FreeCADGui, Part, Draft
import numpy as np
from .FCADLogger import FCADLogger
rootlogger = FCADLogger('asm3')
logger = FCADLogger('asm3.main',parent=rootlogger)
guilogger = FCADLogger('asm3.gui',parent=rootlogger)
cstrlogger = FCADLogger('asm3.cstr',parent=rootlogger)
syslogger = FCADLogger('asm3.sys',parent=rootlogger)
proxylogger = FCADLogger('asm3.proxy',parent=rootlogger)
import sys, os
modulePath = os.path.dirname(os.path.realpath(__file__))
from PySide.QtCore import Qt
from PySide.QtGui import QIcon, QPainter, QPixmap
iconPath = os.path.join(modulePath,'Gui','Resources','icons')
pixmapDisabled = QPixmap(os.path.join(iconPath,'Assembly_Disabled.svg'))
iconSize = (16,16)
def getIcon(obj,disabled=False,path=None):
if not path:
path = iconPath
if not getattr(obj,'_icon',None):
obj._icon = QIcon(os.path.join(path,obj._iconName))
if not disabled:
return obj._icon
if not getattr(obj,'_iconDisabled',None):
name = getattr(obj,'_iconDisabledName',None)
if name:
obj._iconDisabled = QIcon(os.path.join(path,name))
else:
pixmap = obj._icon.pixmap(*iconSize,mode=QIcon.Disabled)
icon = QIcon(pixmapDisabled)
icon.paint(QPainter(pixmap),
0,0,iconSize[0],iconSize[1],Qt.AlignCenter)
obj._iconDisabled = QIcon(pixmap)
return obj._iconDisabled
def addIconToFCAD(iconFile,path=None):
if not path:
path = iconPath
try:
path = os.path.join(path,iconFile)
FreeCADGui.addIcon(path,path)
except AssertionError:
pass
return path
def objName(obj):
try:
return getattr(obj,'FullName',obj.Name)
except Exception:
return '?'
def isLine(param):
if hasattr(Part,"LineSegment"):
return isinstance(param,(Part.Line,Part.LineSegment))
else:
return isinstance(param,Part.Line)
def deduceSelectedElement(obj,subname):
shape = obj.getSubObject(subname)
if not shape:
return
count = shape.countElement('Face')
if count==1:
return 'Face1'
elif not count:
count = shape.countElement('Edge')
if count==1:
return 'Edge1'
elif not count:
count = shape.countElement('Vertex')
if count==1:
return 'Vertex1'
def getElementShape(obj,tp=None,transform=False,noElementMap=True):
if not isinstance(obj,(tuple,list)):
shape = obj
else:
shape,mat,sobj = Part.getShape(obj[0],subname=obj[1],
needSubElement=True,retType=2,
transform=transform,noElementMap=noElementMap)
if not sobj:
logger.trace('no sub object {}',obj,frame=1)
return
if sobj.isDerivedFrom('App::Line'):
if tp not in (None,Part.Shape,Part.Edge):
logger.trace('wrong type of shape {}',obj)
return
size = sobj.ViewObject.Size
shape = Part.makeLine(FreeCAD.Vector(-size,0,0),
FreeCAD.Vector(size,0,0))
shape.transformShape(mat,False,True)
return shape
elif sobj.isDerivedFrom('App::Plane'):
if tp not in (None, Part.Shape, Part.Face):
logger.trace('wrong type of shape {}',obj)
return
size = sobj.ViewObject.Size
shape = Part.makePlane(size*2,size*2,
FreeCAD.Vector(-size,-size,0))
shape.transformShape(mat,False,True)
return shape
elif shape.isNull():
logger.trace('no shape {}',obj)
return
if not isinstance(shape,Part.Shape) or shape.isNull():
logger.trace('null shape {}',obj)
return
if not tp or isinstance(shape,tp):
return shape
elif isinstance(shape,(Part.Vertex,Part.Edge,Part.Face)):
logger.trace('wrong shape type {}',obj)
return
elif tp is Part.Vertex:
if shape.countElement('Edge'):
return
if shape.countElement('Vertex')==1:
return shape.Vertex1
elif tp is Part.Edge:
if shape.countElement('Face'):
return
if shape.countElement('Edge')==1:
return shape.Edge1
elif tp is Part.Face:
if shape.countElement('Face')==1:
return shape.Face1
else:
logger.trace('wrong shape type {}',obj)
def isDraftWire(obj):
proxy = getattr(obj,'Proxy',None)
if isinstance(proxy,Draft._Wire) and \
not obj.Subdivisions and \
not obj.Base and \
not obj.Tool and \
obj.Points:
return obj
def isDraftCircle(obj):
proxy = getattr(obj,'Proxy',None)
if isinstance(proxy,Draft._Circle):
return obj
def isDraftObject(obj):
o = isDraftWire(obj)
if o:
return o
return isDraftCircle(obj)
def isElement(obj):
if not isinstance(obj,(tuple,list)):
shape = obj
else:
sobj,_,shape = obj[0].getSubObject(obj[1],2)
if not sobj:
return
if not shape:
return sobj.TypeId in ('App::Line','App::Plane')
if isinstance(obj,(Part.Vertex,Part.Face,Part.Edge)):
return True
if isinstance(shape,Part.Shape) and not shape.isNull():
return shape.countElement('Vertex')==1 or \
shape.countElement('Edge')==1 or \
shape.countElement('Face')==1
def isPlanar(obj):
if isCircularEdge(obj):
return True
shape = getElementShape(obj,Part.Face)
if not shape:
return False
elif str(shape.Surface) == '<Plane object>':
return True
elif hasattr(shape.Surface,'Radius'):
return False
elif str(shape.Surface).startswith('<SurfaceOfRevolution'):
return False
else:
_plane_norm,_plane_pos,error = fit_plane_to_surface1(shape.Surface)
error_normalized = error / shape.BoundBox.DiagonalLength
return error_normalized < 10**-6
def isCylindricalPlane(obj):
face = getElementShape(obj,Part.Face)
if not face:
return False
elif hasattr(face.Surface,'Radius'):
return True
elif str(face.Surface).startswith('<SurfaceOfRevolution'):
return True
elif str(face.Surface) == '<Plane object>':
return False
else:
_axis,_center,error=fit_rotation_axis_to_surface1(face.Surface)
error_normalized = error / face.BoundBox.DiagonalLength
return error_normalized < 10**-6
def isAxisOfPlane(obj):
face = getElementShape(obj,Part.Face)
if not face:
return False
if str(face.Surface) == '<Plane object>':
return True
else:
_axis,_center,error=fit_rotation_axis_to_surface1(face.Surface)
error_normalized = error / face.BoundBox.DiagonalLength
return error_normalized < 10**-6
def isCircularEdge(obj):
edge = getElementShape(obj,Part.Edge)
if not edge:
return False
elif not hasattr(edge, 'Curve'): #issue 39
return False
if hasattr( edge.Curve, 'Radius' ):
return True
elif isLine(edge.Curve):
return False
else:
BSpline = edge.Curve.toBSpline()
try:
arcs = BSpline.toBiArcs(10**-6)
except Exception: #FreeCAD exception thrown ()
return False
if all( hasattr(a,'Center') for a in arcs ):
centers = np.array([a.Center for a in arcs])
sigma = np.std( centers, axis=0 )
return max(sigma) < 10**-6
return False
def isLinearEdge(obj):
edge = getElementShape(obj,Part.Edge)
if not edge:
return False
elif not hasattr(edge, 'Curve'): #issue 39
return False
if isLine(edge.Curve):
return True
elif hasattr( edge.Curve, 'Radius' ):
return False
else:
BSpline = edge.Curve.toBSpline()
try:
arcs = BSpline.toBiArcs(10**-6)
except Exception: #FreeCAD exception thrown ()
return False
if all(isLine(a) for a in arcs):
lines = arcs
D = np.array([L.tangent(0)[0] for L in lines]) #D(irections)
return np.std( D, axis=0 ).max() < 10**-9
return False
def isVertex(obj):
return getElementShape(obj,Part.Vertex) is not None
def hasCenter(_obj):
# Any shape has no center?
# return isVertex(obj) or isCircularEdge(obj) or \
# isAxisOfPlane(obj) or isSphericalSurface(obj)
return True
def isSphericalSurface(obj):
face = getElementShape(obj,Part.Face)
if not face:
return False
return str( face.Surface ).startswith('Sphere ')
def getElementPos(obj):
vertex = getElementShape(obj,Part.Vertex)
if vertex:
return vertex.Point
face = getElementShape(obj,Part.Face)
if face:
surface = face.Surface
if str(surface) == '<Plane object>':
return face.BoundBox.Center
# pos = surface.Position
elif all( hasattr(surface,a) for a in ['Axis','Center','Radius'] ):
return surface.Center
elif str(surface).startswith('<SurfaceOfRevolution'):
return face.Edge1.Curve.Center
else: #numerically approximating surface
_plane_norm, plane_pos, error = \
fit_plane_to_surface1(face.Surface)
error_normalized = error / face.BoundBox.DiagonalLength
if error_normalized < 10**-6: #then good plane fit
return plane_pos
_axis, center, error = \
fit_rotation_axis_to_surface1(face.Surface)
error_normalized = error / face.BoundBox.DiagonalLength
if error_normalized < 10**-6: #then good rotation_axis fix
return center
return face.BoundBox.Center
else:
edge = getElementShape(obj,Part.Edge)
if not edge:
return FreeCAD.Vector()
if isLine(edge.Curve):
# pos = edge.Vertexes[-1].Point
return (edge.Vertex1.Point+edge.Vertex2.Point)*0.5
elif hasattr( edge.Curve, 'Center'): #circular curve
return edge.Curve.Center
else:
BSpline = edge.Curve.toBSpline()
arcs = BSpline.toBiArcs(10**-6)
if all( hasattr(a,'Center') for a in arcs ):
centers = np.array([a.Center for a in arcs])
sigma = np.std( centers, axis=0 )
if max(sigma) < 10**-6: #then circular curce
return FreeCAD.Vector(*centers[0])
return edge.BoundBox.Center
def getEdgeRotation(edge):
curve = edge.Curve
base = getattr(curve,'BasisCurve',None)
if base:
curve = base
rot = getattr(curve,'Rotation',None)
if rot:
return rot
if isLine(curve):
axis = curve.tangent(0)[0]
elif hasattr( curve, 'Axis'): #circular curve
axis = curve.Axis
else:
axis = None
BSpline = curve.toBSpline()
arcs = BSpline.toBiArcs(10**-6)
if all( hasattr(a,'Center') for a in arcs ):
centers = np.array([a.Center for a in arcs])
sigma = np.std( centers, axis=0 )
if max(sigma) < 10**-6: #then circular curce
axis = arcs[0].Axis
elif all(isLine(a) for a in arcs):
lines = arcs
D = np.array(
[L.tangent(0)[0] for L in lines]) #D(irections)
if np.std( D, axis=0 ).max() < 10**-9: #then linear curve
axis = FreeCAD.Vector(*D[0])
if not axis:
return edge.Placement.Rotation
return FreeCAD.Rotation(FreeCAD.Vector(0,0,1),axis)
def getElementRotation(obj,reverse=False):
axis = None
face = getElementShape(obj,Part.Face)
if not face:
edge = getElementShape(obj,Part.Edge)
if edge:
return getEdgeRotation(edge)
return FreeCAD.Rotation()
else:
if face.Orientation == 'Reversed':
reverse = not reverse
surface = face.Surface
base = getattr(surface,'BasisSurface',None)
if base:
surface = base
rot = getattr(surface,'Rotation',None)
if rot:
return rot
if hasattr(surface,'Axis'):
axis = surface.Axis
elif str(surface).startswith('<SurfaceOfRevolution'):
return getEdgeRotation(face.Edge1)
else: #numerically approximating surface
plane_norm, _plane_pos, error = \
fit_plane_to_surface1(face.Surface)
error_normalized = error / face.BoundBox.DiagonalLength
if error_normalized < 10**-6: #then good plane fit
axis = FreeCAD.Vector(plane_norm)
else:
axis_fitted, _center, error = \
fit_rotation_axis_to_surface1(face.Surface)
error_normalized = error / face.BoundBox.DiagonalLength
if error_normalized < 10**-6: #then good rotation_axis fix
axis = FreeCAD.Vector(axis_fitted)
if not axis:
return face.Placement.Rotation
return FreeCAD.Rotation(FreeCAD.Vector(0,0,-1 if reverse else 1),axis)
def getElementPlacement(obj,mat=None):
'''Get the placement of an element
obj: either a document object or a tuple(obj,subname)
mat: if not None, then this should be a matrix, and the returned
placement will be relative to this transformation matrix.
'''
if not isElement(obj):
if not isinstance(obj,(tuple,list)):
pla = obj.Placement
else:
_,mat = obj[0].getSubObject(obj[1],1,FreeCAD.Matrix())
pla = FreeCAD.Placement(mat)
else:
pla = FreeCAD.Placement(getElementPos(obj),getElementRotation(obj))
if not mat:
return pla
return FreeCAD.Placement(mat.inverse()).multiply(pla)
def getNormal(obj):
if isinstance(obj,FreeCAD.Rotation):
rot = obj
elif isinstance(obj,FreeCAD.Placement):
rot = obj.Rotation
else:
rot = getElementRotation(obj)
q = rot.Q
# return as w,x,y,z
return q[3],q[0],q[1],q[2]
def getElementDirection(rot,pla=None):
if not isinstance(rot,FreeCAD.Rotation):
rot = getElementRotation(rot)
v = rot.multVec(FreeCAD.Vector(0,0,1))
if pla:
v = pla.Rotation.multVec(v)
return v
def getElementsAngle(o1,o2,pla1=None,pla2=None,proj=None):
v1 = getElementDirection(o1,pla1)
v2 = getElementDirection(o2,pla2)
if proj:
v1,v2 = project2D(proj,v1,v2)
return math.degrees(v1.getAngle(v2))
def getElementCircular(obj,radius=False):
'return radius if it is closed, or a list of two endpoints'
edge = getElementShape(obj,Part.Edge)
if not edge:
return
elif not hasattr(edge, 'Curve'): #issue 39
return
c = edge.Curve
if hasattr( c, 'Radius' ):
if radius or edge.Closed:
return c.Radius
elif isLine(edge.Curve):
return
else:
BSpline = edge.Curve.toBSpline()
try:
arc = BSpline.toBiArcs(10**-6)[0]
except Exception: #FreeCAD exception thrown ()
return
if radius or edge.Closed:
return arc[0].Radius
return [v.Point for v in edge.Vertexes]
def fit_plane_to_surface1( surface, n_u=3, n_v=3 ):
'borrowed from assembly2 lib3D.py'
uv = sum( [ [ (u,v) for u in np.linspace(0,1,n_u)]
for v in np.linspace(0,1,n_v) ], [] )
# positions at u,v points
P = [ surface.value(u,v) for u,v in uv ]
N = [ np.cross( *surface.tangent(u,v) ) for u,v in uv ]
# plane's normal, averaging done to reduce error
plane_norm = sum(N) / len(N)
plane_pos = P[0]
error = sum([ abs( np.dot(p - plane_pos, plane_norm) ) for p in P ])
return plane_norm, plane_pos, error
def fit_rotation_axis_to_surface1( surface, n_u=3, n_v=3 ):
'''
should work for cylinders and pssibly cones (depending on the u,v mapping)
borrowed from assembly2 lib3D.py
'''
uv = sum( [ [ (u,v) for u in np.linspace(0,1,n_u)]
for v in np.linspace(0,1,n_v) ], [] )
# positions at u,v points
P = [ np.array(surface.value(u,v)) for u,v in uv ]
N = [ np.cross( *surface.tangent(u,v) ) for u,v in uv ]
intersections = []
for i in range(len(N)-1):
for j in range(i+1,len(N)):
# based on the distance_between_axes( p1, u1, p2, u2) function,
if 1 - abs(np.dot( N[i], N[j])) < 10**-6:
continue #ignore parallel case
p1_x, p1_y, p1_z = P[i]
u1_x, u1_y, u1_z = N[i]
p2_x, p2_y, p2_z = P[j]
u2_x, u2_y, u2_z = N[j]
t1_t1_coef = u1_x**2 + u1_y**2 + u1_z**2 #should equal 1
# collect( expand(d_sqrd), [t1*t2] )
t1_t2_coef = -2*u1_x*u2_x - 2*u1_y*u2_y - 2*u1_z*u2_z
t2_t2_coef = u2_x**2 + u2_y**2 + u2_z**2 #should equal 1 too
t1_coef = 2*p1_x*u1_x + 2*p1_y*u1_y + 2*p1_z*u1_z - \
2*p2_x*u1_x - 2*p2_y*u1_y - 2*p2_z*u1_z
t2_coef =-2*p1_x*u2_x - 2*p1_y*u2_y - 2*p1_z*u2_z + \
2*p2_x*u2_x + 2*p2_y*u2_y + 2*p2_z*u2_z
A = np.array([ [ 2*t1_t1_coef , t1_t2_coef ],
[ t1_t2_coef, 2*t2_t2_coef ] ])
b = np.array([ t1_coef, t2_coef])
try:
t1, t2 = np.linalg.solve(A,-b)
except np.linalg.LinAlgError:
continue
pos_t1 = P[i] + np.array(N[i])*t1
pos_t2 = P[j] + N[j]*t2
intersections.append( pos_t1 )
intersections.append( pos_t2 )
if len(intersections) < 2:
error = np.inf
return 0, 0, error
else:
# fit vector to intersection points;
# http://mathforum.org/library/drmath/view/69103.html
X = np.array(intersections)
centroid = np.mean(X,axis=0)
M = np.array([i - centroid for i in intersections ])
A = np.dot(M.transpose(), M)
# np docs: s : (..., K) The singular values for every matrix,
# sorted in descending order.
_U,s,V = np.linalg.svd(A)
axis_pos = centroid
axis_dir = V[0]
error = s[1] #don't know if this will work
return axis_dir, axis_pos, error
_tol = 10e-7
def roundPlacement(pla):
pos = [ 0.0 if abs(v)<_tol else v for v in pla.Base ]
q = [ 0.0 if abs(v)<_tol else v for v in pla.Rotation.Q ]
return FreeCAD.Placement(FreeCAD.Vector(*pos),FreeCAD.Rotation(*q))
def isSameValue(v1,v2):
if isinstance(v1,(tuple,list)):
assert(len(v1)==len(v2))
vs = zip(v1,v2)
else:
vs = (v1,v2),
return all([abs(v1-v2)<_tol for v1,v2 in vs])
def isSamePos(p1,p2):
return p1.distanceToPoint(p2) < _tol
def isSamePlacement(pla1,pla2):
return isSamePos(pla1.Base,pla2.Base) and \
isSameValue(pla1.Rotation.Q,pla2.Rotation.Q)
def getElementIndex(name,check=None):
'Return element index (starting with 1), 0 if invalid'
for i,c in enumerate(reversed(name)):
if not c.isdigit():
if not i:
break
idx = int(name[-i:])
if check and '{}{}'.format(check,idx)!=name:
break
return idx
return 0
def draftWireVertex2PointIndex(obj,name):
'Convert vertex index to draft wire point index, None if invalid'
obj = isDraftWire(obj)
if not obj:
return
idx = getElementIndex(name,'Vertex')
# We don't support subdivision yet (checked in isDraftWire())
if idx <= 0:
return
idx -= 1
if idx < len(obj.Points):
return idx
def edge2VertexIndex(obj,name,retInteger=False):
'deduct the vertex index from the edge index'
idx = getElementIndex(name,'Edge')
if not idx:
return None,None
dwire = isDraftWire(obj)
if dwire and dwire.Closed and idx==len(dwire.Points):
idx2 = 1
else:
idx2 = idx+1
if retInteger:
return idx-1,idx2-1
return 'Vertex{}'.format(idx),'Vertex{}'.format(idx2)
def getLabel(obj):
'''Return object's label without trailing index'''
label = obj.Label
for i,c in enumerate(reversed(label)):
if not c.isdigit():
if i:
label = label[:-i]
break
return label
def project2D(rot,*vectors):
vx = rot.multVec(FreeCAD.Vector(1,0,0))
vy = rot.multVec(FreeCAD.Vector(0,1,0))
return [FreeCAD.Vector(v.dot(vx),v.dot(vy),0) for v in vectors]
def projectToLine(p,a,b):
ap = p-a
ab = b-a
return a + ap.dot(ab)/ab.dot(ab) * ab