diff --git a/assembly.py b/assembly.py index 33ba97d..13e422c 100644 --- a/assembly.py +++ b/assembly.py @@ -796,7 +796,8 @@ class ViewProviderAsmElementLink(ViewProviderAsmOnTop): return (1.0,60.0/255.0,60.0/255.0) def doubleClicked(self,_vobj): - return movePart() + from . import mover + return mover.movePart() def canDropObjectEx(self,_obj,owner,subname): if logger.catchTrace('Cannot drop to AsmLink {}'.format( @@ -1554,227 +1555,6 @@ class Assembly(AsmGroup): obj,subname,AsmConstraintGroup,False,relativeToChild) -class AsmMovingPart(object): - def __init__(self,hierarchy,info): - self.objs = [h.Assembly for h in reversed(hierarchy)] - self.assembly = resolveAssembly(info.Parent) - self.parent = info.Parent - self.subname = info.SubnameRef - self.undos = None - self.part = info.Part - self.partName = info.PartName - - fixed = Constraint.getFixedTransform(self.assembly.getConstraints()) - fixed = fixed.get(info.Part,None) - self.fixedTransform = fixed - if fixed and fixed.Shape: - shape = fixed.Shape - else: - shape = info.Shape - - rot = utils.getElementRotation(shape) - if not rot: - # in case the shape has no normal, like a vertex, just use an empty - # rotation, which means having the same rotation as the owner part. - rot = FreeCAD.Rotation() - - hasBound = True - if not utils.isVertex(shape): - self.bbox = shape.BoundBox - else: - bbox = info.Object.ViewObject.getBoundingBox() - if bbox.isValid(): - self.bbox = bbox - else: - logger.warn('empty bounding box of part {}'.format( - info.PartName)) - self.bbox = FreeCAD.BoundBox(0,0,0,5,5,5) - hasBound = False - - pos = utils.getElementPos(shape) - if not pos: - if hasBound: - pos = self.bbox.Center - else: - pos = shape.Placement.Base - pla = FreeCAD.Placement(pos,rot) - - self.oldPlacement = info.Placement.copy() - self.offset = pla.copy() - self.offsetInv = pla.inverse() - self.draggerPlacement = info.Placement.multiply(pla) - self.tracePoint = self.draggerPlacement.Base - self.trace = None - - @classmethod - def onRollback(cls): - doc = FreeCADGui.editDocument() - if not doc: - return - vobj = doc.getInEdit() - if vobj and isTypeOf(vobj,ViewProviderAssembly): - movingPart = getattr(vobj.Proxy,'_movingPart',None) - if movingPart: - vobj.Object.recompute(True) - movingPart.tracePoint = movingPart.draggerPlacement.Base - - def update(self): - info = getElementInfo(self.parent,self.subname) - self.oldPlacement = info.Placement.copy() - self.part = info.Part - self.partName = info.PartName - pla = info.Placement.multiply(self.offset) - logger.trace('part move update {}: {}'.format(objName(self.parent),pla)) - self.draggerPlacement = pla - return pla - - def move(self): - obj = self.assembly.Object - pla = obj.ViewObject.DraggingPlacement - - rollback = [] - if self.fixedTransform: - fixed = self.fixedTransform - movement = self.draggerPlacement.inverse().multiply(pla) - if fixed.Shape: - # fixed position, so reset translation - movement.Base = FreeCAD.Vector() - if not utils.isVertex(fixed.Shape): - yaw,_,_ = movement.Rotation.toEuler() - # when dragging with a fixed axis, we align the dragger Z - # axis with that fixed axis. So we shall only keep the yaw - # among the euler angles - movement.Rotation = FreeCAD.Rotation(yaw,0,0) - pla = self.draggerPlacement.multiply(movement) - - # obtain and update the part placement - pla = pla.multiply(self.offsetInv) - setPlacement(self.part,pla) - rollback.append((self.partName,self.part,self.oldPlacement.copy())) - - if not gui.AsmCmdManager.AutoRecompute or \ - QtGui.QApplication.keyboardModifiers()==QtCore.Qt.ControlModifier: - # AsmCmdManager.AutoRecompute means auto re-solve the system. The - # recompute() call below is only for updating linked element and - # stuff - obj.recompute(True) - return - - # calls solver.solve(obj) and redirect all the exceptions message - # to logger only. - from . import solver - if not logger.catch('solver exception when moving part', - solver.solve, self.objs, dragPart=self.part, rollback=rollback): - obj.recompute(True) - - if gui.AsmCmdManager.Trace and \ - not self.tracePoint.isEqual(self.draggerPlacement.Base,1e-5): - try: - # check if the object is deleted - self.trace.Name - except Exception: - self.trace = None - mat = FreeCADGui.editDocument().EditingTransform - if not self.trace: - self.trace = FreeCAD.ActiveDocument.addObject( - 'Part::Polygon','AsmTrace') - self.trace.Nodes = {-1:mat.multiply(self.tracePoint)} - self.tracePoint = self.draggerPlacement.Base - self.trace.Nodes = {-1:mat.multiply(self.draggerPlacement.Base)} - self.trace.recompute() - - # self.draggerPlacement, which holds the intended dragger placement, is - # updated by the above solver call through the following chain, - # solver.solve() -> (triggers dependent objects recompute when done) - # Assembly.execute() -> - # ViewProviderAssembly.onExecute() -> - # AsmMovingPart.update() - return self.draggerPlacement - -def getMovingElementInfo(): - '''Extract information from current selection for part moving - - It returns a tuple containing the selected assembly hierarchy (obtained from - 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. - - If there are two selections, then one selection must be a parent assembly - containing the other child object. The moving object will then be the - immediate child part object of the owner assembly. The actual selected sub - element, i.e. vertex, edge, face will determine the dragger placement - ''' - - sels = FreeCADGui.Selection.getSelectionEx('',False) - if not sels: - raise RuntimeError('no selection') - - if not sels[0].SubElementNames: - raise RuntimeError('no sub object in selection') - - if len(sels)>1 or len(sels[0].SubElementNames)>2: - raise RuntimeError('too many selection') - - ret = Assembly.findChildren(sels[0].Object,sels[0].SubElementNames[0]) - if not ret: - raise RuntimeError('invalid selection {}, subname {}'.format( - objName(sels[0].Object),sels[0].SubElementNames[0])) - - if len(sels[0].SubElementNames)==1: - info = getElementInfo(ret[0].Assembly,ret[0].Subname) - if not info: - return - return (ret, info) - - ret2 = Assembly.findChildren(sels[0].Object,sels[0].SubElementNames[1]) - if not ret2: - raise RuntimeError('invalid selection {}, subname {}'.format( - objName(sels[0].Object),sels[0].SubElementNames[1])) - - if len(ret) == len(ret2): - if not ret2[-1].Object: - ret,ret2 = ret2,ret - elif len(ret) > len(ret2): - ret,ret2 = ret2,ret - - assembly = ret[-1].Assembly - for r in ret2: - if assembly == r.Assembly: - return (ret2, getElementInfo(r.Assembly,r.Subname)) - raise RuntimeError('not child parent selection') - -def canMovePart(): - return logger.catchTrace('',getMovingElementInfo) is not None - -def movePart(useCenterballDragger=None): - ret = logger.catch('exception when moving part', getMovingElementInfo) - if not ret: - return False - - info = ret[1] - doc = FreeCADGui.editDocument() - if doc: - doc.resetEdit() - vobj = resolveAssembly(info.Parent).Object.ViewObject - doc = info.Parent.ViewObject.Document - if useCenterballDragger is not None: - vobj.UseCenterballDragger = useCenterballDragger - vobj.Proxy._movingPart = AsmMovingPart(*ret) - return doc.setEdit(vobj,1) - -class AsmDocumentObserver: - def slotUndoDocument(self,_doc): - AsmMovingPart.onRollback() - - def slotRedoDocument(self,_doc): - AsmMovingPart.onRollback() - - def slotChangedObject(self,obj,prop): - Assembly.checkPartChange(obj,prop) - - class ViewProviderAssembly(ViewProviderAsmGroup): def __init__(self,vobj): self._movingPart = None @@ -1811,7 +1591,8 @@ class ViewProviderAssembly(ViewProviderAsmGroup): return System.getIcon(self.ViewObject.Object) def doubleClicked(self, _vobj): - return movePart() + from . import mover + return mover.movePart() def onExecute(self): if not getattr(self,'_movingPart',None): @@ -1844,14 +1625,15 @@ class ViewProviderAssembly(ViewProviderAsmGroup): def onDragStart(self): Assembly.cancelAutoSolve(); - AsmMovingPart._Busy = True + FreeCADGui.Selection.clearSelection() + self.__class__._Busy = True FreeCAD.setActiveTransaction('Assembly move') def onDragMotion(self): return self._movingPart.move() def onDragEnd(self): - AsmMovingPart._Busy = False + self.__class__._Busy = False FreeCAD.closeActiveTransaction() def unsetEdit(self,_vobj,_mode): diff --git a/constraint.py b/constraint.py index 56c8d5e..7b1c215 100644 --- a/constraint.py +++ b/constraint.py @@ -423,7 +423,7 @@ class Constraint(ProxyType): if elements: info = elements[0].Proxy.getInfo() firstPart = info.Part - if not found and firstPart: + if not found and firstPart and not utils.isDraftObject(firstPart): ret[firstPart] = False return ret @@ -594,9 +594,10 @@ class Locked(Base): ret = [] for e in obj.Proxy.getElements(): info = e.Proxy.getInfo() + if utils.isDraftObject(info): + continue 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)) diff --git a/gui.py b/gui.py index 915c9cf..ed15260 100644 --- a/gui.py +++ b/gui.py @@ -106,6 +106,7 @@ class AsmCmdBase(object): _toolbarName = 'Assembly3' _menuGroupName = '' _contextMenuName = 'Assembly' + _accel = None @classmethod def checkActive(cls): @@ -113,17 +114,20 @@ class AsmCmdBase(object): @classmethod def GetResources(cls): - return { + ret = { 'Pixmap':addIconToFCAD(cls._iconName), 'MenuText':cls.getMenuText(), 'ToolTip':cls.getToolTip() } - + if cls._accel: + ret['Accel'] = cls._accel + return ret class AsmCmdNew(AsmCmdBase): _id = 0 _menuText = 'Create assembly' _iconName = 'Assembly_New_Assembly.svg' + _accel = 'A, N' @classmethod def Activated(cls): @@ -134,6 +138,7 @@ class AsmCmdSolve(AsmCmdBase): _id = 1 _menuText = 'Solve constraints' _iconName = 'AssemblyWorkbench.svg' + _accel = 'A, S' @classmethod def Activated(cls): @@ -149,16 +154,17 @@ class AsmCmdMove(AsmCmdBase): _menuText = 'Move part' _iconName = 'Assembly_Move.svg' _useCenterballDragger = True + _accel = 'A, M' @classmethod def Activated(cls): - from . import assembly - assembly.movePart(cls._useCenterballDragger) + from . import mover + mover.movePart(cls._useCenterballDragger) @classmethod def checkActive(cls): - from . import assembly - cls._active = assembly.canMovePart() + from . import mover + cls._active = mover.canMovePart() @classmethod def onClearSelection(cls): @@ -169,6 +175,7 @@ class AsmCmdAxialMove(AsmCmdMove): _menuText = 'Axial move part' _iconName = 'Assembly_AxialMove.svg' _useCenterballDragger = False + _accel = 'A, A' class AsmCmdCheckable(AsmCmdBase): _id = -2 diff --git a/init_gui.py b/init_gui.py index f0b67b9..5b0820b 100644 --- a/init_gui.py +++ b/init_gui.py @@ -34,7 +34,7 @@ class Assembly3Workbench(FreeCADGui.Workbench): cmd.workbenchDeactivated() def Initialize(self): - from .assembly import AsmDocumentObserver + from .mover import AsmDocumentObserver from .gui import AsmCmdManager,SelectionObserver cmdSet = set() for name,cmds in AsmCmdManager.Toolbars.items(): @@ -60,7 +60,6 @@ class Assembly3Workbench(FreeCADGui.Workbench): self.appendContextMenu(name,cmds) def ContextMenu(self, _recipient): - from .utils import logger logger.catch('',self._contextMenu) FreeCADGui.addWorkbench(Assembly3Workbench) diff --git a/mover.py b/mover.py new file mode 100644 index 0000000..2ff0c15 --- /dev/null +++ b/mover.py @@ -0,0 +1,280 @@ +import math +import FreeCAD, FreeCADGui +from PySide import QtCore, QtGui +from . import utils, gui +from .assembly import isTypeOf, Assembly, ViewProviderAssembly, \ + resolveAssembly, getElementInfo, setPlacement +from .utils import logger, objName +from .constraint import Constraint + +class AsmMovingPart(object): + def __init__(self,hierarchy,info): + self.objs = [h.Assembly for h in reversed(hierarchy)] + self.assembly = resolveAssembly(info.Parent) + self.viewObject = self.assembly.Object.ViewObject + self.info = info + self.undos = None + + fixed = Constraint.getFixedTransform(self.assembly.getConstraints()) + fixed = fixed.get(info.Part,None) + self.fixedTransform = fixed + if fixed and fixed.Shape: + shape = fixed.Shape + else: + shape = info.Shape + + rot = utils.getElementRotation(shape) + if not rot: + # in case the shape has no normal, like a vertex, just use an empty + # rotation, which means having the same rotation as the owner part. + rot = FreeCAD.Rotation() + + hasBound = True + if not utils.isVertex(shape): + self.bbox = shape.BoundBox + else: + bbox = info.Object.ViewObject.getBoundingBox() + if bbox.isValid(): + self.bbox = bbox + else: + logger.warn('empty bounding box of part {}'.format( + info.PartName)) + self.bbox = FreeCAD.BoundBox(0,0,0,5,5,5) + hasBound = False + + pos = utils.getElementPos(shape) + if not pos: + if hasBound: + pos = self.bbox.Center + else: + pos = shape.Placement.Base + pla = FreeCAD.Placement(pos,rot) + + self.offset = pla.copy() + self.offsetInv = pla.inverse() + self.draggerPlacement = info.Placement.multiply(pla) + self.tracePoint = self.draggerPlacement.Base + self.trace = None + + @classmethod + def onRollback(cls): + doc = FreeCADGui.editDocument() + if not doc: + return + vobj = doc.getInEdit() + if vobj and isTypeOf(vobj,ViewProviderAssembly): + movingPart = getattr(vobj.Proxy,'_movingPart',None) + if movingPart: + vobj.Object.recompute(True) + movingPart.tracePoint = movingPart.draggerPlacement.Base + + def update(self): + info = getElementInfo(self.info.Parent,self.info.SubnameRef) + self.info = info + if utils.isDraftObject(info): + pos = utils.getElementPos(info.Shape) + rot = utils.getElementRotation(info.Shape) + pla = info.Placement.multiply(FreeCAD.Placement(pos,rot)) + else: + pla = info.Placement.multiply(self.offset) + logger.trace('part move update {}: {}'.format(objName(info.Parent),pla)) + self.draggerPlacement = pla + return pla + + @property + def Movement(self): + pla = self.viewObject.DraggingPlacement.multiply( + self.draggerPlacement.inverse()) + return utils.roundPlacement(pla) + + def move(self): + info = self.info + part = info.Part + obj = self.assembly.Object + pla = self.viewObject.DraggingPlacement + updatePla = True + + rollback = [] + if utils.isDraftWire(part): + updatePla = False + if info.Subname.startswith('Vertex'): + idx = utils.draftWireVertex2PointIndex(part,info.Subname) + if idx is None: + logger.error('Invalid draft wire vertex {} {}'.format( + info.Subname, info.PartName)) + return + change = [idx] + else: + change = utils.draftWireEdge2PointIndex(part,info.Subname) + if change[0] is None or change[1] is None: + logger.error('Invalid draft wire edge {} {}'.format( + info.Subname, info.PartName)) + return + + movement = self.Movement + points = part.Points + for idx in change: + pt = points[idx] + rollback.append((info.PartName, part, (idx,pt))) + points[idx] = movement.multVec(pt) + part.Points = points + + elif info.Subname.startswith('Vertex') and \ + utils.isDraftCircle(part): + updatePla = False + a1 = part.FirstAngle + a2 = part.LastAngle + r = part.Radius + rollback.append((info.PartName, part, (r,a1,a2))) + pt = info.Placement.inverse().multVec(pla.Base) + part.Radius = pt.Length + if a1 != a2: + pt.z = 0 + a = math.degrees(FreeCAD.Vector(1,0,0).getAngle(pt)) + if info.Subname.endswith('1'): + part.FirstAngle = a + else: + part.LastAngle = a + + elif self.fixedTransform: + fixed = self.fixedTransform + movement = self.Movement + if fixed.Shape: + # fixed position, so reset translation + movement.Base = FreeCAD.Vector() + if not utils.isVertex(fixed.Shape): + yaw,_,_ = movement.Rotation.toEuler() + # when dragging with a fixed axis, we align the dragger Z + # axis with that fixed axis. So we shall only keep the yaw + # among the euler angles + movement.Rotation = FreeCAD.Rotation(yaw,0,0) + pla = self.draggerPlacement.multiply(movement) + + if updatePla: + # obtain and update the part placement + pla = pla.multiply(self.offsetInv) + setPlacement(info.Part,pla) + rollback.append((info.PartName,info.Part,info.Placement.copy())) + + if not gui.AsmCmdManager.AutoRecompute or \ + QtGui.QApplication.keyboardModifiers()==QtCore.Qt.ControlModifier: + # AsmCmdManager.AutoRecompute means auto re-solve the system. The + # recompute() call below is only for updating linked element and + # stuff + obj.recompute(True) + return + + # calls solver.solve(obj) and redirect all the exceptions message + # to logger only. + from . import solver + if not logger.catch('solver exception when moving part', + solver.solve, self.objs, dragPart=info.Part, rollback=rollback): + obj.recompute(True) + + if gui.AsmCmdManager.Trace and \ + not self.tracePoint.isEqual(self.draggerPlacement.Base,1e-5): + try: + # check if the object is deleted + self.trace.Name + except Exception: + self.trace = None + mat = FreeCADGui.editDocument().EditingTransform + if not self.trace: + self.trace = FreeCAD.ActiveDocument.addObject( + 'Part::Polygon','AsmTrace') + self.trace.Nodes = {-1:mat.multiply(self.tracePoint)} + self.tracePoint = self.draggerPlacement.Base + self.trace.Nodes = {-1:mat.multiply(self.draggerPlacement.Base)} + self.trace.recompute() + + # self.draggerPlacement, which holds the intended dragger placement, is + # updated by the above solver call through the following chain, + # solver.solve() -> (triggers dependent objects recompute when done) + # Assembly.execute() -> + # ViewProviderAssembly.onExecute() -> + # AsmMovingPart.update() + return self.draggerPlacement + +def getMovingElementInfo(): + '''Extract information from current selection for part moving + + It returns a tuple containing the selected assembly hierarchy (obtained from + 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. + + If there are two selections, then one selection must be a parent assembly + containing the other child object. The moving object will then be the + immediate child part object of the owner assembly. The actual selected sub + element, i.e. vertex, edge, face will determine the dragger placement + ''' + + sels = FreeCADGui.Selection.getSelectionEx('',False) + if not sels: + raise RuntimeError('no selection') + + if not sels[0].SubElementNames: + raise RuntimeError('no sub object in selection') + + if len(sels)>1 or len(sels[0].SubElementNames)>2: + raise RuntimeError('too many selection') + + ret = Assembly.findChildren(sels[0].Object,sels[0].SubElementNames[0]) + if not ret: + raise RuntimeError('invalid selection {}, subname {}'.format( + objName(sels[0].Object),sels[0].SubElementNames[0])) + + if len(sels[0].SubElementNames)==1: + info = getElementInfo(ret[0].Assembly,ret[0].Subname) + if not info: + return + return (ret, info) + + ret2 = Assembly.findChildren(sels[0].Object,sels[0].SubElementNames[1]) + if not ret2: + raise RuntimeError('invalid selection {}, subname {}'.format( + objName(sels[0].Object),sels[0].SubElementNames[1])) + + if len(ret) == len(ret2): + if not ret2[-1].Object: + ret,ret2 = ret2,ret + elif len(ret) > len(ret2): + ret,ret2 = ret2,ret + + assembly = ret[-1].Assembly + for r in ret2: + if assembly == r.Assembly: + return (ret2, getElementInfo(r.Assembly,r.Subname)) + raise RuntimeError('not child parent selection') + +def canMovePart(): + return logger.catchTrace('',getMovingElementInfo) is not None + +def movePart(useCenterballDragger=None): + ret = logger.catch('exception when moving part', getMovingElementInfo) + if not ret: + return False + + info = ret[1] + doc = FreeCADGui.editDocument() + if doc: + doc.resetEdit() + vobj = resolveAssembly(info.Parent).Object.ViewObject + doc = info.Parent.ViewObject.Document + if useCenterballDragger is not None: + vobj.UseCenterballDragger = useCenterballDragger + vobj.Proxy._movingPart = AsmMovingPart(*ret) + return doc.setEdit(vobj,1) + +class AsmDocumentObserver: + def slotUndoDocument(self,_doc): + AsmMovingPart.onRollback() + + def slotRedoDocument(self,_doc): + AsmMovingPart.onRollback() + + def slotChangedObject(self,obj,prop): + Assembly.checkPartChange(obj,prop) + diff --git a/solver.py b/solver.py index 10270d1..14ca42f 100644 --- a/solver.py +++ b/solver.py @@ -59,7 +59,7 @@ class Solver(object): addDragPoint = getattr(self.system,'addWhereDragged',None) if addDragPoint: info = self._partMap.get(dragPart,None) - if info: + if info and info.Workplane: # add dragging point self.system.log('add drag point ' '{}'.format(info.Workplane[1])) diff --git a/utils.py b/utils.py index 0b732fd..74ae2f9 100644 --- a/utils.py +++ b/utils.py @@ -469,18 +469,18 @@ def fit_rotation_axis_to_surface1( surface, n_u=3, n_v=3 ): _tol = 10e-7 +def roundPlacement(pla): + pos = [ 0.0 if abs(v)<_tol else v for v in pla.Base ] + q = [ 0.0 if abs(v)<_tol else v for v in pla.Rotation.Q ] + return FreeCAD.Placement(FreeCAD.Vector(*pos),FreeCAD.Rotation(*q)) + 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 + return all([abs(v1-v2)<_tol for v1,v2 in vs]) def isSamePos(p1,p2): return p1.distanceToPoint(p2) < _tol @@ -511,6 +511,13 @@ def draftWireVertex2PointIndex(obj,name): if idx < len(obj.Points): return idx +def draftWireEdge2PointIndex(obj,name): + vname1,vname2 = edge2VertexIndex(name) + if not vname1: + return None,None + return (draftWireVertex2PointIndex(obj,vname1), + draftWireVertex2PointIndex(obj,vname2)) + def edge2VertexIndex(name): 'deduct the vertex index from the edge index' idx = getElementIndex(name,'Edge')