From 05595e3f41b6c70ca1a03d8c9c042a725c88451c Mon Sep 17 00:00:00 2001 From: "Zheng, Lei" Date: Fri, 19 Jan 2018 14:10:43 +0800 Subject: [PATCH] Add support for draft wire Special treatment for non-closed-or-subdivided draft wire object directly added into part group. Instead of constraining on the object placement, we shall constraint on individual points. In other word, instead of making the draft wire's placement as free parameters, we make the cooridnates of indvidual points as free parameters. --- assembly.py | 24 ++--- constraint.py | 274 +++++++++++++++++++++++++++++++++++++++----------- solver.py | 94 ++++++++++++----- sys_sympy.py | 4 +- utils.py | 61 ++++++++++- 5 files changed, 354 insertions(+), 103 deletions(-) diff --git a/assembly.py b/assembly.py index 19929db..26bba54 100644 --- a/assembly.py +++ b/assembly.py @@ -488,17 +488,14 @@ class ViewProviderAsmElement(ViewProviderAsmOnTop): Group=owner, Subname=subname),undo=True) -PartInfo = namedtuple('AsmPartInfo', ('Parent','SubnameRef','Part', - 'PartName','Placement','Object','Subname','Shape')) - def getPartInfo(parent, subname): '''Return a named tuple containing the part object element information Parameters: - parent: the parent document object, either an assembly, or a part group + parent: the parent document object, either an assembly, or a part group - subname: subname reference to the part element (i.e. edge, face, vertex) + subname: subname reference to the part element (i.e. edge, face, vertex) Return a named tuple with the following fields: @@ -513,8 +510,8 @@ def getPartInfo(parent, subname): Placement: the placement of the part - Object: the object that owns the element. In case 'Part' is an assembly, we - the element owner will always be some (grand)child of the 'Part' + Object: the object that owns the element. In case 'Part' is an assembly, the + element owner will always be some (grand)child of the 'Part' Subname: the subname reference to the element owner object. The reference is realtive to the 'Part', i.e. Object = Part.getSubObject(subname), or if @@ -633,7 +630,7 @@ def getPartInfo(parent, subname): obj = part.getLinkedObject(False) partName = part.Name - return PartInfo(Parent = parent, + return utils.PartInfo(Parent = parent, SubnameRef = subnameRef, Part = part, PartName = partName, @@ -876,16 +873,16 @@ class AsmConstraint(AsmGroup): ret = getattr(self,'elements',None) if ret or Constraint.isDisabled(obj): return ret - shapes = [] + infos = [] elements = [] for o in obj.Group: checkType(o,AsmElementLink) info = o.Proxy.getInfo() if not info: return - shapes.append(info.Shape) + infos.append(info) elements.append(o) - Constraint.check(obj,shapes,True) + Constraint.check(obj,infos,True) self.elements = elements return self.elements @@ -981,7 +978,7 @@ class AsmConstraint(AsmGroup): if not Constraint.isDisabled(cstr): if cstr: typeid = Constraint.getTypeID(cstr) - check = [o.Proxy.getInfo().Shape for o in cstr.Group] + elements + check = [o.Proxy.getInfo() for o in cstr.Group] + elements else: check = elements Constraint.check(typeid,check) @@ -1137,7 +1134,8 @@ class AsmElementGroup(AsmGroup): return for i,c in enumerate(reversed(label)): if not c.isdigit(): - label = label[:i+1] + if i: + label = label[:-i] break; i=0 while True: diff --git a/constraint.py b/constraint.py index ba49fe2..d0031ba 100644 --- a/constraint.py +++ b/constraint.py @@ -7,12 +7,18 @@ from .proxy import ProxyType, PropertyInfo, propGet, propGetValue import os _iconPath = os.path.join(utils.iconPath,'constraints') -def _p(solver,partInfo,subname,shape): +def _p(solver,partInfo,subname,shape,retAll=False): 'return a handle of a transformed point derived from "shape"' if not solver: - if utils.hasCenter(shape): + if not utils.hasCenter(shape): + return 'a vertex or circular edge/face' + if not utils.isDraftWire(partInfo): return - return 'a vertex or circular edge/face' + if utils.draftWireVertex2PointIndex(partInfo,subname) is None: + raise RuntimeError('Invalid draft wire vertex "{}" {}'.format( + subname,objName(partInfo))) + return + key = subname+'.p' h = partInfo.EntityMap.get(key,None) system = solver.system @@ -20,20 +26,37 @@ def _p(solver,partInfo,subname,shape): system.log('cache {}: {}'.format(key,h)) else: v = utils.getElementPos(shape) - system.NameTag = key - e = system.addPoint3dV(*v) - system.NameTag = partInfo.PartName + '.' + key - h = system.addTransform(e,*partInfo.Params,group=partInfo.Group) - system.log('{}: {},{}'.format(key,h,partInfo.Group)) + 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 - return 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 utils.isPlanar(shape): - return - return 'an edge or face with a surface normal' + 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(objName(partInfo))) + return + key = subname+'.n' h = partInfo.EntityMap.get(key,None) system = solver.system @@ -43,17 +66,17 @@ def _n(solver,partInfo,subname,shape,retAll=False): h = [] rot = utils.getElementRotation(shape) - system.NameTag = key - e = system.addNormal3dV(*utils.getNormal(rot)) 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 rot = FreeCAD.Rotation(FreeCAD.Vector(0,1,0),90).multiply(rot) - system.NameTag = key + 'x' - e = system.addNormal3dV(*utils.getNormal(rot)) system.NameTag = nameTag + 'x' + e = system.addNormal3dV(*utils.getNormal(rot)) + system.NameTag = nameTag + 'xt' h.append(system.addTransform(e,*partInfo.Params,group=partInfo.Group)) system.log('{}: {},{}'.format(key,h,partInfo.Group)) @@ -63,29 +86,50 @@ def _n(solver,partInfo,subname,shape,retAll=False): 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 utils.isLinearEdge(shape): + if not utils.isLinearEdge(shape): + return 'a linear edge' + if not utils.isDraftWire(partInfo): return - return 'a linear edge' + part = partInfo + vname1,vname2 = utils.edge2VertexIndex(subname) + if not vname1: + raise RuntimeError('Invalid draft subname {} or {}'.format( + subname,objName(part))) + v = shape.Edges[0].Vertexes + return _p(solver,partInfo,vname1,v[0]) or \ + _p(solver,partInfo,vname2,v[1]) + key = subname+'.l' h = partInfo.EntityMap.get(key,None) system = solver.system if h: system.log('cache {}: {}'.format(key,h)) else: - system.NameTag = key - v = shape.Edges[0].Vertexes - p1 = system.addPoint3dV(*v[0].Point) - p2 = system.addPoint3dV(*v[-1].Point) nameTag = partInfo.PartName + '.' + key - system.NameTag = nameTag + '.p1' - tp1 = system.addTransform(p1,*partInfo.Params,group=partInfo.Group) - system.NameTag = nameTag + '.p2' - tp2 = system.addTransform(p2,*partInfo.Params,group=partInfo.Group) + v = shape.Edges[0].Vertexes + if utils.isDraftWire(partInfo.Part): + vname1,vname2 = utils.edge2VertexIndex(subname) + if not vname1: + raise RuntimeError('Invalid draft subname {} or {}'.format( + subname,objName(partInfo.Part))) + 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,p1,p2) + h = (h,tp1,tp2) system.log('{}: {},{}'.format(key,h,partInfo.Group)) partInfo.EntityMap[key] = h + return h if retAll else h[0] def _ln(solver,partInfo,subname,shape,retAll=False): @@ -120,10 +164,11 @@ def _w(solver,partInfo,subname,shape,retAll=False): partInfo.EntityMap[key] = h return h if retAll else h[0] -def _wa(solver,partInfo,subname,shape): +def _wa(solver,partInfo,subname,shape,retAll=False): + _ = retAll return _w(solver,partInfo,subname,shape,True) -def _c(solver,partInfo,subname,shape,requireArc=False): +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) @@ -143,23 +188,24 @@ def _c(solver,partInfo,subname,shape,requireArc=False): 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 = partInfo.PartName + '.' + key + system.NameTag = nameTag h = system.addArcOfCircle(w,p,l[1],l[2],group=partInfo.Group) else: - nameTag = partInfo.PartName + '.' + key - system.NameTag = nameTag + '.r' - hr = system.addDistanceV(r) system.NameTag = nameTag h = system.addCircle(p,n,hr,group=partInfo.Group) + h = (h,hr) system.log('{}: {},{}'.format(key,h,partInfo.Group)) partInfo.EntityMap[key] = h - return h + return h if retAll else h[0] -def _a(solver,partInfo,subname,shape): +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) + return _c(solver,partInfo,subname,shape,True,retAll) class ConstraintCommand: @@ -312,6 +358,7 @@ def _makeProp(name,tp,doc='',getter=propGet,internal=False): group='Constraint',internal=internal) _makeProp('Distance','App::PropertyDistance',getter=propGetValue) +_makeProp('Length','App::PropertyDistance',getter=propGetValue) _makeProp('Offset','App::PropertyDistance',getter=propGetValue) _makeProp('Cascade','App::PropertyBool',internal=True) _makeProp('Angle','App::PropertyAngle',getter=propGetValue) @@ -381,7 +428,10 @@ class Base(object): entities = cls.getEntityDef(group,checkCount) for i,e in enumerate(entities): o = group[i] - msg = e(None,None,None,o) + if isinstance(o,utils.PartInfo): + msg = e(None,o.Part,o.Subname,o.Shape) + else: + msg = e(None,None,None,o) if not msg: continue if i == len(cls._entityDef): @@ -396,7 +446,7 @@ class Base(object): return utils.getIcon(cls,Constraint.isDisabled(obj),_iconPath) @classmethod - def getEntities(cls,obj,solver): + def getEntities(cls,obj,solver,retAll=False): '''maps fcad element shape to entities''' elements = obj.Proxy.getElements() entities = cls.getEntityDef(elements,True,obj) @@ -404,7 +454,7 @@ class Base(object): for e,o in zip(entities,elements): info = o.Proxy.getInfo() partInfo = solver.getPartInfo(info) - ret.append(e(solver,partInfo,info.Subname,info.Shape)) + ret.append(e(solver,partInfo,info.Subname,info.Shape,retAll=retAll)) solver.system.log('{} entities: {}'.format(cstrName(obj),ret)) return ret @@ -476,32 +526,64 @@ class Locked(Base): @classmethod def prepare(cls,obj,solver): ret = [] + system = solver.system + for element in obj.Proxy.getElements(): info = element.Proxy.getInfo() - if not utils.isVertex(info.Shape) and \ - not utils.isLinearEdge(info.Shape): + 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 + partInfo = solver.getPartInfo(info) - system = solver.system + + 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): - subname = info.Subname+'.'+str(i) - system.NameTag = subname + '.tp' + surfix = '.fp{}'.format(i) + system.NameTag = nameTag + surfix + + # Create an entity for the transformed constant point e1 = system.addPoint3dV(*info.Placement.multVec(v.Point)) - e2 = _p(solver,partInfo,subname,v) - if i==0: + + # 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 = partInfo.PartName + '.' + info.Subname - ret.append(system.addPointsCoincident( - e1,e2,group=solver.group)) + system.NameTag = nameTag + surfix + e = system.addPointsCoincident(e1,e2,group=solver.group) + system.log('{}: fix point {},{},{}'.format( + cstrName(obj),e,e1,e2)) else: - system.NameTag = info.Subname + 'tl' + # 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 = partInfo.PartName + '.' + info.Subname - ret.append(system.addPointOnLine(e2,l,group=solver.group)) + 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 @@ -522,7 +604,10 @@ class BaseMulti(Base): raise RuntimeError('Constraint "{}" requires at least two ' 'elements'.format(cls.getName())) for o in group: - msg = cls._entityDef[0](None,None,None,o) + if isinstance(o,utils.PartInfo): + msg = cls._entityDef[0](None,o.Part,o.Subname,o.Shape) + else: + msg = cls._entityDef[0](None,None,None,o) if msg: raise RuntimeError('Constraint "{}" requires all the element ' 'to be of {}'.format(cls.getName())) @@ -842,7 +927,43 @@ class BaseSketch(Base): _toolbarName = 'Assembly3 Sketch Constraints' -class EqualLength(BaseSketch): +class BaseDraftWire(BaseSketch): + _id = -1 + + @classmethod + def check(cls,group,checkCount=False): + super(BaseDraftWire,cls).check(group,checkCount) + if not checkCount: + return + for o in group: + if utils.isDraftWire(o): + return + raise RuntimeError('Constraint "{}" requires at least one linear edge ' + 'from a non-closed-or-subdivided Draft.Wire'.format( + cls.getName())) + +class LineLength(BaseDraftWire): + _id = 34 + _entityDef = (_l,) + _workplane = True + _props = ["Length"] + _iconName = 'Assembly_ConstraintLineLength.svg' + _tooltip='Add a "{}" constrain the length of a non-closed-or-subdivided '\ + 'Draft.Wire' + + @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)) + else: + logger.warn('{} no constraint func'.format(cstrName(obj))) + + +class EqualLength(BaseDraftWire): _id = 9 _entityDef = (_l,_l) _workplane = True @@ -850,7 +971,7 @@ class EqualLength(BaseSketch): _tooltip='Add a "{}" constraint to make two lines of the same length.' -class LengthRatio(BaseSketch): +class LengthRatio(BaseDraftWire): _id = 10 _entityDef = (_l,_l) _workplane = True @@ -859,7 +980,7 @@ class LengthRatio(BaseSketch): _tooltip='Add a "{}" to constrain the length ratio of two lines.' -class LengthDifference(BaseSketch): +class LengthDifference(BaseDraftWire): _id = 11 _entityDef = (_l,_l) _workplane = True @@ -877,13 +998,27 @@ class EqualLengthPointLineDistance(BaseSketch): '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,group,checkCount=False): + super(EqualLineArcLength,cls).check(group,checkCount) + if not checkCount: + return + for i,o in enumerate(group): + if i: + if utils.isDraftCircle(o): + return + elif utils.isDraftWire(o): + return + raise RuntimeError('Constraint "{}" requires at least one ' + 'non-closed-or-subdivided Draft.Wire or one Draft.Circle'.format( + cls.getName())) + class MidPoint(BaseSketch): _id = 20 @@ -893,13 +1028,25 @@ class MidPoint(BaseSketch): _tooltip='Add a "{}" to constrain a point to the middle point of a line.' - class Diameter(BaseSketch): _id = 25 _entityDef = (_c,) _prop = ("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 @@ -907,6 +1054,17 @@ class EqualRadius(BaseSketch): _iconName = 'Assembly_ConstraintEqualRadius.svg' _tooltip='Add a "{}" constraint to make two circles/arcs of the same radius' + @classmethod + def check(cls,group,checkCount=False): + super(EqualRadius,cls).check(group,checkCount) + if not checkCount: + return + for o in group: + if utils.isDraftCircle(o): + return + raise RuntimeError('Constraint "{}" requires at least one ' + 'Draft.Circle'.format(cls.getName())) + # class CubicLineTangent(BaseSketch): # _id = 31 diff --git a/solver.py b/solver.py index e404a9a..fbe30e7 100644 --- a/solver.py +++ b/solver.py @@ -2,10 +2,12 @@ import random 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 @@ -14,8 +16,8 @@ from .system import System # 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')) +PartInfo = namedtuple('SolverPartInfo', ('Part','PartName','Placement', + 'Params','Workplane','EntityMap','Group')) class Solver(object): def __init__(self,assembly,reportFailed,dragPart,recompute,rollback): @@ -93,21 +95,48 @@ class Solver(object): 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)) + if utils.isDraftWire(part): + pointChanged = False + points = part.Points + for subname,h in partInfo.EntityMap.items(): + if not subname.endswith('.p') or\ + not subname.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]) + if utils.isSamePos(points[idx],v): + self.system.log('not moving {} point {}'.format( + partInfo.PartName,idx)) + else: + pointChanged = 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 pointChanged: + touched = True + part.Points = points else: - 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())) + 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)) + setPlacement(part,pla) + if rollback is not None: + rollback.append((partInfo.PartName, + part, + partInfo.Placement.copy())) if recompute and touched: assembly.recompute(True) @@ -125,18 +154,25 @@ class Solver(object): else: g = self.group - self.system.NameTag = info.PartName - params = self.system.addPlacement(info.Placement,group=g) + 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 + '.w' - w = self.system.addWorkplane(p,n,group=g) - h = (w,p,n) + 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 + '.w' + w = self.system.addWorkplane(p,n,group=g) + h = (w,p,n) - partInfo = PartInfo(PartName = info.PartName, + partInfo = PartInfo(Part = info.Part, + PartName = info.PartName, Placement = info.Placement.copy(), Params = params, Workplane = h, @@ -211,7 +247,11 @@ def _solve(objs=None,recursive=None,reportFailed=True, if rollback is not None: for name,part,pla in reversed(rollback): logger.debug('roll back {} to {}'.format(name,pla)) - setPlacement(part,pla) + if utils.isDraftWire(part): + idx,pt = pla + part.Points[idx] = pt + else: + setPlacement(part,pla) raise return True diff --git a/sys_sympy.py b/sys_sympy.py index 3c9bf94..1954643 100644 --- a/sys_sympy.py +++ b/sys_sympy.py @@ -455,7 +455,7 @@ def _directionConsine(wrkpln,l1,l2,supplement=False): v1,v2 = _project(wrkpln,l1,l2) if supplement: v1 = v1 * -1.0 - return v1.cross(v2)/(v1.magnitude()*v2.magnitude()) + return v1.dot(v2)/(v1.magnitude()*v2.magnitude()) _x = 'i' _y = 'j' @@ -1016,7 +1016,7 @@ class _Angle(_ProjectingConstraint): return _directionConsine(self.wrkpln,self.l1,self.l2,self.supplement) def getEq(self): - return self.DirectionCosine - sp.cos(self.degree.SymObj*sp.pi/180.0) + return self.DirectionCosine - sp.cos(sp.pi*self.degree/180.0) class _Perpendicular(_Angle): _args = ('l1', 'l2',) diff --git a/utils.py b/utils.py index 1d2fb34..818d157 100644 --- a/utils.py +++ b/utils.py @@ -5,7 +5,8 @@ Most of the functions are borrowed directly from assembly2lib.py or lib3D.py in assembly2 ''' -import FreeCAD, FreeCADGui, Part +from collections import namedtuple +import FreeCAD, FreeCADGui, Part, Draft import numpy as np from .FCADLogger import FCADLogger rootlogger = FCADLogger('asm3') @@ -125,8 +126,30 @@ def getElementShape(obj,tp): if len(f)==1: return f[0] +PartInfo = namedtuple('AsmPartInfo', ('Parent','SubnameRef','Part', + 'PartName','Placement','Object','Subname','Shape')) + +def isDraftWire(obj): + if isinstance(obj,PartInfo): + obj = obj.Part + proxy = getattr(obj,'Proxy',None) + return isinstance(proxy,Draft._Wire) and \ + not obj.Closed and \ + not obj.Subdivisions + +def isDraftCircle(obj): + if isinstance(obj,PartInfo): + obj = obj.Part + proxy = getattr(obj,'Proxy',None) + return isinstance(proxy,Draft._Circle) + +def isDraftObject(obj): + return isDraftWire(obj) or isDraftCircle(obj) + def isElement(obj): - if not isinstance(obj,(tuple,list)): + if isinstance(obj,PartInfo): + shape = obj.Shape + elif not isinstance(obj,(tuple,list)): shape = obj else: sobj,_,shape = obj[0].getSubObject(obj[1],2) @@ -446,6 +469,38 @@ def fit_rotation_axis_to_surface1( surface, n_u=3, n_v=3 ): _tol = 10e-7 +def isSamePos(p1,p2): + return p1.distanceToPoint(p2) < _tol + def isSamePlacement(pla1,pla2): - return pla1.Base.distanceToPoint(pla2.Base) < _tol and \ + return isSamePos(pla1.Base,pla2.Base) and \ np.linalg.norm(np.array(pla1.Rotation.Q)-np.array(pla2.Rotation.Q))<_tol + +def getElementIndex(name,check=None): + 'Return element index, 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' + idx = getElementIndex(name,'Vertex') + # We don't support subdivision yet (checked in isDraftWire()) + if idx <= 0 or not isDraftWire(obj): + return + idx -= 1 + if idx < len(obj.Points): + return idx + +def edge2VertexIndex(name): + 'deduct the vertex index from the edge index' + idx = getElementIndex(name,'Edge') + if not idx: + return None,None + return 'Vertex{}'.format(idx),'Vertex{}'.format(idx+1)