diff --git a/assembly.py b/assembly.py index 26bba54..33ba97d 100644 --- a/assembly.py +++ b/assembly.py @@ -488,7 +488,7 @@ class ViewProviderAsmElement(ViewProviderAsmOnTop): Group=owner, Subname=subname),undo=True) -def getPartInfo(parent, subname): +def getElementInfo(parent, subname): '''Return a named tuple containing the part object element information Parameters: @@ -630,7 +630,7 @@ def getPartInfo(parent, subname): obj = part.getLinkedObject(False) partName = part.Name - return utils.PartInfo(Parent = parent, + return utils.ElementInfo(Parent = parent, SubnameRef = subnameRef, Part = part, PartName = partName, @@ -749,7 +749,7 @@ class AsmElementLink(AsmBase): self.info = None if not getattr(self,'Object',None): return - self.info = getPartInfo(self.getAssembly().getPartGroup(), + self.info = getElementInfo(self.getAssembly().getPartGroup(), self.getElementSubname()) return self.info @@ -1619,7 +1619,7 @@ class AsmMovingPart(object): movingPart.tracePoint = movingPart.draggerPlacement.Base def update(self): - info = getPartInfo(self.parent,self.subname) + info = getElementInfo(self.parent,self.subname) self.oldPlacement = info.Placement.copy() self.part = info.Part self.partName = info.PartName @@ -1691,11 +1691,12 @@ class AsmMovingPart(object): # AsmMovingPart.update() return self.draggerPlacement -def getMovingPartInfo(): +def getMovingElementInfo(): '''Extract information from current selection for part moving It returns a tuple containing the selected assembly hierarchy (obtained from - Assembly.findChildren()), and AsmPartInfo of the selected child part object. + Assembly.findChildren()), and AsmElementInfo of the selected child part + object. If there is only one selection, then the moving part will be one belong to the highest level assembly in selected hierarchy. @@ -1722,7 +1723,7 @@ def getMovingPartInfo(): objName(sels[0].Object),sels[0].SubElementNames[0])) if len(sels[0].SubElementNames)==1: - info = getPartInfo(ret[0].Assembly,ret[0].Subname) + info = getElementInfo(ret[0].Assembly,ret[0].Subname) if not info: return return (ret, info) @@ -1741,14 +1742,14 @@ def getMovingPartInfo(): assembly = ret[-1].Assembly for r in ret2: if assembly == r.Assembly: - return (ret2, getPartInfo(r.Assembly,r.Subname)) + return (ret2, getElementInfo(r.Assembly,r.Subname)) raise RuntimeError('not child parent selection') def canMovePart(): - return logger.catchTrace('',getMovingPartInfo) is not None + return logger.catchTrace('',getMovingElementInfo) is not None def movePart(useCenterballDragger=None): - ret = logger.catch('exception when moving part', getMovingPartInfo) + ret = logger.catch('exception when moving part', getMovingElementInfo) if not ret: return False diff --git a/constraint.py b/constraint.py index d0031ba..56c8d5e 100644 --- a/constraint.py +++ b/constraint.py @@ -1,5 +1,5 @@ from collections import namedtuple -import FreeCAD, FreeCADGui +import FreeCAD, FreeCADGui, Part from . import utils, gui from .utils import objName,cstrlogger as logger, guilogger from .proxy import ProxyType, PropertyInfo, propGet, propGetValue @@ -12,11 +12,10 @@ def _p(solver,partInfo,subname,shape,retAll=False): if not solver: if not utils.hasCenter(shape): return 'a vertex or circular edge/face' - if not utils.isDraftWire(partInfo): - return - if utils.draftWireVertex2PointIndex(partInfo,subname) is None: - raise RuntimeError('Invalid draft wire vertex "{}" {}'.format( - subname,objName(partInfo))) + if utils.isDraftWire(partInfo): + if utils.draftWireVertex2PointIndex(partInfo,subname) is None: + raise RuntimeError('Invalid draft wire vertex "{}" {}'.format( + subname,objName(partInfo))) return key = subname+'.p' @@ -24,27 +23,46 @@ def _p(solver,partInfo,subname,shape,retAll=False): system = solver.system if h: system.log('cache {}: {}'.format(key,h)) + return h if retAll else h[0] + + if utils.isDraftWire(partInfo.Part): + v = utils.getElementPos(shape) + 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)) + + elif utils.isDraftCircle(partInfo.Part): + shape = utils.getElementShape((partInfo.Part,'Edge1'),Part.Edge) + if subname == 'Vertex1': + e = _c(solver,partInfo,'Edge1',shape,retAll=True) + h = [e[2]] + elif subname == 'Vertex2': + e = _a(solver,partInfo,'Edge1',shape,retAll=True) + h = [e[1]] + else: + raise RuntimeError('Invalid draft circle vertex {} of ' + '{}'.format(subname,objName(partInfo.Part))) + + system.log('{}: add circle point {},{}'.format(key,h,e)) + else: v = utils.getElementPos(shape) nameTag = partInfo.PartName + '.' + key - if utils.isDraftWire(partInfo.Part): - 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)) - else: - system.NameTag = nameTag - e = system.addPoint3dV(*v) - system.NameTag = nameTag + 't' - h = system.addTransform(e[0],*partInfo.Params,group=partInfo.Group) - h = [h,e] - system.log('{}: {},{}'.format(key,h,partInfo.Group)) - partInfo.EntityMap[key] = h + system.NameTag = nameTag + e = system.addPoint3dV(*v) + system.NameTag = nameTag + 't' + h = system.addTransform(e[0],*partInfo.Params,group=partInfo.Group) + h = [h,e] + system.log('{}: {},{}'.format(key,h,partInfo.Group)) + + partInfo.EntityMap[key] = h return h if retAll else h[0] def _n(solver,partInfo,subname,shape,retAll=False): @@ -52,9 +70,9 @@ def _n(solver,partInfo,subname,shape,retAll=False): if not solver: if not utils.isPlanar(shape): return 'an edge or face with a surface normal' - if utils.isDraftWire(partInfo): + if utils.isDraftWire(partInfo.Part): logger.warn('Use draft wire {} for normal. Draft wire placement' - ' is not transformable'.format(objName(partInfo))) + ' is not transformable'.format(partInfo.PartName)) return key = subname+'.n' @@ -88,7 +106,7 @@ def _l(solver,partInfo,subname,shape,retAll=False): if not solver: if not utils.isLinearEdge(shape): return 'a linear edge' - if not utils.isDraftWire(partInfo): + if not utils.isDraftWire(partInfo.Part): return part = partInfo vname1,vname2 = utils.edge2VertexIndex(subname) @@ -132,6 +150,14 @@ def _l(solver,partInfo,subname,shape,retAll=False): return h if retAll else h[0] +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-closed-or-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: @@ -173,8 +199,10 @@ def _c(solver,partInfo,subname,shape,requireArc=False,retAll=False): if not solver: r = utils.getElementCircular(shape) if r: + if requireArc and not isinstance(r,tuple): + return 'an arc edge' return - return 'an cicular edge' + return 'a cicular edge' if requireArc: key = subname+'.a' else: @@ -183,26 +211,76 @@ def _c(solver,partInfo,subname,shape,requireArc=False,retAll=False): 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 + 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) r = utils.getElementCircular(shape) if not r: raise RuntimeError('shape is not cicular') - nameTag = partInfo.PartName + '.' + key 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=partInfo.Group) + h = system.addArcOfCircle(w,p,l[1],l[2],group=g) else: system.NameTag = nameTag - h = system.addCircle(p,n,hr,group=partInfo.Group) + h = system.addCircle(p,n,hr,group=g) h = (h,hr) - system.log('{}: {},{}'.format(key,h,partInfo.Group)) - partInfo.EntityMap[key] = h + 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) @@ -298,31 +376,34 @@ class Constraint(ProxyType): return mcs.getProxy(obj).prepare(obj,solver) @classmethod - def getFixedParts(mcs,cstrs): - firstPart = None - firstPartName = None + 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(obj): + for info in cstr.getFixedParts(solver,obj): logger.debug('fixed part ' + info.PartName) ret.add(info.Part) - if not found and not firstPart: + if not found and not firstInfo: elements = obj.Proxy.getElements() if elements: - info = elements[0].Proxy.getInfo() - firstPart = info.Part - firstPartName = info.PartName + firstInfo = elements[0].Proxy.getInfo() if not found: - if not firstPart: + if not firstInfo: return None - logger.debug('lock first part {}'.format(firstPartName)) - ret.add(firstPart) + 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 @@ -353,20 +434,20 @@ class Constraint(ProxyType): return cstr.getIcon(obj) -def _makeProp(name,tp,doc='',getter=propGet,internal=False): +def _makeProp(name,tp,doc='',getter=propGet,internal=False,default=None): PropertyInfo(Constraint,name,tp,doc,getter=getter, - group='Constraint',internal=internal) + group='Constraint',internal=internal,default=default) _makeProp('Distance','App::PropertyDistance',getter=propGetValue) -_makeProp('Length','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') +_makeProp('Ratio','App::PropertyFloat',default=1.0) _makeProp('Difference','App::PropertyFloat') -_makeProp('Diameter','App::PropertyFloat') -_makeProp('Radius','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', @@ -428,7 +509,7 @@ class Base(object): entities = cls.getEntityDef(group,checkCount) for i,e in enumerate(entities): o = group[i] - if isinstance(o,utils.PartInfo): + if isinstance(o,utils.ElementInfo): msg = e(None,o.Part,o.Subname,o.Shape) else: msg = e(None,None,None,o) @@ -496,12 +577,13 @@ class Locked(Base): _tooltip = 'Add a "{}" constraint to fix part(s)' @classmethod - def getFixedParts(cls,obj): + 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): + not utils.isLinearEdge(info.Shape) and \ + not utils.isDraftCircle(info): ret.append(info) return ret @@ -512,11 +594,12 @@ class Locked(Base): ret = [] for e in obj.Proxy.getElements(): info = e.Proxy.getInfo() - if not utils.isVertex(info.Shape) and \ - not utils.isLinearEdge(info.Shape): - ret.append(cls.Info(Part=info.Part,Shape=None)) - continue - ret.append(cls.Info(Part=info.Part,Shape=info.Shape)) + shape = None + if utils.isVertex(info.Shape) or \ + utils.isDraftCircle(info) or \ + utils.isLinearEdge(info.Shape): + shape = info.Shape + ret.append(cls.Info(Part=info.Part,Shape=shape)) return ret @classmethod @@ -524,67 +607,77 @@ class Locked(Base): return len(obj.Proxy.getElements())>0 @classmethod - def prepare(cls,obj,solver): + def lockElement(cls,info,solver): ret = [] system = solver.system - for element in obj.Proxy.getElements(): - info = element.Proxy.getInfo() - isVertex = utils.isVertex(info.Shape) - if not isVertex and not utils.isLinearEdge(info.Shape): - continue - if solver.isFixedPart(info): - logger.warn('redundant locking element "{}" in constraint ' - '{}'.format(info.Subname,objName(obj))) - continue + isVertex = utils.isVertex(info.Shape) + if not isVertex and utils.isDraftCircle(info): + solver.getPartInfo(info,True,solver.group) + return ret - partInfo = solver.getPartInfo(info) + if not isVertex and not utils.isLinearEdge(info.Shape): + return ret - fixPoint = False - if isVertex: - names = [info.Subname] - elif utils.isDraftObject(info): - fixPoint = True - names = utils.edge2VertexIndex(info.Subname) - else: - names = [info.Subname+'.fp1', info.Subname+'.fp2'] + if solver.isFixedPart(info): + logger.warn('redundant locking element "{}" in constraint ' + '{}'.format(info.Subname,info.PartName)) + return ret - nameTag = partInfo.PartName + '.' + info.Subname + partInfo = solver.getPartInfo(info) - for i,v in enumerate(info.Shape.Vertexes): - surfix = '.fp{}'.format(i) + fixPoint = False + if isVertex: + names = [info.Subname] + elif utils.isDraftObject(info): + fixPoint = True + names = utils.edge2VertexIndex(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) + + 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 + e = system.addPointsCoincident(e1,e2,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)) - # Create an entity for the transformed constant point - e1 = system.addPoint3dV(*info.Placement.multVec(v.Point)) + ret.append(e) - # Get the entity for the point expressed in variable parameters - e2 = _p(solver,partInfo,names[i],v) - - 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 - e = system.addPointsCoincident(e1,e2,group=solver.group) - system.log('{}: fix point {},{},{}'.format( - cstrName(obj),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(cstrName(obj),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 @@ -604,7 +697,7 @@ class BaseMulti(Base): raise RuntimeError('Constraint "{}" requires at least two ' 'elements'.format(cls.getName())) for o in group: - if isinstance(o,utils.PartInfo): + if isinstance(o,utils.ElementInfo): msg = cls._entityDef[0](None,o.Part,o.Subname,o.Shape) else: msg = cls._entityDef[0](None,None,None,o) @@ -942,9 +1035,9 @@ class BaseDraftWire(BaseSketch): 'from a non-closed-or-subdivided Draft.Wire'.format( cls.getName())) -class LineLength(BaseDraftWire): +class LineLength(BaseSketch): _id = 34 - _entityDef = (_l,) + _entityDef = (_dl,) _workplane = True _props = ["Length"] _iconName = 'Assembly_ConstraintLineLength.svg' @@ -1030,23 +1123,11 @@ class MidPoint(BaseSketch): class Diameter(BaseSketch): _id = 25 - _entityDef = (_c,) - _prop = ("Diameter",) + _entityDef = (_dc,) + _props = ("Diameter",) _iconName = 'Assembly_ConstraintDiameter.svg' _tooltip='Add a "{}" to constrain the diameter of a circle/arc' - @classmethod - def check(cls,group,checkCount=False): - super(Diameter,cls).check(group,checkCount) - if not checkCount: - return - if len(group): - o = group[0] - if utils.isDraftCircle(o): - return - raise RuntimeError('Constraint "{}" requires a ' - 'Draft.Circle'.format(cls.getName())) - class EqualRadius(BaseSketch): _id = 33 diff --git a/proxy.py b/proxy.py index 33f07f2..3a5ad25 100644 --- a/proxy.py +++ b/proxy.py @@ -11,7 +11,8 @@ class PropertyInfo(object): 'For holding information to create dynamic properties' def __init__(self,host,name,tp,doc='', enum=None, - getter=propGet,group='Base',internal=False,duplicate=False): + getter=propGet,group='Base',internal=False, + duplicate=False,default=None): self.Name = name self.Type = tp self.Group = group @@ -19,6 +20,7 @@ class PropertyInfo(object): self.Enum = enum self.get = getter.__get__(self,self.__class__) self.Internal = internal + self.Default = default self.Key = host.addPropertyInfo(self,duplicate) class ProxyType(type): @@ -106,6 +108,8 @@ class ProxyType(type): obj.addProperty(prop.Type,prop.Name,prop.Group,prop.Doc) if prop.Enum: setattr(obj,prop.Name,prop.Enum) + if prop.Default is not None: + setattr(obj,prop.Name,prop.Default) setattr(obj.Proxy,mcs._proxyName,cls(obj)) obj.ViewObject.signalChangeIcon() diff --git a/solver.py b/solver.py index fbe30e7..10270d1 100644 --- a/solver.py +++ b/solver.py @@ -1,4 +1,4 @@ -import random +import random, math from collections import namedtuple import FreeCAD, FreeCADGui from .assembly import Assembly, isTypeOf, setPlacement @@ -35,9 +35,12 @@ class Solver(object): self.system.GroupHandle = self._fixedGroup - self._fixedParts = Constraint.getFixedParts(cstrs) + # convenience constant of zero + self.v0 = self.system.addParamV(0,group=self._fixedGroup) + + self._fixedParts = Constraint.getFixedParts(self,cstrs) if self._fixedParts is None: - logger.warn('no fixed part found') + self.system.log('no fixed part found') return for cstr in cstrs: @@ -96,21 +99,21 @@ class Solver(object): if part in self._fixedParts: continue if utils.isDraftWire(part): - pointChanged = False + changed = False points = part.Points - for subname,h in partInfo.EntityMap.items(): - if not subname.endswith('.p') or\ - not subname.startswith('Vertex'): + 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,subname[:-2]) + idx = utils.draftWireVertex2PointIndex(part,key[:-2]) if utils.isSamePos(points[idx],v): self.system.log('not moving {} point {}'.format( partInfo.PartName,idx)) else: - pointChanged = True + changed = True self.system.log('moving {} point{} from {}->{}'.format( partInfo.PartName,idx,points[idx],v)) if rollback is not None: @@ -118,7 +121,7 @@ class Solver(object): part, (idx, points[idx]))) points[idx] = v - if pointChanged: + if changed: touched = True part.Points = points else: @@ -132,11 +135,43 @@ class Solver(object): touched = True self.system.log('moving {} {} {} {}'.format( partInfo.PartName,partInfo.Params,params,pla)) - setPlacement(part,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) @@ -144,12 +179,12 @@ class Solver(object): def isFixedPart(self,info): return info.Part in self._fixedParts - def getPartInfo(self,info): + def getPartInfo(self,info,fixed=False,group=0): partInfo = self._partMap.get(info.Part,None) if partInfo: return partInfo - if info.Part in self._fixedParts: + if fixed or info.Part in self._fixedParts: g = self._fixedGroup else: g = self.group @@ -177,9 +212,9 @@ class Solver(object): Params = params, Workplane = h, EntityMap = {}, - Group = g) + Group = group if group else g) - self.system.log('{}'.format(partInfo)) + self.system.log('{}, {}'.format(partInfo,g)) self._partMap[info.Part] = partInfo return partInfo @@ -245,13 +280,19 @@ def _solve(objs=None,recursive=None,reportFailed=True, System.touch(assembly,False) except Exception: if rollback is not None: - for name,part,pla in reversed(rollback): - logger.debug('roll back {} to {}'.format(name,pla)) - if utils.isDraftWire(part): - idx,pt = pla + 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 - else: - setPlacement(part,pla) + elif utils.isDraftWire(part): + r,a1,a2 = v + part.Radius = r + part.FirstAngle = a1 + part.LastAngle = a2 + raise return True diff --git a/utils.py b/utils.py index 818d157..0b732fd 100644 --- a/utils.py +++ b/utils.py @@ -126,11 +126,11 @@ def getElementShape(obj,tp): if len(f)==1: return f[0] -PartInfo = namedtuple('AsmPartInfo', ('Parent','SubnameRef','Part', +ElementInfo = namedtuple('AsmElementInfo', ('Parent','SubnameRef','Part', 'PartName','Placement','Object','Subname','Shape')) def isDraftWire(obj): - if isinstance(obj,PartInfo): + if isinstance(obj,ElementInfo): obj = obj.Part proxy = getattr(obj,'Proxy',None) return isinstance(proxy,Draft._Wire) and \ @@ -138,7 +138,7 @@ def isDraftWire(obj): not obj.Subdivisions def isDraftCircle(obj): - if isinstance(obj,PartInfo): + if isinstance(obj,ElementInfo): obj = obj.Part proxy = getattr(obj,'Proxy',None) return isinstance(proxy,Draft._Circle) @@ -147,7 +147,7 @@ def isDraftObject(obj): return isDraftWire(obj) or isDraftCircle(obj) def isElement(obj): - if isinstance(obj,PartInfo): + if isinstance(obj,ElementInfo): shape = obj.Shape elif not isinstance(obj,(tuple,list)): shape = obj @@ -469,12 +469,25 @@ def fit_rotation_axis_to_surface1( surface, n_u=3, n_v=3 ): _tol = 10e-7 +def isSameValue(v1,v2): + if isinstance(v1,(tuple,list)): + assert(len(v1)==len(v2)) + vs = zip(v1,v2) + else: + vs = (v1,v2), + + for v1,v2 in vs: + v = v1-v2 + if v>=_tol or v<=-_tol: + return False + return True + def isSamePos(p1,p2): return p1.distanceToPoint(p2) < _tol def isSamePlacement(pla1,pla2): return isSamePos(pla1.Base,pla2.Base) and \ - np.linalg.norm(np.array(pla1.Rotation.Q)-np.array(pla2.Rotation.Q))<_tol + isSameValue(pla1.Rotation.Q,pla2.Rotation.Q) def getElementIndex(name,check=None): 'Return element index, 0 if invalid'