FreeCAD_assembly3/solver.py
Zheng, Lei 80bceefcc6 system: auto relax some redundant composit constraints
The order of appearance of the constraints determines how redundancies
are handled.

PlaneCoincident: the second PlaneCoincident of the same pair or parts is
relax to a 2d point horizontal or vertical constraint. The third and
onwards are ignored.  Any PlaneCoincident constraint will be ignored if
there are any existing PlaneCoincident or AxialAlignment.

PlaneAlignment: the second one is relaxed to two PointInPlane or
PointPlaneDistance constraint. The third one is relaxed to one
PointInPlane or PointPlaneDistance. The forth and onwards are ignored.
Any PlaneCoincident constraint will be ignored if there are any existing
PlaneCoincident or AxialAlignment.

AxialAlignment: ignored if there are any existing PlaneAlignment or
PlaneCoincident constraints
2018-03-24 12:12:22 +08:00

342 lines
14 KiB
Python

import random, math
from collections import namedtuple
import FreeCAD, FreeCADGui
from .assembly import Assembly, isTypeOf, setPlacement
from . import utils
from .utils import syslogger as logger, objName, isSamePlacement
from .constraint import Constraint, cstrName
from .system import System
# Part: the part object
# PartName: text name of the part
# Placement: the original placement of the part
# Params: 7 parameters that defines the transformation of this part
# Workplane: a tuple of four entity handles, that is the workplane, the origin
# point, and the normal, and x pointing normal. The workplane,
# defined by the origin and norml, is essentially the XY reference
# plane of the part.
# EntityMap: string -> entity handle map, for caching
# Group: transforming entity group handle
# CstrMap: map from other part to the constrains between this and the othe part.
# This is for auto constraint DOF reduction. Only some composite
# constraints will be mapped.
PartInfo = namedtuple('SolverPartInfo', ('Part','PartName','Placement',
'Params','Workplane','EntityMap','Group','CstrMap'))
class Solver(object):
def __init__(self,assembly,reportFailed,dragPart,recompute,rollback):
self.system = System.getSystem(assembly)
cstrs = assembly.Proxy.getConstraints()
if not cstrs:
logger.debug('skip assembly {} with no constraint'.format(
objName(assembly)))
return
self._fixedGroup = 2
self.group = 1 # the solving group
self._partMap = {}
self._cstrMap = {}
self._fixedElements = set()
self.system.GroupHandle = self._fixedGroup
# convenience constant of zero and one
self.v0 = self.system.addParamV(0,group=self._fixedGroup)
self.v1 = self.system.addParamV(1,group=self._fixedGroup)
# convenience normals
rotx = FreeCAD.Rotation(FreeCAD.Vector(0,1,0),-90)
self.nx = self.system.addNormal3dV(*utils.getNormal(rotx))
# convenience x pointing vector
self.px = self.system.addPoint3d(self.v1,self.v0,self.v0)
self._fixedParts = Constraint.getFixedParts(self,cstrs)
for part in self._fixedParts:
self._fixedElements.add((part,None))
for cstr in cstrs:
self.system.log('preparing {}'.format(cstrName(cstr)))
self.system.GroupHandle += 1
ret = Constraint.prepare(cstr,self)
if ret:
if isinstance(ret,(list,tuple)):
for h in ret:
if not isinstance(h,(list,tuple)):
self._cstrMap[h] = cstr
else:
self._cstrMap[ret] = cstr
if dragPart:
# TODO: this is ugly, need a better way to expose dragging interface
addDragPoint = getattr(self.system,'addWhereDragged',None)
if addDragPoint:
info = self._partMap.get(dragPart,None)
if info and info.Workplane:
# add dragging point
self.system.log('add drag point '
'{}'.format(info.Workplane[1]))
# TODO: slvs addWhereDragged doesn't work as expected, need
# to investigate more
# addDragPoint(info.Workplane[1],group=self.group)
self.system.log('solving {}'.format(objName(assembly)))
try:
self.system.solve(group=self.group,reportFailed=reportFailed)
except RuntimeError as e:
if reportFailed and self.system.Failed:
msg = 'List of failed constraint:'
for h in self.system.Failed:
cstr = self._cstrMap.get(h,None)
if not cstr:
try:
c = self.system.getConstraint(h)
except Exception as e2:
logger.error('cannot find failed constraint '
'{}: {}'.format(h,e2))
continue
if c.group <= self._fixedGroup or \
c.group-self._fixedGroup >= len(cstrs):
logger.error('failed constraint in unexpected group'
' {}'.format(c.group))
continue
cstr = cstrs[c.group-self._fixedGroup]
msg += '\n{}, handle: {}'.format(cstrName(cstr),h)
logger.error(msg)
raise RuntimeError('Failed to solve {}: {}'.format(
objName(assembly),e.message))
self.system.log('done sloving')
touched = False
for part,partInfo in self._partMap.items():
if part in self._fixedParts:
continue
if utils.isDraftWire(part):
changed = False
points = part.Points
for key,h in partInfo.EntityMap.items():
if not key.endswith('.p') or\
not key.startswith('Vertex'):
continue
v = [ self.system.getParam(p).val for p in h[1] ]
v = FreeCAD.Vector(*v)
v = partInfo.Placement.inverse().multVec(v)
idx = utils.draftWireVertex2PointIndex(part,key[:-2])
if utils.isSamePos(points[idx],v):
self.system.log('not moving {} point {}'.format(
partInfo.PartName,idx))
else:
changed = True
self.system.log('moving {} point{} from {}->{}'.format(
partInfo.PartName,idx,points[idx],v))
if rollback is not None:
rollback.append((partInfo.PartName,
part,
(idx, points[idx])))
points[idx] = v
if changed:
touched = True
part.Points = points
else:
params = [self.system.getParam(h).val for h in partInfo.Params]
p = params[:3]
q = (params[4],params[5],params[6],params[3])
pla = FreeCAD.Placement(FreeCAD.Vector(*p),FreeCAD.Rotation(*q))
if isSamePlacement(partInfo.Placement,pla):
self.system.log('not moving {}'.format(partInfo.PartName))
else:
touched = True
self.system.log('moving {} {} {} {}'.format(
partInfo.PartName,partInfo.Params,params,pla))
if rollback is not None:
rollback.append((partInfo.PartName,
part,
partInfo.Placement.copy()))
setPlacement(part,pla)
if utils.isDraftCircle(part):
changed = False
h = partInfo.EntityMap.get('Edge1.c',None)
if not h:
continue
v0 = (part.Radius.Value,
part.FirstAngle.Value,
part.LastAngle.Value)
if part.FirstAngle == part.LastAngle:
v = (self.system.getParam(h[1]).val,v0[1],v0[2])
else:
params = [self.system.getParam(p).val for p in h[3]]
p0 = FreeCAD.Vector(1,0,0)
p1 = FreeCAD.Vector(params[0],params[1],0)
p2 = FreeCAD.Vector(params[2],params[3],0)
v = (p1.Length,
math.degrees(p0.getAngle(p1)),
math.degrees(p0.getAngle(p2)))
if utils.isSameValue(v0,v):
self.system.log('not change draft circle {}'.format(
partInfo.PartName))
else:
touched = True
self.system.log('change draft circle {} {}->{}'.format(
partInfo.PartName,v0,v))
if rollback is not None:
rollback.append((partInfo.PartName, part, v0))
part.Radius = v[0]
part.FirstAngle = v[1]
part.LastAngle = v[2]
if recompute and touched:
assembly.recompute(True)
def isFixedPart(self,part):
return part in self._fixedParts
def isFixedElement(self,part,subname):
return (part,None) in self._fixedElements or \
(part,subname) in self._fixedElements
def addFixedElement(self,part,subname):
self._fixedElements.add((part,subname))
def getPartInfo(self,info,fixed=False,group=0):
partInfo = self._partMap.get(info.Part,None)
if partInfo:
return partInfo
if fixed or info.Part in self._fixedParts:
g = self._fixedGroup
else:
g = self.group
if utils.isDraftWire(info):
# Special treatment for draft wire. We do not change its placement,
# but individual point position, instead.
params = None
h = None
else:
self.system.NameTag = info.PartName
params = self.system.addPlacement(info.Placement,group=g)
self.system.NameTag = info.PartName + '.p'
p = self.system.addPoint3d(*params[:3],group=g)
self.system.NameTag = info.PartName + '.n'
n = self.system.addNormal3d(*params[3:],group=g)
self.system.NameTag = info.PartName + '.nx'
nx = self.system.addTransform(self.nx,
self.v0,self.v0,self.v0,*params[3:],group=g)
px = self.system.addTransform(self.px,self.v0,self.v0,
self.v0,*params[3:],group=g)
self.system.NameTag = info.PartName + '.w'
w = self.system.addWorkplane(p,n,group=g)
h = (w,p,(n,nx,px))
partInfo = PartInfo(Part = info.Part,
PartName = info.PartName,
Placement = info.Placement.copy(),
Params = params,
Workplane = h,
EntityMap = {},
Group = group if group else g,
CstrMap = {})
self.system.log('{}, {}'.format(partInfo,g))
self._partMap[info.Part] = partInfo
return partInfo
def _solve(objs=None,recursive=None,reportFailed=True,
recompute=True,dragPart=None,rollback=None):
if not objs:
sels = FreeCADGui.Selection.getSelectionEx('',False)
if len(sels):
objs = Assembly.getSelection()
if not objs:
raise RuntimeError('No assembly found in selection')
else:
objs = FreeCAD.ActiveDocument.Objects
if recursive is None:
recursive = True
elif not isinstance(objs,(list,tuple)):
objs = [objs]
assemblies = []
for obj in objs:
if not isTypeOf(obj,Assembly):
continue
if System.isDisabled(obj):
logger.debug('bypass disabled assembly {}'.format(objName(obj)))
continue
logger.debug('adding assembly {}'.format(objName(obj)))
assemblies.append(obj)
if not assemblies:
raise RuntimeError('no assembly found')
if recursive:
# Get all dependent object, including external ones, and return as a
# topologically sorted list.
#
# TODO: it would be ideal if we can filter out those disabled assemblies
# found during the recrusive search. Can't think of an easy way right
# now
objs = FreeCAD.getDependentObjects(assemblies,False,True)
assemblies = []
for obj in objs:
if not isTypeOf(obj,Assembly):
continue
if System.isDisabled(obj):
logger.debug('skip disabled assembly {}'.format(objName(obj)))
continue
logger.debug('adding assembly {}'.format(objName(obj)))
assemblies.append(obj)
if not assemblies:
raise RuntimeError('no assembly need to be solved')
try:
for assembly in assemblies:
if recompute:
assembly.recompute(True)
if not System.isTouched(assembly):
logger.debug('skip untouched assembly '
'{}'.format(objName(assembly)))
continue
Solver(assembly,reportFailed,dragPart,recompute,rollback)
System.touch(assembly,False)
except Exception:
if rollback is not None:
for name,part,v in reversed(rollback):
logger.debug('roll back {} to {}'.format(name,v))
if isinstance(v,FreeCAD.Placement):
setPlacement(part,v)
elif utils.isDraftWire(part):
idx,pt = v
part.Points[idx] = pt
elif utils.isDraftWire(part):
r,a1,a2 = v
part.Radius = r
part.FirstAngle = a1
part.LastAngle = a2
raise
return True
_SolverBusy = False
def solve(*args, **kargs):
global _SolverBusy
if _SolverBusy:
raise RuntimeError("Recursive call of solve() is not allowed")
try:
Assembly.cancelAutoSolve();
_SolverBusy = True
return _solve(*args,**kargs)
finally:
_SolverBusy = False
def isBusy():
return _SolverBusy