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)