FreeCAD_assembly3/constraint.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

1338 lines
45 KiB
Python

from collections import namedtuple
import FreeCAD, FreeCADGui, Part
from . import utils, gui
from .utils import objName,cstrlogger as logger, guilogger
from .proxy import ProxyType, PropertyInfo, propGet, propGetValue
import os
_iconPath = os.path.join(utils.iconPath,'constraints')
def _d(solver,partInfo,subname,shape,retAll=False):
'return a handle of any supported element of a draft object'
if not solver:
if utils.isDraftObject(partInfo):
return
raise RuntimeError('Expects only elements from a draft wire or '
'draft circle/arc')
if subname.startswith('Vertex'):
return _p(solver,partInfo,subname,shape,retAll)
elif subname.startswith('Edge'):
return _l(solver,partInfo,subname,shape,retAll)
else:
raise RuntimeError('Invalid element {} of object {}'.format(subname,
partInfo.PartName))
def _prepareDraftCircle(solver,partInfo,requireArc=False):
part = partInfo.Part
shape = utils.getElementShape((part,'Edge1'),Part.Edge)
func = _a if requireArc else _c
return func(solver,partInfo,'Edge1',shape,retAll=True)
def _p(solver,partInfo,subname,shape,retAll=False):
'return a handle of a transformed point derived from "shape"'
if not solver:
if not utils.hasCenter(shape):
return 'a vertex or circular edge/face'
if utils.isDraftWire(partInfo):
if utils.draftWireVertex2PointIndex(partInfo,subname) is None:
raise RuntimeError('Invalid draft wire vertex "{}" {}'.format(
subname,objName(partInfo)))
return
part = partInfo.Part
key = subname+'.p'
h = partInfo.EntityMap.get(key,None)
system = solver.system
if h:
system.log('cache {}: {}'.format(key,h))
return h if retAll else h[0]
v = utils.getElementPos(shape)
if utils.isDraftWire(part):
nameTag = partInfo.PartName + '.' + key
v = partInfo.Placement.multVec(v)
params = []
for n,val in (('.x',v.x),('.y',v.y),('.z',v.z)):
system.NameTag = nameTag+n
params.append(system.addParamV(val,group=partInfo.Group))
system.NameTag = nameTag
e = system.addPoint3d(*params)
h = [e, params]
system.log('{}: add draft point {},{}'.format(key,h,v))
if system.sketchPlane and not solver.isFixedElement(part,subname):
system.NameTag = nameTag + '.i'
e2 = system.addPointInPlane(e,system.sketchPlane[0],
group=partInfo.Group)
system.log('{}: add draft point in plane {},{}'.format(
partInfo.PartName,e2,system.sketchPlane[0]))
elif utils.isDraftCircle(part):
requireArc = subname=='Vertex2'
e = _prepareDraftCircle(solver,partInfo,requireArc)
if requireArc:
h = [e[1]]
elif subname=='Vertex1':
h = [e[2]]
elif subname=='Edge1':
# center point
h = [partInfo.Workplane[1]]
else:
raise RuntimeError('Invalid draft circle subname {} of '
'{}'.format(subname,partInfo.PartName))
system.log('{}: add circle point {},{}'.format(key,h,e))
else:
nameTag = partInfo.PartName + '.' + key
system.NameTag = nameTag
e = system.addPoint3dV(*v)
system.NameTag = nameTag + 't'
h = system.addTransform(e,*partInfo.Params,group=partInfo.Group)
h = [h,e]
system.log('{}: {},{}'.format(key,h,partInfo.Group))
# use the point entity as key to store its original position vector
partInfo.EntityMap[h[0]] = [v]
partInfo.EntityMap[key] = h
return h if retAll else h[0]
def _n(solver,partInfo,subname,shape,retAll=False):
'return a handle of a transformed normal quaterion derived from shape'
if not solver:
if not utils.isPlanar(shape):
return 'an edge or face with a surface normal'
if utils.isDraftWire(partInfo):
logger.warn('Use draft wire {} for normal. Draft wire placement'
' is not transformable'.format(partInfo.PartName))
return
key = subname+'.n'
h = partInfo.EntityMap.get(key,None)
system = solver.system
if h:
system.log('cache {}: {}'.format(key,h))
else:
h = []
if utils.isDraftCircle(partInfo.Part):
_prepareDraftCircle(solver,partInfo)
rot = utils.getElementRotation(shape)
nameTag = partInfo.PartName + '.' + key
system.NameTag = nameTag
e = system.addNormal3dV(*utils.getNormal(rot))
system.NameTag += 't'
h.append(system.addTransform(e,*partInfo.Params,group=partInfo.Group))
# also add x axis pointing quaterion for convenience
xrot = FreeCAD.Rotation(FreeCAD.Vector(0,1,0),90).multiply(rot)
system.NameTag = nameTag + 'x'
e = system.addNormal3dV(*utils.getNormal(xrot))
system.NameTag = nameTag + 'xt'
h.append(system.addTransform(e,*partInfo.Params,group=partInfo.Group))
# also add local x pointing vector (1,0,0)
px = rot.multVec(FreeCAD.Vector(1,0,0))
hx = system.addPoint3dV(px.x,px.y,px.z)
h.append(system.addTransform(hx,*partInfo.Params,group=partInfo.Group))
system.log('{}: {},{}'.format(key,h,partInfo.Group))
# use the normal entity as key to store its original rotation
partInfo.EntityMap[h[0]] = [rot]
partInfo.EntityMap[key] = h
return h if retAll else h[0]
def _l(solver,partInfo,subname,shape,retAll=False):
'return a pair of handle of the end points of an edge in "shape"'
if not solver:
if not utils.isLinearEdge(shape):
return 'a linear edge'
if not utils.isDraftWire(partInfo):
return
vname1,vname2 = utils.edge2VertexIndex(partInfo,subname)
if not vname1:
raise RuntimeError('Invalid draft subname {} or {}'.format(
subname,objName(partInfo)))
v = shape.Edges[0].Vertexes
ret = _p(solver,partInfo,vname1,v[0])
if ret:
return ret
return _p(solver,partInfo,vname2,v[1])
part = partInfo.Part
key = subname+'.l'
h = partInfo.EntityMap.get(key,None)
system = solver.system
if h:
system.log('cache {}: {}'.format(key,h))
else:
nameTag = partInfo.PartName + '.' + key
v = shape.Edges[0].Vertexes
if utils.isDraftWire(part):
vname1,vname2 = utils.edge2VertexIndex(part,subname)
if not vname1:
raise RuntimeError('Invalid draft subname {} or {}'.format(
subname,partInfo.PartName))
tp1 = _p(solver,partInfo,vname1,v[0])
tp2 = _p(solver,partInfo,vname2,v[1])
else:
system.NameTag = nameTag + 'p1'
p1 = system.addPoint3dV(*v[0].Point)
system.NameTag = nameTag + 'p1t'
tp1 = system.addTransform(p1,*partInfo.Params,group=partInfo.Group)
system.NameTag = nameTag + 'p2'
p2 = system.addPoint3dV(*v[-1].Point)
system.NameTag = nameTag + 'p2t'
tp2 = system.addTransform(p2,*partInfo.Params,group=partInfo.Group)
system.NameTag = nameTag
h = system.addLineSegment(tp1,tp2,group=partInfo.Group)
h = (h,tp1,tp2)
system.log('{}: {},{}'.format(key,h,partInfo.Group))
partInfo.EntityMap[key] = h
return h if retAll else h[0]
def _la(solver,partInfo,subname,shape,retAll=False):
_ = retAll
return _l(solver,partInfo,subname,shape,True)
def _dl(solver,partInfo,subname,shape,retAll=False):
'return a handle of a draft wire'
if not solver:
if utils.isDraftWire(partInfo):
return
raise RuntimeError('Requires a non-subdivided draft wire')
return _l(solver,partInfo,subname,shape,retAll)
def _ln(solver,partInfo,subname,shape,retAll=False):
'return a handle for either a line or a normal depends on the shape'
if not solver:
if utils.isLinearEdge(shape) or utils.isPlanar(shape):
return
return 'a linear edge or edge/face with planar surface'
if utils.isLinearEdge(shape):
return _l(solver,partInfo,subname,shape,retAll)
return _n(solver,partInfo,subname,shape)
def _lw(solver,partInfo,subname,shape,retAll=False):
'return a handle for either a line or a plane depending on the shape'
_ = retAll
if not solver:
if utils.isLinearEdge(shape) or utils.isPlanar(shape):
return
return 'a linear edge or edge/face with planar surface'
if utils.isLinearEdge(shape):
return _l(solver,partInfo,subname,shape,False)
return _wa(solver,partInfo,subname,shape)
def _w(solver,partInfo,subname,shape,retAll=False):
'return a handle of a transformed plane/workplane from "shape"'
if not solver:
if utils.isPlanar(shape):
return
return 'an edge/face with a planar surface'
key = subname+'.w'
h = partInfo.EntityMap.get(key,None)
system = solver.system
if h:
system.log('cache {}: {}'.format(key,h))
else:
p = _p(solver,partInfo,subname,shape)
n = _n(solver,partInfo,subname,shape,True)
system.NameTag = partInfo.PartName + '.' + key
w = system.addWorkplane(p,n[0],group=partInfo.Group)
h = [w,p,n]
system.log('{}: {},{}'.format(key,h,partInfo.Group))
return h if retAll else h[0]
def _wa(solver,partInfo,subname,shape,retAll=False):
_ = retAll
return _w(solver,partInfo,subname,shape,True)
def _c(solver,partInfo,subname,shape,requireArc=False,retAll=False):
'return a handle of a transformed circle/arc derived from "shape"'
if not solver:
r = utils.getElementCircular(shape)
if r:
if requireArc and not isinstance(r,tuple):
return 'an arc edge'
return
return 'a cicular edge'
if requireArc:
key = subname+'.a'
else:
key = subname+'.c'
h = partInfo.EntityMap.get(key,None)
system = solver.system
if h:
system.log('cache {}: {}'.format(key,h))
return h if retAll else h[0]
g = partInfo.Group
nameTag = partInfo.PartName + '.' + key
if utils.isDraftCircle(partInfo.Part):
part = partInfo.Part
w,p,n = partInfo.Workplane[:3]
n = n[0]
if system.sketchPlane and not solver.isFixedElement(part,subname):
system.NameTag = nameTag + '.o'
e1 = system.addSameOrientation(n,system.sketchPlane[2],group=g)
system.NameTag = nameTag + '.i'
e2 = system.addPointInPlane(p,system.sketchPlane[0],group=g)
system.log('{}: fix draft circle in plane {},{}'.format(
partInfo.PartName,e1,e2))
if part.FirstAngle == part.LastAngle:
if requireArc:
raise RuntimeError('expecting an arc from {}'.format(
partInfo.PartName))
system.NameTag = nameTag + '.r'
r = system.addParamV(part.Radius.Value,group=g)
system.NameTag = nameTag + '.p0'
p0 = system.addPoint2d(w,r,solver.v0,group=g)
system.NameTag = nameTag
e = system.addCircle(p,n,system.addDistance(r),group=g)
h = [e,r,p0]
system.log('{}: add draft circle {}, {}'.format(key,h,g))
else:
system.NameTag = nameTag + '.c'
center = system.addPoint2d(w,solver.v0,solver.v0,group=g)
params = []
points = []
v = shape.Vertexes
for i in 0,1:
for n,val in ('.x{}',v[i].Point.x),('.y{}',v[i].Point.y):
system.NameTag = nameTag+n.format(i)
params.append(system.addParamV(val,group=g))
system.NameTag = nameTag + '.p{}'.format(i)
points.append(system.addPoint2d(w,*params[-2:],group=g))
system.NameTag = nameTag
e = system.addArcOfCircle(w,center,*points,group=g)
h = [e,points[1],points[0],params]
system.log('{}: add draft arc {}, {}'.format(key,h,g))
# exhaust all possible keys from a draft circle to save
# recomputation
sub = subname + '.c' if requireArc else '.a'
partInfo.EntityMap[sub] = h
else:
w,p,n = _w(solver,partInfo,subname,shape,True)[:3]
n = n[0]
r = utils.getElementCircular(shape)
if not r:
raise RuntimeError('shape is not cicular')
system.NameTag = nameTag + '.r'
hr = system.addDistanceV(r)
if requireArc or isinstance(r,(list,tuple)):
l = _l(solver,partInfo,subname,shape,True)
system.NameTag = nameTag
h = system.addArcOfCircle(w,p,l[1],l[2],group=g)
else:
system.NameTag = nameTag
h = system.addCircle(p,n,hr,group=g)
h = (h,hr)
system.log('{}: {},{}'.format(key,h,g))
partInfo.EntityMap[key] = h
return h if retAll else h[0]
def _dc(solver,partInfo,subname,shape,requireArc=False,retAll=False):
'return a handle of a draft circle'
if not solver:
if utils.isDraftCircle(partInfo):
return
raise RuntimeError('Requires a draft circle')
return _c(solver,partInfo,subname,shape,requireArc,retAll)
def _a(solver,partInfo,subname,shape,retAll=False):
'return a handle of a transformed arc derived from "shape"'
return _c(solver,partInfo,subname,shape,True,retAll)
class ConstraintCommand:
_menuGroupName = ''
def __init__(self,tp):
self.tp = tp
self._id = 100 + tp._id
self._active = None
@property
def _toolbarName(self):
return self.tp._toolbarName
def workbenchActivated(self):
self._active = None
def workbenchDeactivated(self):
pass
def getContextMenuName(self):
pass
def getName(self):
return 'asm3Add'+self.tp.getName()
def GetResources(self):
return self.tp.GetResources()
def Activated(self):
from .assembly import AsmConstraint
guilogger.report('constraint "{}" command exception'.format(
self.tp.getName()), AsmConstraint.make,self.tp._id)
def IsActive(self):
if not FreeCAD.ActiveDocument:
return False
if self._active is None:
self.checkActive()
return self._active
def checkActive(self):
from .assembly import AsmConstraint
if guilogger.catchTrace('selection "{}" exception'.format(
self.tp.getName()), AsmConstraint.getSelection, self.tp._id):
self._active = True
else:
self._active = False
def onClearSelection(self):
self._active = False
class Constraint(ProxyType):
'constraint meta class'
_typeID = '_ConstraintType'
_typeEnum = 'ConstraintType'
_disabled = 'Disabled'
@classmethod
def register(mcs,cls):
super(Constraint,mcs).register(cls)
if cls._id>=0 and cls._iconName is not Base._iconName:
try:
gui.AsmCmdManager.register(ConstraintCommand(cls))
except Exception:
logger.error('failed to register {}'.format(cls.getName()))
raise
@classmethod
def attach(mcs,obj,checkType=True):
if checkType:
if not mcs._disabled in obj.PropertiesList:
obj.addProperty("App::PropertyBool",mcs._disabled,"Base",'')
return super(Constraint,mcs).attach(obj,checkType)
@classmethod
def onChanged(mcs,obj,prop):
if prop == mcs._disabled:
obj.ViewObject.signalChangeIcon()
if super(Constraint,mcs).onChanged(obj,prop):
try:
if obj.Name==obj.Label or \
mcs.getType(utils.getLabel(obj)):
obj.Label = mcs.getTypeName(obj)
except Exception as e:
logger.debug('auto constraint label failed: {}'.format(e))
@classmethod
def isDisabled(mcs,obj):
return getattr(obj,mcs._disabled,False)
@classmethod
def check(mcs,tp,elements,checkCount=False):
mcs.getType(tp).check(elements,checkCount)
@classmethod
def prepare(mcs,obj,solver):
return mcs.getProxy(obj).prepare(obj,solver)
@classmethod
def getFixedParts(mcs,solver,cstrs):
firstInfo = None
found = False
ret = set()
for obj in cstrs:
cstr = mcs.getProxy(obj)
if cstr.hasFixedPart(obj):
found = True
for info in cstr.getFixedParts(solver,obj):
logger.debug('fixed part ' + info.PartName)
ret.add(info.Part)
if not found and not firstInfo:
elements = obj.Proxy.getElements()
if elements:
firstInfo = elements[0].Proxy.getInfo()
if not found:
if not firstInfo:
return ret
if utils.isDraftObject(firstInfo.Part):
Locked.lockElement(firstInfo,solver)
logger.debug('lock first draft object {}'.format(
firstInfo.PartName))
solver.getPartInfo(firstInfo,True,solver.group)
else:
logger.debug('lock first part {}'.format(firstInfo.PartName))
ret.add(firstInfo.Part)
return ret
@classmethod
def getFixedTransform(mcs,cstrs):
firstPart = None
found = False
ret = {}
for obj in cstrs:
cstr = mcs.getProxy(obj)
if cstr.hasFixedPart(obj):
for info in cstr.getFixedTransform(obj):
found = True
ret[info.Part] = info
if not found and not firstPart:
elements = obj.Proxy.getElements()
if elements:
info = elements[0].Proxy.getInfo()
firstPart = info.Part
if not found and firstPart and not utils.isDraftObject(firstPart):
ret[firstPart] = False
return ret
@classmethod
def getIcon(mcs,obj):
cstr = mcs.getProxy(obj)
if cstr:
return cstr.getIcon(obj)
@classmethod
def init(mcs,obj):
cstr = mcs.getProxy(obj)
if cstr:
cstr.init(obj)
def _makeProp(name,tp,doc='',getter=propGet,internal=False,default=None):
PropertyInfo(Constraint,name,tp,doc,getter=getter,
group='Constraint',internal=internal,default=default)
_makeProp('Distance','App::PropertyDistance',getter=propGetValue)
_makeProp('Length','App::PropertyDistance',getter=propGetValue,default=5.0)
_makeProp('Offset','App::PropertyDistance',getter=propGetValue)
_makeProp('Cascade','App::PropertyBool',internal=True)
_makeProp('Angle','App::PropertyAngle',getter=propGetValue)
_makeProp('LockAngle','App::PropertyBool')
_makeProp('Ratio','App::PropertyFloat',default=1.0)
_makeProp('Difference','App::PropertyFloat')
_makeProp('Diameter','App::PropertyDistance',getter=propGetValue,default=10.0)
_makeProp('Radius','App::PropertyDistance',getter=propGetValue,default=5.0)
_makeProp('Supplement','App::PropertyBool',
'If True, then the second angle is calculated as 180-angle')
_makeProp('AtEnd','App::PropertyBool',
'If True, then tangent at the end point, or else at the start point')
_ordinal = ('1st', '2nd', '3rd', '4th', '5th', '6th', '7th')
def cstrName(obj):
return '{}<{}>'.format(objName(obj),Constraint.getTypeName(obj))
class Base(object):
__metaclass__ = Constraint
_id = -1
_entityDef = ()
_workplane = False
_props = []
_toolbarName = 'Assembly3 Constraints'
_iconName = 'Assembly_ConstraintGeneral.svg'
_menuText = 'Create "{}" constraint'
def __init__(self,_obj):
pass
@classmethod
def init(cls,_obj):
pass
@classmethod
def getPropertyInfoList(cls):
return cls._props
@classmethod
def constraintFunc(cls,obj,solver):
try:
return getattr(solver.system,'add'+cls.getName())
except AttributeError:
logger.warn('{} not supported in solver "{}"'.format(
cstrName(obj),solver.getName()))
@classmethod
def getEntityDef(cls,elements,checkCount,obj=None):
entities = cls._entityDef
if len(elements) == len(entities):
return entities
if cls._workplane and len(elements)==len(entities)+1:
return list(entities) + [_w]
if not checkCount and len(elements)<len(entities):
return entities[:len(elements)]
if not obj:
name = cls.getName()
else:
name += cstrName(obj)
if len(elements)<len(entities):
msg = entities[len(elements)](None,None,None,None)
raise RuntimeError('Constraint "{}" requires the {} element to be'
' {}'.format(cls.getName(), _ordinal[len(elements)], msg))
raise RuntimeError('Constraint {} has too many elements, expecting '
'only {}'.format(name,len(entities)))
@classmethod
def check(cls,elements,checkCount=False):
entities = cls.getEntityDef(elements,checkCount)
for i,e in enumerate(entities):
info = elements[i]
msg = e(None,info.Part,info.Subname,info.Shape)
if not msg:
continue
if i == len(cls._entityDef):
raise RuntimeError('Constraint "{}" requires an optional {} '
'element to be a planar face for defining a '
'workplane'.format(cls.getName(), _ordinal[i], msg))
raise RuntimeError('Constraint "{}" requires the {} element to be'
' {}'.format(cls.getName(), _ordinal[i], msg))
@classmethod
def getIcon(cls,obj):
return utils.getIcon(cls,Constraint.isDisabled(obj),_iconPath)
@classmethod
def getEntities(cls,obj,solver,retAll=False):
'''maps fcad element shape to entities'''
elements = obj.Proxy.getElements()
entities = cls.getEntityDef(elements,True,obj)
ret = []
for e,o in zip(entities,elements):
info = o.Proxy.getInfo()
partInfo = solver.getPartInfo(info)
ret.append(e(solver,partInfo,info.Subname,info.Shape,retAll=retAll))
if cls._workplane and len(elements)==len(cls._entityDef):
if solver.system.sketchPlane:
ret.append(solver.system.sketchPlane[0])
elif int(cls._workplane)>1:
raise RuntimeError('Constraint "{}" requires a sketch plane '
'or a {} element to define a projection plane'.format(
cstrName(obj), _ordinal[len(elements)]))
solver.system.log('{} entities: {}'.format(cstrName(obj),ret))
return ret
@classmethod
def prepare(cls,obj,solver):
func = cls.constraintFunc(obj,solver)
if func:
params = cls.getPropertyValues(obj) + cls.getEntities(obj,solver)
ret = func(*params,group=solver.group)
solver.system.log('{}: {}'.format(cstrName(obj),ret))
return ret
else:
logger.warn('{} no constraint func'.format(cstrName(obj)))
@classmethod
def hasFixedPart(cls,_obj):
return False
@classmethod
def getMenuText(cls):
return cls._menuText.format(cls.getName())
@classmethod
def getToolTip(cls):
tooltip = getattr(cls,'_tooltip',None)
if not tooltip:
return cls.getMenuText()
return tooltip.format(cls.getName())
@classmethod
def GetResources(cls):
return {'Pixmap':utils.addIconToFCAD(cls._iconName,_iconPath),
'MenuText':cls.getMenuText(),
'ToolTip':cls.getToolTip()}
class Locked(Base):
_id = 0
_iconName = 'Assembly_ConstraintLock.svg'
_tooltip = 'Add a "{}" constraint to fix part(s)'
@classmethod
def getFixedParts(cls,_solver,obj):
ret = []
for e in obj.Proxy.getElements():
info = e.Proxy.getInfo()
if not utils.isVertex(info.Shape) and \
not utils.isLinearEdge(info.Shape) and \
not utils.isDraftCircle(info.Part):
ret.append(info)
return ret
Info = namedtuple('AsmCstrTransformInfo', ('Part', 'Shape'))
@classmethod
def getFixedTransform(cls,obj):
ret = []
for e in obj.Proxy.getElements():
info = e.Proxy.getInfo()
if utils.isDraftObject(info.Part):
continue
shape = None
if utils.isVertex(info.Shape) or \
utils.isLinearEdge(info.Shape):
shape = info.Shape
ret.append(cls.Info(Part=info.Part,Shape=shape))
return ret
@classmethod
def hasFixedPart(cls,obj):
return len(obj.Proxy.getElements())>0
@classmethod
def lockElement(cls,info,solver):
ret = []
system = solver.system
isVertex = utils.isVertex(info.Shape)
if solver.isFixedElement(info.Part,info.Subname):
return ret
if not isVertex and utils.isDraftCircle(info.Part):
if solver.sketchPlane:
_c(solver,solver.getPartInfo(info),info.Subname,info.Shape)
else:
solver.getPartInfo(info,True,solver.group)
solver.addFixedElement(info.Part,info.Subname)
return ret
if not isVertex and not utils.isLinearEdge(info.Shape):
return ret
partInfo = solver.getPartInfo(info)
fixPoint = False
if isVertex:
names = [info.Subname]
if utils.isDraftCircle(info.Part):
_c(solver,partInfo,'Edge1',info.Shape)
solver.addFixedElement(info.Part,'Edge1')
elif utils.isDraftWire(info.Part):
fixPoint = True
names = utils.edge2VertexIndex(info.Part,info.Subname)
else:
names = [info.Subname+'.fp1', info.Subname+'.fp2']
nameTag = partInfo.PartName + '.' + info.Subname
for i,v in enumerate(info.Shape.Vertexes):
surfix = '.fp{}'.format(i)
system.NameTag = nameTag + surfix
# Create an entity for the transformed constant point
e1 = system.addPoint3dV(*info.Placement.multVec(v.Point))
# Get the entity for the point expressed in variable parameters
e2 = _p(solver,partInfo,names[i],v)
solver.addFixedElement(info.Part,names[i])
if i==0 or fixPoint:
# We are fixing a vertex, or a linear edge. Either way, we
# shall add a point coincidence constraint here.
e0 = e1
system.NameTag = nameTag + surfix
if system.sketchPlane and utils.isDraftObject(info.Part):
w = system.sketchPlane[0]
else:
w = 0
e = system.addPointsCoincident(e1,e2,w,group=solver.group)
system.log('{}: fix point {},{},{}'.format(
info.PartName,e,e1,e2))
else:
# The second point, so we are fixing a linear edge. We can't
# add a second coincidence constraint, which will cause
# over-constraint. We constraint the second point to be on
# the line defined by the linear edge.
#
# First, get an entity of the transformed constant line
system.NameTag = nameTag + '.fl'
l = system.addLineSegment(e0,e1)
system.NameTag = nameTag
# Now, constraint the second variable point to the line
e = system.addPointOnLine(e2,l,group=solver.group)
system.log('{}: fix line {},{}'.format(info.PartName,e,l))
ret.append(e)
return ret
@classmethod
def prepare(cls,obj,solver):
ret = []
for element in obj.Proxy.getElements():
ret += cls.lockElement(element.Proxy.getInfo(),solver)
return ret
@classmethod
def check(cls,elements,_checkCount=False):
if not all([utils.isElement(info.Shape) for info in elements]):
raise RuntimeError('Constraint "{}" requires all children to be '
'of element (Vertex, Edge or Face)'.format(cls.getName()))
class BaseMulti(Base):
_id = -1
_entityDef = (_wa,)
@classmethod
def check(cls,elements,checkCount=False):
if checkCount and len(elements)<2:
raise RuntimeError('Constraint "{}" requires at least two '
'elements'.format(cls.getName()))
for info in elements:
msg = cls._entityDef[0](None,info.Part,info.Subname,info.Shape)
if msg:
raise RuntimeError('Constraint "{}" requires all the element '
'to be of {}'.format(cls.getName()))
return
@classmethod
def prepare(cls,obj,solver):
func = cls.constraintFunc(obj,solver);
if not func:
logger.warn('{} no constraint func'.format(cstrName(obj)))
return
parts = set()
ref = None
elements = []
props = cls.getPropertyValues(obj)
for e in obj.Proxy.getElements():
info = e.Proxy.getInfo()
if info.Part in parts:
logger.warn('{} skip duplicate parts {}'.format(
cstrName(obj),info.PartName))
continue
parts.add(info.Part)
if solver.isFixedPart(info.Part):
if ref:
logger.warn('{} skip more than one fixed part {}'.format(
cstrName(obj),info.PartName))
continue
ref = info
elements.insert(0,e)
else:
elements.append(e)
if len(elements)<=1:
logger.warn('{} has no effective constraint'.format(cstrName(obj)))
return
e0 = None
ret = []
firstInfo = None
for e in elements:
info = e.Proxy.getInfo()
partInfo = solver.getPartInfo(info)
if not e0:
e0 = cls._entityDef[0](solver,partInfo,info.Subname,info.Shape)
firstInfo = partInfo
else:
e = cls._entityDef[0](solver,partInfo,info.Subname,info.Shape)
params = props + [e0,e]
solver.system.checkRedundancy(obj,firstInfo,partInfo)
h = func(*params,group=solver.group)
if isinstance(h,(list,tuple)):
ret += list(h)
else:
ret.append(h)
return ret
class BaseCascade(BaseMulti):
@classmethod
def prepare(cls,obj,solver):
if not getattr(obj,'Cascade',True):
return super(BaseCascade,cls).prepare(obj,solver)
func = cls.constraintFunc(obj,solver);
if not func:
logger.warn('{} no constraint func'.format(cstrName(obj)))
return
props = cls.getPropertyValues(obj)
prev = None
ret = []
for e in obj.Proxy.getElements():
info = e.Proxy.getInfo()
if not prev or prev.Part==info.Part:
prev = info
continue
prevInfo = solver.getPartInfo(prev)
e1 = cls._entityDef[0](solver,prevInfo,prev.Subname,prev.Shape)
partInfo = solver.getPartInfo(info)
e2 = cls._entityDef[0](solver,partInfo,info.Subname,info.Shape)
prev = info
if solver.isFixedPart(info.Part):
params = props + [e1,e2]
else:
params = props + [e2,e1]
solver.system.checkRedendancy(obj,prevInfo,partInfo)
h = func(*params,group=solver.group)
if isinstance(h,(list,tuple)):
ret += list(h)
else:
ret.append(h)
if not ret:
logger.warn('{} has no effective constraint'.format(cstrName(obj)))
return ret
class PlaneCoincident(BaseCascade):
_id = 35
_iconName = 'Assembly_ConstraintCoincidence.svg'
_props = ['Cascade','Offset','LockAngle','Angle']
_tooltip = \
'Add a "{}" constraint to conincide planes of two or more parts.\n'\
'The planes are coincided at their centers with an optional distance.'
class PlaneAlignment(BaseCascade):
_id = 37
_iconName = 'Assembly_ConstraintAlignment.svg'
_props = ['Cascade','Offset','LockAngle','Angle']
_tooltip = 'Add a "{}" constraint to rotate planes of two or more parts\n'\
'into the same orientation'
class AxialAlignment(BaseMulti):
_id = 36
_iconName = 'Assembly_ConstraintAxial.svg'
_props = ['LockAngle','Angle']
_tooltip = 'Add a "{}" constraint to align planes of two or more parts.\n'\
'The planes are aligned at the direction of their surface normal axis.'
class SameOrientation(BaseMulti):
_id = 2
_entityDef = (_n,)
_iconName = 'Assembly_ConstraintOrientation.svg'
_tooltip = 'Add a "{}" constraint to align planes of two or more parts.\n'\
'The planes are aligned to have the same orientation (i.e. rotation)'
class MultiParallel(BaseMulti):
_id = 291
_entityDef = (_lw,)
_iconName = 'Assembly_ConstraintMultiParallel.svg'
_props = ['LockAngle','Angle']
_tooltip = 'Add a "{}" constraint to make planes normal or linear edges\n'\
'of two or more parts parallel.'
class Base2(Base):
_id = -1
_toolbarName = 'Assembly3 Constraints2'
class Angle(Base2):
_id = 27
_entityDef = (_ln,_ln)
_workplane = True
_props = ["Angle","Supplement"]
_iconName = 'Assembly_ConstraintAngle.svg'
_tooltip = 'Add a "{}" constraint to set the angle of planes or linear\n'\
'edges of two parts.'
@classmethod
def init(cls,obj):
shapes = [ info.Shape for info in obj.Proxy.getElementsInfo() ]
obj.Angle = utils.getElementsAngle(shapes[0],shapes[1])
class Perpendicular(Base2):
_id = 28
_entityDef = (_lw,_lw)
_workplane = True
_iconName = 'Assembly_ConstraintPerpendicular.svg'
_tooltip = 'Add a "{}" constraint to make planes or linear edges of two\n'\
'parts perpendicular.'
@classmethod
def prepare(cls,obj,solver):
system = solver.system
params = cls.getEntities(obj,solver)
e1,e2 = params[0],params[1]
isPlane = isinstance(e1,list),isinstance(e2,list)
if all(isPlane):
ret = system.addPerpendicular(e1[2],e2[2],group=solver.group)
elif not any(isPlane):
ret = system.addPerpendicular(e1,e2,group=solver.group)
elif isPlane[0]:
ret = system.addParallel(e1[2],e2,group=solver.group)
else:
ret = system.addParallel(e1,e2[2],group=solver.group)
return ret
class Parallel(Base2):
_id = -1
_entityDef = (_ln,_ln)
_workplane = True
_iconName = 'Assembly_ConstraintParallel.svg'
_tooltip = 'Add a "{}" constraint to make planes or linear edges of two\n'\
'parts parallel.'
class PointsCoincident(Base2):
_id = 1
_entityDef = (_p,_p)
_workplane = True
_iconName = 'Assembly_ConstraintPointsCoincident.svg'
_tooltip = 'Add a "{}" constraint to conincide two points.'
class PointInPlane(Base2):
_id = 3
_entityDef = (_p,_w)
_iconName = 'Assembly_ConstraintPointInPlane.svg'
_tooltip = 'Add a "{}" to constrain a point inside a plane.'
class PointOnLine(Base2):
_id = 4
_entityDef = (_p,_l)
_workplane = True
_iconName = 'Assembly_ConstraintPointOnLine.svg'
_tooltip = 'Add a "{}" to constrain a point on to a line.'
class PointsDistance(Base2):
_id = 5
_entityDef = (_p,_p)
_workplane = True
_props = ["Distance"]
_iconName = 'Assembly_ConstraintPointsDistance.svg'
_tooltip = 'Add a "{}" to constrain the distance of two points.'
@classmethod
def init(cls,obj):
points = [ info.Placement.multVec(info.Shape.Vertex1.Point)
for info in obj.Proxy.getElementsInfo() ]
obj.Distance = points[0].distanceToPoint(points[1])
class PointsProjectDistance(Base2):
_id = 6
_entityDef = (_p,_p,_l)
_props = ["Distance"]
_iconName = 'Assembly_ConstraintPointsProjectDistance.svg'
_tooltip = 'Add a "{}" to constrain the distance of two points\n' \
'projected on a line.'
class PointPlaneDistance(Base2):
_id = 7
_entityDef = (_p,_w)
_props = ["Distance"]
_iconName = 'Assembly_ConstraintPointPlaneDistance.svg'
_tooltip='Add a "{}" to constrain the distance between a point and a plane'
class PointLineDistance(Base2):
_id = 8
_entityDef = (_p,_l)
_workplane = True
_props = ["Distance"]
_iconName = 'Assembly_ConstraintPointLineDistance.svg'
_tooltip='Add a "{}" to constrain the distance between a point and a line'
class EqualPointLineDistance(Base2):
_id = 13
_entityDef = (_p,_l,_p,_l)
_workplane = True
_iconName = 'Assembly_ConstraintEqualPointLineDistance.svg'
_tooltip='Add a "{}" to constrain the distance between a point and a\n'\
'line to be the same as the distance between another point\n'\
'and line.'
class EqualAngle(Base2):
_id = 14
_entityDef = (_ln,_ln,_ln,_ln)
_workplane = True
_props = ["Supplement"]
_iconName = 'Assembly_ConstraintEqualAngle.svg'
_tooltip='Add a "{}" to equate the angles between two lines or normals.'
class Symmetric(Base2):
_id = 16
_entityDef = (_p,_p,_w)
_iconName = 'Assembly_ConstraintSymmetric.svg'
_tooltip='Add a "{}" constraint to make two points symmetric about a plane.'
class SymmetricHorizontal(Base2):
_id = 17
_entityDef = (_p,_p)
_workplane = 2
class SymmetricVertical(Base2):
_id = 18
_entityDef = (_p,_p)
_workplane = 2
class SymmetricLine(Base2):
_id = 19
_entityDef = (_p,_p,_l)
_workplane = 2
_iconName = 'Assembly_ConstraintSymmetricLine.svg'
_tooltip='Add a "{}" constraint to make two points symmetric about a line.'
class PointsHorizontal(Base2):
_id = 21
_entityDef = (_p,_p)
_workplane = 2
_iconName = 'Assembly_ConstraintPointsHorizontal.svg'
_tooltip='Add a "{}" constraint to make two points horizontal with each\n'\
'other when projected onto a plane.'
class PointsVertical(Base2):
_id = 22
_entityDef = (_p,_p)
_workplane = 2
_iconName = 'Assembly_ConstraintPointsVertical.svg'
_tooltip='Add a "{}" constraint to make two points vertical with each\n'\
'other when projected onto a plane.'
class LineHorizontal(Base2):
_id = 23
_entityDef = (_l,)
_workplane = 2
_iconName = 'Assembly_ConstraintLineHorizontal.svg'
_tooltip='Add a "{}" constraint to make a line segment horizontal when\n'\
'projected onto a plane.'
class LineVertical(Base2):
_id = 24
_entityDef = (_l,)
_workplane = 2
_iconName = 'Assembly_ConstraintLineVertical.svg'
_tooltip='Add a "{}" constraint to make a line segment vertical when\n'\
'projected onto a plane.'
class PointOnCircle(Base2):
_id = 26
_entityDef = (_p,_c)
_iconName = 'Assembly_ConstraintPointOnCircle.svg'
_tooltip='Add a "{}" to constrain a point on to a clyndrical plane\n' \
'defined by a cricle.'
class ArcLineTangent(Base2):
_id = 30
_entityDef = (_a,_l)
_props = ["AtEnd"]
_iconName = 'Assembly_ConstraintArcLineTangent.svg'
_tooltip='Add a "{}" constraint to make a line tangent to an arc\n'\
'at the start or end point of the arc.'
class Colinear(Base2):
_id = 39
_entityDef = (_la, _l)
_workplane = True
_iconName = 'Assembly_ConstraintColinear.svg'
_tooltip='Add a "{}" constraint to make to line colinear'
class BaseSketch(Base):
_id = -1
_toolbarName = 'Assembly3 Sketch Constraints'
class SketchPlane(BaseSketch):
_id = 38
_iconName = 'Assembly_ConstraintSketchPlane.svg'
_tooltip='Add a "{0}" to define the work plane of any draft element\n'\
'inside or following this constraint. Add an empty "{0}" to\n'\
'undefine the previous work plane'
@classmethod
def getEntityDef(cls,elements,checkCount,obj=None):
_ = checkCount
_ = obj
if not elements:
# If no element, then this constraint serves the prupose of clearing
# the current sketch plane
return []
# if there is any child element in this constraint, we expect the first
# one to be a planar face or edge to define the work plane. The rest of
# entities must be from some draft wire or circle/arc
#
# Base.prepare() will call system.addSketchPlane() with all contained
# element below. However, the default implementation,
# SystemExtension.addSketchPlane(), only really uses the first one,
# i.e. the one obtained by _wa(), i.e. a tuple of entities
# (workplane,base,normal).
return [_wa] + [_d]*(len(elements)-1)
class BaseDraftWire(BaseSketch):
_id = -1
@classmethod
def check(cls,elements,checkCount=False):
super(BaseDraftWire,cls).check(elements,checkCount)
if not checkCount:
return
for info in elements:
if utils.isDraftWire(info.Part):
return
raise RuntimeError('Constraint "{}" requires at least one linear edge '
'from a none-subdivided Draft.Wire'.format(
cls.getName()))
class LineLength(BaseSketch):
_id = 34
_entityDef = (_dl,)
_workplane = True
_props = ["Length"]
_iconName = 'Assembly_ConstraintLineLength.svg'
_tooltip='Add a "{}" constrain the length of a none-subdivided Draft.Wire'
@classmethod
def init(cls,obj):
info = obj.Proxy.getElementsInfo()[0]
obj.Length = info.Shape.Edge1.Length
@classmethod
def prepare(cls,obj,solver):
func = PointsDistance.constraintFunc(obj,solver)
if func:
_,p1,p2 = cls.getEntities(obj,solver,retAll=True)[0]
params = cls.getPropertyValues(obj) + [p1,p2]
ret = func(*params,group=solver.group)
solver.system.log('{}: {}'.format(cstrName(obj),ret))
return ret
else:
logger.warn('{} no constraint func'.format(cstrName(obj)))
class EqualLength(BaseDraftWire):
_id = 9
_entityDef = (_l,_l)
_workplane = True
_iconName = 'Assembly_ConstraintEqualLength.svg'
_tooltip='Add a "{}" constraint to make two lines of the same length.'
class LengthRatio(BaseDraftWire):
_id = 10
_entityDef = (_l,_l)
_workplane = True
_props = ["Ratio"]
_iconName = 'Assembly_ConstraintLengthRatio.svg'
_tooltip='Add a "{}" to constrain the length ratio of two lines.'
class LengthDifference(BaseDraftWire):
_id = 11
_entityDef = (_l,_l)
_workplane = True
_props = ["Difference"]
_iconName = 'Assembly_ConstraintLengthDifference.svg'
_tooltip='Add a "{}" to constrain the length difference of two lines.'
class EqualLengthPointLineDistance(BaseSketch):
_id = 12
_entityDef = (_p,_l,_l)
_workplane = True
_iconName = 'Assembly_ConstraintLengthEqualPointLineDistance.svg'
_tooltip='Add a "{}" to constrain the distance between a point and a\n' \
'line to be the same as the length of a another line.'
class EqualLineArcLength(BaseSketch):
_id = 15
_entityDef = (_l,_a)
_workplane = True
_tooltip='Add a "{}" constraint to make a line of the same length as an arc'
@classmethod
def check(cls,elements,checkCount=False):
super(EqualLineArcLength,cls).check(elements,checkCount)
if not checkCount:
return
for i,info in enumerate(elements):
if i:
if utils.isDraftCircle(info.Part):
return
elif utils.isDraftWire(info.Part):
return
raise RuntimeError('Constraint "{}" requires at least one '
'non-subdivided Draft.Wire or one Draft.Circle'.format(
cls.getName()))
class MidPoint(BaseSketch):
_id = 20
_entityDef = (_p,_l)
_workplane = True
_iconName = 'Assembly_ConstraintMidPoint.svg'
_tooltip='Add a "{}" to constrain a point to the middle point of a line.'
class Diameter(BaseSketch):
_id = 25
_entityDef = (_dc,)
_props = ("Diameter",)
_iconName = 'Assembly_ConstraintDiameter.svg'
_tooltip='Add a "{}" to constrain the diameter of a circle/arc'
class EqualRadius(BaseSketch):
_id = 33
_entityDef = (_c,_c)
_iconName = 'Assembly_ConstraintEqualRadius.svg'
_tooltip='Add a "{}" constraint to make two circles/arcs of the same radius'
@classmethod
def check(cls,elements,checkCount=False):
super(EqualRadius,cls).check(elements,checkCount)
if not checkCount:
return
for info in elements:
if utils.isDraftCircle(info.Part):
return
raise RuntimeError('Constraint "{}" requires at least one '
'Draft.Circle'.format(cls.getName()))
# class CubicLineTangent(BaseSketch):
# _id = 31
#
#
# class CurvesTangent(BaseSketch):
# _id = 32