FreeCAD_assembly3/solver.py

224 lines
8.5 KiB
Python

import random
from collections import namedtuple
import FreeCAD, FreeCADGui
import asm3.assembly as asm
from asm3.utils import logger, objName, isSamePlacement
from asm3.constraint import Constraint, cstrName
from asm3.system import System
# 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 three entity handles, that is the workplane, the origin
# point, and the 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
PartInfo = namedtuple('SolverPartInfo',
('PartName','Placement','Params','Workplane','EntityMap','Group'))
class Solver(object):
def __init__(self,assembly,reportFailed):
self.system = System.getSystem(assembly)
cstrs = assembly.Proxy.getConstraints()
if not cstrs:
self.system.log('no constraint found in assembly '
'{}'.format(objName(assembly)))
return
parts = assembly.Proxy.getPartGroup().Group
if len(parts)<=1:
self.system.log('not enough parts in {}'.format(objName(assembly)))
return
self._fixedGroup = 2
self.group = 1 # the solving group
self._partMap = {}
self._cstrMap = {}
self._cstrs = []
self._entityMap = {}
self._fixedParts = set()
self.system.GroupHandle = self._fixedGroup
for cstr in cstrs:
if Constraint.isLocked(cstr):
Constraint.prepare(cstr,self)
elif not Constraint.isDisabled(cstr) and \
System.isConstraintSupported(
assembly,Constraint.getTypeName(cstr)):
self._cstrs.append(cstr)
if not self._fixedParts:
self.system.log('lock first part {}'.format(objName(parts[0])))
self._fixedParts.add(parts[0])
for cstr in self._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:
self._cstrMap[h] = cstr
else:
self._cstrMap[ret] = cstr
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:
c = self.system.getConstraint(h)
if c.group <= self._fixedGroup or \
c.group-self._fixedGroup >= len(self._cstrs):
logger.error('failed constraint in unexpected group'
' {}'.format(c.group))
continue
cstr = self._cstrs[c.group-self._fixedGroup]
msg += '\n{}, handle: {}'.format(cstrName(cstr),h)
logger.error(msg)
raise RuntimeError('Failed to solve {}: {}'.format(
assembly,e.message))
self.system.log('done sloving')
undoDocs = set()
for part,partInfo in self._partMap.items():
if part in self._fixedParts:
continue
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:
self.system.log('moving {} {} {} {}'.format(
partInfo.PartName,partInfo.Params,params,pla))
asm.setPlacement(part,pla,undoDocs)
for doc in undoDocs:
doc.commitTransaction()
def isFixedPart(self,info):
return info.Part in self._fixedParts
def addFixedPart(self,info):
self.system.log('lock part ' + info.PartName)
self._fixedParts.add(info.Part)
def getPartInfo(self,info):
partInfo = self._partMap.get(info.Part,None)
if partInfo:
return partInfo
# info.Object below is supposed to be the actual part geometry, while
# info.Part may be a link to that object. We use info.Object as a key so
# that multiple info.Part can share the same entity map.
#
# TODO: It is actually more complicated than that. Becuase info.Object
# itself may be a link, and followed by a chain of multiple links. It's
# complicated because each link can either have a linked placement or
# not, depends on its LinkTransform property, meaning that their
# Placement may be chained or independent. Ideally, we should explode
# the link chain, and recreate the transformation dependency using slvs
# transfomation entity. We'll leave that for another day, maybe...
#
# The down side for now is that we may have redundant entities, and
# worse, the solver may not be able to get correct result if there are
# placement dependent links among parts. So, one should avoid using
# LinkTransform in most cases.
#
entityMap = self._entityMap.get(info.Object,None)
if not entityMap:
entityMap = {}
self._entityMap[info.Object] = entityMap
if info.Part in self._fixedParts:
g = self._fixedGroup
else:
g = self.group
self.system.NameTag = info.PartName
params = self.system.addPlacement(info.Placement,group=g)
p = self.system.addPoint3d(*params[:3],group=g)
n = self.system.addNormal3d(*params[3:],group=g)
w = self.system.addWorkplane(p,n,group=g)
h = (w,p,n)
partInfo = PartInfo(PartName = info.PartName,
Placement = info.Placement.copy(),
Params = params,
Workplane = h,
EntityMap = entityMap,
Group = g)
self.system.log('{}'.format(partInfo))
self._partMap[info.Part] = partInfo
return partInfo
def solve(objs=None,recursive=True,reportFailed=True,recompute=True):
if not objs:
objs = FreeCAD.ActiveDocument.Objects
elif not isinstance(objs,(list,tuple)):
objs = [objs]
assemblies = []
for obj in objs:
if not asm.isTypeOf(obj,asm.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:
logger.error('no assembly found')
return
if recompute:
docs = set()
for o in assemblies:
docs.add(o.Document)
for d in docs:
logger.debug('recomputing {}'.format(d.Name))
d.recompute()
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 asm.isTypeOf(obj,asm.Assembly):
continue
if System.isDisabled(obj):
logger.debug('skip disabled assembly {}'.format(objName(obj)))
continue
if not System.isTouched(obj):
logger.debug('skip untouched assembly {}'.format(objName(obj)))
continue
logger.debug('adding assembly {}'.format(objName(obj)))
assemblies.append(obj)
if not assemblies:
logger.error('no assembly found')
return
for assembly in assemblies:
Solver(assembly,reportFailed)
if recompute:
assembly.Document.recompute()
System.touch(assembly,False)