FreeCAD_assembly3/mover.py
Zheng, Lei e0939d1ec2 assembly: support for derived assembly
Deriving assembly directly contains all parts from derived assembly.
And all derived parts are added as fixed part. The intention is mostly
for user to customize visibilities of the derived parts as a way of
controling the level of details.
2018-07-30 13:03:55 +08:00

405 lines
14 KiB
Python

import math
from collections import namedtuple
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
MovingPartInfo = namedtuple('MovingPartInfo',
('Hierarchy','ElementInfo','SelObj','SelSubname'))
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.trace = None
self.tracePoint = 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.TracePosition
def begin(self):
self.tracePoint = self.TracePosition
def update(self):
info = getElementInfo(self.info.Parent,self.info.SubnameRef)
self.info = info
if utils.isDraftObject(info.Part):
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)
@property
def TracePosition(self):
pos = gui.AsmCmdTrace.getPosition()
if pos:
return pos
mat = FreeCADGui.editDocument().EditingTransform
return mat.multiply(self.draggerPlacement.Base)
def move(self):
info = self.info
part = info.Part
obj = self.assembly.Object
pla = self.viewObject.DraggingPlacement
updatePla = True
rollback = []
if not info.Subname.startswith('Face') and 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.edge2VertexIndex(part,info.Subname,True)
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:
pos = self.TracePosition
if not self.tracePoint.isEqual(pos,1e-5):
try:
# check if the object is deleted
self.trace.Name
except Exception:
self.trace = None
if not self.trace:
self.trace = FreeCAD.ActiveDocument.addObject(
'Part::Polygon','AsmTrace')
self.trace.Nodes = [self.tracePoint]
self.tracePoint = pos
self.trace.Nodes = {-1:pos}
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 checkFixedPart(info):
if not gui.AsmCmdManager.LockMover:
return
assembly = resolveAssembly(info.Parent)
cstrs = assembly.getConstraints()
partGroup = assembly.getPartGroup()
if info.Part in Constraint.getFixedParts(None,cstrs,partGroup):
raise RuntimeError('cannot move fixed part')
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')
selObj = sels[0].Object
selSub = sels[0].SubElementNames[0]
ret = Assembly.findChildren(selObj,selSub)
if not ret:
raise RuntimeError('invalid selection {}, subname {}'.format(
objName(selObj),selSub))
if len(sels[0].SubElementNames)==1:
info = getElementInfo(ret[0].Assembly,
ret[0].Subname, checkPlacement=True)
return MovingPartInfo(SelObj=selObj,
SelSubname=selSub,
Hierarchy=ret,
ElementInfo=info)
ret2 = Assembly.findChildren(selObj,sels[0].SubElementNames[1])
if not ret2:
raise RuntimeError('invalid selection {}, subname {}'.format(
objName(selObj,sels[0].SubElementNames[1])))
if len(ret) == len(ret2):
if not ret2[-1].Object:
ret,ret2 = ret2,ret
else:
selSub = sels[0].SubElementNames[1]
elif len(ret) > len(ret2):
ret,ret2 = ret2,ret
else:
selSub = sels[0].SubElementNames[1]
assembly = ret[-1].Assembly
for r in ret2:
if assembly == r.Assembly:
info = getElementInfo(r.Assembly,r.Subname,checkPlacement=True)
return MovingPartInfo(SelObj=selObj,
SelSubname=selSub,
Hierarchy=ret2,
ElementInfo=info)
raise RuntimeError('not child parent selection')
def movePart(useCenterballDragger=None,moveInfo=None):
if not moveInfo:
moveInfo = logger.catch(
'exception when moving part', getMovingElementInfo)
if not moveInfo:
return False
info = moveInfo.ElementInfo
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(moveInfo.Hierarchy,info)
FreeCADGui.Selection.clearSelection()
return doc.setEdit(vobj,1)
class AsmQuickMover:
def __init__(self, info):
self.info = info.ElementInfo
idx = len(self.info.Subname)
if idx:
sub = info.SelSubname[:-idx]
else:
sub = info.SelSubname
_,mat = info.SelObj.getSubObject(sub,1,FreeCAD.Matrix())
pos = utils.getElementPos(self.info.Shape)
if not pos:
pos = self.info.Shape.BoundBox.Center
pos = mat.multiply(pos)
self.matrix = mat*self.info.Placement.inverse().toMatrix()
base = self.matrix.multiply(self.info.Placement.Base)
self.offset = pos - base
self.matrix.invert()
self.view = info.SelObj.ViewObject.Document.ActiveView
self.callbackMove = self.view.addEventCallback(
"SoLocation2Event",self.moveMouse)
self.callbackClick = self.view.addEventCallback(
"SoMouseButtonEvent",self.clickMouse)
self.callbackKey = self.view.addEventCallback(
"SoKeyboardEvent",self.keyboardEvent)
FreeCAD.setActiveTransaction('Assembly quick move')
self.active = True
def moveMouse(self, info):
pla = self.info.Placement
pos = self.view.getPoint(*info['Position'])
pla.Base = self.matrix.multiply(pos-self.offset)
setPlacement(self.info.Part,pla)
def removeCallbacks(self, abort=False):
if not self.active:
return
self.view.removeEventCallback("SoLocation2Event",self.callbackMove)
self.view.removeEventCallback("SoMouseButtonEvent",self.callbackClick)
self.view.removeEventCallback("SoKeyboardEvent",self.callbackKey)
FreeCAD.closeActiveTransaction(abort)
self.active = False
self.info = None
self.view = None
def clickMouse(self, info):
if info['State'] == 'DOWN':
self.removeCallbacks(info['Button']!='BUTTON1')
def keyboardEvent(self, info):
if info['Key'] == 'ESCAPE':
self.removeCallbacks(True)
class AsmDocumentObserver:
_quickMover = None
@classmethod
def closeMover(cls):
if cls._quickMover:
cls._quickMover.removeCallbacks(True)
cls._quickMover = None
@classmethod
def quickMove(cls,info):
cls.closeMover()
cls._quickMover = AsmQuickMover(info)
def slotNewDocument(self,_doc):
self.closeMover()
def slotDeleteDocument(self,_doc):
self.closeMover()
def slotUndoDocument(self,_doc):
self.closeMover()
AsmMovingPart.onRollback()
Assembly.cancelAutoSolve()
def slotRedoDocument(self,_doc):
self.slotUndoDocument(_doc)
def slotTransactionAbort(self,_doc):
self.slotUndoDocument(_doc)
def slotChangedObject(self,obj,prop):
Assembly.checkPartChange(obj,prop)
def quickMove():
ret = logger.catch('exception when moving part', getMovingElementInfo)
if not ret:
return False
doc = FreeCADGui.editDocument()
if doc:
doc.resetEdit()
FreeCADGui.Selection.clearSelection()
FreeCADGui.Selection.addSelection(ret.SelObj,ret.SelSubname)
AsmDocumentObserver.quickMove(ret)